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>
126 lines
6.3 KiB
HTML
126 lines
6.3 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 { createWind, loadStorm } 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.
|
|
// probe3 (on an unlimited budget) found tr1+q1+q3+q4 on site_02 holds every
|
|
// corner through the WILD night and the ICE night and reads hp 91.5 / 89.2
|
|
// against a bare bed's 35.7 / 0.0. Its peak loads were tr1 4.72, q1 3.74,
|
|
// q3 2.97, q4 1.02 kN — which price EXACTLY to
|
|
// RATED(6.5) + RATED(6.5) + SHACKLE(3.2) + CARABINER(1.2)
|
|
// $30 + $30 + $15 + $5 = $80
|
|
// ...the whole budget, to the dollar. If that holds through the REAL shop at the
|
|
// REAL START_BUDGET, then the garden already responds to rigging, gate 1 needs
|
|
// no model change, and what was actually broken is that nobody had found the
|
|
// line. So buy it the way a player must, and fly it.
|
|
async function fly(anchors, bed, plan, stormName) {
|
|
const def = await loadStorm(stormName);
|
|
const wind = createWind(def);
|
|
const trees = anchors.filter((a) => a.type === 'tree');
|
|
wind.setSheltersFromTrees(trees);
|
|
const calmWind = createWind(await loadStorm('storm_01_gentle'));
|
|
calmWind.setSheltersFromTrees(trees);
|
|
|
|
// 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 }));
|
|
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 = 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();
|
|
const zero = { 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: zero, site: await loadSite('site_02_corner_block') });
|
|
await world.dress(); // NOT optional: undressed, the carport trap does not exist
|
|
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;
|
|
|
|
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]],
|
|
};
|
|
log(`site_02_corner_block budget $${START_BUDGET} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z})`);
|
|
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(anchors, bed, plan, storm);
|
|
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>
|