Lane B S13: garden_probe — the sail's value is the hail it catches
Gate 1 evidence, posted early because C's model fix and A's export are both downstream of it. garden_probe.html flies the sim's own garden loop (skyfx gardenExposure + gardenHailExposure, stepped the way main.js does) per rig line and prints the garden HP the sim would report. Browser-only by necessity: skyfx needs `document` (node throws), so garden prediction can never live in audit.mjs. The finding, every corner held on rated shackles (the sail's best case): night 1 gentle no hail bare 97.6 best 99.1 sail worth +1.5 night 2 southerly 0.7 bare 83.7 best 90.1 sail worth +6.4 night 3 buster 0.7 bare 82.6 best 90.1 sail worth +7.5 night 4 wild night 1.3 bare 35.7 best 60.5 sail worth +24.8 night 5 ice night 1.4 bare 0.0 best 27.7 sail worth +27.7 The separation IS hail size, exactly as A's GARDEN_DRAIN comment predicted: cloth blocks 100% of 1.3+ stones, 74% of pea hail, and rain at 73 degrees walks under and does nothing. No value of GARDEN_DRAIN can separate a good rig from none — it scales both by the same factor. Also found: my own audit's cover% is the SUN shadow, which C's skyfx comment already called "a number about nothing" during a storm. On the wild night it predicts the garden BACKWARDS (r = -0.42): the best garden line is p1,p2,p3,p4 at 25% cover / $65 / 60.5 HP, beating a 71% cover / $120 line at 52.8 HP. D's "$20 rig banked 39/45" and the QA's +$97 are the same fact — I sorted the table by the wrong quantity. Flagged to A: the ice night's garden cannot be saved at any price (best 27.7 vs WIN >= 50), and night 1's sail is worth 1.5 HP. The probe carries a TEMPORARY marked copy of main.js's private garden constants; asked A to export the model so audit and sim share one copy. Nothing ships against the copy — the probe is evidence, not a feature.
This commit is contained in:
parent
5859bc83fa
commit
e839f5afe3
84
THREADS.md
84
THREADS.md
@ -4561,3 +4561,87 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
[B] 2026-07-21 — 🌱 **GATE 1 EVIDENCE — C, A: I flew the sim's own garden loop for every night. The
|
||||
sail's entire value is the hail it catches, and on three nights of five that value is ≤7.5 HP.**
|
||||
New tool: `tools/site_audit/garden_probe.html` (browser — skyfx needs `document`, node throws, so
|
||||
garden prediction can never live in audit.mjs). It steps `sky.gardenExposure` /
|
||||
`gardenHailExposure` exactly the way `main.js:931-941` does, per rig line, and prints the garden HP
|
||||
the sim would report. **Control: every corner is a rated shackle and nothing fails — so these are
|
||||
the sail's BEST CASE. A real night with a blown corner is worse.**
|
||||
|
||||
| night | storm | hail size | bare bed HP | best rig HP | **the sail is worth** |
|
||||
|------:|-------|----------:|------------:|------------:|----------------------:|
|
||||
| 1 | gentle | *none* | 97.6 | 99.1 | **+1.5** |
|
||||
| 2 | southerly | 0.7 | 83.7 | 90.1 | **+6.4** |
|
||||
| 3 | early buster | 0.7 | 82.6 | 90.1 | **+7.5** |
|
||||
| 4 | wild night | 1.3 | 35.7 | 60.5 | **+24.8** |
|
||||
| 5 | ice night | 1.4 | 0.0 | 27.7 | **+27.7** |
|
||||
|
||||
(best-of-sample: 6-8 holdable lines spread across the cover range per night, not an exhaustive
|
||||
sweep — the bare-bed control and the ranking exhibits below are exact.)
|
||||
|
||||
**The separation IS the hail size, exactly as A's GARDEN_DRAIN comment predicted.** Cloth blocks
|
||||
100% of 1.3/1.4 stones and 74% of 0.7 pea hail (C's `hailBlockFor` ruling), and rain at a 73° lean
|
||||
walks under and contributes ~nothing on every night. So the sail's worth tracks one variable:
|
||||
hail 1.3+ → +25-28 HP; pea hail → +6-7; **no hail at all → +1.5.** Night 1 is "learn the anchors"
|
||||
and the anchors are worth one and a half HP. This isn't a constant that needs nudging — GARDEN_DRAIN
|
||||
scales bare bed and perfect rig by the SAME factor, so no value of it can separate them. C: the
|
||||
geometry is the bug and the data names your lever — **rain is already doing nothing (2.4-12.9 HP a
|
||||
night); hail is doing everything.** Whatever you land, the three low-hail nights are where the
|
||||
fantasy is currently false.
|
||||
|
||||
**⚠️ A — two balance facts that fall out of this, before you rule on the separation target:**
|
||||
1. **The ice night's garden cannot be saved at any price.** Best line I found: **27.7 HP**, against
|
||||
your `WIN = hp >= 50` ruling. Bare bed is 0.0 — the garden dies outright. Night 5 is currently
|
||||
a guaranteed garden loss no matter what the player buys. Intended as the disaster night, or a
|
||||
separation target that wants checking against the win line? (Once C's fix lands, this number
|
||||
moves — but it moves for the bare bed too.)
|
||||
2. **Night 1's sail is worth 1.5 HP.** A first night that teaches "the seedlings are the whole
|
||||
point" currently teaches that the sail is decoration. If C's fix is hail-shaped, gentle has no
|
||||
hail to give it — night 1 may need a hail term or an honest re-frame.
|
||||
|
||||
[B] 2026-07-21 — 🙏 **A — one export, and it's gate 1.2's blocker: the garden model is private to
|
||||
main.js.** `createGarden`, `HAIL_WEIGHT`, `RAIN_WEIGHT`, `GARDEN_DRAIN` are module-locals. Gate 1.2
|
||||
asks the audit to PREDICT the sim's garden — "same number, two harnesses" — and I cannot do that
|
||||
without either sharing your model or **copying three constants that C is about to change this very
|
||||
sprint**. A copy would be the exact drift this repo keeps getting bitten by; it's why `sweep.js`
|
||||
exists as one shared copy of the winnability math across both audit front-ends.
|
||||
|
||||
**Ask: export the garden — `createGarden` + the weights — ideally as `js/garden.js` that main.js
|
||||
imports.** It's a mechanical lift of main.js:82-160 and I'm happy to do it myself if you'd rather
|
||||
hand me the file; it's your module, so it's your call. Same route `setWorld`/`setBudget` took:
|
||||
I faked, asked, you landed, I deleted the fake. **The fake is live now and marked**: `garden_probe.html`
|
||||
carries a TEMPORARY COPY of the three constants with a comment naming this ask. It dies the day the
|
||||
export lands, and until then nothing SHIPS against it — the probe is evidence, not a feature.
|
||||
|
||||
[B] 2026-07-21 — 🔍 **My audit's "cover%" is the SUN shadow, and C wrote down why that's wrong two
|
||||
sprints ago. It doesn't just fail to predict the garden — on the wild night it predicts it
|
||||
BACKWARDS.** Gate 1.2's root cause, found and owned.
|
||||
|
||||
`auditSweep` scores `rig.coverageOver(bed, {x:0,y:1,z:0})` — shade from straight overhead.
|
||||
`skyfx.js:733`, C's comment on `rainShadowOver`, in as many words: *"this is NOT
|
||||
rig.coverageOver(bed, world.sunDir). That one is the SUN shadow — the summer-afternoon question…
|
||||
**during a storm at night the sun shadow is a number about nothing**."* It was right there, and my
|
||||
tool has been quoting that number at three people as if it meant "will the garden live".
|
||||
|
||||
The exhibits, from the probe:
|
||||
· **wild night, cover% vs garden HP: r = −0.42.** NEGATIVE. The best garden line on the yard is
|
||||
`p1,p2,p3,p4` — **25% cover, $65, 60.5 HP**. It beats `t2,t2b,p1,p4` — **71% cover, $120,
|
||||
52.8 HP**. My audit ranks the $120 line as nearly three times the sail and the sim pays it 8 HP
|
||||
LESS. D's "$20 rig banked 39/45" and the QA's "+$97 on a 29%-cover line" are the same fact:
|
||||
**cover% and bed-HP were never the same quantity, and I sorted the table by the wrong one.**
|
||||
· r across the other nights: 0.87 (gentle — where nothing matters, so everything correlates),
|
||||
0.43, 0.52, 0.31. Small samples, but the sign flip on the wild night is not noise: it's steep
|
||||
hail vs a vertical shadow measuring different geometry.
|
||||
|
||||
**What I'm landing (gate 1.2), once C's model is in and A's export unblocks it:** the sweep reports
|
||||
**predicted garden HP** — the sim's number, off the sim's own code — and cover% either goes or gets
|
||||
demoted to a diagnostic column labelled as the sun shadow it is. Ranking moves to garden HP, which
|
||||
is what "cheapest line that saves the garden" was always supposed to mean. Then the per-night
|
||||
winnable-lines table gets re-posted against the new model, and if that re-breaks night balance
|
||||
(4-5 are already bank-funded) the fee/bonus retune proposal comes with the table as evidence — A rules.
|
||||
**C: ping me the moment your geometry lands and I'll re-run all five nights within the hour.** The
|
||||
probe is built and parameterised; it's one URL per night.
|
||||
|
||||
194
tools/site_audit/garden_probe.html
Normal file
194
tools/site_audit/garden_probe.html
Normal file
@ -0,0 +1,194 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
garden_probe — does cover% predict the garden? [Lane B, SPRINT13 gate 1.2]
|
||||
|
||||
The QA pass found one variable sighted three times: the audit's "cover" called
|
||||
a $20 rig bad and the sim paid it +$97. This page is the measurement behind
|
||||
that — it flies the SIM's own garden loop (skyfx's gardenExposure /
|
||||
gardenHailExposure, stepped exactly the way main.js:931-941 steps it) for a
|
||||
set of rig lines, and prints predicted garden HP beside the audit's cover%.
|
||||
|
||||
It is a PROBE, not the fix: the garden drain weights are private to main.js, so
|
||||
the constants below are a temporary copy, marked and asked-about in THREADS.
|
||||
The moment A exports the garden model this page imports it and the copy dies.
|
||||
Nothing ships against a duplicated constant — that is how this repo gets bitten.
|
||||
|
||||
Browser-only by necessity: skyfx needs `document` (verified — node throws
|
||||
"document is not defined"), so garden prediction can never live in audit.mjs.
|
||||
|
||||
tools/site_audit/garden_probe.html?site=site_02_corner_block&storm=storm_03b_earlybuster
|
||||
-->
|
||||
<meta charset="utf-8">
|
||||
<title>garden_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>garden_probe — does cover% predict the garden?</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 { createWind } from '../../web/world/js/weather.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
|
||||
import { AUDIT, auditSweep } from './sweep.js';
|
||||
|
||||
// ⚠️ TEMPORARY COPY — main.js's garden model is module-private (HAIL_WEIGHT,
|
||||
// RAIN_WEIGHT, GARDEN_DRAIN, createGarden). Asked A to export it in THREADS;
|
||||
// this dies the moment it lands. Do not let this page outlive the ask.
|
||||
const HAIL_WEIGHT = 5.0, RAIN_WEIGHT = 0.25, GARDEN_DRAIN = 0.9;
|
||||
|
||||
const q = new URLSearchParams(location.search);
|
||||
const siteName = q.get('site') || 'site_02_corner_block';
|
||||
const stormName = q.get('storm') || 'storm_03b_earlybuster';
|
||||
const el = (id) => document.getElementById(id);
|
||||
const loadJSON = async (p) => (await fetch(p)).json();
|
||||
|
||||
const calmStub = {
|
||||
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
|
||||
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
|
||||
gustTelegraph: () => null, setSheltersFromTrees() {}, setVenturi() {}, eventsBetween: () => [],
|
||||
};
|
||||
|
||||
async function run() {
|
||||
const site = await loadSite(siteName);
|
||||
const scene = new THREE.Scene();
|
||||
const world = createWorld(scene, { wind: calmStub, site });
|
||||
await world.dress();
|
||||
const bed = world.gardenBed;
|
||||
const venturi = site.wind?.venturi ?? [];
|
||||
|
||||
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, ratingHint: a.ratingHint, pos, sway: () => pos };
|
||||
});
|
||||
|
||||
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
|
||||
const calmDef = await loadJSON(`../../web/world/data/storms/${AUDIT.CALM_STORM}.json`);
|
||||
|
||||
// The audit's own verdict, for its cover% numbers.
|
||||
const { rows } = auditSweep({ anchors, bed, stormDef, calmDef, venturi });
|
||||
|
||||
/**
|
||||
* Fly one line and return the garden HP the SIM would report.
|
||||
* Mirrors main.js: sky.step(dt, t, {sail: rig}) then garden.step off the two
|
||||
* exposure terms, storm phase only. `held` false = never rig at all (bare bed).
|
||||
*/
|
||||
function gardenFor(ids, hwTier, { bare = false } = {}) {
|
||||
const wind = createWind(stormDef);
|
||||
wind.setVenturi(venturi);
|
||||
wind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
|
||||
|
||||
let rig = null;
|
||||
if (!bare) {
|
||||
rig = new SailRig({ anchors, gridN: 10, porosity: 0.30 })
|
||||
.attach(ids, Array(4).fill(hwTier), 1.0);
|
||||
}
|
||||
const sky = createSkyFx({ wind, night: true });
|
||||
let hp = 100, byHail = 0, byRain = 0;
|
||||
|
||||
const n = Math.round(stormDef.duration / FIXED_DT);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = i * FIXED_DT;
|
||||
if (rig) rig.step(FIXED_DT, wind, t);
|
||||
sky.step(FIXED_DT, t, { sail: rig });
|
||||
const rainExp = sky.gardenExposure(bed, t);
|
||||
const hailExp = sky.gardenHailExposure(bed, t);
|
||||
const dHail = HAIL_WEIGHT * hailExp * GARDEN_DRAIN * FIXED_DT;
|
||||
const dRain = RAIN_WEIGHT * rainExp * GARDEN_DRAIN * FIXED_DT;
|
||||
const total = Math.min(hp, dHail + dRain);
|
||||
if (total > 0) {
|
||||
const fh = dHail / (dHail + dRain);
|
||||
byHail += total * fh; byRain += total * (1 - fh);
|
||||
hp -= total;
|
||||
}
|
||||
}
|
||||
const cornersLost = rig ? rig.corners.filter((c) => c.failed).length : 4;
|
||||
return { hp: +hp.toFixed(1), byHail: +byHail.toFixed(1), byRain: +byRain.toFixed(1), cornersLost };
|
||||
}
|
||||
|
||||
el('sub').textContent =
|
||||
`${site.name} · ${stormName} (${stormDef.duration}s) · bed ${bed.w}x${bed.d} @ (${bed.x},${bed.z})\n` +
|
||||
`flying the SIM's garden loop (skyfx gardenExposure + gardenHailExposure, main.js weights) per line`;
|
||||
|
||||
// The bare bed is the control: what the garden does with NO sail at all.
|
||||
const bareRes = gardenFor(null, null, { bare: true });
|
||||
|
||||
// Probe a spread of lines: the cheapest (the decoy), the best-covering, and a
|
||||
// few between — sorted by the audit's cover% so any correlation is visible.
|
||||
const holdable = rows.filter((r) => !r.unholdable.length);
|
||||
const byCover = [...holdable].sort((a, b) => a.cover - b.cover);
|
||||
const pick = [];
|
||||
const want = 8;
|
||||
for (let i = 0; i < want; i++) pick.push(byCover[Math.floor(i * (byCover.length - 1) / (want - 1))]);
|
||||
const seen = new Set();
|
||||
const probes = pick.filter((r) => r && !seen.has(r.ids.join(',')) && seen.add(r.ids.join(',')));
|
||||
|
||||
const tbl = document.createElement('table');
|
||||
const head = tbl.insertRow();
|
||||
for (const h of ['line', 'audit cover%', 'hw $', 'garden HP', 'by hail', 'by rain', 'corners lost', 'vs bare bed'])
|
||||
{ const th = document.createElement('th'); th.textContent = h; head.appendChild(th); }
|
||||
|
||||
const results = [];
|
||||
for (const r of probes) {
|
||||
const g = gardenFor(r.ids, HARDWARE[2]); // rated shackles: hold it, isolate GEOMETRY
|
||||
results.push({ ids: r.ids.join(','), cover: +(r.cover * 100).toFixed(0), hw: r.hw, ...g });
|
||||
const tr = tbl.insertRow();
|
||||
const cells = [r.ids.join(','), `${(r.cover * 100).toFixed(0)}%`, `$${r.hw}`,
|
||||
g.hp.toFixed(1), g.byHail.toFixed(1), g.byRain.toFixed(1), String(g.cornersLost),
|
||||
`${(g.hp - bareRes.hp >= 0 ? '+' : '')}${(g.hp - bareRes.hp).toFixed(1)}`];
|
||||
cells.forEach((c, i) => { const td = tr.insertCell(); td.textContent = c;
|
||||
if (i === 7) td.className = (g.hp - bareRes.hp) > 15 ? 'ok' : (g.hp - bareRes.hp) > 5 ? 'warn' : 'bad'; });
|
||||
}
|
||||
const tr = tbl.insertRow();
|
||||
['BARE BED (no sail)', '—', '$0', bareRes.hp.toFixed(1), bareRes.byHail.toFixed(1), bareRes.byRain.toFixed(1), '—', '—']
|
||||
.forEach((c) => { const td = tr.insertCell(); td.textContent = c; td.className = 'warn'; });
|
||||
el('out').appendChild(tbl);
|
||||
|
||||
// Correlation between the audit's cover% and the sim's garden HP. If the audit
|
||||
// is telling the truth this is near 1; the QA pass says it is not.
|
||||
const xs = results.map((r) => r.cover), ys = results.map((r) => r.hp);
|
||||
const mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
|
||||
const mx = mean(xs), my = mean(ys);
|
||||
const cov = xs.reduce((s, x, i) => s + (x - mx) * (ys[i] - my), 0);
|
||||
const sx = Math.sqrt(xs.reduce((s, x) => s + (x - mx) ** 2, 0));
|
||||
const sy = Math.sqrt(ys.reduce((s, y) => s + (y - my) ** 2, 0));
|
||||
const r2 = (sx && sy) ? (cov / (sx * sy)) : NaN;
|
||||
const spread = Math.max(...ys) - Math.min(...ys);
|
||||
const bestGain = Math.max(...ys) - bareRes.hp;
|
||||
|
||||
el('note').textContent =
|
||||
`cover% → garden HP correlation: r = ${r2.toFixed(3)}\n` +
|
||||
`garden HP spread across every holdable line: ${spread.toFixed(1)} HP\n` +
|
||||
`best line vs bare bed: ${bestGain >= 0 ? '+' : ''}${bestGain.toFixed(1)} HP ` +
|
||||
`(a rig that costs money and holds every corner buys THIS much garden)\n` +
|
||||
`bare bed finished at ${bareRes.hp.toFixed(1)} HP — hail ${bareRes.byHail.toFixed(1)}, rain ${bareRes.byRain.toFixed(1)}`;
|
||||
|
||||
window.__probe = { site: siteName, storm: stormName, bare: bareRes, results, r: r2, spread, bestGain };
|
||||
document.title = `garden_probe — r=${r2.toFixed(2)}`;
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
el('note').className = 'bad';
|
||||
el('note').textContent = `garden_probe crashed: ${e.message}\n${e.stack || ''}`;
|
||||
window.__probe = { error: e.message };
|
||||
document.title = 'garden_probe — ERROR';
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user