Default stays the pinned 0.30 shade cloth (asserted: omitting porosity === explicit 0.30 to the digit, hp and every corner peak), so no pinned number moves. porosity 0 flies genuinely heavier (per-corner strictly greater, measured 1.23x average on the synthetic funnel yard). C's storm_06 gate-3.1 membrane-vs-cloth pin and B's membrane-balance tables ride this seam. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
183 lines
9.9 KiB
JavaScript
183 lines
9.9 KiB
JavaScript
/**
|
||
* gardenfly.js — fly a rig line through the REAL scoring chain and return the
|
||
* garden verdict the sim would hand the invoice. [Lane B, SPRINT13 gate 1.2]
|
||
*
|
||
* This module exists because the audit's old ranking quantity — static cover%
|
||
* of the taut attach-shape — predicted the wild night's garden BACKWARDS
|
||
* (r = −0.42, THREADS [B] gate-1 evidence): it told three people a $20 rig was
|
||
* bad while the sim paid it +$97. The quantity that scores is the FLOWN
|
||
* vertical (hail) shadow through the storm: hail is 5.0 against rain's 0.25
|
||
* (garden.js), stones fall near-vertical, and a sail either blocks the hail
|
||
* over the bed or blocks nothing that matters. Only flying the real chain
|
||
* measures that, so this is the audit's scoring engine now; cover% is demoted
|
||
* to a geometry diagnostic in both front-ends.
|
||
*
|
||
* THE chain, one copy of it, shared by audit.html and garden_probe.html:
|
||
*
|
||
* windForSite(stormDef, siteDef, anchors) (C's shared builder —
|
||
* venturi + tree shelters, byte-for-byte main.js's site-load wiring)
|
||
* → SailRig.attach(ids, hw, tension) (the real attach)
|
||
* → rig.step + sky.step(dt, t, {sail: rig}) (main.js's exact call)
|
||
* → createGarden(...).step(dt, hailExp, rainExp) (A's garden.js, no copy)
|
||
*
|
||
* NO calm settle, deliberately: in the real game the rig does not exist before
|
||
* commit, and commit→attach→storm is one keypress (the rig.t fix's own words),
|
||
* so the attach transient flies under STORM wind and counts. C measured the
|
||
* phantom settle's skew and removed it from the bench the same day.
|
||
*
|
||
* Landmines this file exists to never re-arm (each earned this sprint):
|
||
*
|
||
* · D's skipped-attach trap (skyfx.js:803): a harness that never runs
|
||
* attach() scores EVERY rig as bare — the bare-bed constant lies politely.
|
||
* flyGarden throws if the rig it built has no shadow mesh.
|
||
* · The venturi lives in the SITE def, not the storm def. THREE harnesses
|
||
* shipped that bug (my Sprint-11 audit, C's bench, D's first harness half);
|
||
* windForSite is the one door now, and the selftest mutation-checks that
|
||
* the funnel reaches this module's wind.
|
||
* · The FROZEN TREE (C's landmine 2): remapping anchors to `sway: () => pos`
|
||
* freezes the gum tree whose sway is the dynamic load a tr1 rig eats —
|
||
* q4 read 1.02 frozen vs 1.24 live. Callers hand this module
|
||
* world.anchors THEMSELVES and re-point the world's wind proxy via `use`
|
||
* so the sway closures sample the storm that is actually flying.
|
||
* · THE MARGIN RULE (C's residual): even corrected, benches under-read the
|
||
* real UI's peaks by ~5–15% on site_02 (cause unfound, in the pool). Until
|
||
* it is found, any corner within 15% of its effective rating is scored
|
||
* "breaks in the game" — a flight that holds on paper with q4 at 1.24 vs
|
||
* 1.2 is D's 39.8 TATTERED in play, not the bench's 91.9 FULL. flyGarden
|
||
* reports `marginal` and callers must not print PASS over it.
|
||
*
|
||
* Per-corner peaks are labelled from `corners[k].anchorId`, never input order —
|
||
* C's label-permutation warning (attach reorders picks into ring order).
|
||
*
|
||
* Browser-only by necessity: skyfx needs `document` (node throws), which is
|
||
* why audit.mjs (node) prints winnability and POINTS HERE for garden truth.
|
||
*/
|
||
|
||
import { SailRig } from '../../web/world/js/sail.js';
|
||
import { windForSite } from '../../web/world/js/weather.js';
|
||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||
import { createGarden } from '../../web/world/js/garden.js';
|
||
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
|
||
import { AUDIT } from './sweep.js';
|
||
|
||
// The margin rule's knob is AUDIT.MARGIN — sweep.js holds the ONE copy and
|
||
// both audit engines read it (a local copy here would be the exact drift this
|
||
// sprint spent itself killing).
|
||
|
||
/** Shop hardware by its contracts.js name ("rated shackle") — throws on a
|
||
* stranger, because a typo'd tier silently becoming a carabiner is exactly
|
||
* the setHardware() lesson from gate 3. */
|
||
export function hardwareByName(name) {
|
||
const hw = HARDWARE.find((h) => h.name === name);
|
||
if (!hw) throw new Error(`unknown hardware "${name}" — the shop sells: ${HARDWARE.map((h) => h.name).join(', ')}`);
|
||
return hw;
|
||
}
|
||
|
||
/**
|
||
* Fly one line (or a bare bed) and return the garden verdict.
|
||
*
|
||
* @param {object} o
|
||
* @param {Array} o.anchors world.anchors THEMSELVES (live sway), or resolved
|
||
* {id, type, pos, sway, ratingHint} for synthetic yards
|
||
* @param {object} o.bed garden bed rect {x, z, w, d}
|
||
* @param {object} o.stormDef storm JSON to fly
|
||
* @param {object} [o.siteDef] site JSON — its wind.venturi is half the yard's
|
||
* weather; omit ONLY for a site with no funnel
|
||
* @param {function} [o.use] yard's wind-proxy re-pointer (C's bench pattern):
|
||
* called with this flight's wind so world.anchors'
|
||
* tree-sway closures sample the storm actually flying
|
||
* @param {Array} [o.ids] 4 anchor ids; null/omitted = bare bed (the control)
|
||
* @param {Array|object} [o.hw] hardware per pick (aligned with ids), or one tier for all 4
|
||
* @param {number} [o.tension]
|
||
* @param {number} [o.porosity] fabric flown. DEFAULT 0.30 (shade cloth — the
|
||
* fabric a competent player flies, same as sweep.js,
|
||
* and every pinned number to date was measured on
|
||
* it); pass 0 for membrane. SPRINT16: the F key is
|
||
* live and storm_06's whole bet is membrane-vs-
|
||
* cloth, so the audit engine must be able to fly
|
||
* both — C's gate-3.1 pin and B's membrane-balance
|
||
* tables ride this parameter.
|
||
* @returns {{ hp, state, byHail, byRain, cornersLost, peaks, marginal, cost }}
|
||
*/
|
||
export function flyGarden({ anchors, bed, stormDef, siteDef = null, use = null, ids = null, hw = null, tension = 1.0, porosity = 0.30 }) {
|
||
const wind = windForSite(stormDef, siteDef, anchors);
|
||
use?.(wind);
|
||
|
||
let rig = null, cost = 0;
|
||
if (ids) {
|
||
const hwArr = Array.isArray(hw) ? hw : Array(4).fill(hw ?? hardwareByName('rated shackle'));
|
||
cost = hwArr.reduce((s, h) => s + (h.cost || 0), 0);
|
||
rig = new SailRig({ anchors, gridN: 10, porosity }).attach(ids, hwArr, tension);
|
||
// D's skipped-attach trap: a rig without its shadow mesh scores as a bare
|
||
// bed and the number LOOKS plausible. Refuse to fly it.
|
||
if (!rig.rigged || !rig.pos || !rig.tris || !rig.tris.length) {
|
||
throw new Error('flyGarden: rig is not attached (no shadow mesh) — a bypassed attach scores ' +
|
||
'every rig as bare (skyfx.js:803, D\'s landmine). Fix the harness, do not trust the number.');
|
||
}
|
||
}
|
||
|
||
const sky = createSkyFx({ wind, night: true });
|
||
const garden = createGarden({ setPlants() {} }); // THE model — garden.js, main.js's own
|
||
|
||
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 }); // main.js's exact third param
|
||
garden.step(FIXED_DT, sky.gardenHailExposure(bed, t), sky.gardenExposure(bed, t));
|
||
}
|
||
sky.dispose?.();
|
||
|
||
// labelled from the rig's OWN anchorId — attach reorders picks into ring
|
||
// order, and a peak quoted against input order swaps corners (C's warning).
|
||
const peaks = rig ? rig.corners.map((c) => {
|
||
const eff = c.hw.rating * (c.anchor.ratingHint ?? 1);
|
||
return { id: c.anchorId, peakN: Math.round(c.peakLoad), effN: Math.round(eff),
|
||
headroom: +(1 - c.peakLoad / eff).toFixed(3) };
|
||
}) : [];
|
||
|
||
return {
|
||
hp: +garden.hp.toFixed(1),
|
||
state: garden.state, // 'full' | 'tattered' | 'dead' — garden.js's own thresholds
|
||
byHail: +garden.damage.hail.toFixed(1),
|
||
byRain: +garden.damage.rain.toFixed(1),
|
||
cornersLost: rig ? rig.corners.filter((c) => c.broken).length : 4,
|
||
peaks,
|
||
// C's margin rule: corners that held on paper but sit within MARGIN of
|
||
// their effective rating — in the real game these break. A prediction
|
||
// carrying names here is NOT a clean hold, whatever the hp says.
|
||
marginal: peaks.filter((p) => !rig.corners.find((c) => c.anchorId === p.id).broken && p.headroom < AUDIT.MARGIN)
|
||
.map((p) => p.id),
|
||
cost,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Judge a site against its pinned `separation` block (A's gate-1.4 ruling,
|
||
* backyard_01.json): fly the pinned recipe AND a bare bed on the block's own
|
||
* storm, and answer the only question the target asks — does the best line the
|
||
* budget buys read FULL and WIN, and does a bare bed LOSE the night.
|
||
*
|
||
* `heldWin` is main.js's win rule (hp >= 50 && corners lost < 2) — EXACTLY the
|
||
* rule, so this stays in lockstep with a.test's pin of the same block.
|
||
* `heldClean` is the margin rule's opinion, kept SEPARATE from `ok` on
|
||
* purpose: the pinned target is A's ruling and this module doesn't get to
|
||
* overrule it with a working rule whose cause is still unfound — but a pinned
|
||
* line carrying a corner inside the 15% band is a knife edge the front-end
|
||
* must SAY (it's a flag for A/D, not a verdict).
|
||
*
|
||
* @returns {{ held, bare, heldOk, heldWin, heldClean, bareOk, ok }}
|
||
*/
|
||
export function flySeparation({ anchors, bed, separation, stormDef, siteDef = null, use = null }) {
|
||
const ids = separation.line.map((l) => l.anchor);
|
||
const hw = separation.line.map((l) => hardwareByName(l.hw));
|
||
const tension = separation.tension ?? 1.0;
|
||
const held = flyGarden({ anchors, bed, stormDef, siteDef, use, ids, hw, tension });
|
||
const bare = flyGarden({ anchors, bed, stormDef, siteDef, use });
|
||
const heldOk = held.hp > separation.heldMustExceed;
|
||
const heldWin = held.hp >= 50 && held.cornersLost < 2;
|
||
const heldClean = held.marginal.length === 0;
|
||
const bareOk = bare.hp < separation.bareMustLoseBelow;
|
||
return { held, bare, heldOk, heldWin, heldClean, bareOk, ok: heldOk && heldWin && bareOk };
|
||
}
|