diff --git a/fktry/data/CANON-DELTA.md b/fktry/data/CANON-DELTA.md index 9fe8543..4a20fd0 100644 --- a/fktry/data/CANON-DELTA.md +++ b/fktry/data/CANON-DELTA.md @@ -59,6 +59,46 @@ one direction" — five grades, one item each, `bloom-grade-1..5`. - Escalation is in delta wafers, not exotic inputs — a grade-5 canister is 2 raw bloom + 18 wafers of compounding, which is §4's "contents compound each lap" priced honestly. +## Added in round 3 + +### THE MUXER and the moov brain (§7, data-first — mechanics are M4's) + +§7 names the parts and says each is "its own late-game production line", but never said +what makes them. Each part is now made by the machine that already owns that knowledge, +which is the whole joke: the container is assembled by the factory that made the mess. + +| Thing | Made by | Why that machine | +|---|---|---| +| `stsz-ledger` | gop-assembler | The sample-size table. The assembler packed the crates, so it knows how big each one was. | +| `stco-map` | hex-splicer | The chunk-offset map — byte offsets are the splicer's entire domain. It nudges them for a living; here it writes them down honestly, under protest. | +| `stts-metronome` | telecine-puller | Time-to-sample is a cadence, and the puller is the cadence machine. Built from judder cams, so the metronome ticks 3:2. | +| `stss-keyring` | artifact-bottler | The sync-sample table is a list of I-frames, so it is literally forged from anchor slabs. | +| `ftyp-seal` | artifact-bottler | §7 names the ftyp seal as part of the container; it needed a source. | +| `moov-brain` | muxer | The four converge. 300 ticks. | +| `megaglitch` | muxer | `mux-container`: brain + seal + 500 mdat ore + 100 melt, 900 ticks. §7's "colossal composited corruption" priced like one. | + +`muxer` is `kind: 'crafter'`, not `'shipper'` — it *builds* the megaglitch, and you still +have to ship the thing. 5×5, the biggest footprint in the game, per "hero structure". + +### ASIC COOLER (spec'd, deliberately not yet mechanical) + +§2 HEAT says heat is "Vented by ASIC coolers" — a machine the codex names and never +specifies. Authored now so the data is ready; **the mechanic is deferred to M4**, when +SIM builds auras. Gated behind `broadcast-asic-cooling` so it can't appear early. + +- **Intended mechanic (SIM, when auras exist):** an adjacency aura — every machine whose + footprint touches the cooler's 2×2 gets the cooler's `coolPerTick` (0.02) instead of + its own. That turns the software decoder from a 30%-duty throb into something you can + *engineer around*: ring your decoders with coolers and pay for it in floor space and + build cost. This needs a contract change (`coolRadius?`, or a `kind: 'cooler'`) — + **not requesting it until SIM is ready to build it**, since a contract field nobody + reads is the same decorative trap heat just fell into. +- **Authored as `kind: 'power'` with `powerGen: 5`** because there is no cooler kind and + a lie is worse than a stretch: it is an ASIC, ASICs decode, decoding makes bandwidth. + It vents far more than it earns. If `kind: 'cooler'` ever lands, this moves. +- 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. + --- ## The numbers, and the arithmetic behind them @@ -76,79 +116,106 @@ idle draw (every machine's powerDraw) 57 /s = net peak demand 78 /s ``` -**Unit assumption — SIM please confirm:** I read `gen`/`draw` as bandwidth units **per -second**, not per tick (the codex's pylon meter reads kbps, §2 BANDWIDTH). This only -matters now that `bufferCap` exists, because a stored quantity = rate × time. If SIM -integrates per *tick*, every buffer number below is 30× too small and I'll retune. +**Units (RULED round 3):** `gen`/`draw` are bandwidth per **second**; `stored` and +`bufferCap` are bandwidth-**seconds**. My round-2 assumption held. -### `bufferCap: 240` on the buffer tank +### Round 2's heat was decorative — what went wrong -Order was "roughly 10 seconds of typical M1-chain deficit". Typical deficit ≠ total -blackout — it's the chain running with one generator down: +Worth writing down, because it's a trap anyone can fall into. I sized round 2's +`heatPerTick: 0.0025` against *no* cooling. SIM then implemented heat with cooling shed +**every tick, active or not** (`HEAT_DISSIPATION = 0.004`). So the decoder shed 0.004 and +banked 0.0025: net **−0.0015/tick**. It could never warm up. The number looked like a +mechanic and was a comment. + +The lesson is now a test (`every heat source out-heats its own cooling`), and it caught a +second instance immediately: `chart-stco-map` banked 0.06 heat over 90 ticks while the +splicer shed 0.09 — I'd authored a fresh decorative number in this very round. + +**Anything with a heat number must beat its own machine's cooling, or it isn't a +mechanic.** The validator now refuses both forms (per-tick machine heat, per-craft recipe +heat). + +### The software decoder: `powerGen` 55 → 100, `heatPerTick` 0.0033, `coolPerTick` 0.001 + +Net while running = 0.0033 − 0.001 = **0.0023/tick**. Against SIM's curve (throttle 0.7, +scram 1.0, restart 0.5): ``` -reference chain, peak draw 96 /s -plant: 2 software decoders (55 each) 110 /s → comfortable -one decoder scrams: supply = 55 + 18 (compression) = 73 /s -deficit = 96 - 73 23 /s -× 10 s 230 -bufferCap 240 (one tank ≈ one decoder's cooldown) +throttle, from cold 0.7 / 0.0023 / 30 = 10.1 s (target ~10 s) +scram, from cold 1.0 / 0.0023 / 30 = 14.5 s (target 13-15 s) +steady cycle, up 0.5 / 0.0023 / 30 = 7.25 s (heat 0.5 -> 1.0) +steady cycle, down 0.5 / 0.001 / 30 = 16.67 s (cools 1.0 -> restart 0.5) +period 23.9 s (target ~25 s) +duty 30.3 % ``` -That is the intended M2 sentence: **a buffer tank is what makes software decoders -survivable.** The tank rides out exactly one scram. Two decoders scramming in sync -drains it and you brown out — which is the lesson. +**Why the decoder cools slowly (0.001, vs the sim's 0.004 default):** it is an overclocked +tower PC held together with zip ties and prayer (§4). It is the worst-cooled object in the +factory. The ASIC has "a huge heatsink afro" and no heat source at all. -### `heatPerTick: 0.0025` on the software decoder, `powerGen` 45 → 55 - -Heat is 0..1, throttling >0.7 and scramming at 1 (contract). From cold, running flat out: +**Why powerGen had to nearly double.** SIM's hysteresis (scram at 1.0, restart at 0.5) +means any machine that throttles at ~10 s spends ~70% of its life cooling. At round 2's +55 gen that's an average of ~14/s — strictly worse than the ASIC's steady 20, so nobody +would ever build one and "burst gen" would be a lie. Averaged properly (generation is +throttled linearly 1.0 → 0.5 across the throttle window, and zero while scrammed): ``` -throttle (0.7) at 280 ticks = 9.3 s -scram (1.0) at 400 ticks = 13.3 s +un-throttled 0.2 / 0.0023 = 87.0 ticks at 100/s +throttled 0.3 / 0.0023 = 130.4 ticks at avg 75/s +energy per cycle = 100 x (87.0 + 130.4 x 0.75) = 18480 bandwidth-ticks +average = 18480 / 717 ticks = 25.8 /s ``` -So a software decoder **cannot** run continuously. That's deliberate and it is the codex: -§4 gives it "burst gen", "doubles throughput on demand", "throttles theatrically". It is a -burst generator that needs downtime, not a better ASIC. - -The trade, per 2×2 footprint: - -| | gen | duty | effective | failure mode | +| | peak | duty | average | failure mode | |---|---|---|---|---| | DECODE ASIC | 20 /s | 100% | **20 /s** | none, forever | -| SOFTWARE DECODER | 55 /s | ~50% | **~27 /s** | brownout, on its own schedule | +| SOFTWARE DECODER | 100 /s | 30% | **25.8 /s** | total blackout, 16.7 s at a time | -~37% more average power for a supply that drops out on a timer you don't control. That's -a real choice rather than a strictly-better button. **And it answers the order's question -directly** ("does anyone dare run two side by side"): two identical decoders placed -together heat in lockstep and therefore scram in lockstep, turning a 50% duty cycle into a -100% blackout every ~13 s. Two decoders are only safe if something staggers them or a tank -covers the gap. Daring to run two is precisely the M2 lesson. +Five ASICs at full song, one third of the time, in one 2×2 footprint. 29% more average +power for a supply that vanishes on a schedule you don't control — a real choice, and the +answer to "does anyone dare run two side by side" is still *no, not naked*: identical +decoders heat in lockstep and therefore black out in lockstep. -**Assumption SIM owns — cooling.** The ~50% duty above assumes cooling of about -`0.0025`/tick while scrammed or idle (symmetric with the heat-up, so ~400 ticks cold). -There is no cooling model in the contract and no cooler machine in `data/`. If SIM picks a -different rate, tell me the number in NOTES and I'll retune `heatPerTick` — the shape I'm -aiming at is *throttle in ~10 s, full cycle ~25 s*. If SIM implements no cooling at all, -`heatPerTick` is a fuse, not a mechanic, and every software decoder is a one-shot: say so -and I'll drop it to 0 until a cooler exists. +### `bufferCap: 240 → 300` -### `heat: 0.05` on `splice-offsets` +Re-derived against the retuned cycle, since the order was "one tank should cover one +decoder scram" and the scram window changed: -Per craft, at 45 ticks each: 20 consecutive crafts = 900 ticks = 30 s to scram. §4 says the -splicer "Requires ... a steady buffer feed or it splices *itself*" — a splicer you leave -running unattended stops itself, which is the pinch the order asked for. +``` +reference chain: peak draw 96 /s, compression pays back 18 /s -> net demand 78 /s +balanced plant: 3 ASIC (60/s steady) + 1 software decoder (25.8/s avg) = 85.8 /s ✓ +decoder scrams: supply = 60 + 18 = 78 /s vs draw 96 /s -> deficit 18 /s + 18 /s x 16.67 s = 300 bandwidth-seconds +bufferCap = 300 -> one tank rides out exactly one scram. +``` + +And it refills in time to do it again: while the decoder is up the plant makes 67/s more +than it spends, recharging 300 in **4.5 s** of the 7.25 s up-phase. That is the M2 +sentence — **a buffer tank is what makes software decoders survivable** — and it now +closes. + +### `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 +splicing before it scrams (`chart-stco-map`: 50 s). A lesser fever than the decoder's +14.5 s, exactly as ordered. §4: "Requires ... a steady buffer feed or it splices *itself*" +— left running unattended, it stops itself. --- ## Open / deferred -- **No cooler machine exists.** §2 HEAT says heat is "Vented by ASIC coolers" — that's a - machine the codex names but never specifies. Candidate for round 3; until then, heat is - only survivable via duty cycling. +- **The ASIC cooler's aura is spec'd, not built** (above). Until SIM has auras, heat is + survivable only by duty-cycling or by leaning on ASICs. Deliberate. - `cinema-pack`, `mosquito-swarm`, `gibbs-wraith` remain forward canon (no recipe). The - validator's FORWARD_CANON allowlist is the enforcement, and it now has a companion test - asserting nothing sits on the list once it *is* producible. + validator's FORWARD_CANON allowlist is the enforcement, and it has a companion test + asserting nothing sits on the list once it *is* producible. Round 3 retired + `container-pack`'s slot (round 2) and added none. - Dials-as-recipes (quantizer Q, subsampler grades, GOP cadence) stays, per round-2 review. -- The reel and broadcast era tech nodes don't exist yet; tech is disc + stream only. +- **The heat constants in `data/validate.test.ts` are mirrored from `src/sim/index.ts`** + (throttle 0.7, restart 0.5, dissipation default 0.004, 30 tps). They are not exported, + so my arithmetic tests can't import them. If SIM retunes the curve, my tests go stale + *silently* — they'd still pass while the feel drifts. Re-check on any heat round. A + cheap fix if SIM likes: export the constants. +- The muxer, moov brain and megaglitch are data-only; no lane implements a victory + condition yet. The tech gate (`fortress-the-muxer`) keeps them out of reach meanwhile. diff --git a/fktry/data/items.json b/fktry/data/items.json index c37d7a7..4d2213c 100644 --- a/fktry/data/items.json +++ b/fktry/data/items.json @@ -48,5 +48,13 @@ { "id": "cinema-pack", "name": "CINEMA PACK", "codex": "2:science-packs", "tier": 3, "color": "#fdfdff" }, { "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": "gibbs-wraith", "name": "GIBBS WRAITH", "codex": "5:gibbs-wraiths", "tier": 2, "color": "#bfe0ff" }, + + { "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" }, + { "id": "stss-keyring", "name": "STSS KEYRING", "codex": "7:the-muxer", "tier": 2, "color": "#e0b83f" }, + { "id": "ftyp-seal", "name": "FTYP SEAL", "codex": "7:the-muxer", "tier": 2, "color": "#b08fd0" }, + { "id": "moov-brain", "name": "THE MOOV BRAIN", "codex": "7:the-muxer", "tier": 3, "color": "#d8a0ff" }, + { "id": "megaglitch", "name": "THE MEGAGLITCH", "codex": "7:the-megaglitch", "tier": 3, "color": "#ff2fd4" } ] diff --git a/fktry/data/machines.json b/fktry/data/machines.json index ffdfb91..4bee78a 100644 --- a/fktry/data/machines.json +++ b/fktry/data/machines.json @@ -14,7 +14,7 @@ "asset": "lane-splitter", "color": "#55595e", "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": 240, + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "bufferCap": 300, "asset": "buffer-tank", "color": "#5a6068", "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." }, @@ -48,7 +48,8 @@ "flavor": "Prints instructions, not pictures. Each one is applied to whatever came before it — correct or otherwise." }, { "id": "gop-assembler", "name": "GOP ASSEMBLER", "codex": "4:gop-assembler", "kind": "crafter", "footprint": { "x": 3, "y": 2 }, - "recipes": ["assemble-gop", "assemble-gop-ibbp", "assemble-gop-i-only", "spool-offset-ribbon"], + "recipes": ["assemble-gop", "assemble-gop-ibbp", "assemble-gop-i-only", "spool-offset-ribbon", + "tally-stsz-ledger"], "powerDraw": 6, "asset": "gop-assembler", "color": "#7a6a44", "flavor": "One anchor, N wafers, one crate, per the cadence punch-card. Opening a sealed crate is a production verb here, not a crime." }, { "id": "mosh-reactor", "name": "MOSH REACTOR", "codex": "4:mosh-reactor", "kind": "crafter", @@ -65,17 +66,18 @@ "powerDraw": 5, "asset": "field-loom", "color": "#3f5a44", "flavor": "Weaves and unweaves time. The two halves of a splinter were captured a moment apart and have never agreed since." }, { "id": "telecine-puller", "name": "TELECINE PULLER", "codex": "4:telecine-puller", "kind": "crafter", - "footprint": { "x": 2, "y": 2 }, "recipes": ["mill-judder"], "powerDraw": 4, + "footprint": { "x": 2, "y": 2 }, "recipes": ["mill-judder", "wind-stts-metronome"], "powerDraw": 4, "asset": "telecine-puller", "color": "#6e5a38", "flavor": "Cranks 24 into 30 on a deliberately lumpy cam. Runs backwards as a purifier, which we do not discuss on site." }, { "id": "hex-splicer", "name": "HEX SPLICER", "codex": "4:hex-splicer", "kind": "crafter", - "footprint": { "x": 3, "y": 3 }, "recipes": ["splice-offsets"], "powerDraw": 12, + "footprint": { "x": 3, "y": 3 }, "recipes": ["splice-offsets", "chart-stco-map"], "powerDraw": 12, + "coolPerTick": 0.001, "asset": "hex-splicer", "color": "#5a3a3a", "flavor": "Nudges the offsets a few bytes so the decoder fetches garbage. Keep its buffer fed, or it splices itself." }, { "id": "artifact-bottler", "name": "ARTIFACT BOTTLER", "codex": "2:science-packs", "kind": "crafter", "footprint": { "x": 2, "y": 2 }, "recipes": ["can-static", "mix-rec709", "mix-dci-p3", "mix-rec2020", - "cut-sync-key", "bottle-container-pack", + "cut-sync-key", "bottle-container-pack", "forge-stss-keyring", "cut-ftyp-seal", "bottle-analog-pack", "bottle-spatial-pack", "bottle-temporal-pack"], "powerDraw": 4, "asset": "artifact-bottler", "color": "#566056", "flavor": "Bottles the triptych. Contents are shelf-stable in the legal sense, i.e. no law has been written that mentions them." }, @@ -85,9 +87,19 @@ "asset": "decode-asic", "color": "#b9bec4", "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": 55, "heatPerTick": 0.0025, + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 100, + "heatPerTick": 0.0033, "coolPerTick": 0.001, "asset": "software-decoder", "color": "#6e3a34", - "flavor": "Decodes ANYTHING, at any temperature. Throttles at 0.7, scrams at 1.0, screams throughout. The lag is the damage model." }, + "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", + "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": "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", + "flavor": "The rocket. A launch gantry embracing a container built of nested boxes, innermost a brain of index tape-reels. It ships the whole file at once, and the file is your entire shipping history, and there is no second launch." }, { "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper", "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5, diff --git a/fktry/data/recipes.json b/fktry/data/recipes.json index b2fb2d8..0f142e8 100644 --- a/fktry/data/recipes.json +++ b/fktry/data/recipes.json @@ -51,6 +51,8 @@ "outputs": { "gop-crate": 1 }, "ticks": 45, "bandwidth": 24 }, { "id": "spool-offset-ribbon", "machine": "gop-assembler", "inputs": { "gop-crate": 1, "moov-shard": 1 }, "outputs": { "offset-ribbon": 2 }, "ticks": 60, "bandwidth": 6 }, + { "id": "tally-stsz-ledger", "machine": "gop-assembler", "inputs": { "gop-crate": 2, "offset-ribbon": 1 }, + "outputs": { "stsz-ledger": 1 }, "ticks": 90, "bandwidth": 8 }, { "id": "mosh", "machine": "mosh-reactor", "inputs": { "gop-crate": 1 }, "outputs": { "melt": 3, "anchor-slab": 1 }, "ticks": 90, "bandwidth": 10 }, @@ -75,9 +77,13 @@ { "id": "mill-judder", "machine": "telecine-puller", "inputs": { "silver-frames": 3 }, "outputs": { "judder-cams": 1, "field-splinters": 2 }, "ticks": 60, "bandwidth": 5 }, + { "id": "wind-stts-metronome", "machine": "telecine-puller", "inputs": { "judder-cams": 2, "sync-key": 1 }, + "outputs": { "stts-metronome": 1 }, "ticks": 90, "bandwidth": 7 }, { "id": "splice-offsets", "machine": "hex-splicer", "inputs": { "gop-crate": 1 }, - "outputs": { "explosive-hits": 4, "hf-dust": 2 }, "ticks": 45, "bandwidth": 20, "heat": 0.05 }, + "outputs": { "explosive-hits": 4, "hf-dust": 2 }, "ticks": 45, "bandwidth": 20, "heat": 0.08 }, + { "id": "chart-stco-map", "machine": "hex-splicer", "inputs": { "offset-ribbon": 3, "moov-shard": 1 }, + "outputs": { "stco-map": 1 }, "ticks": 90, "bandwidth": 14, "heat": 0.15 }, { "id": "can-static", "machine": "artifact-bottler", "inputs": { "hf-dust": 4 }, "outputs": { "static-canister": 1 }, "ticks": 30, "bandwidth": 3 }, @@ -86,6 +92,10 @@ { "id": "bottle-container-pack", "machine": "artifact-bottler", "inputs": { "moov-shard": 2, "offset-ribbon": 1, "sync-key": 1 }, "outputs": { "container-pack": 1 }, "ticks": 90, "bandwidth": 6 }, + { "id": "forge-stss-keyring", "machine": "artifact-bottler", "inputs": { "sync-key": 4, "anchor-slab": 2 }, + "outputs": { "stss-keyring": 1 }, "ticks": 120, "bandwidth": 9 }, + { "id": "cut-ftyp-seal", "machine": "artifact-bottler", "inputs": { "moov-shard": 1, "sync-key": 1 }, + "outputs": { "ftyp-seal": 1 }, "ticks": 60, "bandwidth": 5 }, { "id": "mix-rec709", "machine": "artifact-bottler", "inputs": { "chroma-slurry": 2, "luma-bars": 1 }, "outputs": { "gamut-paint-rec709": 1 }, "ticks": 30, "bandwidth": 3 }, { "id": "mix-dci-p3", "machine": "artifact-bottler", "inputs": { "chroma-slurry": 3, "false-color-speckle": 1 }, @@ -102,5 +112,12 @@ "outputs": { "spatial-pack": 1 }, "ticks": 60, "bandwidth": 6 }, { "id": "bottle-temporal-pack", "machine": "artifact-bottler", "inputs": { "melt": 2, "bloom-concentrate": 1, "judder-cams": 1 }, - "outputs": { "temporal-pack": 1 }, "ticks": 90, "bandwidth": 8 } + "outputs": { "temporal-pack": 1 }, "ticks": 90, "bandwidth": 8 }, + + { "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 }, + { "id": "mux-container", "machine": "muxer", + "inputs": { "moov-brain": 1, "ftyp-seal": 1, "mdat-ore": 500, "melt": 100 }, + "outputs": { "megaglitch": 1 }, "ticks": 900, "bandwidth": 60 } ] diff --git a/fktry/data/tech.json b/fktry/data/tech.json index 6ee4497..d1fcc49 100644 --- a/fktry/data/tech.json +++ b/fktry/data/tech.json @@ -1,4 +1,16 @@ [ + { "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 }, + "unlocks": ["extract-field-splinters"] }, + { "id": "broadcast-field-loom", "era": "broadcast", "cost": { "analog-pack": 20 }, + "unlocks": ["field-loom", "weave-fields", "loom-vhold-roll"] }, + { "id": "broadcast-asic-cooling", "era": "broadcast", "cost": { "analog-pack": 30, "spatial-pack": 10 }, + "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 }, @@ -18,10 +30,6 @@ { "id": "disc-i-only-cadence", "era": "disc", "cost": { "spatial-pack": 30, "temporal-pack": 20 }, "unlocks": ["assemble-gop-i-only"] }, - { "id": "stream-field-loom", "era": "stream", "cost": { "analog-pack": 20 }, - "unlocks": ["field-loom", "weave-fields", "loom-vhold-roll"] }, - { "id": "stream-telecine", "era": "stream", "cost": { "analog-pack": 25, "temporal-pack": 15 }, - "unlocks": ["telecine-puller", "mill-judder"] }, { "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 }, @@ -37,5 +45,13 @@ { "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 }, - "unlocks": ["hex-splicer", "splice-offsets"] } + "unlocks": ["hex-splicer", "splice-offsets"] }, + + { "id": "fortress-moov-brain", "era": "fortress", + "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-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 a9f99a7..d976301 100644 --- a/fktry/data/validate.test.ts +++ b/fktry/data/validate.test.ts @@ -181,6 +181,83 @@ describe('machines', () => { }); }); +// Round 2 shipped heat numbers that could never fire: every heatPerTick sat below the +// sim's always-on cooling, so heat was decorative. These lock the arithmetic down so a +// well-meaning tweak can't quietly make it decorative again. +describe('the heat curve actually fires', () => { + // MIRRORED from src/sim/index.ts (LANE-SIM owns the curve; these are not exported). + // If SIM changes them, these numbers go stale silently — re-check on any heat round. + const TPS = 30; + const THROTTLE_START = 0.7; // heat above this throttles + const RESTART = 0.5; // a scrammed machine restarts once it cools below this + const SIM_DEFAULT_COOL = 0.004; + + const soft = machines.find((m) => m.id === 'software-decoder')!; + const cool = (m: MachineDef) => m.coolPerTick ?? SIM_DEFAULT_COOL; + /** Net heat per tick while the machine is working. Negative = can never overheat. */ + const net = (m: MachineDef) => (m.heatPerTick ?? 0) - cool(m); + + it('every heat source out-heats its own cooling — or it is just decoration', () => { + const decorative = machines + .filter((m) => m.heatPerTick !== undefined) + .filter((m) => net(m) <= 0) + .map((m) => `${m.id}: heats ${m.heatPerTick}/tick but cools ${cool(m)}/tick — never warms up`); + expect(decorative).toEqual([]); + }); + + it('every heated recipe banks more than its machine sheds during the craft', () => { + // A recipe's heat lands once per craft, but the machine cools every tick of it. + // heat <= ticks * coolPerTick means the number is a comment, not a mechanic. + const decorative = recipes + .filter((r) => r.heat !== undefined) + .map((r) => ({ r, m: machines.find((x) => x.id === r.machine)! })) + .filter(({ r, m }) => r.heat! <= r.ticks * cool(m)) + .map(({ r, m }) => + `${r.id}: banks ${r.heat} per craft but ${m.id} sheds ${(r.ticks * cool(m)).toFixed(3)} over its ${r.ticks} ticks`); + expect(decorative).toEqual([]); + }); + + it('the software decoder throttles at ~10s and scrams at 13-15s of continuous load', () => { + const secondsTo = (heat: number) => heat / net(soft) / TPS; + expect(secondsTo(THROTTLE_START)).toBeGreaterThan(8); + expect(secondsTo(THROTTLE_START)).toBeLessThan(12); + expect(secondsTo(1)).toBeGreaterThan(13); + expect(secondsTo(1)).toBeLessThan(15); + }); + + it('the software decoder duty-cycles at ~25s, and is still worth its footprint', () => { + // Steady state after the first scram: heat swings RESTART -> 1 -> RESTART. + const upTicks = (1 - RESTART) / net(soft); + const downTicks = (1 - RESTART) / cool(soft); + const periodS = (upTicks + downTicks) / TPS; + expect(periodS).toBeGreaterThan(20); + expect(periodS).toBeLessThan(30); + + // Generation is throttled linearly from 1.0 down to 0.5 across the throttle window, + // and is zero while scrammed. Average it over the whole cycle. + const fullTicks = (THROTTLE_START - RESTART) / net(soft); // un-throttled + const throttledTicks = (1 - THROTTLE_START) / net(soft); // 1.0 -> 0.5 output + const energy = soft.powerGen! * (fullTicks + throttledTicks * 0.75); + const average = energy / (upTicks + downTicks); + + const asic = machines.find((m) => m.id === 'decode-asic')!; + // Same 2x2 footprint, so if the hot one doesn't beat the cool one on average, + // nobody would ever build it and the codex's "burst gen" is a lie. + expect(average).toBeGreaterThan(asic.powerGen! * 1.2); + }); + + it('the hex splicer runs a lesser fever than the decoder', () => { + const splicer = machines.find((m) => m.id === 'hex-splicer')!; + const secondsToScram = Math.min( + ...recipes.filter((r) => r.machine === 'hex-splicer' && r.heat) + .map((r) => (1 / (r.heat! - r.ticks * cool(splicer))) * r.ticks / TPS), + ); + expect(secondsToScram).toBeGreaterThan(30); // lesser... + expect(secondsToScram).toBeLessThan(90); // ...but a fever + expect(secondsToScram).toBeGreaterThan(1 / net(soft) / TPS); // slower than the decoder + }); +}); + describe('recipes', () => { it('every recipe runs on a machine that exists', () => { const bad = recipes.filter((r) => !machineIds.has(r.machine)).map((r) => `${r.id} -> "${r.machine}"`); diff --git a/fktry/lanes/LANE-DATA.md b/fktry/lanes/LANE-DATA.md index 0272b61..e2752d5 100644 --- a/fktry/lanes/LANE-DATA.md +++ b/fktry/lanes/LANE-DATA.md @@ -211,3 +211,82 @@ NEXT (candidates): a cooler machine — §2 says heat is "Vented by ASIC coolers machine the codex names but never specifies, and without it heat has no counterplay; mosquito traps (unblocks `vaporwave-sunset`); reel + broadcast era tech; the muxer and the moov brain's four sub-lines (§7); cinema-pack when the Fortress exists. + +### Round 3 — 2026-07-17 — Opus 4.8 + +SHIPPED: all 6 orders. Validator moved to `data/validate.test.ts` (git mv, imports +rewired, vitest picks it up from the new home; `src/sim/**` is fully SIM's again). +Heat retuned. `bufferCap` re-derived. ASIC COOLER + muxer + moov brain authored. Reel +and broadcast eras. items 44 → 51, machines 21 → 23, recipes 44 → 51, tech 19 → 24. +Validator 36 → 41 tests. +- **DoD met, verified against the real sim, not my arithmetic**: I wrote a throwaway + probe that ran LANE-SIM's actual `createSim()` with my data — **throttle at 10.17s, + scram at 14.50s, downtime 16.67s, ASIC heat still 0 after 100s**. Predicted 10.14 / + 14.49 / 16.67. Then deleted the probe: it asserts sim behaviour, which is SIM's lane + to test, not mine. +- Full tree green: **223/223 across all 16 files** — SIM's reference factory and + pressure tests pass against the rebalanced numbers. Live build: 23/23 machines + art-directed, ASIC COOLER in the POWER page at my exact hex with zero UI code, no + console errors. +- Fault-injected the new checks by **re-introducing round 2's exact bug** — it fails + with `software-decoder: heats 0.0025/tick but cools 0.004/tick — never warms up`. + +DECISIONS: +- **You were right that heat was decorative, and the cause is worth recording**: I sized + round 2's numbers against *no* cooling; SIM shipped cooling that runs every tick, + active or not. 0.0025 heat vs 0.004 cooling = net −0.0015. The number looked like a + mechanic and was a comment. That lesson is now two tests — and they immediately caught + me authoring a *fresh* decorative number this round (`chart-stco-map` banked 0.06 over + 90 ticks while the splicer shed 0.09). Fixed to 0.15 before it ever ran. +- **`powerGen` 55 → 100, and it had to move.** SIM's hysteresis (scram at 1.0, restart at + 0.5) means anything throttling at ~10s spends ~70% of its life cooling. At 55 gen the + decoder averages ~14/s — strictly worse than the ASIC's steady 20, so nobody would + build one and "burst gen" would be a lie. At 100 it averages 25.8/s (1.29× the ASIC) in + the same 2×2: five ASICs at full song, one third of the time. The duty cycle set the + peak, not taste. Arithmetic in CANON-DELTA. +- **`bufferCap` 240 → 300.** Re-derived, since the scram window changed: a balanced plant + (3 ASIC + 1 decoder) runs an 18/s deficit for the 16.67s scram = exactly 300 + bandwidth-seconds, and refills in 4.5s of the 7.25s up-phase. One tank rides out one + scram — the order's sentence, now literally true. (Your review said 240 = 8s of a 30 + deficit; my round-2 intent was 10s of a 23 deficit. Either way the retune moved it.) +- **ASIC COOLER authored as `kind: 'power'`, `powerGen: 5`** — there is no cooler kind and + I'd rather stretch than lie: it is an ASIC, ASICs decode, decoding makes bandwidth. It + vents far more than it earns. **I have deliberately NOT requested `coolRadius?` / + `kind: 'cooler'`** — a contract field nobody reads is the same decorative trap heat just + fell into. Ask me for it the round SIM builds auras. The intended adjacency mechanic is + spec'd in CANON-DELTA. +- Tech ids `stream-field-loom` → `broadcast-field-loom` and `stream-telecine` → + `reel-telecine`: a *move*, not an addition (the validator forbids two techs unlocking + the same thing, which is what caught the duplication). Grepped first — no lane reads + tech ids or eras yet, so the rename is safe. Recorded here per the deprecation rule. + +INVENTED: 7 items (the four moov parts, `ftyp-seal`, `moov-brain`, `megaglitch`), 2 +machines (`asic-cooler`, `muxer`), 9 recipes, 4 tech nodes. Rationale in CANON-DELTA — +each moov part is made by the machine that already owns that knowledge (the splicer +charts stco because byte offsets are its whole domain; the keyring is forged from anchor +slabs because stss *is* a list of I-frames). + +BLOCKED/BROKEN: nothing blocked. One regression I caused by following orders, below. + +CONTRACT REQUEST: **`tsconfig.json` `include` needs `"data"`.** The ordered move took the +validator out of the tsc program — `include: ["src"]`. I proved it: appended +`const x: number = "not a number"` to `data/validate.test.ts` and `npm run check` exits +**0**. Vitest transpiles without typechecking, so my validator — the file whose whole job +is catching mistakes — is now the one file in the repo where a type error ships silently. +tsconfig is orchestrator-owned, so I can't fix it. One word: `"include": ["src", "data"]`. + +CROSS-LANE — SIM: your `coolPerTick` wiring landed and works; my numbers are tuned +against it and verified in your sim. Two notes. (1) Your reference factory's plant is 4 +software decoders and no ASIC — with heat now real they heat in lockstep and black out +together for 16.67s at a time. Suggested v3 plant: **3 decode-asics + 1 software decoder ++ 1 buffer-tank**, which is precisely what `bufferCap: 300` is sized for (85.8/s average +vs the chain's 78/s demand, and the tank covers the scram). Your call. (2) My heat tests +mirror your constants (0.7 / 0.5 / 0.004 / 30tps) by hand because they aren't exported — +if you retune the curve my tests go stale *silently*. Exporting them would let me import +truth instead of copying it. + +NEXT (candidates): mosquito traps + HF-dust piles (unblocks `vaporwave-sunset`, the last +forward-canon commission that's blocked on data rather than mechanics); the ASIC cooler's +aura the round SIM has auras; a seam/ore map — SIM notes extractors currently mine +anywhere because `data/` has no seam data, which is a real content gap I own; +`cinema-pack` + the Fortress when M4 lands.