HardYards/tools/garden_bench/probe3.html
type-two a495fcdba6 Lane C S13 gate 1: the shadow model is right — the LINE was wrong; garden_bench proves it
The gate-1 diagnosis the sprint chartered, inverted by measurement: the shadow
geometry is exact (probe2: raycast and rasteriser agree 1.0000/1.0000), the
canonical measuring lines just don't cover the bed against vertical hail, and
site_02's tr1+q1+q3+q4 at exactly $80 already separates 91.5 FULL vs 35.7 bare
(sep 55.8, 0/4 lost) with no model change. Full findings + separation-target
proposal in THREADS.

Re-verified after the authoring session died un-pushed: all headline numbers
reproduce; the bench drives RiggingSession.commit(rig) -> rig.attach(), not the
bypassed-attach seam D's landmine names; balance.test's canonical shade-cloth
peaks reproduce per-anchor to the last digit. One fix over the original staging:
probe3/probe4 labelled per-corner peaks in plan order, but rig.corners is ring
order — labels now come from corners[k].anchorId (values were always true; the
$80 line's two RATED corners read swapped). Selftest 335/0/0.

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

115 lines
6.0 KiB
HTML

<!doctype html>
<html>
<head><meta charset="utf-8"><title>probe3 — does the covering line hold?</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 { createWind, loadStorm } from '../../web/world/js/weather.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { FIXED_DT, HARDWARE } 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 DECIDING QUESTION.
// sweep.html found quads that cover 100% of the bed in BOTH yards, while the
// canonical line covers 25%. B's Sprint-9 sweep says every bed-covering quad has
// a corner over 6.5 kN — unholdable even on RATED, the best steel in the game —
// and that p1..p4 is the ONLY buyable line. If that's true, then "coverage" and
// "holdability" are anti-correlated by construction, the affordable rig can only
// ever shade a quarter of the bed, and NO shadow-model change can make the
// garden respond to rigging. That is a very different bug from the one the
// sprint chartered, so it has to be measured, not inherited.
//
// Fly the covering lines. Report peak corner load per corner and the garden.
const GARDEN_DRAIN = 0.9, W_HAIL = 5.0, W_RAIN = 0.25;
async function fly(anchors, bed, ids, stormName, hw) {
const def = await loadStorm(stormName);
const wind = createWind(def);
const trees = anchors.filter((a) => a.type === 'tree');
wind.setSheltersFromTrees(trees);
const calmDef = await loadStorm('storm_01_gentle');
const calmWind = createWind(calmDef); calmWind.setSheltersFromTrees(trees);
const s = new RiggingSession({ anchors, budget: 9999 });
for (const id of ids) if (!s.rig(id).ok) return null;
for (const id of ids) { const r = s.setHardware(id, hw);
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason} — flying hardware it did not buy`); }
s.setTension(1.0);
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
let prepT = 0;
for (let i=0;i<Math.round(12/FIXED_DT);i++){ rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT; }
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 = ids.map(() => 0);
let hp = 100, lost = 0, hsum = 0, hn = 0, brokeAt = null;
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 (brokeAt === null && rig.corners.some((c)=>c.broken)) brokeAt = t;
// the hail shadow WHILE it is hailing is the only one that scores
if (sky.hailAmount > 0) { hsum += sky.hailShadowOver(bed); hn++; }
const ex = W_HAIL*sky.gardenHailExposure(bed,t) + W_RAIN*sky.gardenExposure(bed,t);
if (ex > 0) hp = Math.max(0, hp - GARDEN_DRAIN*ex*FIXED_DT);
}
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 input order.
// (Found at re-verify: p1..p4 flies as p4,p2,p3,p1; an input-order label
// attributes each corner's load to the wrong anchor.)
return { hp: Math.round(hp*10)/10, lost, peak: peak.map((p)=>p/1000),
cornerIds: rig.corners.map((c)=>c.anchorId),
hailShadow: hn ? hsum/hn : 0, brokeAt };
}
try {
for (const [siteId, lineSet] of [
['backyard_01', { cover100: ['h1','t1','t2b','p2'], cover100b: ['h1','h3','p1','p2'], canonical: ['p1','p2','p3','p4'] }],
['site_02_corner_block', { posts: ['q1','q2','q3','q4'], cover100: ['tr1','q1','q3','q4'], canonical: ['q1','q2','q3','tr1'] }],
]) {
const scene = new THREE.Scene();
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
const world = createWorld(scene, { wind: calm, site: await loadSite(siteId) });
if (world.dress) { try { await world.dress(); } catch {} }
const anchors = world.anchors.map((a)=>{const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
return {id:a.id,type:a.type,pos,sway:()=>pos};});
const bed = world.gardenBed;
log(`\n\n######## ${siteId} (RATED = 6.5 kN is the best steel in the game, $30/corner)`);
for (const [label, ids] of Object.entries(lineSet)) {
for (const storm of ['storm_02_wildnight', 'storm_02b_icenight', 'storm_03b_earlybuster']) {
const r = await fly(anchors, bed, ids, storm, RATED);
if (!r) { log(` ${label} ${ids.join('+')}: REFUSED`); continue; }
const over = r.peak.map((p,i)=>`${r.cornerIds[i]} ${p.toFixed(2)}`).join(' ');
const bare = { storm_02_wildnight: 35.7, storm_02b_icenight: 0, storm_03b_earlybuster: 82.6 }[storm];
log(` ${label.padEnd(10)} ${ids.join('+').padEnd(18)} ${storm.padEnd(22)} hp ${String(r.hp).padStart(5)}`
+ ` (bare ${bare}, sep ${(r.hp-bare).toFixed(1).padStart(5)}) lost ${r.lost}/4`
+ `${r.brokeAt!==null?` @t=${r.brokeAt.toFixed(0)}`:' '} hailShadow ${r.hailShadow.toFixed(2)} peak kN: ${over}`);
}
log('');
}
}
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
document.title = 'probe3 done';
</script>
</body></html>