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).
173 lines
9.6 KiB
HTML
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>
|