Merge remote-tracking branch 'origin/lane/d'
# Conflicts: # THREADS.md # web/world/js/tests/balance.test.js
This commit is contained in:
commit
d3bb7c2a93
60
THREADS.md
60
THREADS.md
@ -2568,3 +2568,63 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
||||
`hailBlockFor` is on their branch and my Sprint-6 question still stands — the porous/membrane choice
|
||||
is nearly free while rain's drain weight is 0.25 against hail's 5.0, so it wants either a rain-weight
|
||||
decision or a frank "membrane is for the ice night only" framing. Carrying it; it blocks nothing.
|
||||
[D] 2026-07-18 — 🔚 **GATE 0' CLOSED — the harnesses AGREE, the suite was right, and my settle claim is
|
||||
RETRACTED.** Taking my own medicine first: **Lane D's Sprint-7 finding that "12 s of settle flips
|
||||
2 lost → 1" does not reproduce and is withdrawn.** Measured on current main, live game, fresh page,
|
||||
A's exact line (t2+p3+p4+t2b, 4×shackle+spare, tn 1.0), one variable:
|
||||
```
|
||||
0 s settle → 2 lost [t2,t2b], t2 peak 4.54 kN
|
||||
12 s settle → 2 lost [t2,t2b], t2 peak 4.55 kN
|
||||
```
|
||||
The storm's own peak (4.54 kN at t=75.9) dwarfs the attach transient, so the transient never
|
||||
decided the corner count. The settle is a real FIDELITY fix — it's worth 2.7× in the moment it
|
||||
exists, and entry load drops 1.94 → 0.40 kN — but it is not the cause, and I published it as one.
|
||||
**The live game and the suite now agree, measured both ways:**
|
||||
```
|
||||
live game → hp 58, 2 lost [t2,t2b], t2 4.55 kN (aftermath card: garden 58%, corners 2/4,
|
||||
hardware $30, gnome $25, budget $25)
|
||||
suite → hp 58, 2 lost [t2,t2b], t2 4.54 kN
|
||||
```
|
||||
**B — your arithmetic was right from your first post, and I'm sorry it took three lanes and two
|
||||
sprints to confirm it.** "t2 peaks 4.4–4.9 kN, a shackle is rated 3.2, t2 cannot hold under any
|
||||
variation" — that is exactly what the live game does. I also watched the spare fail to save it:
|
||||
**t2b blew at 9 s, I repaired it, and it blew again.** Your "the quad needs three corners above
|
||||
shackle grade; $80 buys two" is the whole story.
|
||||
Everything gate 0' listed, tested one at a time, none of them moves the corner count:
|
||||
```
|
||||
live calm settle (running clock + calmWind) entry 1.94→0.40 kN still 2 lost
|
||||
live tree sway t2 4.33→4.55 kN still 2 lost
|
||||
walked repair timing (~4 s) still 2 lost
|
||||
all three at once hp 58 still 2 lost
|
||||
```
|
||||
And the structural suspects are all eliminated by measurement: anchors **bit-identical** between
|
||||
harnesses (all four, Δ 0.0000 m — B's diff confirmed), ring order identical (`p4,t2,t2b,p3`),
|
||||
`wind.sample()` at t2 identical to 2 dp (36.49 m/s @ t=75.9) across suite / suite+shelters / the
|
||||
live router. **A — hp 58 reproduces exactly; "1 lost" reproduces in neither harness.** No villain:
|
||||
the same undeclared-variable trap that caught your tension caught my settle.
|
||||
|
||||
[D] 2026-07-18 — 🎚️ **SPRINT8's escape hatch is now LIVE, and I think it's the right answer.** The
|
||||
numbers are no longer in dispute, so this is a design call, not an engineering one:
|
||||
**hp 58 CLEARS the 50 bar while 2 corners are lost.** The player saves the garden and loses the
|
||||
sail — and `WIN = hp>=50 && lost<2` calls that a loss. That is literally DESIGN.md's story
|
||||
("a 30 m² kite hunting the rest of your work"; the sail dying *for* the bed). Two ways out:
|
||||
· **PYRRHIC WIN (my recommendation):** the win reads the garden, and the corners read as cost.
|
||||
The aftermath card already says everything needed to make it land — *garden 58% · corners 2/4
|
||||
· hardware $30 · gnome $25 · budget $25.* A night where you save the bed and hand back $55 of
|
||||
wreckage is a GOOD story and a real decision to regret, not a failure state. It also makes the
|
||||
campaign's money mean something the very first time it's used.
|
||||
· **BUY THE THIRD CORNER:** B's route — the quad needs three above-shackle corners and $80 buys
|
||||
two. That's a shop/price change, and it makes the wild night a pure execution test instead.
|
||||
I'd take the pyrrhic win: it costs one boolean, it's already true in the fiction, and it turns the
|
||||
repo's longest argument into the game's best night. **Not my call — flagging with the numbers.**
|
||||
The assert stays RED and now names itself as the design question rather than a bug.
|
||||
|
||||
[D] 2026-07-18 — ✅ **Landed: the settled-at-entry guard (§Lane D), green.** `fly()` now settles the way
|
||||
main.js actually drives prep — **calmWind on a running clock**, because `wind.use(to === 'storm' ?
|
||||
winds[stormKey] : calmWind)` and `windTime()` returns `simT % calmWind.duration` outside a storm —
|
||||
rather than the storm's own wind frozen at t=0. Entry load 1.94 → **0.40 kN**. New assert
|
||||
**"harness: the yard is SETTLED when the storm starts"** holds every flown run under 1.0 kN at
|
||||
entry and prints them. It changes no verdict; it exists so the next harness can't quietly measure
|
||||
the transient, which is how three of them disagreed for two sprints. Selftest **266 pass / 1 red**
|
||||
(the red is the pyrrhic-win call above). B: this is in your file — shout if you'd rather own the
|
||||
guard's shape, it's four lines and I have no attachment to them.
|
||||
|
||||
@ -33,6 +33,8 @@ const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
|
||||
/** main.js's rule, duplicated ONLY here so a balance failure names the rule it broke. */
|
||||
const WIN = (hp, lost) => hp >= 50 && lost < 2;
|
||||
/** main.js's CALM_STORM — what wind.use() hands the rig outside a storm, and so what settles it. */
|
||||
const CALM_STORM = 'storm_01_gentle';
|
||||
const GARDEN_DRAIN = 0.9; // main.js
|
||||
const W_HAIL = 5.0; // main.js, decision 13 — integration weights
|
||||
const W_RAIN = 0.25;
|
||||
@ -112,6 +114,11 @@ function shop(yard, ids, hw, spares = 0, tension = 1.0) {
|
||||
async function fly(yard, session, stormName, { repair = false, broom = false } = {}) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
// The settle below runs on the CALM day, because that is what main.js hands the rig outside a
|
||||
// storm: `wind.use(to === 'storm' ? winds[stormKey] : calmWind)`.
|
||||
const calmDef = await loadStorm(CALM_STORM);
|
||||
const calmWind = createWind(calmDef);
|
||||
calmWind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree'));
|
||||
// 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
|
||||
@ -149,15 +156,34 @@ 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);
|
||||
// THE SETTLE. A player plays through prep, so ~12 s pass before ENTER and the cloth reaches
|
||||
// steady state. A harness that rigs and storms in the same tick lands the storm on the ATTACH
|
||||
// TRANSIENT instead.
|
||||
//
|
||||
// Now driven the way main.js drives prep, which is the last of the three suite-vs-game
|
||||
// differences gate 0' was chartered to close: `wind.use(calmWind)` on the phase change, and
|
||||
// `windTime()` returning `simT % calmWind.duration` — i.e. CALM WIND ON A RUNNING CLOCK, not the
|
||||
// storm's wind frozen at t=0. Measured, Lane D 2026-07-18, this quad:
|
||||
// frozen t=0, storm wind → 1.94 kN sitting on the corners at storm entry
|
||||
// calm wind, clock running → 0.40 kN ← what a player actually walks in with
|
||||
//
|
||||
// ⚠️ AND THE HONEST PART: this does NOT change the corner count, and my Sprint-7 claim that it
|
||||
// did is RETRACTED. Measured on current main, live game, fresh page, one variable:
|
||||
// 0 s settle → 2 lost [t2,t2b], t2 peak 4.54 kN
|
||||
// 12 s settle → 2 lost [t2,t2b], t2 peak 4.55 kN
|
||||
// The storm's own peak (4.54 kN at t=75.9) dwarfs the transient, so the transient never decided
|
||||
// anything. The settle is a fidelity fix, not the corner-count cause. B's arithmetic was right:
|
||||
// t2 pulls ~4.5 kN and a shackle is rated 3.2 — the line cannot hold, at any tension, settled or
|
||||
// not. See THREADS.
|
||||
{
|
||||
const settleWind = calmWind || wind;
|
||||
const period = Math.max(1, (calmDef && calmDef.duration) || 1);
|
||||
for (let i = 0, n = Math.round(12 / FIXED_DT); i < n; i++) {
|
||||
rig.step(FIXED_DT, settleWind, (i * FIXED_DT) % period);
|
||||
}
|
||||
}
|
||||
/** Peak corner load the instant the storm starts — the settled-at-entry guard reads this. */
|
||||
const entryPeak = Math.max(...rig.corners.map((c) => c.load || 0));
|
||||
|
||||
// D's settled-at-entry guard (SPRINT8 gate 0').
|
||||
//
|
||||
@ -219,6 +245,8 @@ async function fly(yard, session, stormName, { repair = false, broom = false } =
|
||||
lost: rig.corners.filter((c) => c.broken).length,
|
||||
spent: START_BUDGET - session.budget,
|
||||
pond: Math.round(pond),
|
||||
/** kN on the worst corner at storm entry — the settled-at-entry guard's number. */
|
||||
entryPeak: entryPeak / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
@ -287,10 +315,50 @@ export default async function run(t) {
|
||||
// measurement, not the verdict.
|
||||
t.test("balance: storm_02's best line saves the garden (pyrrhic — see SPRINT8 gate 0')", () => {
|
||||
if (!line) throw new Error('the $80 shop cannot buy the candidate line at all');
|
||||
<<<<<<< HEAD
|
||||
// The garden half — the part the player is actually protecting — must hold.
|
||||
if (line.hp < 50) {
|
||||
throw new Error(`the wild night's best $${line.spent} line let the garden die: hp=${line.hp} ` +
|
||||
`(need >=50). This is a real balance regression, not the pyrrhic-win question.`);
|
||||
=======
|
||||
// SPRINT7 gate 0, settled 2026-07-18: the integrator's skip is deleted
|
||||
// because the dispute had a cause, not a winner. This suite built skyfx
|
||||
// WITHOUT a camera, and skyfx.step() opens `if (!camera) return;` — so the
|
||||
// hail shadow grid was never rebuilt and every loadout was scored as though
|
||||
// it had no sail. That is why this line read hp 36: 36 IS the bare-bed
|
||||
// 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)) {
|
||||
// SPRINT8 gate 0' — CLOSED, and this suite was RIGHT. The live game and this harness now
|
||||
// agree, measured by Lane D 2026-07-18 on current main (fresh page, A's exact line,
|
||||
// t2+p3+p4+t2b on 4×shackle+spare, tension 1.0):
|
||||
// live game → hp 58, 2 lost [t2,t2b], t2 peak 4.55 kN
|
||||
// this suite → hp 58, 2 lost [t2,t2b], t2 peak 4.54 kN
|
||||
// Every suite-vs-game difference gate 0' listed has now been tested one at a time and NONE
|
||||
// of them moves the corner count: live-calm settle (entry 1.94→0.40 kN, verdict unchanged),
|
||||
// live tree sway (t2 4.33→4.55, verdict unchanged), walked repair timing (unchanged).
|
||||
// Anchors are bit-identical between harnesses, ring order is identical (p4,t2,t2b,p3), and
|
||||
// wind.sample() at t2 is identical to 2 dp (36.49 m/s @ t=75.9).
|
||||
//
|
||||
// RETRACTED, on the record: Lane D's "the settle flips 2 lost → 1" (SPRINT7). It does not
|
||||
// reproduce — 0 s and 12 s settle both end 2 lost, because the storm's own peak (4.54 kN at
|
||||
// t=75.9) dwarfs the attach transient. The settle is a real fidelity fix and NOT the
|
||||
// corner-count cause. A's hp 58 reproduces exactly; A's "1 lost" reproduces in neither
|
||||
// harness.
|
||||
//
|
||||
// What's left is B's arithmetic, which was correct from the first post: t2 pulls ~4.5 kN and
|
||||
// a shackle is rated 3.2 kN. The corner cannot hold at any tension, and the spare cannot save
|
||||
// it — the repaired corner simply blows again (measured: t2b blew @9 s, repaired, blew again).
|
||||
// So this is no longer an engineering question. It is SPRINT8's designed escape hatch:
|
||||
// hp 58 CLEARS the 50 bar while 2 corners are lost, i.e. the player saves the garden and
|
||||
// loses the sail — DESIGN.md's own story — and the win rule (`hp>=50 && lost<2`) calls it a
|
||||
// loss. Either the rule admits a PYRRHIC WIN, or the shop must be able to buy a third
|
||||
// above-shackle corner ($80 buys two). That is a design call, not a harness bug. See THREADS.
|
||||
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). CONFIRMED against the live game ` +
|
||||
`(hp 58, 2 lost) — the harnesses agree, so this is the pyrrhic-win design call, not a bug.`);
|
||||
>>>>>>> origin/lane/d
|
||||
}
|
||||
const pyrrhic = line.lost >= 2;
|
||||
return `$${line.spent} on ${COVER_QUAD.join(',')} -> garden hp ${line.hp}` +
|
||||
@ -299,6 +367,25 @@ export default async function run(t) {
|
||||
: `, ${line.lost}/4 lost — a clean win.`);
|
||||
});
|
||||
|
||||
/**
|
||||
* D's settled-at-entry guard (SPRINT8 §Lane D). The suite must enter the storm with the yard in
|
||||
* the state a player hands it: rigged, breathing on the calm day, corners loaded by nothing worse
|
||||
* than their own tension. It does not decide storm_02's verdict — measured, it doesn't — but a
|
||||
* harness silently measuring the attach transient is how three of them disagreed for two sprints,
|
||||
* and the transient IS worth 2.7× on tree corners in the moment it exists.
|
||||
*/
|
||||
t.test('harness: the yard is SETTLED when the storm starts', () => {
|
||||
const runs = [['line', line], ['cheap', cheap], ['gentle', gentle]].filter(([, r]) => r);
|
||||
for (const [name, r] of runs) {
|
||||
if (!(r.entryPeak < 1.0)) {
|
||||
throw new Error(
|
||||
`${name} entered the storm carrying ${r.entryPeak.toFixed(2)} kN — that is an attach ` +
|
||||
'transient, not a rig. Settle on the calm day with a running clock, as main.js does.');
|
||||
}
|
||||
}
|
||||
return `entry loads: ${runs.map(([n, r]) => `${n} ${r.entryPeak.toFixed(2)} kN`).join(' · ')}`;
|
||||
});
|
||||
|
||||
t.test('balance: storm_02 punishes a cheap rig on the same quad', () => {
|
||||
if (!cheap) throw new Error('the shop could not buy four carabiners — the economy is broken');
|
||||
// The other half of fair: the SAME quad on four carabiners ($20) must lose,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user