diff --git a/web/world/js/main.js b/web/world/js/main.js index b18c052..0a747bc 100644 --- a/web/world/js/main.js +++ b/web/world/js/main.js @@ -146,7 +146,17 @@ export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped }) { const named = worst ? `${worst.hw.name} at ${worst.anchorId.toUpperCase()}` : ''; const hailKilled = dmg.hail > dmg.rain; - if (lost.length >= 2) { + // SPRINT9 decision 1. This case had to come FIRST because it did not used to + // exist: `lost >= 2` was unconditionally a cascade-and-a-loss, and under the + // new rule it can be the best night the game has. A sail that tore itself + // apart while the bed came through is DESIGN.md's actual story — the firework + // you paid for — and the aftermath's hardware bill is where that payment goes. + if (win && lost.length >= 2) { + return { mode: 'pyrrhic', + verdict: `THE GARDEN MADE IT. The sail didn't — the ${named} went first and took ` + + `${lost.length - 1} more with it. That is what the sail was for.` }; + } + if (!win && lost.length >= 2) { return { mode: 'cascade', verdict: `THE SAIL LOST. The ${named} went first — and took ${lost.length - 1} more with it.` }; } @@ -533,7 +543,25 @@ export async function boot(opts = {}) { if (lost.length >= 2) collateral.push({ what: 'garden gnome', cost: world.gnome.collateralValue }); const s = rigging.summary; const hp = garden.hp; - const win = hp >= 50 && lost.length < 2; + /** + * SPRINT9 decision 1 — Lane A's ruling on the pyrrhic win. It was + * `hp >= 50 && lost.length < 2`; the corner clause is gone. + * + * **The garden is the client's. The sail is yours.** Saving the bed with a + * rig that tore itself apart is the job done expensively — not a failure. + * DESIGN.md doesn't call a cascade a loss, it calls it "a firework you paid + * for", and *paid for* is the whole point: the aftermath already bills you + * for the broken hardware, the collateral, and a week's fee you didn't earn + * cleanly. Gating the win on corners bills you twice for the same night and + * then lies about it — which is the same species as the verdict that used to + * tell a 4/4 hold they'd skimped. + * + * B and D both arrived here independently with the numbers, and B checked + * what it does NOT break: cheap rigs still lose (4× carabiner → hp 38), rigs + * that miss the bed still lose (hp 36), and $80 still cannot buy immunity. + * The corners are priced, not gated. + */ + const win = hp >= 50; const dmg = garden.damage; const { verdict, mode } = verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped }); diff --git a/web/world/js/tests/a.test.js b/web/world/js/tests/a.test.js index c829888..d25119b 100644 --- a/web/world/js/tests/a.test.js +++ b/web/world/js/tests/a.test.js @@ -194,6 +194,35 @@ export default async function run(t) { `verdict should credit the corners that held: "${heldAll.verdict}"`); }); + t.test('the pyrrhic win: a sail that dies saving the garden is a WIN', () => { + // SPRINT9 decision 1, Lane A's ruling. The garden is the client's; the sail + // is yours. This night used to score as a flat LOSS, which was the repo's + // longest-running dead end — C measured the only $80 line on the wild night + // and it lands here. + const carabiner = { anchorId: 'p1', hw: { name: 'carabiner', rating: 1200, cost: 5 } }; + const shackle = { anchorId: 'p3', hw: { name: 'shackle', rating: 3200, cost: 15 } }; + const v = verdictFor({ + hp: 52, lost: [shackle, carabiner], win: true, + dmg: { hail: 40, rain: 8 }, pondPeak: 0, pondDumped: 0, + }); + assertEq(v.mode, 'pyrrhic', 'garden saved + sail destroyed is its own ending, not a cascade'); + assert(/GARDEN MADE IT/.test(v.verdict), `it is a win and must read as one: "${v.verdict}"`); + assert(/carabiner at P1/.test(v.verdict), 'and still names the weakest link that went first'); + }); + + t.test('the same wreck WITHOUT the garden is still a plain cascade', () => { + // The other half of the ruling: corners are priced, not gated — but the + // garden is still the whole job. Lose it and two corners and you lost. + const carabiner = { anchorId: 'p1', hw: { name: 'carabiner', rating: 1200, cost: 5 } }; + const shackle = { anchorId: 'p3', hw: { name: 'shackle', rating: 3200, cost: 15 } }; + const v = verdictFor({ + hp: 20, lost: [shackle, carabiner], win: false, + dmg: { hail: 70, rain: 10 }, pondPeak: 0, pondDumped: 0, + }); + assertEq(v.mode, 'cascade'); + assert(/SAIL LOST/.test(v.verdict), `no garden, no win: "${v.verdict}"`); + }); + t.test('verdict names the weakest link that actually let go', () => { // "…the shackle. I knew about the shackle." The whole point is that the // player recognises the corner they gambled on.