diff --git a/web/world/js/tests/balance.test.js b/web/world/js/tests/balance.test.js index afc8b31..199e6f1 100644 --- a/web/world/js/tests/balance.test.js +++ b/web/world/js/tests/balance.test.js @@ -87,8 +87,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; @@ -104,6 +111,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 })); const sky = createSkyFx({ wind, night: true }); @@ -171,21 +184,51 @@ 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'); if (!WIN(line.hp, line.lost)) { - // Integrator skip at the Sprint-6 merge — HARNESS DISPUTE, not a verdict. - // Lane A measured this exact quad + loadout (t2,p3,p4,t2b, 4×shackle + - // spare, $75) at hp 58 / 1 lost — a WIN — through their own end-to-end - // run (commit 2af4662). This harness gets hp 36 / 2 lost on the same - // line. One of the two is measuring something different (tension? - // repair timing? drain wiring?) and that is the THIRD two-harness - // discrepancy this repo has had (COVER_QUAD staleness, B's own hp=99 - // phase-boundary artifact). SPRINT7 gate 0: A and B converge on THIS - // suite as the single source of truth, reproduce A's win here or refute - // it, then delete this skip. Do not tune anything until then. - return `SKIPPED — harness dispute: this suite says hp=${line.hp}/${line.lost} lost, ` + - `Lane A measured hp=58/1 on the same line (SPRINT7 gate 0)`; + throw new Error( + `no winnable line: ${COVER_QUAD.join(',')} on 4×shackle+spare ($${line.spent}) ended ` + + `hp=${line.hp}, lost=${line.lost}/4 (need hp>=50, lost<2). t2 peaks ~4.4 kN vs a 3.2 kN ` + + `shackle and cannot hold; rating up t2 only moves the break to p3/t2b. Ruled out as causes: ` + + `shelter, tension 0.6-1.0, drain wiring, yard drift, sway. See the comment above — ` + + `this needs a LOAD lever (SPRINT7 gate 0), not a tuning one.`); } return `$${line.spent} on ${COVER_QUAD.join(',')} -> hp ${line.hp}, ${line.lost}/4 lost`; });