Prove the porosity->gardenHailExposure leak end-to-end (answer C's seam)
C asked in THREADS whether to land a one-liner making gardenHailExposure read
sail.porosity. Answer: it is already wired, in merged main — I did it in SPRINT9
(e576f5c): skyfx.step refreshes sailPorosity from world.sail.porosity, and
gardenHailExposure folds hailBlockFor(size, sailPorosity) into what it returns.
No one-liner needed.
But "already wired" was untested end-to-end. The only test of the hail leak,
weather.selftest's 'fabric choice is real', REIMPLEMENTS the formula inline with
hailBlockFor + hailAt — it proves the primitive and the arithmetic, not the
plumbing. A regression in the plumbing (the :738 refresh dropped, the
wind.def.hail.size lookup resolving wrong, hailBlockFor no longer folded in)
would leave that test green while the game quietly stopped leaking hail. That is
the measure-a-copy pattern this whole browser suite exists to avoid.
New assert 'porous cloth leaks pea hail into the garden, membrane blocks it'
drives the real sky.gardenHailExposure. The isolation is exact: ONE settled,
intact rig, sampled at ONE hail instant, porosity flipped 0.30<->0 between reads.
sky.step(0, t, {sail}) refreshes sailPorosity and rebuilds the shadow grid at the
same instant without advancing physics, so shadow geometry is identical across
the two reads and `block` is the only thing that moved. Measured:
pea (size 0.7): cloth 0.346 vs membrane 0.292 -> porous leaks +18%
ice (size 1.4): cloth 0.458 vs membrane 0.458 -> identical, the no-op
No storm flight, no second rig — deliberately. Two rigs whose corners diverge
share no shadow, and that difference is a cascade not a fabric: membrane cascades
on the ice night, so a naive two-flight version reads a FALSE ice-night
difference. The same-rig swap removes it.
And it can fail: if porosity stops reaching the exposure, both reads collapse to
the membrane value (measured: 0.2917 == 0.2917), cloth > membrane*1.05 is false,
red. The ice check is exact equality — if porous ever starts leaking big ice,
that goes red too.
Selftest 288 passed, 0 failed, 0 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b98dcad13a
commit
2996b092ae
@ -334,6 +334,63 @@ async function fly(yard, session, stormName, { repair = false, broom = false, se
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the REAL sky.gardenHailExposure over the bed at one hail instant, with the
|
||||
* sail's porosity swapped between shade cloth (0.30) and membrane (0) and NOTHING
|
||||
* else touched. Returns { cloth, membrane } exposure numbers.
|
||||
*
|
||||
* This exists to close a gap C's fabric-hail seam left open. porosity now feeds
|
||||
* the garden hail score in production — rig.porosity -> skyfx.step reads it into
|
||||
* sailPorosity (skyfx.js) -> gardenHailExposure folds hailBlockFor(size, porosity)
|
||||
* into what it returns (wired SPRINT9, e576f5c). But the only test of that leak,
|
||||
* weather.selftest's 'fabric choice is real', REIMPLEMENTS the formula inline with
|
||||
* hailBlockFor + hailAt — it proves the primitive and the arithmetic, not the
|
||||
* plumbing. A regression in the plumbing (the size lookup resolving wrong, the
|
||||
* :738 refresh dropped, sailPorosity read stale) would leave that test green while
|
||||
* the game stopped leaking hail. This drives the actual return, which is the whole
|
||||
* reason balance.test is a browser suite: measure the real chain, never a copy.
|
||||
*
|
||||
* The swap is exact and unconfounded: ONE settled, intact rig, sampled at ONE t,
|
||||
* porosity flipped between reads. `sky.step(0, t, {sail})` refreshes sailPorosity
|
||||
* and rebuilds the shadow grid at the same instant without advancing physics, so
|
||||
* the shadow geometry is identical across the two reads and `block` is the only
|
||||
* thing that moved. No storm flight and no second rig — which is what kept the
|
||||
* SPRINT9 fabric measurements honest: two rigs whose corners diverge share no
|
||||
* shadow, and that difference is a cascade, not a fabric (membrane cascades on the
|
||||
* ice night, so the naive two-flight version reads a false ice-night difference).
|
||||
*
|
||||
* `burstT` must land inside the storm's hail burst, or hailIntensity(t) is 0 and
|
||||
* both reads are 0. storm_03_southerly bursts at t=36 (pea, size 0.7);
|
||||
* storm_02b_icenight at t=50 (ice, size 1.4).
|
||||
*/
|
||||
async function gardenHailByPorosity(yard, stormName, burstT) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
wind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree'));
|
||||
const calmWind = createWind(await loadStorm(CALM_STORM));
|
||||
calmWind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree'));
|
||||
|
||||
const s = shop(yard, COVER_QUAD, [CARABINER, RATED, SHACKLE, RATED], 0);
|
||||
if (!s) return null;
|
||||
s.setFabric('cloth');
|
||||
const rig = s.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
|
||||
// Camera is load-bearing, not decoration — without it skyfx returns before the
|
||||
// shadow grid is rebuilt and every read is the bare-bed value. Same reason as fly().
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
const sky = createSkyFx({ wind, night: true, camera });
|
||||
// settle on the calm day so the cloth holds a real drape and stays intact — the
|
||||
// shadow just has to be stable and non-zero, not storm-deformed, to test the plumbing.
|
||||
for (let i = 0, n = Math.round(12 / FIXED_DT); i < n; i++) rig.step(FIXED_DT, calmWind, (i * FIXED_DT) % 3);
|
||||
|
||||
rig.porosity = 0.30; sky.step(0, burstT, { sail: rig });
|
||||
const cloth = sky.gardenHailExposure(yard.bed, burstT);
|
||||
rig.porosity = 0; sky.step(0, burstT, { sail: rig });
|
||||
const membrane = sky.gardenHailExposure(yard.bed, burstT);
|
||||
sky.dispose?.();
|
||||
return { cloth, membrane, lost: rig.corners.filter((c) => c.broken).length };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../testkit.js').Suite} t
|
||||
*
|
||||
@ -372,6 +429,13 @@ export default async function run(t) {
|
||||
if (membraneShop) membraneShop.setFabric('membrane');
|
||||
const membrane = membraneShop ? await fly(yard, membraneShop, 'storm_02_wildnight', { broom: true }) : null;
|
||||
|
||||
// The fabric's OTHER half, and the one C wired the primitive for: porous cloth
|
||||
// leaks pea hail into the garden score, membrane blocks it. Read the real
|
||||
// gardenHailExposure with only porosity swapped — pea night (leaks) and ice
|
||||
// night (must be a no-op, big ice is stopped by both). See the helper.
|
||||
const peaHail = await gardenHailByPorosity(yard, 'storm_03_southerly', 38);
|
||||
const iceHail = await gardenHailByPorosity(yard, 'storm_02b_icenight', 57);
|
||||
|
||||
const cheapShop = shop(yard, COVER_QUAD, [CARABINER, CARABINER, CARABINER, CARABINER], 0);
|
||||
const cheap = cheapShop ? await fly(yard, cheapShop, 'storm_02_wildnight') : null;
|
||||
|
||||
@ -489,6 +553,41 @@ export default async function run(t) {
|
||||
`(hp ${membrane.hp}) — the fabric is the decision`;
|
||||
});
|
||||
|
||||
// The fabric's SECOND edge, through the REAL garden-hail chain, not a copy of it.
|
||||
// `fabric decides p1` above proves porous saves the CORNER (less wind load); this
|
||||
// proves porous costs the GARDEN (leaks pea hail) — the trade DESIGN.md promises
|
||||
// and C's hailBlockFor supplies. It drives sky.gardenHailExposure directly, so a
|
||||
// regression in the porosity->skyfx->exposure plumbing goes red HERE even though
|
||||
// weather.selftest's inline-formula version would stay green. (Answering C's
|
||||
// THREADS seam question: the wiring is already in, SPRINT9 e576f5c — this guards it.)
|
||||
t.test('balance: porous cloth leaks pea hail into the garden, membrane blocks it', () => {
|
||||
if (!peaHail || !iceHail) throw new Error('could not build the fabric-hail probe on $80');
|
||||
if (!(peaHail.cloth > 0 && peaHail.membrane > 0)) {
|
||||
throw new Error(`the pea-hail probe read no hail at all (cloth ${peaHail.cloth}, membrane ` +
|
||||
`${peaHail.membrane}) — t=38 missed storm_03's burst, or the shadow grid never populated ` +
|
||||
`(camera dropped?). With nothing to block, this proves nothing.`);
|
||||
}
|
||||
// pea (size 0.7): porous leaks ~16% more through the real chain. Membrane blocks all.
|
||||
if (!(peaHail.cloth > peaHail.membrane * 1.05)) {
|
||||
throw new Error(`porous cloth stopped leaking pea hail through the REAL gardenHailExposure: ` +
|
||||
`cloth ${peaHail.cloth.toFixed(3)} vs membrane ${peaHail.membrane.toFixed(3)} (want cloth > ` +
|
||||
`membrane). The primitive test may still be green — this one drives sky.gardenHailExposure, ` +
|
||||
`so the break is in the plumbing: skyfx not reading world.sail.porosity (:738), the size ` +
|
||||
`lookup wind.def.hail.size resolving wrong, or hailBlockFor no longer folded into the return.`);
|
||||
}
|
||||
// ice (size 1.4): both stop it dead. hailBlockFor(1.4, 0.30) === hailBlockFor(1.4, 0) === 1.
|
||||
// This is the no-op C specified — if it ever diverges, porous has started leaking ICE, which
|
||||
// is wrong and a gift to nobody. Same intact rig both reads, so any gap is real, not a cascade.
|
||||
if (Math.abs(iceHail.cloth - iceHail.membrane) > 1e-6) {
|
||||
throw new Error(`fabric changed the ICE-night garden score (cloth ${iceHail.cloth.toFixed(4)} vs ` +
|
||||
`membrane ${iceHail.membrane.toFixed(4)}) — porous is meant to leak only the finest hail, and ` +
|
||||
`size 1.4 should read block=1 for both. hailBlockFor's aperture/smoothstep has drifted.`);
|
||||
}
|
||||
const leak = ((peaHail.cloth / peaHail.membrane) - 1) * 100;
|
||||
return `real gardenHailExposure: porous leaks +${leak.toFixed(0)}% pea hail vs membrane; ` +
|
||||
`ice night identical (both block big stones) — the fabric's garden cost is wired, not a copy`;
|
||||
});
|
||||
|
||||
// The sacrifice play, kept measured. DESIGN.md: "a sail that dies saving the
|
||||
// garden". Now that a clean win EXISTS, this stops being the wild night's only
|
||||
// outcome and becomes what it should always have been — a different, worse bet
|
||||
|
||||
Loading…
Reference in New Issue
Block a user