HardYards/tools/garden_bench/probe4.html
type-two 45bdc2d079 Lane C S13 correction: the bench flew site_02 funnel-OFF and tree-FROZEN; wild-night headline dies
D's live replay caught landmine 1: bench.js read def.wind.venturi off the
STORM def where a venturi never lives (it is SITE data, main.js:453), and the
probes never attempted it — every site_02 bench number was funnel-off, the
third harness to make B's Sprint-11 mistake. Verifying the fix exposed
landmine 2: the tools remapped anchors to a static sway:()=>pos, freezing the
gum tree whose sway is the dynamic load a tr1 rig eats (ablation on the $80
line's q4: 1.02 funnel-off/static, 1.03 funnel-on/static, 1.24 funnel-on/real
anchors). Backyard numbers matched D's UI to the decimal precisely because
p1..p4 are posts — both landmines are no-ops there.

Fix: windForSite(stormDef, siteDef, anchors) in weather.js is the one shared
wind builder (venturi + shelters, exactly main.js's wiring); all garden_bench
tools build wind through it, fly world.anchors themselves via a re-pointable
wind proxy, print the venturi in their headers, and drop the phantom 12 s
settle (commit->attach->storm is one keypress, and the settle skewed storm
sampling via SailRig's internal clock). c.test.js pins the helper with B's
strictly-raises assert against the real shipped funnel (mutation-checked).

Corrected numbers: $80 line wildnight 91.9 with q4 at 1.24-vs-1.2 rating - a
knife edge the real game falls off (D UI: q3+q4 break, 39.8 TATTERED; D's UI
is canonical there, and a corner over its rating is not a safe line either
way). Buster verdict stands (95.2 bench / 97.9 D live). Icenight 9 DEAD 2/4.
Bench still under-reads UI peaks ~5-15% on site_02 - residual in the pool,
stated in THREADS. Separation-target site_02 example withdrawn to A.

Selftest 341/0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:08:21 +10:00

154 lines
8.1 KiB
HTML

<!doctype html>
<html>
<head><meta charset="utf-8"><title>probe4 — the $80 line that saves the garden</title>
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
<body>
<pre id="out">running…</pre>
<script type="importmap">
{ "imports": {
"three": "../../web/world/vendor/three.module.js",
"three/addons/": "../../web/world/vendor/addons/"
} }
</script>
<script type="module">
import * as THREE from '../../web/world/vendor/three.module.js';
import { RiggingSession } from '../../web/world/js/rigging.js';
import { SailRig } from '../../web/world/js/sail.js';
import { createSkyFx } from '../../web/world/js/skyfx.js';
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { FIXED_DT, HARDWARE, START_BUDGET } from '../../web/world/js/contracts.js';
const [CARABINER, SHACKLE, RATED] = HARDWARE;
const el = document.getElementById('out');
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
el.textContent = '';
// THE PAYOFF — and the correction (2026-07-18).
// The first run of this probe read the $80 line tr1+q1+q3+q4 at 91.5 FULL on
// the wild night and called it the headline. TWO harness landmines, both mine:
// 1. No venturi — the funnel lives in the SITE def, and this probe never set
// it (bench.js read it off the STORM def, where it never lives). Third
// harness to make B's Sprint-11 mistake.
// 2. Static anchors — the remap `sway: () => pos` stripped tr1's TREE sway,
// the dynamic load a rig on a gum tree has to eat (DESIGN.md's whole
// warning about tree anchors). The backyard bench numbers matched D's UI
// replays to the decimal BECAUSE p1..p4 are all posts; any tr1 line was
// flown against a tree that never moved.
// With both fixed the probe agrees with D's live UI replays in direction and
// closely in number (buster peaks within ~15%); on the wild night the line's
// cheapest corner rides AT its rating (q4 carabiner ~1.2 kN) — a knife edge
// the real game falls off (D: q3+q4 break t≈45, 39.8 TATTERED). Where this
// probe and D's UI disagree on which side of that edge a corner lands, D's
// UI number is canonical. What survives: the line holds the BUSTER — site_02's
// actual shipped night — with room (D live: 97.9 FULL, 0/4).
async function fly(world, plan, stormName, siteDef, use) {
const def = await loadStorm(stormName);
const anchors = world.anchors; // REAL anchors — tr1 sways
const bed = world.gardenBed;
const wind = windForSite(def, siteDef, anchors);
use(wind); // point the world's sway closures at the flight wind
// THE REAL SHOP, at the REAL budget. Every refusal is loud.
const s = new RiggingSession({ anchors }); // budget = START_BUDGET
for (const [id] of plan) {
const r = s.rig(id);
if (!r.ok) return { err: `rig ${id}: ${r.reason}` };
}
for (const [id, hw] of plan) {
const r = s.setHardware(id, hw);
if (!r.ok) return { err: `setHardware ${id} ${hw.name}: ${r.reason}` };
}
s.setTension(1.0);
const spent = START_BUDGET - s.budget;
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
// NO calm settle: in the real game the rig does not exist until commit, and
// commit → attach → advance() → storm land in one keypress (B's clock-reset
// comment, sail.js attach). The attach transient rides the storm's own calm
// opening, exactly as it does for a player.
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
camera.position.set(0, 2, 8);
const sky = createSkyFx({ wind, night: true, camera });
const peak = plan.map(() => 0);
let hp = 100, hsum = 0, hn = 0;
const steps = Math.round(def.duration / FIXED_DT);
for (let i=0;i<steps;i++){
const t = i * FIXED_DT;
rig.step(FIXED_DT, wind, t);
sky.step(FIXED_DT, t, { sail: rig });
for (let k=0;k<rig.corners.length;k++) if ((rig.corners[k].load||0) > peak[k]) peak[k] = rig.corners[k].load;
if (sky.hailAmount > 0) { hsum += sky.hailShadowOver(bed); hn++; }
const ex = 5.0*sky.gardenHailExposure(bed,t) + 0.25*sky.gardenExposure(bed,t);
if (ex > 0) hp = Math.max(0, hp - 0.9*ex*FIXED_DT);
}
const lost = rig.corners.filter((c)=>c.broken).length;
sky.dispose?.();
// Peaks are indexed by rig.corners — the RING order commit() reordered the
// picks into — so label them with corners[k].anchorId, NOT the plan order.
// (Found at re-verify: the $80 line flies as q1,tr1,q3,q4 — a plan-order
// label swaps the two RATED corners' loads. q3/q4 were unaffected.)
return { hp: Math.round(hp*10)/10, lost, spent, peak: peak.map((p)=>p/1000),
cornerIds: rig.corners.map((c)=>c.anchorId), hailShadow: hn?hsum/hn:0 };
}
// main.js's own win rule
const WIN = (hp, lost) => hp >= 50 && lost < 2;
const state = (hp) => hp > 66 ? 'FULL' : hp > 33 ? 'tattered' : 'DEAD';
try {
const scene = new THREE.Scene();
// A re-pointable wind, the way main.js's router serves world.js: the world's
// tree-sway closures capture THIS object, and fly() re-points it per storm.
// Building the world on a dead stub and remapping anchors to static
// `sway: () => pos` was landmine #2 — it froze the gum tree.
let current = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
rainMmPerHour:()=>0, gustTelegraph:()=>null, eventsBetween:()=>[] };
const proxy = {
sample: (p,t,o) => current.sample(p, t, o || new THREE.Vector3()),
speedAt: (p,t) => current.speedAt(p, t),
rainAt: (t) => current.rainAt(t),
rainMmPerHour: (t) => (current.rainMmPerHour ? current.rainMmPerHour(t) : 0),
gustTelegraph: (t) => current.gustTelegraph(t),
eventsBetween: (a,b) => current.eventsBetween(a, b),
setSheltersFromTrees() {},
get seed() { return current.seed ?? 1; },
get def() { return current.def ?? {}; },
};
const use = (w) => { current = w; };
const siteDef = await loadSite('site_02_corner_block');
const world = createWorld(scene, { wind: proxy, site: siteDef });
await world.dress(); // NOT optional: undressed, the carport trap does not exist
const bed = world.gardenBed;
const PLANS = {
'THE $80 LINE tr1=rated q1=rated q3=shackle q4=carabiner':
[['tr1',RATED],['q1',RATED],['q3',SHACKLE],['q4',CARABINER]],
'all-shackle tr1+q1+q3+q4 ($60)':
[['tr1',SHACKLE],['q1',SHACKLE],['q3',SHACKLE],['q4',SHACKLE]],
'canonical q1+q2+q3+tr1, best steel $80 can buy':
[['q1',RATED],['q2',SHACKLE],['q3',CARABINER],['tr1',RATED]],
};
const vl = siteDef.wind?.venturi ?? [];
const vtxt = vl.length
? vl.map((v)=>`throat (${v.x},${v.z}) gain ${v.gain} r${v.radius}`).join(' · ') : 'none';
log(`site_02_corner_block budget $${START_BUDGET} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z}) venturi: ${vtxt}`);
// bare-bed refs are funnel-independent (no sail = exposure is the storm's own
// rain/hail timeline, wind never enters it); D confirmed 82.6 live on the buster
log(`bare bed for reference: wildnight 35.7 (DEAD) icenight 0.0 (DEAD) buster 82.6 (FULL)\n`);
for (const [label, plan] of Object.entries(PLANS)) {
log(`\n## ${label}`);
for (const storm of ['storm_02_wildnight','storm_02b_icenight','storm_03b_earlybuster']) {
const r = await fly(world, plan, storm, siteDef, use);
if (r.err) { log(` ${storm.padEnd(22)} SHOP REFUSED: ${r.err}`); continue; }
const bare = { storm_02_wildnight:35.7, storm_02b_icenight:0, storm_03b_earlybuster:82.6 }[storm];
log(` ${storm.padEnd(22)} spent $${String(r.spent).padStart(3)} hp ${String(r.hp).padStart(5)} ${state(r.hp).padEnd(8)}`
+ ` (bare ${String(bare).padStart(4)} ${state(bare)}) sep ${(r.hp-bare).toFixed(1).padStart(5)}`
+ ` lost ${r.lost}/4 hailShadow ${r.hailShadow.toFixed(2)} ${WIN(r.hp,r.lost)?'WIN ':'LOSS'}`
+ ` peak kN: ${r.peak.map((p,i)=>`${r.cornerIds[i]} ${p.toFixed(2)}`).join(' ')}`);
}
}
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
document.title = 'probe4 done';
</script>
</body></html>