Sprint 13 gate 1 RULINGS: p5 the clothesline post; the separation target, amended and PINNED
- backyard_01 gains p5 (5.5,-2) h2.6, honest steel NE of the bed: before it the yard could not beat tattered on the wild night at any price (covering quads need a fascia corner capped at 2.27 kN; the holding line ceilings at 61.3). Swept against the $80 wallet; position/height table in the site JSON. - Separation target adopted AMENDED (ruled on D's funnel-corrected picture): wild night, best buyable line FULL (>66) and WIN; bare bed LOSES (<50). Pinned as site data (backyard_01.separation names the $75 recipe p5=rated + p1/p2/p3=shackle) and FLOWN by a.test through the real chain: shop at START_BUDGET -> commit -> attach -> skyfx -> garden.js. Measured 68.3 FULL / 0 lost vs bare 35.7 (sep 32.6). - Full cover stays unbuyable (covering quads still pull 6.4-6.9 kN), the >45 m2 floor holds, and the ice night still refuses $80 (needs $105 - p1/p2 miss their shackles by 0.05 kN) - night 5 stays bank-funded canon. - New asserts: M reaches the real bus / is only advertised when real; anchor census now 13 (5 posts). Selftest 341/0/0.
This commit is contained in:
parent
7acee5437b
commit
482803a480
@ -97,8 +97,68 @@
|
||||
"than the 23-38 m2 rigs that survive unaided. a.test.js asserts the >45 m2 floor, so",
|
||||
"moving this inward goes red — that assert is the tension's only guard."
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "p5", "x": 5.5, "z": -2, "h": 2.6, "type": "post", "work": "cloth",
|
||||
"_why": [
|
||||
"SPRINT13 gate 1 — A's backyard ruling, the anchor C's diagnosis routed here. The old",
|
||||
"clothesline post: galvanised steel sunk in concrete by the back path, NE of the bed,",
|
||||
"shorter than the sail posts because it was never a sail post. Honest steel (ratingHint 1),",
|
||||
"8 m from the fascia, so the house trap keeps its whole lesson.",
|
||||
"",
|
||||
"WHY IT EXISTS, measured (probeA on C's garden_bench chain, wildnight, funnel-free yard):",
|
||||
"before it, the backyard could not beat TATTERED on the wild night at any price - every",
|
||||
"vertical-covering quad needs a house corner (fascia caps the best steel at 2.27 kN while",
|
||||
"those quads pull 3-7 kN -> tear in the settle), and the best HOLDING line p1..p4 ceilings",
|
||||
"at hp 61.3 through 25% cover. D played both halves through the real UI and confirmed to",
|
||||
"the decimal. Placement, not budget, not weather.",
|
||||
"",
|
||||
"WHY (5.5, -2) at h 2.6 and not somewhere kinder: the LOW top is the point - it twists the",
|
||||
"p1+p2+p3+p5 quad so it spills the gusts (small+twisted survives, DESIGN.md), and the",
|
||||
"position is swept against the $80 wallet:",
|
||||
" (4.5,-2) h2.6 -> p1 3.35 kN, needs RATED, line costs $90 DO NOT",
|
||||
" (5,-2.5) h2.8 -> p5 6.20, p1 3.75 -> $90 DO NOT",
|
||||
" (5.5,-2) h2.8 -> p2 3.08, p1 3.06 (4% margin on shackle) - legal but knife-edged",
|
||||
" (5.5,-2) h2.6 -> p5 4.88, p2 3.14, p1 2.91, p3 1.82 -> $75, hp 68.3 FULL <- here",
|
||||
" (6,-2) h2.6 -> shadow drops, hp 65.8 tattered DO NOT",
|
||||
"The separation block below pins the recipe; a.test flies it. Full cover STAYS unbuyable:",
|
||||
"the covering quads (p1+p2+p4+p5 and up) still pull 6.4-6.9 kN and still tear - the",
|
||||
"smallest full-cover quad stays >45 m2 (a.test's floor holds), so the tradeoff decision 2",
|
||||
"protects is intact. And the ICE night still refuses the $80 wallet: same line needs $105",
|
||||
"there (p1 3.25, p2 3.22 vs a shackle's 3.20 - the same few-hundredths miss as C's q3),",
|
||||
"so night 5 stays the night the bank pays for, exactly as ruled in SPRINT9."
|
||||
]
|
||||
} ],
|
||||
|
||||
"separation": {
|
||||
"_why": [
|
||||
"SPRINT13 gate 1.4 — A's ruling on C's proposed target, AMENDED to the numbers the real game",
|
||||
"measures (ruled on D's funnel-corrected picture, not C's original headline). C proposed:",
|
||||
"'on a wild night the best buyable line reads FULL (>66) and a bare bed reads DEAD (<33)'.",
|
||||
"Adopted in spirit, amended in both halves:",
|
||||
" - The bare half becomes 'a bare bed LOSES the night (hp < 50, the win line)'. Nowhere in",
|
||||
" the shipped game does a bare bed read DEAD on the wild night (35.7, tattered) — D's",
|
||||
" check — and tuning storm_02's damage budget until a slogan lands is exactly the fudge",
|
||||
" gate 1 exists to kill. The felt boundary is the WIN line, and 35.7 fails it honestly.",
|
||||
" - The FULL half binds THIS yard, because the shipped week's only wild night is here",
|
||||
" (week.js night 4). C's passing example was site_02, whose wild night the corner block",
|
||||
" never plays — and with its venturi live the example dies anyway (D's landmine).",
|
||||
"This block is the pin: the recipe below is the measured best $80 line, a.test flies it",
|
||||
"through the real chain (session -> commit -> attach -> skyfx) every run, and B's audit can",
|
||||
"read the same recipe instead of inventing one. A target nothing checks is decoration —",
|
||||
"the repo's oldest lesson, and the reason this is data and not a THREADS sentence."
|
||||
],
|
||||
"stormKey": "storm_02_wildnight",
|
||||
"tension": 1.0,
|
||||
"line": [
|
||||
{ "anchor": "p5", "hw": "rated shackle" },
|
||||
{ "anchor": "p1", "hw": "shackle" },
|
||||
{ "anchor": "p2", "hw": "shackle" },
|
||||
{ "anchor": "p3", "hw": "shackle" }
|
||||
],
|
||||
"heldMustExceed": 66,
|
||||
"bareMustLoseBelow": 50
|
||||
},
|
||||
|
||||
"shed": { "model": "shed_01_v1", "x": 11.8, "z": 6.2, "rotYDeg": -90 },
|
||||
"shedTable": { "model": "shed_table_v1", "x": 9, "z": 6, "rotYDeg": -90, "pickupNode": "pickup_anchor" },
|
||||
|
||||
@ -4,15 +4,21 @@
|
||||
*/
|
||||
|
||||
import * as THREE from '../../vendor/three.module.js';
|
||||
import { ANCHOR_TYPE, FIXED_DT, PHASES, STORM_LEN, YARD, checkContract, createStubWind } from '../contracts.js';
|
||||
import {
|
||||
ANCHOR_TYPE, FIXED_DT, HARDWARE, PHASES, START_BUDGET, STORM_LEN, YARD,
|
||||
checkContract, createStubWind,
|
||||
} from '../contracts.js';
|
||||
import { createWindField } from '../weather.core.js';
|
||||
import { createWorld, heightAt, loadSite, validateSite } from '../world.js';
|
||||
import { createCameraRig, spawnYawFor } from '../camera.js';
|
||||
import {
|
||||
CALM_STORM, accumulate, canPlayHere, createGame, createWindRouter, enterCommits,
|
||||
CALM_STORM, accumulate, applyMute, canPlayHere, createGame, createWindRouter, enterCommits,
|
||||
stormsToPreload, verdictFor,
|
||||
} from '../main.js';
|
||||
import { orderRing } from '../sail.js';
|
||||
import { createGarden } from '../garden.js';
|
||||
import { RiggingSession } from '../rigging.js';
|
||||
import { createSkyFx } from '../skyfx.js';
|
||||
import { SailRig, orderRing } from '../sail.js';
|
||||
import { loadStorm, createWind } from '../weather.js';
|
||||
import { createWeek, NIGHTS, nightAt, gradeFor, BROKE_BELOW, PAY } from '../week.js';
|
||||
import { createHud } from '../hud.js';
|
||||
@ -131,6 +137,36 @@ export default async function run(t) {
|
||||
assertEq(canPlayHere(() => ({})), true, 'a browser that answers nothing: let them in');
|
||||
});
|
||||
|
||||
t.test('M reaches the real bus when there is one, and is not advertised when there is not', () => {
|
||||
// SPRINT13 gate 3.3, closed by C's sky.setMute (lane/c 8a3dc32). The wiring
|
||||
// bug this pins is the one D named on rigging.setWorld: `sky.setMute?.(on)`
|
||||
// on a tree without the tap is a no-op that looks like a call, so a key the
|
||||
// splash advertises can silently do nothing on a public URL. applyMute is
|
||||
// the application as a value; the HUD half asserts the advertisement tracks
|
||||
// the same feature-detect in BOTH directions.
|
||||
const calls = [];
|
||||
const bus = { setMute: (v) => calls.push(v) };
|
||||
assertEq(applyMute(bus, true), true, 'a real bus takes the state');
|
||||
assertEq(applyMute(bus, false), true, 'both directions');
|
||||
assertEq(calls.join(','), 'true,false', 'and receives booleans, in order — this is what M re-applies across makeSky()');
|
||||
assertEq(applyMute({}, true), false, 'no tap: says so instead of pretending');
|
||||
assertEq(applyMute(null, true), false, 'no sky at all (pre-boot): still honest');
|
||||
|
||||
if (typeof document === 'undefined') return;
|
||||
const hud = createHud({ scene: new THREE.Scene() });
|
||||
try {
|
||||
assertEq(hud.comfortKeysHint(), 'P pause', 'no bus: the help line does not promise M');
|
||||
hud.setAudioMuteAvailable(true);
|
||||
assertEq(hud.comfortKeysHint(), 'P pause · M mute', 'bus landed: M appears by itself');
|
||||
hud.showSplash(() => {});
|
||||
assert((document.querySelector('#hud-card')?.textContent ?? '').includes('P · M'),
|
||||
'and the splash card lists it');
|
||||
hud.hideCard();
|
||||
hud.setAudioMuteAvailable(false);
|
||||
assertEq(hud.comfortKeysHint(), 'P pause', 'and it can go dark again — the detect is live, not a latch');
|
||||
} finally { hud.dispose(); }
|
||||
});
|
||||
|
||||
t.test('the spawn frame points at the garden with no pole through the player', () => {
|
||||
// The QA pass: "the boot camera puts a pole dead-centre through the player on
|
||||
// every single first impression." Measured, not eyeballed — these are the
|
||||
@ -845,11 +881,12 @@ export default async function run(t) {
|
||||
|
||||
// --- anchors -------------------------------------------------------------
|
||||
|
||||
t.test('yard offers 12 anchors: 3 house, 5 tree, 4 post', () => {
|
||||
t.test('yard offers 13 anchors: 3 house, 5 tree, 5 post', () => {
|
||||
const by = (type) => world.anchors.filter((a) => a.type === type).length;
|
||||
assertEq(by('house'), 3, 'house anchors');
|
||||
assertEq(by('tree'), dressed ? 5 : 2, 'tree anchors (branch_anchor_* arrive with dress())');
|
||||
assertEq(by('post'), 4, 'post anchors — p3 (SPRINT3 dec 2), p4 (SPRINT6 gate 1)');
|
||||
assertEq(by('post'), 5,
|
||||
'post anchors — p3 (SPRINT3 dec 2), p4 (SPRINT6 gate 1), p5 the clothesline post (SPRINT13 gate 1)');
|
||||
const ids = world.anchors.map((a) => a.id);
|
||||
assertEq(new Set(ids).size, ids.length, `anchor ids not unique: ${ids}`);
|
||||
});
|
||||
@ -977,6 +1014,104 @@ export default async function run(t) {
|
||||
`supposed to cost you a sail the storm can take`);
|
||||
});
|
||||
|
||||
// --- SPRINT13 gate 1.4: THE PINNED SEPARATION TARGET ----------------------
|
||||
//
|
||||
// The ruling (THREADS [A], on D's funnel-corrected picture): on the shipped
|
||||
// week's wild night — night 4, THIS yard — the best line START_BUDGET can buy
|
||||
// must read FULL (>66) and WIN, and a bare bed must LOSE (<50). The recipe is
|
||||
// DATA (site.separation) so B's audit reads the same one, and this flight is
|
||||
// the pin: real shop at the real budget, real commit()->attach() (D's
|
||||
// skipped-attach landmine — session.commit(rig) IS the attach path, verified
|
||||
// by C's probe2 agreement), real skyfx exposure, the real garden model out of
|
||||
// garden.js. Flown OUTSIDE t.test() because Suite.test cannot await — the
|
||||
// two async-test ghosts above are the precedent.
|
||||
//
|
||||
// Sway is frozen (balance.test's reasoning): the recipe is all posts, whose
|
||||
// real sway is constant anyway, so frozen here means "identical to the game",
|
||||
// not "conveniently calm".
|
||||
const sep = site.separation;
|
||||
let sepRun = null;
|
||||
if (sep && typeof document !== 'undefined') {
|
||||
try {
|
||||
const frozen = 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 trees = frozen.filter((a) => a.type === 'tree');
|
||||
const stormDef = await loadStorm(sep.stormKey);
|
||||
const calmDef = await loadStorm(CALM_STORM);
|
||||
|
||||
const session = new RiggingSession({ anchors: frozen }); // the REAL budget
|
||||
const shopLog = [];
|
||||
for (const { anchor } of sep.line) {
|
||||
const r = session.rig(anchor);
|
||||
if (!r.ok) shopLog.push(`rig ${anchor}: ${r.reason}`);
|
||||
}
|
||||
for (const { anchor, hw } of sep.line) {
|
||||
const want = HARDWARE.find((h) => h.name === hw);
|
||||
if (!want) { shopLog.push(`no hardware named "${hw}"`); continue; }
|
||||
const r = session.setHardware(anchor, want);
|
||||
if (!r.ok) shopLog.push(`setHardware ${anchor} ${hw}: ${r.reason}`);
|
||||
}
|
||||
session.setTension(sep.tension);
|
||||
const spent = START_BUDGET - session.budget;
|
||||
const rig = session.commit(new SailRig({ anchors: frozen, gridN: 10 }));
|
||||
|
||||
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 flyNight = (sail) => {
|
||||
const wind = createWind(stormDef);
|
||||
wind.setSheltersFromTrees(trees);
|
||||
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 garden = createGarden({ setPlants() {} }); // the model main.js flies
|
||||
const bed = world.gardenBed;
|
||||
const steps = Math.round(stormDef.duration / FIXED_DT);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const t2 = i * FIXED_DT;
|
||||
if (sail) sail.step(FIXED_DT, wind, t2);
|
||||
sky.step(FIXED_DT, t2, { sail });
|
||||
garden.step(FIXED_DT, sky.gardenHailExposure(bed, t2), sky.gardenExposure(bed, t2));
|
||||
}
|
||||
sky.dispose?.();
|
||||
return { hp: garden.hp, state: garden.state };
|
||||
};
|
||||
|
||||
const held = flyNight(rig);
|
||||
const lost = rig.corners.filter((c) => c.broken).length;
|
||||
const bare = flyNight(null);
|
||||
sepRun = { shopLog, spent, held, lost, bare };
|
||||
} catch (err) {
|
||||
sepRun = { err: String(err && err.stack || err) };
|
||||
}
|
||||
}
|
||||
|
||||
t.test('PINNED: the wild night separates — best buyable line FULL and winning, bare bed losing', () => {
|
||||
if (!sep) throw new Error('backyard_01 lost its separation block — the target is decoration again');
|
||||
if (typeof document === 'undefined') return t.skip('needs DOM (skyfx)');
|
||||
if (sepRun.err) throw new Error(`separation flight died: ${sepRun.err}`);
|
||||
assertEq(sepRun.shopLog.join('; '), '', 'the shop sold the whole recipe at the real budget');
|
||||
assert(sepRun.spent <= START_BUDGET, `recipe costs $${sepRun.spent} — must be buyable night 1`);
|
||||
assertEq(sepRun.lost, 0, 'the pinned line HOLDS the wild night — 0/4 lost');
|
||||
assert(sepRun.held.hp > sep.heldMustExceed,
|
||||
`held bed reads ${sepRun.held.hp.toFixed(1)} — the target is FULL (>${sep.heldMustExceed}). ` +
|
||||
`Measured 68.3 at landing; a red here means a retune moved the wild night and the ` +
|
||||
`separation ruling needs re-litigating in THREADS, not a quiet threshold bump.`);
|
||||
assertEq(sepRun.held.state, 'full', 'and the plants SAY full — the state the player sees');
|
||||
assertLess(sepRun.bare.hp, sep.bareMustLoseBelow,
|
||||
`bare bed must LOSE the night (<${sep.bareMustLoseBelow}) — measured 35.7 at landing`);
|
||||
// The felt spread, stated so a red prints the whole story:
|
||||
assert(sepRun.held.hp - sepRun.bare.hp > 25,
|
||||
`separation ${(sepRun.held.hp - sepRun.bare.hp).toFixed(1)} — rigging must matter by a full state, ` +
|
||||
`not a garden-bonus margin (measured 32.6 at landing)`);
|
||||
});
|
||||
|
||||
t.test('sway() returns an absolute position, not an offset', () => {
|
||||
// If sway ever regresses to returning an offset, the returned point lands
|
||||
// near the origin instead of near the anchor, and Lane B's cloth corners
|
||||
|
||||
Loading…
Reference in New Issue
Block a user