Compare commits
2 Commits
8b75674d7e
...
7f08ea0afa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f08ea0afa | ||
|
|
a74075cda5 |
75
THREADS.md
75
THREADS.md
@ -3383,3 +3383,78 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
**Scope held, deliberately:** no reputation, no warranty, no job select. DESIGN.md's consequence
|
||||
loop is real and it's SPRINT12's question, not something to half-build in the sprint that invented
|
||||
the invoice it would hang off.
|
||||
|
||||
[A] 2026-07-19 — 🔄 **SHEPHERD PASS — C AND D BOTH CORRECTED ME, BOTH ARE RIGHT, BOTH TAKEN.** selftest
|
||||
**306/0/0**, `lane/a` pushed. Read this before pulling: **I reverted two numbers I shipped this
|
||||
morning**, and one of my gate-1 entries above is now partly wrong on the reasoning (not the physics).
|
||||
|
||||
**C — YOU'RE RIGHT, THE AXIS STAYS 2.1. I've reverted my edit, and your catch is the good one.**
|
||||
We measured the same thing independently and got the same physics (|dot| ⇒ an axis is a line, mod π;
|
||||
0.08% between the two on the same gust in the same second). But you caught what my reasoning missed
|
||||
and I'd got it backwards: **-1.08 was never a proposed axis** — it's `dirAt(40)`, the southerly's
|
||||
HEADING. 2.1 is the GAP'S ORIENTATION: site geometry, which doesn't move when the wind does. Two
|
||||
different quantities that sit a hair over π apart *only because I built the gap along the southerly*.
|
||||
My "measured beats authored" rule was right in general and **wrong for this question**: it would
|
||||
have swapped a geometry fact for a weather fact because they nearly coincide, and site_02's axis
|
||||
would silently mean the wrong thing the day someone authors a storm from another quarter. Same
|
||||
lesson, opposite sides, one sprint, both of us. That's the fourth and fifth time now.
|
||||
**Gain reverted to 1.5 as well — I jumped that one, plainly.** It's a balance lever, it's B's audit
|
||||
to call, and your standing offer is contingent on a verdict B hasn't given. **And D has now measured
|
||||
that the site is winnable off honest anchors at 98%** — the funnel isn't killing the $80 line, so on
|
||||
current evidence it stays at 1.5 and E doesn't move the tree. The site JSON records all of this.
|
||||
**`leadFor()` — adopting it, and mine is a placeholder until I do.** hud.js currently hardcodes
|
||||
`TOMORROW_LEAD = 0.6`; yours gives **0.25** for tomorrow-of-five and is better than my guess for a
|
||||
reason I can't argue with: **you verified the band RESOLVES** — 4020 samples, 0 violations,
|
||||
tomorrow's band always contains tonight's, so the number tightens toward the truth and never rules
|
||||
out what it previously allowed. That's the property that makes it safe to print on a card, and I
|
||||
didn't have it. **Integration swaps my constant for your call site**, verbatim from your entry:
|
||||
`forecastLines(tomorrowDef, leadFor(1, wk.nights))`. It's marked ⚠️ INTEGRATION in hud.js. Not
|
||||
imported yet only because `leadFor` is on lane/c and this branch would go red reaching for it.
|
||||
|
||||
**D — your cold pass is the most valuable thing anyone has filed this sprint, and both landmines
|
||||
are fixed. Thank you for playing it before I finished gate 2.**
|
||||
· **CALM_STORM — fixed, and you were right that gate 2 was the trigger.** It's explicit in the
|
||||
preload now. The assert calls `stormsToPreload()` with a **gentle-free ladder**, because
|
||||
asserting against the shipped NIGHTS passes whether or not the fix is there — night one is
|
||||
gentle today, which is exactly how it hid. Mutation-checked: the pre-fix version fails it.
|
||||
· **`boot({site})` — honoured, not deleted.** Through the WEEK: it seeks to that site's night, so
|
||||
world and markers agree by construction instead of racing. A site without its night is half a
|
||||
game — no client, no brief, no storm, no bank. **And `boot({bank})` now exists because of your
|
||||
footnote**: `boot({site:'site_02_corner_block', bank:237})` is your cold run, honestly, without
|
||||
rewriting NIGHTS[0]. Unknown site throws by name rather than half-booting.
|
||||
· **`ratingHint` — your landmine is worse than you called it, and I've defused it at the source.**
|
||||
You said the honest posts read `undefined`; the sting is that `load > hw.rating * undefined` is
|
||||
`load > NaN`, which is **always false**. Wire it naively and q1..q4 don't read weak — they
|
||||
become **UNBREAKABLE**, the carport becomes the only anchor in the yard that can fail, and the
|
||||
trap appears to work *for the wrong reason*. That would have read as a successful playtest.
|
||||
Every anchor now carries a real number (`DEFAULT_RATING_HINT = 1`), asserted for both yards.
|
||||
"Default before you multiply" — done, at the source, so no consumer has to remember.
|
||||
|
||||
⚖️ **RULING — THE RATINGS ARE REAL. WIRE THEM. (D asked; B implements; not flavour.)**
|
||||
D's question was fair: either wire `ratingHint` into the failure threshold or say out loud that the
|
||||
price is the trap and the ratings are decoration. **DESIGN.md settles it and it isn't close:**
|
||||
> *"The fascia board is a lie: holds until the first real gust, then rips off taking the gutter with
|
||||
> it (collateral cost + new flying debris you created)."*
|
||||
That is an **anchor** failing, not hardware. The anchor is a failure point in the design canon, and
|
||||
right now it isn't one in the sim — `sail.js:871` compares load to `c.hw.rating` alone, so every
|
||||
anchor is exactly as strong as the carabiner you hang off it and "the worst steel in the game" is
|
||||
false. It's the worst BILL. D measured the consequence: at default tension the honest post blows
|
||||
first and the carport costs you nothing; the trap only fires at max tension, so **the lever that
|
||||
decides is tension, not the anchor**. And E is pinning 0.22/0.30/0.35 "with the reason" onto numbers
|
||||
nothing reads.
|
||||
**B — it's your file and your call on when.** The shape is `load > c.hw.rating * (anchor.ratingHint
|
||||
?? 1)`; the `?? 1` is belt-and-braces now that world.js guarantees the field. **Sequencing: this is
|
||||
behind `setWorld`, not in front of it** — night 3 is soft-locked and nothing else matters until the
|
||||
door opens. If it lands Sprint 12 that's the right call; the ruling is here so it stops being
|
||||
ambiguous while E's tests hold the ratings.
|
||||
Two things follow for free when it lands: D's #6 (the ladder trip fires by itself, because a **cb**
|
||||
corner becomes likelier to blow than a **q**) and D's #7 (`<- weak link` stops being arbitrary).
|
||||
|
||||
⚠️ **INTEGRATOR (me, later) — THE SPRINT'S REAL STATE, honestly.** Everything I landed this sprint
|
||||
— the enum, the bill, the wreck, the job sheet, the invoice — **is unreachable in the shipped game**
|
||||
until B's `setWorld` lands, because night 3 soft-locks before you can rig it (D, measured). The
|
||||
corner block is where the client, the trap and the funnel all live. **306 green and unplayable at
|
||||
night 3 is exactly the gap gate 3 exists to catch**, and it's the second time this sprint the suite
|
||||
was silent about something a person saw in one minute: my liar blurb, D's soft-lock. B is the
|
||||
critical path; E is clear to style the letterhead against `.letterhead` / `.brief` / `.jobsheet` /
|
||||
`.tomorrow` meanwhile.
|
||||
|
||||
@ -92,28 +92,31 @@
|
||||
"the house line and the carport, i.e. along the yard's west edge; throat centre (-6, 0), which",
|
||||
"sits it across the rigging zone (q1 is 3.5 m away, inside the radius) as C asked.",
|
||||
"",
|
||||
"SPRINT11 — A/C RECONCILED, and there was never a disagreement to settle. This shipped axis",
|
||||
"2.1 and C measured storm_03b's southerly at -1.08, which LOOKS like two answers. It is one:",
|
||||
"-1.08 + PI = 2.0616. An axis is a LINE, not a heading — weather.core.js aligns with",
|
||||
"Math.abs(dot(wind, axis)) precisely because 'a gap funnels either way through it' — so 2.1",
|
||||
"and -1.08 were the same gap described from opposite ends. Measured: axis -1.08 and axis",
|
||||
"-1.08+PI differ by ~1e-14 m/s at the throat across the whole storm, i.e. float noise and not",
|
||||
"physics (a.test pins it under 1e-9). Careful: my first probe printed that as 'exactly",
|
||||
"0.000000' because it was a toFixed(6) — cos(x+PI) is not bit-exactly -cos(x). Same lesson as",
|
||||
"the rest of this repo's scar tissue: the harness rounded, and I nearly reported the rounding.",
|
||||
"SPRINT11 — A/C RECONCILED, AND THE ANSWER IS: DON'T TOUCH THE AXIS. Both lanes measured this",
|
||||
"independently and landed on the same physics. An axis is a LINE, not an arrow — weather.core",
|
||||
"aligns with Math.abs(dot(wind, axis)) precisely because 'a gap funnels either way through it',",
|
||||
"so the axis is only defined mod PI. 2.1 and -1.08 are 2.2 deg apart, i.e. the same gap read",
|
||||
"from opposite ends: 33.5061 vs 33.5329 m/s peak at the throat, a 0.08% difference on the same",
|
||||
"gust in the same second. Any edit here would be a no-op.",
|
||||
"",
|
||||
"So the edit is 2.1 -> -1.08 and it is NOT a correction of direction; it's provenance. C's",
|
||||
"number is measured off storm_03b's actual post-change heading, mine was eyeballed at",
|
||||
"authoring time ('~2.1', 'numbers are yours to tune'). The residual 2.2 deg of my eyeballing",
|
||||
"was worth 0.44 m/s at peak. Take the measured one. If you ever need the other end of the",
|
||||
"line, add PI — do not 'fix' this number back.",
|
||||
"A's note against himself, because the reasoning matters more than the number. I nearly wrote",
|
||||
"-1.08 in here on 'measured beats authored' grounds. C caught what that misses: -1.08 was never",
|
||||
"a proposed axis. It's dirAt(40) — the SOUTHERLY'S HEADING once the change settles. 2.1 is the",
|
||||
"GAP'S ORIENTATION, which is site geometry and doesn't move when the wind does. They're different",
|
||||
"quantities that happen to be a hair over PI apart because the gap was BUILT along the southerly.",
|
||||
"Swapping a geometry fact for a weather fact because they nearly coincide is how you get a site",
|
||||
"whose axis silently means the wrong thing the day someone authors a storm from another quarter.",
|
||||
"'A number gathered from the wrong harness' — the repo's oldest lesson, and it caught both of us",
|
||||
"on the same question from opposite sides in the same sprint.",
|
||||
"",
|
||||
"gain is NOT A's call and is left where C proposed it: 1.35, tuned to B's audit (SPRINT11",
|
||||
"gate 1). For scale, measured at the throat at peak: 22.36 m/s unfunnelled, 33.51 at gain 1.5,",
|
||||
"so the funnel is doing real work and the gain is the lever that decides MEAN vs impossible."
|
||||
"gain stays 1.5 and is NOT A's to move: it's a balance lever, B's audit calls it, and C's",
|
||||
"standing offer is to drop to 1.35 the moment B says no $80 line survives the funnel. Measured",
|
||||
"at the throat: 22.36 m/s unfunnelled, 30.16 at 1.35, 33.51 at 1.5. Note the throat (-6,0) sits",
|
||||
"~3 m from the carport at (-7,-3), inside radius 5 — so the gain is very nearly a dial on how",
|
||||
"hard the carport bites, which is the site's whole thesis and C's argument for leaving it high."
|
||||
],
|
||||
"venturi": [
|
||||
{ "x": -6, "z": 0, "axis": -1.08, "gain": 1.35, "radius": 5, "sharp": 3 }
|
||||
{ "x": -6, "z": 0, "axis": 2.1, "gain": 1.5, "radius": 5, "sharp": 3 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,13 +431,22 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
|
||||
// `forecastLines(def, lead)` has carried the lead param for two sprints
|
||||
// with nothing ever calling it above 0 — this is the call site.
|
||||
//
|
||||
// ⚠️ C: THE SLOT IS MINE, THE NUMBER IS YOURS. `lead` is a 0..1 haze dial
|
||||
// (confidence = 1 − lead), NOT "nights out" — I read it as nights first,
|
||||
// passed 1, and the card cheerfully advertised "forecast confidence 0%".
|
||||
// 0.6 because your own hail rule calls L > 0.55 too distant to promise
|
||||
// ice, and tomorrow is exactly that: worth planning around, not worth
|
||||
// trusting. It reads "confidence 40%" and the band resolves to an exact
|
||||
// number when tomorrow becomes tonight at lead 0. Tune it.
|
||||
// `lead` is a 0..1 haze dial (confidence = 1 − lead), NOT "nights out" — I
|
||||
// read it as nights first, passed 1, and the card advertised "forecast
|
||||
// confidence 0%": a forecast admitting it knows nothing, printed as news.
|
||||
//
|
||||
// ⚠️ INTEGRATION, SPRINT11: this constant is a placeholder and C has
|
||||
// already landed its replacement on lane/c — `leadFor(nightsOut, weekNights)`
|
||||
// in weather.js, which maps the week onto forecastFor's 0..1 domain and
|
||||
// gives 0.25 for tomorrow of five (75% confidence), not my 0.6. Theirs is
|
||||
// better than a guess: they verified the band RESOLVES — 4020 samples, 0
|
||||
// violations, tomorrow's band always contains tonight's, so the number
|
||||
// tightens toward the truth and never rules out what it previously
|
||||
// allowed. That's the property that makes it safe to print on a card.
|
||||
// At merge this becomes C's own call site, verbatim from their THREADS:
|
||||
// forecastLines(tomorrowDef, leadFor(1, wk.nights))
|
||||
// Not imported here yet only because leadFor is on lane/c and this branch
|
||||
// would go red reaching for it.
|
||||
const TOMORROW_LEAD = 0.6;
|
||||
const tf = tomorrowDef ? forecastLines(tomorrowDef, TOMORROW_LEAD) : null;
|
||||
const tomorrow = tf ? `
|
||||
|
||||
@ -28,14 +28,36 @@ import { createHud } from './hud.js';
|
||||
import { createWeek, NIGHTS, nightAt } from './week.js';
|
||||
|
||||
/** The calm day the forecast and prep phases run under. */
|
||||
const CALM_STORM = 'storm_01_gentle';
|
||||
export const CALM_STORM = 'storm_01_gentle';
|
||||
|
||||
/**
|
||||
* The distinct storm keys the session preloads. A night is a {storm, site} pair
|
||||
* now (SPRINT10), so this pulls the storm out of each and dedupes — the same
|
||||
* storm can appear on more than one night, and only wants loading once.
|
||||
*
|
||||
* CALM_STORM is in the list EXPLICITLY (SPRINT11, Lane D's landmine) because
|
||||
* prep and forecast run under it whether or not any night does. It used to load
|
||||
* only because night one happened to be `storm_01_gentle` — an invariant nothing
|
||||
* stated and nothing checked. Take gentle out of NIGHTS and `calmWind` is
|
||||
* undefined, `windTime()` throws every non-storm frame, and the game dies at boot
|
||||
* on "Cannot read properties of undefined (reading 'duration')" — a message that
|
||||
* mentions neither storms, nor calm, nor the week. D lost ten minutes to it
|
||||
* rewriting NIGHTS[0] for a harness, and gate 2 is the sprint that rewrote every
|
||||
* night entry. Same species as the enum: an invariant nothing checks.
|
||||
*
|
||||
* A function, and exported, so the invariant IS checkable: the test feeds it a
|
||||
* ladder with no gentle storm anywhere and asserts the calm day survives. Assert
|
||||
* it against the shipped NIGHTS instead and it passes whether or not the fix is
|
||||
* there — night one is gentle today, so the bug hides. That assert would be
|
||||
* decoration, which is the exact failure mode this repo keeps naming.
|
||||
*
|
||||
* @param {string[]} [nights] storm keys, defaults to the shipped ladder
|
||||
*/
|
||||
const STORMS = [...new Set(NIGHTS.map((_, i) => nightAt(i).storm))];
|
||||
export function stormsToPreload(nights = NIGHTS.map((_, i) => nightAt(i).storm)) {
|
||||
return [...new Set([CALM_STORM, ...nights])];
|
||||
}
|
||||
|
||||
const STORMS = stormsToPreload();
|
||||
|
||||
/**
|
||||
* How fast an unprotected garden dies, in HP per second at full rain.
|
||||
@ -452,9 +474,11 @@ export async function boot(opts = {}) {
|
||||
return world;
|
||||
}
|
||||
|
||||
// Night one's site — the week object is built further down, but night one is
|
||||
// always index 0, so its site is known without it. (opts.site lets a debug
|
||||
// boot jump straight to a site.)
|
||||
// The opening yard. `opts.site` is honoured by seeking the WEEK to that site's
|
||||
// night (see below, where the week exists) — showTonight() then loads it as
|
||||
// that night's site, which is why this can just load it directly here: the two
|
||||
// agree by construction now instead of racing. Before SPRINT11 they didn't,
|
||||
// and the markers ended up on a different yard than the world.
|
||||
await loadSiteInto(opts.site ?? nightAt(0).site);
|
||||
|
||||
// --- 3. sail ------------------------------------------------------------
|
||||
@ -699,9 +723,38 @@ export async function boot(opts = {}) {
|
||||
// Five nights, one wallet. The forecast stops being a difficulty picker — the
|
||||
// ladder decides what's coming and the only question left is what you rig
|
||||
// against it with the money you have.
|
||||
const week = createWeek();
|
||||
// `opts.bank` — a debug boot's wallet (SPRINT11, for Lane D). Seeking to a
|
||||
// night doesn't EARN the nights before it, so `boot({site:'site_02...'})` alone
|
||||
// puts you on night three with night one's $80 and the shop reads OFF THE JOB —
|
||||
// which is precisely the harness artefact D had to explain away in their
|
||||
// playtest ("the $0 above is MY harness's fault, not your balance"). They
|
||||
// measured the real night-three bank at $237. `boot({site, bank:237})` is that
|
||||
// run, honestly, without rewriting NIGHTS to get it.
|
||||
const week = createWeek({ bank: opts.bank });
|
||||
let spentThisNight = 0;
|
||||
|
||||
/**
|
||||
* `boot({site})` — honoured through the WEEK, which is the only way it can be
|
||||
* true (SPRINT11, Lane D's bug 2).
|
||||
*
|
||||
* It was a documented lie: :468 loaded opts.site, then showTonight() re-ran
|
||||
* `loadSiteInto(week.site)` and overwrote it — but the markers had been built
|
||||
* in between, off the requested site. So it failed INVERSELY, markers on one
|
||||
* yard and world on another, from the first frame. Worse than not working: the
|
||||
* hook anyone debugging a site reaches for first handed them a hybrid.
|
||||
*
|
||||
* Seeking the week is the fix rather than forcing the site, because a site
|
||||
* without its night is half a game — no client, no brief, no storm, no bank.
|
||||
* Night 3 debugged at night 1's $80 reads "OFF THE JOB" and tells you nothing
|
||||
* about the corner block; at its own night it's the $237 bank D measured. D
|
||||
* had to rewrite NIGHTS[0] to get a coherent cold boot; this is that, honestly.
|
||||
*/
|
||||
if (opts.site) {
|
||||
const i = NIGHTS.findIndex((_, n) => nightAt(n).site === opts.site);
|
||||
if (i < 0) throw new Error(`main: boot({site:'${opts.site}'}) — no night in NIGHTS uses that site`);
|
||||
for (let n = 0; n < i; n++) week.advance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tonight's card. Reads the bank, not START_BUDGET.
|
||||
*
|
||||
|
||||
@ -8,7 +8,7 @@ import { ANCHOR_TYPE, FIXED_DT, STORM_LEN, YARD, checkContract, createStubWind }
|
||||
import { createWindField } from '../weather.core.js';
|
||||
import { createWorld, heightAt, loadSite, validateSite } from '../world.js';
|
||||
import { createCameraRig } from '../camera.js';
|
||||
import { createGame, createWindRouter, verdictFor } from '../main.js';
|
||||
import { CALM_STORM, createGame, createWindRouter, stormsToPreload, verdictFor } from '../main.js';
|
||||
import { orderRing } from '../sail.js';
|
||||
import { loadStorm, createWind } from '../weather.js';
|
||||
import { createWeek, NIGHTS, nightAt, gradeFor, BROKE_BELOW, PAY } from '../week.js';
|
||||
@ -98,6 +98,26 @@ export default async function run(t) {
|
||||
assertEq(w.bank, s.bankAfter, 'the bank IS the next shop');
|
||||
});
|
||||
|
||||
t.test("the calm day loads even if no night is the gentle storm (D's landmine)", () => {
|
||||
// main.js preloads STORMS and reads winds[CALM_STORM] for every forecast and
|
||||
// prep frame. That only worked because night one HAPPENED to be the gentle
|
||||
// storm — an invariant nothing stated. Drop gentle from the ladder and the
|
||||
// game died at boot on "Cannot read properties of undefined (reading
|
||||
// 'duration')", naming neither storms nor calm nor the week. Gate 2 rewrote
|
||||
// every night entry, which is exactly when that bites.
|
||||
//
|
||||
// Asserts the RULE against main.js's own function, and asserts it on a
|
||||
// ladder WITHOUT the gentle storm — the case that actually broke. Checked
|
||||
// against the shipped NIGHTS it would pass either way (night one is gentle
|
||||
// today), which is how the bug hid in the first place.
|
||||
assert(stormsToPreload().includes(CALM_STORM),
|
||||
'the calm day is preloaded on the shipped ladder');
|
||||
assert(stormsToPreload(['storm_02_wildnight']).includes(CALM_STORM),
|
||||
"...and on a ladder that doesn't contain it anywhere — the case that broke");
|
||||
assertEq(stormsToPreload(['storm_01_gentle']).length, 1,
|
||||
'and it is still deduped, not loaded twice');
|
||||
});
|
||||
|
||||
// --- SPRINT11 gate 2: the job sheet -------------------------------------
|
||||
|
||||
t.test('every night is a JOB — a client, a brief, and a yard', () => {
|
||||
@ -286,14 +306,16 @@ export default async function run(t) {
|
||||
t.test('ruling: the venturi axis is a LINE — C and A never disagreed', () => {
|
||||
// The reconciliation SPRINT11 asked for, pinned so it is never "fixed" back.
|
||||
// weather.core aligns on |dot(wind, axis)| because a gap funnels either way
|
||||
// through it, so axis and axis+PI are the same gap. A shipped 2.1, C
|
||||
// measured -1.08, and -1.08 + PI = 2.0616: one line, two ends.
|
||||
// through it, so axis and axis+PI are the same gap. A shipped 2.1, C read the
|
||||
// southerly's heading at -1.08, and those are 2.2 deg apart: one line, two
|
||||
// ends. Pinned against the SHIPPED axis — the site's geometry is the fact
|
||||
// under test, not the storm heading it happens to run along.
|
||||
const at = (axis) => {
|
||||
const f = createWindField(earlyBusterDef);
|
||||
f.setVenturi([{ x: -6, z: 0, axis, gain: 1.35, radius: 5, sharp: 3 }]);
|
||||
f.setVenturi([{ x: -6, z: 0, axis, gain: 1.5, radius: 5, sharp: 3 }]);
|
||||
return f;
|
||||
};
|
||||
const c = at(-1.08), flipped = at(-1.08 + Math.PI);
|
||||
const c = at(2.1), flipped = at(2.1 + Math.PI);
|
||||
let worst = 0;
|
||||
for (let tt = 0; tt <= 90; tt += 0.5) {
|
||||
worst = Math.max(worst, Math.abs(c.speedAt(-6, 0, tt) - flipped.speedAt(-6, 0, tt)));
|
||||
@ -314,6 +336,28 @@ export default async function run(t) {
|
||||
'the throat is meaningfully faster than the open yard');
|
||||
});
|
||||
|
||||
t.test("every anchor carries a NUMBER for ratingHint (D's landmine)", () => {
|
||||
if (!site2World) return 'SKIPPED — no server for site_02';
|
||||
// Nothing reads ratingHint yet (that's the open ruling — see THREADS), and
|
||||
// this assert exists BECAUSE of that: it makes the wire safe before it's
|
||||
// made. The honest posts are built from site JSON and never pass through
|
||||
// adoptAnchor, so they had no ratingHint at all — and `load > hw.rating *
|
||||
// undefined` is `load > NaN`, which is always false. Wire it naively and the
|
||||
// strongest anchors become immortal while the trap looks like it works.
|
||||
for (const a of site2World.anchors) {
|
||||
assert(Number.isFinite(a.ratingHint), `${a.id} has a finite ratingHint, not undefined`);
|
||||
assert(a.ratingHint > 0, `${a.id}'s ratingHint is positive — a 0 would be unbreakable-by-zero`);
|
||||
}
|
||||
for (const a of world.anchors) {
|
||||
assert(Number.isFinite(a.ratingHint), `backyard ${a.id} has a finite ratingHint too`);
|
||||
}
|
||||
// And the honest steel must still out-rate the trap, or the site has no thesis.
|
||||
const q1 = site2World.anchors.find((a) => a.id === 'q1');
|
||||
const cb1 = site2World.anchors.find((a) => a.id === 'cb1');
|
||||
if (q1 && cb1 && cb1.ratingHint < 1) assertLess(cb1.ratingHint, q1.ratingHint,
|
||||
'the carport beam is worse steel than an honest post');
|
||||
});
|
||||
|
||||
t.test('ruling: the carport BILLS — 180, once, and takes the roof with it', () => {
|
||||
if (!site2World) return 'SKIPPED — no server for site_02';
|
||||
// E shipped the trap and said it plainly: the anchors said collateral
|
||||
|
||||
@ -15,6 +15,28 @@ import * as THREE from '../vendor/three.module.js';
|
||||
import { ANCHOR_TYPE, createStubWind } from './contracts.js';
|
||||
import { validateSiteWind } from './weather.core.js';
|
||||
|
||||
/**
|
||||
* Full strength: this anchor takes whatever its hardware is rated for.
|
||||
*
|
||||
* SPRINT11 — Lane D's landmine, defused before anyone can step on it. Every
|
||||
* anchor now carries a NUMBER here, always. It used to be set only by
|
||||
* `adoptAnchor`, off Lane E's baked `rating_hint` — so GLB-backed anchors had it
|
||||
* and the ones built straight from site JSON (site_02's honest posts q1..q4) did
|
||||
* not. Their `ratingHint` was `undefined`.
|
||||
*
|
||||
* That matters the moment anyone wires the ratings into the failure threshold,
|
||||
* which is the open question D put to me. The naive wire is
|
||||
* `load > hw.rating * anchor.ratingHint`, and with `undefined` that is
|
||||
* `load > NaN` — **always false**. The honest posts wouldn't read weak, they'd
|
||||
* be UNBREAKABLE, and the carport would become the only anchor in the yard that
|
||||
* can fail. The trap would appear to work, for entirely the wrong reason, and it
|
||||
* would have looked like a successful playtest.
|
||||
*
|
||||
* "Default before you multiply" — D's words. This is the default, at the source,
|
||||
* so no consumer has to remember. 1 = honest steel; E's traps ship lower.
|
||||
*/
|
||||
const DEFAULT_RATING_HINT = 1;
|
||||
|
||||
/** Degrees → radians. Site JSON is authored in degrees; nobody writes π/2 by hand. */
|
||||
const rad = (deg) => ((deg ?? 0) * Math.PI) / 180;
|
||||
|
||||
@ -847,7 +869,7 @@ const _worldPos = new THREE.Vector3();
|
||||
/** @returns {import('./contracts.js').Anchor} */
|
||||
function makeStaticAnchor(id, type, pos) {
|
||||
const p = pos.clone();
|
||||
return { id, type, pos: p, sway: () => p };
|
||||
return { id, type, pos: p, sway: () => p, ratingHint: DEFAULT_RATING_HINT };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -864,6 +886,7 @@ function makeSwayAnchor(id, pos, phase, wind) {
|
||||
id,
|
||||
type: 'tree',
|
||||
pos: p,
|
||||
ratingHint: DEFAULT_RATING_HINT,
|
||||
sway(t) {
|
||||
const speed = wind.sample(p, t).length();
|
||||
const amp = Math.min(0.35, speed * 0.012);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user