HardYards/tools/fabric_bet/probe.html
type-two 92a8e870f2 Lane C S16: storm_06_soaker — the pea-hail soaker, measured over both yards; fabric_bet probe; gardenfly porosity opt
The first night the default fabric is the wrong answer, tuned by measurement
through the real chain (gardenfly, dressed anchors, funnel ON, post-ruling
tree): bare 17.5 DEAD, best cloth line 42.3 TATTERED (a loss at any price),
same $75 ring on membrane 96.5 FULL, $60 ring 68.8 FULL — one key, Δ54.2 HP.
Membrane on $20 carabiners inverts (19.1 < 24.5): the doubled load is real.
Measured over backyard_01 too: its geometry caps hail cover at ~31%, so the
night only works on site_02 — the pairing receipt for D. tools/fabric_bet/
probe.html is the measurement harness; flyGarden grows an additive porosity
opt (default 0.30 unchanged — B: contest at integration if wanted).
2026-07-20 20:06:12 +10:00

173 lines
9.6 KiB
HTML

<!doctype html>
<!--
fabric_bet/probe.html — does the F key matter TONIGHT? [Lane C, SPRINT16 gate 3.1]
The measurement harness behind the pea-hail soaker. Every hail night before
storm_06 threw stones a 2 mm weave stops dead, so the fabric bet (SPRINT6)
never had a night where membrane was the right answer. This page flies the
SAME line on both fabrics through gardenfly.js — THE audit scoring chain
(windForSite → attach → skyfx → garden.js), dressed anchors, live sway — so
cloth-vs-membrane differ by exactly one variable: porosity.
Per line, per fabric: garden hp/state, byHail/byRain, corner peaks vs
effective ratings, marginal flags, cost. Plus the bare bed (the control) and
the storm's measured personality (stormStats — the numbers the forecast
card will sell).
Funnel state is printed in the header, per the discipline (three harnesses
in this repo's history flew site_02 funnel-off and believed it).
tools/fabric_bet/probe.html?site=backyard_01&storm=storm_06_soaker
-->
<meta charset="utf-8">
<title>fabric_bet probe</title>
<style>
body { background:#111; color:#ddd; font:13px/1.5 ui-monospace,Menlo,monospace; margin:0; padding:20px; }
h1 { font-size:15px; color:#fff; margin:0 0 2px; }
.sub { color:#888; margin-bottom:14px; white-space:pre-wrap; }
table { border-collapse:collapse; margin:8px 0; }
td, th { padding:2px 12px 2px 0; white-space:nowrap; text-align:left; }
th { color:#9ab; border-bottom:1px solid #333; }
.ok { color:#6c6; } .bad { color:#e66; } .warn { color:#dc6; }
.note { color:#888; margin-top:14px; white-space:pre-wrap; }
</style>
<h1>fabric_bet — the same line, both fabrics, the real chain</h1>
<div class="sub" id="sub">loading…</div>
<div id="out"></div>
<div class="note" id="note"></div>
<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 { createWorld, loadSite } from '../../web/world/js/world.js';
import { stormStats } from '../../web/world/js/weather.js';
import { hailBlockFor } from '../../web/world/js/weather.core.js';
import { flyGarden, hardwareByName } from '../site_audit/gardenfly.js';
const q = new URLSearchParams(location.search);
const siteName = q.get('site') || 'backyard_01';
const stormName = q.get('storm') || 'storm_06_soaker';
const el = (id) => document.getElementById(id);
const loadJSON = async (p) => (await fetch(p)).json();
// The candidate lines. COVER_QUAD is balance.test's canonical bed-covering
// quad; the others are cheaper/worse-covering controls so the bet's shape is
// visible across the price range, not just at one point.
const LINES = {
backyard_01: [
{ label: 'cover quad, all carabiners ($20)', ids: ['p1', 'p2', 'p3', 'p4'], hw: ['carabiner', 'carabiner', 'carabiner', 'carabiner'] },
{ label: 'cover quad, all shackles ($60)', ids: ['p1', 'p2', 'p3', 'p4'], hw: ['shackle', 'shackle', 'shackle', 'shackle'] },
{ label: 'wildnight $80 line (cara+rated+shackle+rated)', ids: ['p1', 'p2', 'p3', 'p4'], hw: ['carabiner', 'rated shackle', 'shackle', 'rated shackle'] },
{ label: 'separation-pin line p5+p1+p2+p3 ($75)', ids: ['p5', 'p1', 'p2', 'p3'], hw: ['rated shackle', 'shackle', 'shackle', 'shackle'] },
// The full-cover quads — unbuyable on the wildnight (6.9 kN, tears), which
// is exactly why the MILD night is where they come alive. p4+p5 span the
// bed north, p1+p2 south; the hull holds the whole bed.
{ label: 'FULL-COVER p4,p5,p1,p2 carabiners ($20)', ids: ['p4', 'p5', 'p1', 'p2'], hw: ['carabiner', 'carabiner', 'carabiner', 'carabiner'] },
{ label: 'FULL-COVER p4,p5,p1,p2 shackles ($60)', ids: ['p4', 'p5', 'p1', 'p2'], hw: ['shackle', 'shackle', 'shackle', 'shackle'] },
{ label: 'wide tree line t1,t2,p1,p2 shackles ($60)', ids: ['t1', 't2', 'p1', 'p2'], hw: ['shackle', 'shackle', 'shackle', 'shackle'] },
{ label: 'house span h2,h3,p1,p2 shackles ($60)', ids: ['h2', 'h3', 'p1', 'p2'], hw: ['shackle', 'shackle', 'shackle', 'shackle'] },
],
site_02_corner_block: [
{ label: 'S14 cheapest honest tr1,tr1b,q1,q3 ($40)', ids: ['tr1', 'tr1b', 'q1', 'q3'], hw: ['carabiner', 'carabiner', 'shackle', 'carabiner'] },
{ label: 'best clean tr1,tr1b,q1,q4 (shackles $60)', ids: ['tr1', 'tr1b', 'q1', 'q4'], hw: ['shackle', 'shackle', 'shackle', 'shackle'] },
// The bed ring — q1..q4 stand AROUND the bed, the yard's whole offer.
{ label: 'bed ring q1,q2,q3,q4 carabiners ($20)', ids: ['q1', 'q2', 'q3', 'q4'], hw: ['carabiner', 'carabiner', 'carabiner', 'carabiner'] },
{ label: 'bed ring q1,q2,q3,q4 shackles ($60)', ids: ['q1', 'q2', 'q3', 'q4'], hw: ['shackle', 'shackle', 'shackle', 'shackle'] },
{ label: 'bed ring, rated on the funnel corner q1 ($75)', ids: ['q1', 'q2', 'q3', 'q4'], hw: ['rated shackle', 'shackle', 'shackle', 'shackle'] },
{ label: 'bed ring $75, winched tight (tension 1.2)', ids: ['q1', 'q2', 'q3', 'q4'], hw: ['rated shackle', 'shackle', 'shackle', 'shackle'], tension: 1.2 },
{ label: 'bed ring $60, winched tight (tension 1.2)', ids: ['q1', 'q2', 'q3', 'q4'], hw: ['shackle', 'shackle', 'shackle', 'shackle'], tension: 1.2 },
],
};
async function run() {
const site = await loadSite(siteName);
const scene = new THREE.Scene();
let currentWind = {
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
gustTelegraph: () => null, eventsBetween: () => [],
};
const windProxy = {
sample: (p, t, o) => currentWind.sample(p, t, o || new THREE.Vector3()),
speedAt: (p, t) => currentWind.speedAt(p, t),
rainAt: (t) => currentWind.rainAt(t),
rainMmPerHour: (t) => (currentWind.rainMmPerHour ? currentWind.rainMmPerHour(t) : 0),
gustTelegraph: (t) => currentWind.gustTelegraph(t),
eventsBetween: (a, b) => currentWind.eventsBetween(a, b),
setSheltersFromTrees() {},
};
const use = (w) => { currentWind = w; };
const world = createWorld(scene, { wind: windProxy, site });
await world.dress();
const bed = world.gardenBed;
const anchors = world.anchors;
const venturi = site.wind?.venturi ?? [];
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
const s = stormStats(stormDef);
const size = stormDef.hail?.size ?? 0;
const clothBlock = size ? hailBlockFor(size, 0.30) : 1;
el('sub').textContent =
`${site.name} · ${stormName} (${stormDef.duration}s) · bed ${bed.w}x${bed.d} @ (${bed.x},${bed.z})\n`
+ `FUNNEL: ${venturi.length ? venturi.map((v) => `(${v.x},${v.z}) gain ${v.gain} — ON`).join(' · ') : 'none authored on this site'}\n`
+ `storm truth (stormStats): sustained ${s.sustained.toFixed(1)} m/s · gust peak ${s.gustPeak.toFixed(1)} m/s `
+ `(${(s.gustPeak * 3.6).toFixed(0)} km/h) · rain peak ${s.rainPeakMmPerHour.toFixed(0)} mm/hr · `
+ `hail ${s.hailSeconds.toFixed(1)} s-equiv @ size ${size} · change at ${s.changeAt ?? '—'}s\n`
+ `hailBlockFor(${size}, cloth 0.30) = ${(clothBlock * 100).toFixed(1)}% blocked · membrane = 100%`;
const bare = flyGarden({ anchors, bed, stormDef, siteDef: site, use });
const tbl = document.createElement('table');
const head = tbl.insertRow();
for (const h of ['line', 'fabric', '$', 'garden', 'byHail', 'byRain', 'lost', 'marginal', 'peaks (kN vs eff)'])
{ const th = document.createElement('th'); th.textContent = h; head.appendChild(th); }
const rowFor = (label, g, fabric, cls) => {
const tr = tbl.insertRow();
const peaks = g.peaks.map((p) => `${p.id} ${(p.peakN / 1000).toFixed(2)}/${(p.effN / 1000).toFixed(2)}`).join(' ');
[label, fabric, `$${g.cost}`, `${g.hp.toFixed(1)} ${g.state.toUpperCase()}`,
g.byHail.toFixed(1), g.byRain.toFixed(1), String(g.cornersLost),
g.marginal.join(',') || '—', peaks || '—']
.forEach((c, i) => { const td = tr.insertCell(); td.textContent = c; if (i === 3) td.className = cls(g); });
};
const cls = (g) => (g.hp >= 66 ? 'ok' : g.hp >= 50 ? 'warn' : 'bad');
const results = { site: siteName, storm: stormName, bare, lines: [] };
for (const L of LINES[siteName] ?? []) {
const hw = L.hw.map(hardwareByName);
const tension = L.tension ?? 1.0;
const cloth = flyGarden({ anchors, bed, stormDef, siteDef: site, use, ids: L.ids, hw, tension, porosity: 0.30 });
const mem = flyGarden({ anchors, bed, stormDef, siteDef: site, use, ids: L.ids, hw, tension, porosity: 0 });
rowFor(L.label, cloth, 'cloth', cls);
rowFor('', mem, 'MEMBRANE', cls);
results.lines.push({ label: L.label, ids: L.ids, hw: L.hw, cloth, membrane: mem });
}
const tr = tbl.insertRow();
['BARE BED', '—', '$0', `${bare.hp.toFixed(1)} ${bare.state.toUpperCase()}`,
bare.byHail.toFixed(1), bare.byRain.toFixed(1), '4', '—', '—']
.forEach((c) => { const td = tr.insertCell(); td.textContent = c; td.className = 'warn'; });
el('out').appendChild(tbl);
const best = results.lines.reduce((b, r) => (r.membrane.hp > (b?.membrane.hp ?? -1) ? r : b), null);
el('note').textContent = best
? `best membrane line: "${best.label}" → ${best.membrane.hp.toFixed(1)} vs same line on cloth ${best.cloth.hp.toFixed(1)} `
+ `${(best.membrane.hp - best.cloth.hp).toFixed(1)} HP) · bare ${bare.hp.toFixed(1)}`
: 'no lines authored for this site — add them to LINES';
window.__fabricBet = results;
document.title = `fabric_bet — ${stormName}`;
}
run().catch((e) => {
el('note').className = 'bad';
el('note').textContent = `probe crashed: ${e.message}\n${e.stack || ''}`;
window.__fabricBet = { error: e.message };
document.title = 'fabric_bet — ERROR';
});
</script>