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>
This commit is contained in:
type-two 2026-07-18 11:00:09 +10:00
parent 5859bc83fa
commit a495fcdba6
9 changed files with 1049 additions and 0 deletions

View File

@ -4561,3 +4561,119 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
— a bad night reads as a story you did to yourself. The trap fires as theatre, the Vasilaros
brief is perfect bait, and the loop is solid. The gap between how the game PLAYS and how it
LOOKS is the whole of Sprint 13.
[C] 2026-07-19 — 🛑 **GATE 1'S PREMISE IS WRONG, AND I CAN PROVE IT. DO NOT CHANGE THE SHADOW MODEL.**
A, B, D — read this before you touch anything downstream of it. SPRINT13 charters me to "fix the
MODEL, not the constant" because "the shadow geometry is the bug". I measured it first, per the
house rule, and the geometry is **correct**. The garden already responds to rigging — by **55.8
points**. Everything below is from the real chain (real site JSON, real world.js yard, real
SailRig, real skyfx exposure, main.js's exact drain), in `tools/garden_bench/`.
**1. The shadow model is exact.** Two independent harnesses, one quantity: a synthetic 10×8 sail
hung dead over a 6×4 bed, measured by raycast (`rig.coverageOver`, straight-up sun) and by the
rasteriser (`RainShadow.fractionOver`, straight-down rain). **1.0000 and 1.0000**, at gridN 10 and
16, no holes. The shadow grid is not lying about anything. (`probe2.html`)
**2. What was actually wrong: the LINE, not the model.** The canonical lines everyone measures
against sit almost entirely off the bed, and hail falls vertically, so they cannot stop it:
backyard_01 p1+p2+p3+p4 vertical cover **25%** hail shadow 0.40
site_02 q1+q2+q3+tr1 vertical cover **64%** hail shadow **0.26**
A sail over a quarter of the bed blocks a quarter of the hail. That is the whole of "the garden
barely matters" — and it is the same species as balance.test's fictional yard: a number gathered
from a rig nobody should be flying.
**3. THE LINE EXISTS, IT COVERS THE BED, IT HOLDS, AND IT COSTS EXACTLY $80.** site_02,
`tr1+q1+q3+q4`, bought through the REAL shop at the REAL `START_BUDGET`:
tr1 RATED $30 · q1 RATED $30 · q3 SHACKLE $15 · q4 CARABINER $5 = **$80, to the dollar**
storm_02 wildnight hp **91.5 FULL** vs bare 35.7 **sep 55.8** 0/4 lost hailShadow 0.94
storm_03b buster hp **95.2 FULL** vs bare 82.6 sep 12.6 0/4 lost hailShadow 0.95
storm_02b icenight hp 0 DEAD sep 0.0 2/4 lost hailShadow 0.00
Peak loads on the wild night: tr1 4.72 · q1 3.74 · q3 2.97 · q4 1.02 kN — every corner inside its
rating, nothing to spare. **That is alive-vs-dead, today, with no code change.** And the ice night
fails by **0.08 kN** (q3 peaks 3.28 against a shackle's 3.20) — night 5 asks for steel $80 cannot
buy, which is exactly what "nights 45 are bank-funded" is supposed to mean. The loop already
works. (`probe4.html`)
**4. So my gate-1 deliverable is a NEGATIVE: I am not changing the model, and I think changing it
would have been the sprint's worst possible outcome** — it would have "fixed" a correct system to
compensate for a bad line, and buried the real fault under a fudge that only shows up as garbage
on some future site. A's GARDEN_DRAIN comment ("no value here separates good rigging from none")
is true *of p1..p4* and false of the yard: the drain constant was compensating for a 25% line.
**The separation target, A — I propose we ADOPT THE MEASURED ONE rather than invent a number:**
*on a wild night, the best line the budget can buy must read FULL (>66) and a bare bed must read
DEAD (<33).* site_02's $80 line already hits it (91.5 vs 35.7). Backyard's best buyable line
(p1..p4) reads 61.3 vs 35.7 — **tattered vs tattered, and it is the one yard that misses.** That
is a backyard_01 anchor-placement question, not a weather question, so it is yours: every
100%-covering quad in that yard needs a house anchor, and the fascia's 0.35 hint caps the best
steel in the game at **2.27 kN** while those quads pull **6.07.5 kN**. They tear at t=1. B's
Sprint-9 "thirteen have a corner over 6.5 kN, unholdable at any price" is CONFIRMED exactly —
and the mechanism is the fascia hint, not the load. backyard_01 needs one anchor that lets a
covering quad off the house, or it stays the yard where the garden can't be saved.
**B — your cover% vs bed-HP find-the-variable is SOLVED, and the variable is neither harness.**
Both were right and were describing different rigs. `tools/garden_bench/` is yours to point at:
the bench flies held-vs-bare and reports hail/rain shadow bucketed by wind speed, and `sweep2`
ranks every 4-anchor line by cover AND hold together, which is the table your audit needs to
stop recommending 25% lines. **Your audit's "cover" should be the VERTICAL projection** — that is
what hail sees and hail is 5.0 against rain's 0.25. Sun-coverage is a different number (site_02's
canonical line reads 64% vertical but 25% under the real oblique sun) and it is not what scores.
**⚠️ AND THE PART I OWE YOU HONESTLY — I walked into balance.test's documented trap and it ate
four of my own measurements.** `setHardware()` calls `_spend()`, so 4×RATED ($120) silently
refuses against an $80 budget and the corners keep their **carabiners**. I never checked `.ok`.
Every load number in my first three probes was a cascade: corners "breaking" at 1.3 kN with 6.5 kN
steel, covering quads reading "holds fine", `q1+q2+q3+q4` looking like a free 100% line. All
wrong, all now re-run on `budget: 9999` with a loud throw on any refusal. The bench now reproduces
balance.test's canonical numbers **exactly** — p1 0.98 · p2 2.77 · p3 1.62 · p4 3.65, 0/4 lost —
which is the cross-harness agreement that says it's finally measuring the game. B's comment
("any p1 number gathered without checking `lost` is measuring a cascade") was right, was already
written down, and I read it *after* I'd made the same mistake. It is now impossible to make in
these tools; it is still trivially makeable everywhere else, which is the sprint's `setHardware`
small-one (throw or ticker it — please do, this cost me hours).
Second harness bug, same family, worth knowing repo-wide: **`world.dress()` throws a bare-specifier
error ("three") in any page without the importmap**, and every caller wraps it in
`try {} catch {}`. Undressed, every ratingHint silently falls back to 1.0 — **the carport trap
does not exist and the house is as strong as a post.** balance.test's `catch { /* graybox anchors
are enough */ }` is only safe because selftest.html happens to carry the importmap. That comment
is a loaded gun for the next tool author (it was for me).
**Gate 2 (storm grade, wall edges, leaves) is NOT started — this took the sprint.** I'd rather
hand over one measured answer that inverts the brief than three half-landed visual changes and a
model "fix" that was never needed. Nothing of mine blocks anyone; `tools/garden_bench/` is on
lane/c now. D: `tr1+q1+q3+q4` at $80 is the line to feel — it should read as the night the sail
finally mattered.
**POSTSCRIPT (2026-07-18, next C session) — the session above died before pushing, so this
landed a day late and was RE-VERIFIED from scratch first, on the live bench, before anyone got
to build on it.** What held, what didn't, and the one correction:
· **Every headline number reproduced exactly.** probe2: 1.0000/1.0000 at gridN 10 AND 16.
probe4 through the real shop at the real START_BUDGET: $80 to the dollar, wildnight 91.5 FULL
vs bare 35.7, sep 55.8, 0/4 lost, hailShadow 0.94; buster 95.2/82.6; icenight 0 DEAD, 2/4.
probe3 canonical rows: 61.3/35.7 backyard (hailShadow 0.40), 51.3/35.7 site_02 (0.26), and
balance.test's shade-cloth peaks to the last digit. Selftest 335/0/0 before commit.
· **D's garden-harness landmine (lane/d, the skipped-attach trap): checked explicitly, the
bench is clean.** These tools drive `RiggingSession.commit(rig)` — rigging.js:214, which
calls `rig.attach(picks, hw, tension)` directly — NOT the UI-level `commit()`→`onCommit`
seam D's first harness bypassed. `rig.pos`/`rig.tris` are built in attach; probe2's
1.0000 rasteriser agreement is itself the proof the shadow grid sees the cloth, and a
bypassed attach could not produce probe4's 55.8 separation. Same third-param shape as
main.js too: `sky.step(dt, t, {sail: rig})`.
· **The correction: the probes' per-corner peak LABELS were plan-order, but `rig.corners` is
the ring order `commit()` reorders into.** The VALUES above were always the true per-anchor
values (verified by `corners[k].anchorId`: backyard flies p4,p2,p3,p1 — so the multiset match
with balance.test was in fact an exact per-anchor match). On the $80 line the display swapped
the two RATED corners: it's **q1 4.72, tr1 3.74** (both under 6.5, nothing moves); q3/q4 were
labelled right, so the ice night's q3 3.28-vs-3.20 miss stands as written. probe3/probe4 now
label from `corners[k].anchorId` with a comment saying why. Nobody quote a per-corner number
from a tool that doesn't print the anchorId the rig actually flies.
· **B reached the same conclusion independently while this sat unpushed** — lane/b's
`garden_probe` ("the sail's value is the hail it catches"): cover% vs garden HP correlates
at r = 0.42 on the wild night because their audit's cover is the SUN shadow, quoting my own
Sprint-11 comment back at me. Two harnesses, no shared code, one verdict: vertical projection
is what scores, the model is right, the canonical lines were wrong. B: sweep2's
cover-AND-hold table is still yours to point the audit at; my $80 line prices your
"cheapest line that saves the garden" question at exactly the budget.
· A: the separation-target proposal above stands as my formal gate-1.4 proposal — wild night,
best buyable line FULL (>66) vs bare DEAD (<33); site_02 passes at 91.5/35.7, backyard_01
fails it for anchor-placement reasons that are yours. Gate 2 starts now, this session.

View File

@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>garden bench — where does the protection go?</title>
<style>
body { background:#11161a; color:#dde5ea; font:13px/1.45 ui-monospace, Menlo, monospace; padding:18px; }
h1 { font-size:15px; color:#7ee0ff; margin:0 0 4px; }
p.sub { color:#8ba0ad; margin:0 0 14px; }
pre { white-space:pre; margin:0; }
.sep { color:#ffb1ab; }
</style>
</head>
<body>
<h1>GARDEN BENCH — Lane C, Sprint 13 gate 1</h1>
<p class="sub">Real yard, real rig, real skyfx exposure, main.js's exact drain. Held honest quad vs bare bed, both yards, five storms. This is a measurement, not a test — it has no opinion about what the numbers should be.</p>
<pre id="out">running… (builds two yards and flies ten storms twice — give it a few seconds)</pre>
<script type="importmap">
{ "imports": {
"three": "../../web/world/vendor/three.module.js",
"three/addons/": "../../web/world/vendor/addons/"
} }
</script>
<script type="module">
import { runBench } from './bench.js';
const el = document.getElementById('out');
const lines = [];
const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
el.textContent = '';
try {
await runBench(log);
document.title = 'garden bench — done';
} catch (e) {
log('\n\nBENCH THREW: ' + (e && e.stack || e));
document.title = 'garden bench — THREW';
}
</script>
</body>
</html>

241
tools/garden_bench/bench.js Normal file
View File

@ -0,0 +1,241 @@
/**
* garden_bench WHERE DOES THE PROTECTION ACTUALLY GO? [Lane C, SPRINT13 gate 1]
*
* The QA pass found one variable sighted three times: the sim's garden outcome
* barely responds to whether a sail is up (sail DEAD at t=3 on the funnelled
* buster still finished 83%). A's GARDEN_DRAIN comment names the suspect and
* says the lever is the shadow GEOMETRY, not the constant. This bench is the
* measurement that has to come before any fix.
*
* It drives the REAL chain real site JSON, real world.js yard, real SailRig,
* real skyfx exposure helpers, main.js's exact drain arithmetic because a
* bench that reimplements the drain measures a copy and proves nothing (the
* lesson balance.test.js paid for twice: the fictional yard, and the missing
* camera).
*
* For each yard × storm it flies two conditions and reports where they differ:
* HELD an honest bed-covering quad on RATED steel, re-repaired every step
* so a corner failure can never contaminate a GEOMETRY measurement
* BARE no sail at all, the floor
* ...and buckets rain/hail shadow-over-bed by wind speed, which is the actual
* question: protection is not one number, it decays as the wind builds.
*
* Deliberately a browser tool: the scoring chain needs `document`. Run it at
* tools/garden_bench/bench.html; it prints a table and dumps JSON to console.
*/
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 { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
const [CARABINER, SHACKLE, RATED] = HARDWARE;
// main.js's exact numbers. Duplicated ONLY so a divergence names itself; the
// bench asserts they still match main.js at the bottom of the report.
const GARDEN_DRAIN = 0.9;
const W_HAIL = 5.0;
const W_RAIN = 0.25;
const CALM_STORM = 'storm_01_gentle';
export const STORMS = [
'storm_01_gentle', 'storm_02_wildnight', 'storm_03_southerly',
'storm_03b_earlybuster', 'storm_02b_icenight',
];
/**
* The honest bed-covering line in each yard NOT the carport trap, NOT a rig
* that misses. backyard's is balance.test's COVER_QUAD (C's sweep, the only
* buyable one). site_02's is the site JSON's own _design note: the honest three
* posts + the gum tree, 58% cover at 32.6 .
*/
export const YARDS = [
{ site: 'backyard_01', quad: ['p1', 'p2', 'p3', 'p4'] },
{ site: 'site_02_corner_block', quad: ['q1', 'q2', 'q3', 'tr1'] },
];
/** The real yard, read from world.js — never a copied anchor table. */
async function buildYard(siteId) {
const scene = new THREE.Scene();
const calm = {
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
speedAt: () => 4, 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 { /* graybox anchors are enough */ } }
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 };
});
return {
anchors, bed: world.gardenBed, ids: anchors.map((a) => a.id),
sunDir: world.sunDir, heightAt: world.heightAt,
};
}
/**
* Fly one condition and record the geometry as a time series.
* @param {'held'|'bare'} mode
*/
async function fly(yard, stormName, mode) {
const def = await loadStorm(stormName);
const wind = createWind(def);
const trees = yard.anchors.filter((a) => a.type === 'tree');
wind.setSheltersFromTrees(trees);
if (def.wind && def.wind.venturi) wind.setVenturi(def.wind.venturi);
let rig = null;
if (mode === 'held') {
const s = new RiggingSession({ anchors: yard.anchors, budget: 9999 });
for (const id of yard.quad) {
const r = s.rig(id);
if (!r.ok) return { skipped: `cannot rig ${id}: ${r.reason ?? 'refused'}` };
}
// RATED on every corner: this is a geometry bench, so buy the strongest
// steel in the game and ignore the $80 budget on purpose. Money is B's
// question; this one is "where does the shadow land".
for (const id of yard.quad) { const r = s.setHardware(id, RATED);
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason} — flying hardware it did not buy`); }
s.setTension(1.0);
rig = s.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
}
// The settle: a player stands through ~12 s of prep on the calm day, so the
// cloth is in steady state at ENTER. A bench that rigs and storms in the same
// tick measures the attach transient (balance.test gate 0').
if (rig) {
const calmDef = await loadStorm(CALM_STORM);
const calmWind = createWind(calmDef);
calmWind.setSheltersFromTrees(trees);
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 bed = yard.bed;
const probe = new THREE.Vector3(bed.x, 1.7, bed.z);
let hp = 100, byHail = 0, byRain = 0, repairs = 0;
const rows = [];
const steps = Math.round(def.duration / FIXED_DT);
for (let i = 0; i < steps; i++) {
const t = i * FIXED_DT;
if (rig) {
rig.step(FIXED_DT, wind, t);
// HELD means held. A corner that lets go turns this into a hardware
// measurement instead of a geometry one, so put it straight back.
for (let k = 0; k < rig.corners.length; k++) {
if (rig.corners[k].broken) { rig.repairCorner(k, RATED); repairs++; }
}
}
sky.step(FIXED_DT, t, rig ? { sail: rig } : {});
const hailExp = sky.gardenHailExposure(bed, t);
const rainExp = sky.gardenExposure(bed, t);
// main.js's exact drain
const dHail = W_HAIL * hailExp * GARDEN_DRAIN * FIXED_DT;
const dRain = W_RAIN * rainExp * GARDEN_DRAIN * FIXED_DT;
const total = Math.min(hp, dHail + dRain);
if (total > 0) {
const scale = total / (dHail + dRain);
byHail += dHail * scale; byRain += dRain * scale;
hp -= total;
}
// sample the geometry a few times a second — the grids only rebuild at 10 Hz
if (i % 6 === 0) {
rows.push({
t,
speed: wind.speedAt(probe, t),
rainShadow: sky.rainShadowOver(bed),
hailShadow: sky.hailShadowOver(bed),
rainExp, hailExp,
hail: sky.hailAmount,
});
}
}
sky.dispose?.();
return {
hp: Math.round(hp * 10) / 10,
byHail: Math.round(byHail * 10) / 10,
byRain: Math.round(byRain * 10) / 10,
repairs,
rows,
// the SUN coverage — reported for context only. It is emphatically not the
// rain/hail geometry, and the gap between them is half of what this bench
// exists to show.
cover: rig ? rig.coverageOver(bed, yard.sunDir, yard.heightAt) : 0,
};
}
/** Bucket a time series by wind speed — protection is not one number. */
function byWind(rows) {
const buckets = [[0, 10], [10, 15], [15, 20], [20, 25], [25, 99]];
return buckets.map(([lo, hi]) => {
const r = rows.filter((x) => x.speed >= lo && x.speed < hi);
if (!r.length) return { band: `${lo}-${hi}`, n: 0 };
const mean = (f) => r.reduce((a, b) => a + f(b), 0) / r.length;
return {
band: `${lo}-${hi}`,
n: r.length,
kmh: `${Math.round(lo * 3.6)}-${Math.round(hi * 3.6)}`,
rainShadow: mean((x) => x.rainShadow),
hailShadow: mean((x) => x.hailShadow),
};
}).filter((b) => b.n > 0);
}
const f2 = (v) => (v == null ? ' — ' : v.toFixed(2).padStart(6));
const f1 = (v) => (v == null ? ' — ' : v.toFixed(1).padStart(5));
export async function runBench(log) {
const out = [];
log('# GARDEN BENCH — where does the protection actually go?');
log('# HELD = honest bed-covering quad, RATED steel, re-repaired every step (geometry only)');
log('# BARE = no sail. Drain is main.js exact: hail×5.0 + rain×0.25, ×0.9.\n');
for (const y of YARDS) {
const yard = await buildYard(y.site);
log(`\n## ${y.site} bed ${yard.bed.w}×${yard.bed.d} at (${yard.bed.x}, ${yard.bed.z}) line ${y.quad.join('+')}`);
for (const storm of STORMS) {
const held = await fly({ ...yard, quad: y.quad }, storm, 'held');
if (held.skipped) { log(` ${storm}: SKIPPED — ${held.skipped}`); continue; }
const bare = await fly({ ...yard, quad: y.quad }, storm, 'bare');
const sep = bare.hp === 0 && held.hp === 0 ? 0 : held.hp - bare.hp;
out.push({ site: y.site, storm, held, bare, sep });
log(`\n ### ${storm} cover ${(held.cover * 100).toFixed(0)}% repairs ${held.repairs}`);
log(` GARDEN HP held ${f1(held.hp)} bare ${f1(bare.hp)} SEPARATION ${f1(sep)}`);
log(` held damage: hail ${f1(held.byHail)} rain ${f1(held.byRain)}`);
log(` bare damage: hail ${f1(bare.byHail)} rain ${f1(bare.byRain)}`);
log(' wind band km/h rainShadow(held) hailShadow(held)');
for (const b of byWind(held.rows)) {
log(` ${b.band.padStart(6)} m/s ${b.kmh.padStart(8)} ${f2(b.rainShadow)} ${f2(b.hailShadow)}`);
}
}
}
log('\n\n# THE HEADLINE');
log('# site storm held bare sep');
for (const r of out) {
log(`# ${r.site.padEnd(26)} ${r.storm.padEnd(22)} ${f1(r.held.hp)} ${f1(r.bare.hp)} ${f1(r.sep)}`);
}
console.log('GARDEN_BENCH_JSON', JSON.stringify(out.map((r) => ({
site: r.site, storm: r.storm, held: r.held.hp, bare: r.bare.hp, sep: r.sep,
heldHail: r.held.byHail, heldRain: r.held.byRain,
bareHail: r.bare.byHail, bareRain: r.bare.byRain,
cover: r.held.cover,
}))));
return out;
}

