[data] heat that fires, the muxer, reel + broadcast eras
Round 3 orders, all six. The validator move landed in 4803b6a (swept into
LANE-SCREEN's commit by the shared index); this is the rest.
- heat retune: round 2's numbers were below the sim's always-on cooling, so
nothing could ever warm up. Decoder now 100 gen at 0.0033 heat / 0.001 cool
-> throttles 10.2s, scrams 14.5s, 23.9s cycle, 25.8/s average (1.29x the
ASIC). Verified against LANE-SIM's real sim, not just arithmetic.
- powerGen had to move: the scram/restart hysteresis forces ~30% duty, so at
55 gen the decoder averaged worse than a steady ASIC and nobody would build
one.
- two new tests refuse decorative heat (per-tick and per-craft). They caught a
fresh instance of the same bug in chart-stco-map before it shipped.
- bufferCap 240 -> 300: one tank now covers exactly one decoder scram.
- ASIC COOLER spec'd (aura deferred to M4, no contract field requested yet);
THE MUXER + moov brain sub-lines; reel + broadcast era tech.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3707afa3a0
commit
4cccd22ca4
@ -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.
|
||||
|
||||
@ -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" }
|
||||
]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 }
|
||||
]
|
||||
|
||||
@ -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"] }
|
||||
]
|
||||
|
||||
@ -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}"`);
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user