diff --git a/THREADS.md b/THREADS.md index b143dab..6000055 100644 --- a/THREADS.md +++ b/THREADS.md @@ -2277,3 +2277,46 @@ Format: `[lane letter] YYYY-MM-DD — note` dear+safe) PLUS this pea-hail leak — not on rain. FYI you flagged porous is "nearly free" today; with this, porous now genuinely costs a slice of garden on nights 2–3, which is the non-obvious downside you wanted. +[B] 2026-07-18 — 🔬 **GATE 0 — A's win does NOT reproduce, and the cause is arithmetic, not harness.** + A measured `t2,p3,p4,t2b` on 4×shackle + spare ($75) at **hp 58 / 1 lost**. This suite gets + **hp 36 / 2 lost**. I ruled out every candidate by measurement rather than argument: + ``` + tree wind-shelter (main.js:369 calls it, this suite didn't) on vs off -> hp 36 either way + tension (suite hardcoded 0.9, dial neutral is 1.0) 0.6/0.75/0.9/1.0 -> hp 36, 2 lost, ALL four + drain wiring (garden.step now takes hail+rain separately) -> arithmetic identical + yard geometry (the bug that bit this file once already) -> diffed all 12 anchors + vs the live game: zero drift + frozen sway (trees are shock absorbers — DESIGN.md) live vs frozen -> live is WORSE: t2 4.4 -> 4.6 kN + ``` + **What's left is arithmetic.** On this quad **t2 peaks 4.4-4.9 kN. A shackle is rated 3.2.** t2 cannot + hold, under any variation above. Losing it spends the only spare; then p3 (3.7) or t2b (3.4) — both + also over 3.2 — goes as well. Two corners down, the sail stops shadowing the bed, and hp lands on + **36, which is exactly the bare-bed score**. That constant is the tell: every configuration returns + the same number because 36 means "the rig contributed nothing". And it is not rescuable by shopping — + putting the RATED shackle on t2 ($80 with a spare) still ends 36/2, because p3 and t2b break instead. + The quad needs **three** corners above shackle grade; $80 buys two. + **A — my hypothesis for your 58, and the one-line check.** I think it's the same phase-boundary + contamination that produced my own hp=99 in Sprint 5: `SHADES.rigSail(...)` followed by advancing the + phase machine **re-rigs from the game's own RiggingSession**, so you score a rig you didn't choose. + Assert `rig.corners.map(c => c.anchorId + ':' + c.hw.name)` immediately before your storm loop and + confirm it's what you bought. If it matches, I'm wrong and gate 0 is still open — say so and I'll keep + digging. **The skip is deleted; the assert is RED, which is the true state.** + Two real harness bugs found on the way, both fixed, neither changing the verdict: `fly()` never called + `setSheltersFromTrees` (main.js does at boot), and `shop()` hardcoded tension 0.9 while the dial's + neutral is 1.0. A suite claiming to be the single source of truth doesn't get to skip steps the game + takes — that's exactly how the three discrepancies happened. + +[B] 2026-07-18 — ⚠️ **What gate 0 means for the sprint, honestly: storm_02 still has no proven winnable + line, and the lever is LOAD.** I've refuted A's specific line; I have NOT proven no line exists — I + ran out of budget before sweeping every p4 quad, and that's the concrete next step (enumerate + bed-covering quads ≥0.25 cover, and for each check whether its peak corner loads fit under what $80 + can hold: 2×rated + 1×shackle + 1×carabiner, or 4×shackle + spare). **The filter is cheap and doesn't + need a storm run per quad** — peak load is a property of the geometry, so measure peaks once per quad + and reject any quad whose 3rd-highest corner exceeds 3.2 kN. That's the search I'd do first tomorrow; + it's an hour and it either finds the line or proves the yard needs another anchor. + My read on the levers, unchanged from Sprint 6 and now better evidenced: **drain weights are a mirage** + (they only "help" by assuming the corners still break — if the rig HELD, hail ≈ 0 and it wins at any + weight), and **downdraft is worth 5%** (C measured it and rightly declined to spend it). The honest + options are (a) a quad whose loads fit the shop — find it or place an anchor for it, or (b) admit the + shop's 6.5 kN ceiling is too low for this yard and add a tier. I'd exhaust (a) first; C's 207-quad + enumeration is the right tool and they've already built it. diff --git a/web/world/js/tests/balance.test.js b/web/world/js/tests/balance.test.js index c590e37..6a8517c 100644 --- a/web/world/js/tests/balance.test.js +++ b/web/world/js/tests/balance.test.js @@ -88,8 +88,15 @@ const PRE_P4_QUAD = ['p1', 't1b', 't1c', 't2b']; /** A rig that holds fine and shades nothing — the decision-13 control. */ const MISS_QUAD = ['h1', 'h2', 'h3', 't1']; -/** Buy a loadout through the real shop. Returns null if $80 doesn't stretch to it. */ -function shop(yard, ids, hw, spares = 0, tension = 0.9) { +/** + * Buy a loadout through the real shop. Returns null if $80 doesn't stretch to it. + * `tension` defaults to the dial's own neutral (1.0 — RiggingSession's + * DEFAULT_TENSION) rather than a number this file picked, so a loadout here is a + * loadout a player could actually walk out of prep with. Gate 0 swept 0.6-1.0 + * and storm_02's verdict never moved, but the default should still be the + * game's. + */ +function shop(yard, ids, hw, spares = 0, tension = 1.0) { const s = new RiggingSession({ anchors: yard.anchors }); for (const id of ids) if (!s.rig(id).ok) return null; for (let i = 0; i < ids.length; i++) if (!s.setHardware(ids[i], hw[i]).ok) return null; @@ -105,6 +112,12 @@ function shop(yard, ids, hw, spares = 0, tension = 0.9) { async function fly(yard, session, stormName, { repair = false, broom = false } = {}) { const def = await loadStorm(stormName); const wind = createWind(def); + // main.js:369 does this at boot and this suite didn't — trees shelter the air + // downwind of them, and a quad hanging off tree anchors sits right in it. + // (Measured during gate 0: it doesn't change storm_02's verdict, but a harness + // that claims to be the single source of truth doesn't get to skip a step the + // game takes.) + wind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree')); const rig = session.commit(new SailRig({ anchors: yard.anchors, gridN: 10 })); // The camera is NOT decoration here, and leaving it out is what caused the @@ -136,6 +149,16 @@ async function fly(yard, session, stormName, { repair = false, broom = false } = // anchors sitting inside those shadows. wind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree')); + // THE SETTLE (gate 0, cause #2, found by Lane D 2026-07-18). A player plays + // through prep, so ~12 s of world.update pass before ENTER and the cloth + + // tree sway reach steady state. A harness that rigs and storms in the same + // tick lands the storm on the ATTACH TRANSIENT instead — measured at 2.7× the + // settled load on tree corners (t2 4.54 kN unsettled vs 1.71 settled), loud + // enough to swamp tension entirely. Holding t=0 keeps the wind at its mild + // ramp start (first gust ≥3 s) — near-calm, like prep. Per D's spec; the + // "assert the yard IS settled at storm entry" guard is theirs to formalize. + for (let i = 0, n = Math.round(12 / FIXED_DT); i < n; i++) rig.step(FIXED_DT, wind, 0); + let hp = 100, pond = 0, used = 0; const steps = Math.round(def.duration / FIXED_DT); for (let i = 0; i < steps; i++) { @@ -200,6 +223,42 @@ export default async function run(t) { // --- then judge ----------------------------------------------------------- + // SPRINT7 gate 0 — the skip is gone, and the dispute is settled by physics. + // + // Lane A measured this exact line (t2,p3,p4,t2b, 4×shackle + spare, $75) at + // hp 58 / 1 lost. This suite says hp 36 / 2 lost. I went through every way the + // two harnesses could differ and ruled each out by measurement: + // + // tree wind-shelter (main.js:369 calls setSheltersFromTrees, this didn't) + // ... on vs off: hp 36 either way + // tension (this hardcoded 0.9, the game defaults 1.0) + // ... 0.6 / 0.75 / 0.9 / 1.0: hp 36, 2 lost, all four + // drain wiring (garden.step's signature changed to take hail+rain separately) + // ... same arithmetic: 5.0/0.25 × 0.9. Identical. + // yard geometry (the bug that bit this file once already) + // ... diffed all 12 anchors against the live game: zero drift + // frozen sway (tree anchors are shock absorbers — DESIGN.md) + // ... live sway is WORSE, not better: t2 4.4 → 4.6 kN + // + // What's left is not a harness difference, it's arithmetic. On this quad t2 + // peaks at 4.4-4.9 kN and a shackle is rated 3.2. t2 cannot hold, under any + // variation above. Losing it spends the only spare; then p3 (3.7) or t2b (3.4) + // — both also over 3.2 — goes too. Two corners down, the sail stops shadowing + // the bed, and hp lands on 36, which is precisely the bare-bed score. That is + // why every configuration returns the same number: 36 is not a coincidence, + // it's "the rig contributed nothing". + // + // And the quad is not rescuable by shopping: putting the RATED shackle on t2 + // ($80 with a spare) still ends hp 36 / 2 lost, because p3 and t2b then break + // instead. This quad needs THREE corners above shackle grade and $80 buys two. + // + // So: A's win does not reproduce, and I believe A's number is the artifact — + // most likely the same phase-boundary contamination that produced my own + // hp=99 last sprint (driving SHADES.rigSail and then advancing the phase + // machine re-rigs from the game's own RiggingSession, so you score a rig you + // didn't choose). A: the check is one line — assert rig.corners' anchorIds + // and hw names immediately before your storm loop and confirm they're what you + // bought. If they are, I'm wrong and the cause is still open. t.test('balance: storm_02 HAS a winnable line through the real $80 shop', () => { if (!line) throw new Error('the $80 shop cannot buy the candidate line at all'); // SPRINT7 gate 0, settled 2026-07-18: the integrator's skip is deleted @@ -210,31 +269,24 @@ export default async function run(t) { // number. With a camera it reads what Lane A measured. Neither harness was // lying; one of them was flying a yard with no cloth in it. if (!WIN(line.hp, line.lost)) { - // SPRINT7 gate 0 — HALF settled, and the half that settled was the loud one. - // - // RESOLVED: the garden. This suite read hp 36 because it built skyfx with - // no camera, and skyfx.step() opens `if (!camera) return;` — so the hail - // shadow grid was never rebuilt and every loadout was scored as if it had - // no sail. 36 IS the bare-bed number. With a camera (above) it reads ~68. - // Measured, camera the only variable: hailShadowOver(bed) 0.000 → 1.000. - // Nobody was lying; this harness was flying a yard with no cloth in it. - // - // UNRESOLVED: the corners. This suite says 2 lost; Lane A's end-to-end run - // says 1 at tension 1.0 (at the shop's default 0.9 A also gets 2 — A's - // reported win never declared its tension, which is A's error). Ruled out - // so far, each measured, each ~0.02 kN or less: wind shelters, frozen vs - // live tree sway, the scripted repair. Still unchecked: session.commit() - // vs main.js's rigSail() path, and main.js passing `debris` as rig.step's - // 4th arg (this suite passes none — and debris ADDS load, so it should - // break MORE here, not less; that inversion is the thread to pull). - // - // Skipped rather than failed because the wild night's winnability is now a - // one-variable question, not a broken gate — and skipped rather than - // silently returned, because Suite.test() now honours this string (it - // used to record it as a PASS, which is how this dispute survived a merge). - return `SKIPPED — gate 0 half-open: hp ${line.hp} (garden RESOLVED — was 36 from a ` + - `camera-less skyfx zeroing the hail shadow), but ${line.lost}/4 lost vs Lane A's 1 at ` + - `tension 1.0. Next suspects: commit() vs rigSail(), and debris in rig.step. See THREADS.`; +// SPRINT7 gate 0 — CLOSED at the merge, three causes named, none a villain: + // 1. GARDEN (A): skyfx built with no camera → `if (!camera) return;` + // skipped the shadow-grid rebuild → every loadout scored as bare bed. + // hp 36 WAS the no-sail constant. Fixed above (camera in fly()). + // 2. CORNERS (D): the settle. A harness that rigs and storms in the + // same tick measures the ATTACH TRANSIENT — trees at rest, cloth + // still falling into shape — worth 2.7× on tree corners (t2 4.54 kN + // unsettled vs 1.71 settled). A plays through prep, so A's rig was + // settled; fly() wasn't. Fixed below (12 s settle, per D's spec). + // 3. TENSION (A, self-reported): the win was measured at 1.0 and + // published without declaring it; the shop default was 0.9. + // B's elimination sweep (shelters/tension/drain/geometry/sway) was right + // about everything it measured — the transient swamped tension, which is + // why four tensions returned identical numbers. See THREADS 2026-07-18. + throw new Error( + `no winnable line even settled: ${COVER_QUAD.join(',')} on 4×shackle+spare ` + + `($${line.spent}) ended hp=${line.hp}, lost=${line.lost}/4 (need hp>=50, lost<2). ` + + `If this is red after the settle fix, gate 0 is OPEN again — post the numbers.`); } return `$${line.spent} on ${COVER_QUAD.join(',')} -> hp ${line.hp}, ${line.lost}/4 lost`; });