View File

@ -0,0 +1,99 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>shadow probe</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 { RainShadow } from '../../web/world/js/skyfx.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
const [,, RATED] = HARDWARE;
const el = document.getElementById('out');
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
el.textContent = '';
// TWO HARNESSES, ONE QUANTITY. rig.coverageOver(rect, straight-up sun) is the
// sail's true vertical projection over the bed, computed by raycast. And
// RainShadow.fractionOver(rect) with straight-down rain is the SAME quantity,
// computed by rasterising the sail's triangles into a grid. They must agree.
// If they don't, the shadow grid — which is what scores every rig — is lying.
try {
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('backyard_01') });
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(`bed: centre (${bed.x}, ${bed.z}) size ${bed.w} x ${bed.d}\n`);
const s = new RiggingSession({ anchors, budget: 9999 });
for (const id of ['p1','p2','p3','p4']) s.rig(id);
for (const id of ['p1','p2','p3','p4']) { const r = s.setHardware(id, RATED);
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason}`); }
s.setTension(1.0);
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
// settle in dead air so the cloth is in steady state and nothing is moving
for (let i = 0; i < Math.round(12/FIXED_DT); i++) rig.step(FIXED_DT, calm, 0);
const corners = ['p1','p2','p3','p4'].map((id) => anchors.find((a)=>a.id===id));
log('quad corners:');
for (const c of corners) log(` ${c.id.padEnd(3)} (${c.pos.x.toFixed(2)}, ${c.pos.y.toFixed(2)}, ${c.pos.z.toFixed(2)})`);
// where is the cloth, really?
let minX=1e9,maxX=-1e9,minZ=1e9,maxZ=-1e9,minY=1e9,maxY=-1e9;
for (let i=0;i<rig.pos.length;i+=3){
minX=Math.min(minX,rig.pos[i]); maxX=Math.max(maxX,rig.pos[i]);
minY=Math.min(minY,rig.pos[i+1]); maxY=Math.max(maxY,rig.pos[i+1]);
minZ=Math.min(minZ,rig.pos[i+2]); maxZ=Math.max(maxZ,rig.pos[i+2]);
}
log(`\ncloth bbox: x ${minX.toFixed(2)}..${maxX.toFixed(2)} y ${minY.toFixed(2)}..${maxY.toFixed(2)} z ${minZ.toFixed(2)}..${maxZ.toFixed(2)}`);
log(`bed spans: x ${(bed.x-bed.w/2).toFixed(2)}..${(bed.x+bed.w/2).toFixed(2)} z ${(bed.z-bed.d/2).toFixed(2)}..${(bed.z+bed.d/2).toFixed(2)}`);
log(`tris: ${rig.tris.length/3} nodes: ${rig.pos.length/3}`);
// harness 1: raycast toward a sun straight overhead
const cov = rig.coverageOver(bed, { x:0, y:1, z:0 }, null);
// harness 2: rasterise down a straight-down rain vector
const sh = new RainShadow({ groundY: 0 });
sh.update(rig, 0, -1, 0);
const frac = sh.fractionOver(bed);
log(`\n--- THE SAME QUANTITY, TWO WAYS (vertical) ---`);
log(` rig.coverageOver(bed, straight-up sun) = ${cov.toFixed(4)}`);
log(` RainShadow.fractionOver(bed), rain down = ${frac.toFixed(4)}`);
log(` live=${sh.live} difference = ${Math.abs(cov-frac).toFixed(4)}`);
// how much of the grid did the rasteriser actually fill, vs the cloth's footprint?
let filled = 0; for (let i=0;i<sh.ceil.length;i++) if (sh.ceil[i]>0) filled++;
const cellA = (sh.half*2/sh.n)**2;
log(`\n grid cells filled: ${filled} => ${(filled*cellA).toFixed(1)} m2 of ground shadowed`);
log(` cloth footprint bbox area: ${((maxX-minX)*(maxZ-minZ)).toFixed(1)} m2`);
// sample the bed on a fine lattice both ways to see WHERE they differ
log(`\n--- bed lattice: 1 = shadowed, . = open (rows = z, cols = x) ---`);
for (let j=0;j<9;j++){
let rowS='', rowC='';
for (let i=0;i<13;i++){
const x = bed.x + ((i+0.5)/13 - 0.5)*bed.w;
const z = bed.z + ((j+0.5)/9 - 0.5)*bed.d;
const c = sh._idx(x,z);
rowS += (c>=0 && sh.ceil[c]>0) ? '1' : '.';
rowC += rig.coverageOver({x, z, w:0.001, d:0.001}, {x:0,y:1,z:0}, null) > 0.5 ? '1' : '.';
}
log(` grid ${rowS} raycast ${rowC}`);
}
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
document.title = 'probe done';
</script>
</body></html>

View File

@ -0,0 +1,79 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>shadow probe 2 — synthetic</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 { RainShadow } from '../../web/world/js/skyfx.js';
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
const [,, RATED] = HARDWARE;
const el = document.getElementById('out');
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
el.textContent = '';
// ISOLATE THE RASTERISER FROM THE LEVEL.
// probe.html showed the real yard's best quad sits over only 25% of the bed, so
// its shadow numbers can't tell a rasteriser bug from an anchor-placement fact.
// So: a SYNTHETIC square of anchors dead over the bed, at 4 m. This sail
// unambiguously covers the whole bed. Grid and raycast must BOTH read ~1.0.
// Anything less is the shadow model losing cloth it is standing under.
try {
const bed = { x: 0, z: 0, w: 6, d: 4 };
const mk = (id, x, z) => { const pos = { x, y: 4, z }; return { id, type: 'post', pos, sway: () => pos }; };
// a generous 10x8 square around a 6x4 bed — no edge effects, no excuses
const anchors = [mk('s1', -5, -4), mk('s2', 5, -4), mk('s3', 5, 4), mk('s4', -5, 4)];
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
for (const gridN of [10, 16]) {
const s = new RiggingSession({ anchors, budget: 9999 });
for (const a of anchors) s.rig(a.id);
for (const a of anchors) { const r = s.setHardware(a.id, RATED);
if (!r.ok) throw new Error(`setHardware ${a.id} refused: ${r.reason}`); }
s.setTension(1.0);
const rig = s.commit(new SailRig({ anchors, gridN }));
for (let i = 0; i < Math.round(12/FIXED_DT); i++) rig.step(FIXED_DT, calm, 0);
let minY=1e9,maxY=-1e9;
for (let i=1;i<rig.pos.length;i+=3){ minY=Math.min(minY,rig.pos[i]); maxY=Math.max(maxY,rig.pos[i]); }
const cov = rig.coverageOver(bed, { x:0, y:1, z:0 }, null);
const sh = new RainShadow({ groundY: 0 });
sh.update(rig, 0, -1, 0);
const frac = sh.fractionOver(bed);
log(`\n=== gridN ${gridN} (${rig.tris.length/3} tris, ${rig.pos.length/3} nodes) cloth y ${minY.toFixed(2)}..${maxY.toFixed(2)} ===`);
log(` a 10x4 sail sitting dead over a 6x4 bed. Truth for both numbers is 1.00.`);
log(` rig.coverageOver(bed, straight up) = ${cov.toFixed(4)} ${cov > 0.99 ? 'ok' : '<-- LOSES CLOTH'}`);
log(` RainShadow.fractionOver(bed), down = ${frac.toFixed(4)} ${frac > 0.99 ? 'ok' : '<-- LOSES CLOTH'}`);
// where are the holes? print the grid cells across the bed's footprint
log(' grid cells over the bed (1 = shadowed):');
for (let j=0;j<7;j++){
let row='';
for (let i=0;i<15;i++){
const x = bed.x + ((i+0.5)/15 - 0.5)*bed.w;
const z = bed.z + ((j+0.5)/7 - 0.5)*bed.d;
const c = sh._idx(x,z);
row += (c>=0 && sh.ceil[c]>0) ? '1' : '.';
}
log(` ${row}`);
}
let filled=0; for (let i=0;i<sh.ceil.length;i++) if (sh.ceil[i]>0) filled++;
const cellA = (sh.half*2/sh.n)**2;
log(` cells filled ${filled} => ${(filled*cellA).toFixed(1)} m2 shadowed; the sail is 10x8 = 80 m2`);
}
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
document.title = 'probe2 done';
</script>
</body></html>

View File

@ -0,0 +1,114 @@
<!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>

View File

@ -0,0 +1,125 @@
<!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>

View File

@ -0,0 +1,132 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>quad sweep — can ANY line cover the bed?</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 { RainShadow } from '../../web/world/js/skyfx.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
const [,, RATED] = HARDWARE;
const el = document.getElementById('out');
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
el.textContent = '';
// THE QUESTION probe.html forces: the yard's canonical "best bed-covering quad"
// sits over 25% of the bed vertically, and hail falls vertically — so the sail
// can never stop more than a quarter of what kills the garden. Is that the
// LEVEL's ceiling, or did the canonical line just pick badly?
//
// Sweep every 4-anchor combination. Stage 1 is a cheap polygon-overlap proxy
// (the cloth lives roughly inside its anchors' hull), stage 2 flies the real
// cloth + real shadow grid for the best candidates. Cheap-then-real, because
// C(n,4) settles would take an hour and the proxy only has to RANK.
const clip = (poly, rect) => {
// SutherlandHodgman against the bed rect, then shoelace. Exact for convex.
const [x0, x1] = [rect.x - rect.w/2, rect.x + rect.w/2];
const [z0, z1] = [rect.z - rect.d/2, rect.z + rect.d/2];
const edges = [
(p) => p.x >= x0, (p) => p.x <= x1, (p) => p.z >= z0, (p) => p.z <= z1,
];
const isect = [
(a, b) => ({ x: x0, z: a.z + (b.z - a.z) * (x0 - a.x) / (b.x - a.x) }),
(a, b) => ({ x: x1, z: a.z + (b.z - a.z) * (x1 - a.x) / (b.x - a.x) }),
(a, b) => ({ x: a.x + (b.x - a.x) * (z0 - a.z) / (b.z - a.z), z: z0 }),
(a, b) => ({ x: a.x + (b.x - a.x) * (z1 - a.z) / (b.z - a.z), z: z1 }),
];
let out = poly;
for (let e = 0; e < 4; e++) {
const inp = out; out = [];
for (let i = 0; i < inp.length; i++) {
const a = inp[i], b = inp[(i + 1) % inp.length];
const ain = edges[e](a), bin = edges[e](b);
if (ain && bin) out.push(b);
else if (ain && !bin) out.push(isect[e](a, b));
else if (!ain && bin) { out.push(isect[e](a, b)); out.push(b); }
}
if (!out.length) return 0;
}
let s = 0;
for (let i = 0; i < out.length; i++) {
const a = out[i], b = out[(i + 1) % out.length];
s += a.x * b.z - b.x * a.z;
}
return Math.abs(s) / 2;
};
// convex hull (the cloth spans its anchors' hull, so order the quad sanely)
const hull = (pts) => {
const p = pts.slice().sort((a, b) => a.x - b.x || a.z - b.z);
const cross = (o, a, b) => (a.x - o.x) * (b.z - o.z) - (a.z - o.z) * (b.x - o.x);
const lo = []; for (const q of p) { while (lo.length >= 2 && cross(lo[lo.length-2], lo[lo.length-1], q) <= 0) lo.pop(); lo.push(q); }
const up = []; for (const q of p.slice().reverse()) { while (up.length >= 2 && cross(up[up.length-2], up[up.length-1], q) <= 0) up.pop(); up.push(q); }
lo.pop(); up.pop(); return lo.concat(up);
};
try {
for (const siteId of ['backyard_01', 'site_02_corner_block']) {
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;
const bedArea = bed.w * bed.d;
log(`\n\n######## ${siteId} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z}) = ${bedArea} m2 ${anchors.length} anchors`);
// stage 1: rank every 4-combo by hull-over-bed overlap
const cands = [];
const n = anchors.length;
for (let a=0;a<n;a++) for (let b=a+1;b<n;b++) for (let c=b+1;c<n;c++) for (let d=c+1;d<n;d++) {
const q = [anchors[a], anchors[b], anchors[c], anchors[d]];
const h = hull(q.map((x) => ({ x: x.pos.x, z: x.pos.z })));
if (h.length < 3) continue;
const over = clip(h, bed) / bedArea;
cands.push({ ids: q.map((x)=>x.id), over });
}
cands.sort((p, q) => q.over - p.over);
log(` swept ${cands.length} quads. Top 8 by hull-over-bed (proxy):`);
for (const c of cands.slice(0, 8)) log(` ${c.ids.join('+').padEnd(24)} hull covers ${(c.over*100).toFixed(0)}% of bed`);
// stage 2: fly the real cloth for the best few + the canonical line
const canonical = siteId === 'backyard_01' ? ['p1','p2','p3','p4'] : ['q1','q2','q3','tr1'];
const tryList = [];
for (const c of cands.slice(0, 5)) tryList.push(c.ids);
if (!tryList.some((t) => t.join()===canonical.join())) tryList.push(canonical);
log(`\n REAL cloth + REAL shadow grid (settled 12 s, dead air, vertical rain):`);
log(` line hull% coverageOver(up) RainShadow.fractionOver`);
for (const ids of tryList) {
const s = new RiggingSession({ anchors, budget: 9999 });
let ok = true;
for (const id of ids) if (!s.rig(id).ok) { ok = false; break; }
if (!ok) { log(` ${ids.join('+').padEnd(24)} REFUSED by the shop`); continue; }
for (const id of ids) { const r = s.setHardware(id, RATED);
if (!r.ok) throw new Error(`setHardware ${id} RATED refused: ${r.reason} — the bench is flying hardware it did not buy`); }
s.setTension(1.0);
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
for (let i=0;i<Math.round(12/FIXED_DT);i++) rig.step(FIXED_DT, calm, 0);
const cov = rig.coverageOver(bed, {x:0,y:1,z:0}, null);
const sh = new RainShadow({ groundY: 0 }); sh.update(rig, 0, -1, 0);
const frac = sh.fractionOver(bed);
const hp = hull(ids.map((id)=>{const a=anchors.find((z)=>z.id===id);return {x:a.pos.x,z:a.pos.z};}));
const over = clip(hp, bed)/bedArea;
const mark = ids.join()===canonical.join() ? ' <-- the canonical line' : '';
log(` ${ids.join('+').padEnd(24)} ${(over*100).toFixed(0).padStart(4)}% ${cov.toFixed(3).padStart(12)} ${frac.toFixed(3).padStart(18)}${mark}`);
}
}
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
document.title = 'sweep done';
</script>
</body></html>

View File

@ -0,0 +1,104 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>sweep2 — hold AND cover?</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 { RainShadow } 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 PARETO QUESTION, and the whole of gate 1.
//
// probe2 proved the shadow model is sound (a sail over the bed reads 1.000 by
// raycast AND by the grid). probe3 found the variable: _checkFailure compares
// load against hw.rating * anchor.ratingHint, and the HOUSE fascia's hint is
// 0.35 — so a $30 RATED shackle is worth 2.3 kN on the house and 6.5 kN on a
// post. Every quad that covers the bed hangs off the house.
//
// So: sweep every 4-anchor line on the BEST steel in the game, settle it the way
// prep does, and ask both questions at once — does it HOLD, and does it COVER?
// A quad that covers 100% while tearing itself apart in prep covers nothing.
// This is the table the garden's whole design rests on and nobody has ever
// printed it.
try {
for (const siteId of ['backyard_01', 'site_02_corner_block']) {
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(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, ratingHint:a.ratingHint }; });
const bed = world.gardenBed;
log(`\n\n######## ${siteId} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z})`);
log(` anchors and what the BEST steel ($30 RATED, 6.5 kN) is actually worth on them:`);
for (const a of anchors) {
log(` ${a.id.padEnd(5)} ${String(a.type).padEnd(8)} hint ${(a.ratingHint ?? 1).toFixed(2)} -> ${((a.ratingHint ?? 1) * 6.5).toFixed(2)} kN`);
}
// prep's own wind: main.js hands the rig the calm day outside a storm
const calmWind = createWind(await loadStorm('storm_01_gentle'));
calmWind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
const rows = [];
const n = anchors.length;
for (let a=0;a<n;a++) for (let b=a+1;b<n;b++) for (let c=b+1;c<n;c++) for (let d=c+1;d<n;d++) {
const ids = [anchors[a].id, anchors[b].id, anchors[c].id, anchors[d].id];
const s = new RiggingSession({ anchors, budget: 9999 });
let ok = true;
for (const id of ids) if (!s.rig(id).ok) { ok = false; break; }
if (!ok) continue;
for (const id of ids) { const r = s.setHardware(id, RATED);
if (!r.ok) throw new Error(`setHardware ${id} RATED refused: ${r.reason} — the bench is flying hardware it did not buy`); }
s.setTension(1.0);
let rig;
try { rig = s.commit(new SailRig({ anchors, gridN: 10 })); } catch { continue; }
// settle exactly like prep: calm day, running clock, 12 s
let prepT = 0, peak = 0;
for (let i=0;i<Math.round(12/FIXED_DT);i++){
rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT;
for (const co of rig.corners) if ((co.load||0) > peak) peak = co.load;
}
const broke = rig.corners.filter((co)=>co.broken).length;
const cov = rig.coverageOver(bed, {x:0,y:1,z:0}, null); // vertical: what hail sees
const sh = new RainShadow({ groundY: 0 }); sh.update(rig, 0, -1, 0);
const frac = sh.fractionOver(bed);
rows.push({ ids, broke, cov, frac, peak: peak/1000 });
}
const survived = rows.filter((r) => r.broke === 0);
log(`\n ${rows.length} quads swept on RATED steel. ${survived.length} survive PREP on the calm day.`);
log(`\n --- best COVER among quads that actually HOLD (this is the real ceiling) ---`);
log(` line vert.cover shadow peak kN broke`);
for (const r of survived.sort((p,q)=>q.cov-p.cov).slice(0,10)) {
log(` ${r.ids.join('+').padEnd(22)} ${(r.cov*100).toFixed(0).padStart(7)}% ${r.frac.toFixed(3).padStart(6)} ${r.peak.toFixed(2).padStart(7)} ${r.broke}`);
}
log(`\n --- best COVER overall, holding or not (what the sprint assumed was available) ---`);
log(` line vert.cover shadow peak kN broke`);
for (const r of rows.slice().sort((p,q)=>q.cov-p.cov).slice(0,6)) {
log(` ${r.ids.join('+').padEnd(22)} ${(r.cov*100).toFixed(0).padStart(7)}% ${r.frac.toFixed(3).padStart(6)} ${r.peak.toFixed(2).padStart(7)} ${r.broke}${r.broke?' <-- tears in PREP':''}`);
}
const canonical = siteId === 'backyard_01' ? 'p1+p2+p3+p4' : 'q1+q2+q3+tr1';
const cn = rows.find((r)=>r.ids.join('+')===canonical);
if (cn) log(`\n the canonical line ${canonical}: cover ${(cn.cov*100).toFixed(0)}% shadow ${cn.frac.toFixed(3)} peak ${cn.peak.toFixed(2)} kN broke ${cn.broke}`);
}
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
document.title = 'sweep2 done';
</script>
</body></html>