Lane D S18 gate 1.3: the knife's pins — including one for the feature-detect that lied
Ten asserts in d.test.js. The reach rule (a post cleat yes, the carport beam
never, up a ladder never), the hold and its three interruptions, the read with
both halves of the trade priced through main.js's OWN collateral walk, and the
money split with a vacuity guard in both directions — the same four corners cut
cost $0 and blown cost $205, because a pin where both cases read zero is a pin
that is not working.
Two that exist because of what playing the night found. The shipped emergency's
rig is pinned as DATA — cb1 is a shackle and it is FLYING when you arrive,
because a carport corner already on the ground is a receipt and not a decision.
And every rig door main.js feature-detects on that path is asserted to EXIST:
failCorner, cutAway, seedClock, setFabric, plus attach's {at}. A feature-detect
is only safe if something checks the feature — `?.()` on a renamed method ran
silently for a whole sprint and looked exactly like a call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
db1977f55d
commit
60771875d6
@ -23,14 +23,24 @@ import { loadStorm, createWind } from '../weather.js';
|
||||
// SPRINT16 gate 4 / SPRINT17 gate 0.1: the week-law pins at the bottom of this
|
||||
// file read the ladder. week.js is contracts-only underneath, so this stays
|
||||
// headless-safe.
|
||||
import { NIGHTS, nightAt } from '../week.js';
|
||||
import { NIGHTS, nightAt, REP } from '../week.js';
|
||||
// SPRINT17 gate 3.2: the pool-yard pins at the bottom read the board's POOL
|
||||
// (pure data, zero THREE) and loadSite (fetch + validateSite — the teeth; a
|
||||
// site file this suite merely fetch()ed raw could rot invalid and stay green).
|
||||
// loadSite pulls world.js which pulls THREE — browser-only, same as the GLB
|
||||
// facts everywhere else in this repo; the node runner never imports this file.
|
||||
import { POOL, exposureOf } from '../board.js';
|
||||
import { POOL, exposureOf, EMERGENCIES } from '../board.js';
|
||||
// SPRINT18 gate 1.3 — the knife's physics half, imported for ONE reason: to check that the doors
|
||||
// main.js feature-detects on the emergency path exist. See that test.
|
||||
import { SailRig } from '../sail.js';
|
||||
import { loadSite } from '../world.js';
|
||||
// SPRINT18 gate 1.3 — the knife. knife.js is headless-importable for the same reason ladder.js is
|
||||
// (its GLB-free, and the panel guards on `typeof document`); the ledger halves come from main.js,
|
||||
// which a.test and c.test already import, because scoreRun lives behind a WebGL context and the only
|
||||
// way to assert its rulings is to have them extracted as pure functions.
|
||||
import { createKnife, createVerbPanel, cuttable, KNIFE_TUNE } from '../knife.js';
|
||||
import { answerableFor, collateralWalk, rigRecordFor, verdictFor } from '../main.js';
|
||||
import { createWeek } from '../week.js';
|
||||
|
||||
const DT = FIXED_DT;
|
||||
|
||||
@ -1328,4 +1338,336 @@ export default async function run(t) {
|
||||
assertEq(rustItem.cost, 45,
|
||||
'the rust is the cheapest structure you can break — the stake E sized the gamble in');
|
||||
});
|
||||
|
||||
// ================================================================ SPRINT18 gate 1.3 — THE KNIFE
|
||||
//
|
||||
// DESIGN.md line 165, and the signature decision under load: release, cut, or ride it out. B built
|
||||
// `rig.cutAway`; these pin the VERB — where it reaches, what interrupts it, what the read promises,
|
||||
// and (the half that would have shipped as a trap) what it costs on the paperwork.
|
||||
//
|
||||
// A note on the fixtures: they are the SHIPPED emergency's own geometry, not invented numbers.
|
||||
// site_02's cowboy rig is q3/q1/tr1b/cb1, and cb1 is a carport BEAM — `work:'bracket'`, y=2.36,
|
||||
// ratingHint 0.22, the corner that takes the $180 carport at t=35.9. It is the one corner the knife
|
||||
// must refuse and the whole reason the verb is a whole-sail cut.
|
||||
|
||||
/** A rig shaped like sail.js's: corners with `broken`/`cut`, and a cutAway that behaves like B's. */
|
||||
function fakeRig(corners) {
|
||||
const cuts = [];
|
||||
return {
|
||||
rigged: true,
|
||||
corners,
|
||||
cuts,
|
||||
cutAway(ids = null, reason = null) {
|
||||
const want = ids ?? corners.map((c) => c.anchorId);
|
||||
const cut = [];
|
||||
for (const id of want) {
|
||||
const c = corners.find((x) => x.anchorId === id);
|
||||
if (!c || c.broken) continue;
|
||||
c.broken = true; c.cut = true; cut.push(id);
|
||||
}
|
||||
if (!cut.length) return { ok: false, reason: 'the sail is already down' };
|
||||
cuts.push({ corners: cut, reason });
|
||||
return { ok: true, cut };
|
||||
},
|
||||
};
|
||||
}
|
||||
/** The corner block's real anchors, with the beam's mechanism as E baked it. */
|
||||
const YARD = {
|
||||
anchors: [
|
||||
{ id: 'q1', type: 'post', work: 'cloth', pos: { x: -3.5, y: 3.95, z: -2.5 } },
|
||||
{ id: 'q3', type: 'post', work: 'cloth', pos: { x: 2.0, y: 3.95, z: 4.0 } },
|
||||
{ id: 'tr1b', type: 'tree', pos: { x: -6.0, y: 5.05, z: 3.0 } },
|
||||
{ id: 'cb1', type: 'carport', work: 'bracket', pos: { x: -7.0, y: 2.36, z: -3.0 }, collateral: 'carport' },
|
||||
],
|
||||
gnome: { collateralValue: 25 },
|
||||
anchor(id) { return this.anchors.find((a) => a.id === id) ?? null; },
|
||||
collateralFor(key) { return key === 'carport' ? { cost: 180, label: 'the carport' } : null; },
|
||||
};
|
||||
const fourCorners = () => [
|
||||
{ anchorId: 'q3', broken: false, hw: { name: 'shackle', cost: 15, rating: 3200 } },
|
||||
{ anchorId: 'q1', broken: false, hw: { name: 'carabiner', cost: 5, rating: 1200 } },
|
||||
{ anchorId: 'tr1b', broken: false, hw: { name: 'carabiner', cost: 5, rating: 1200 } },
|
||||
{ anchorId: 'cb1', broken: false, hw: { name: 'shackle', cost: 15, rating: 3200 } },
|
||||
];
|
||||
const knifeAt = (p, rig, extra = {}) => createKnife({
|
||||
player: p, getRig: () => rig, getWorld: () => YARD,
|
||||
collateralAtRisk: collateralWalk, ...extra,
|
||||
});
|
||||
/** Stand on a cleat. The cleat is the anchor's x/z at ground level — see knife.js. */
|
||||
const standAt = (p, id) => { const a = YARD.anchor(id); p.pos.x = a.pos.x; p.pos.z = a.pos.z; };
|
||||
|
||||
t.test('knife: it reaches a post cleat from the grass and NEVER the bracket', () => {
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
|
||||
const corners = fourCorners();
|
||||
const K = knifeAt(p, fakeRig(corners));
|
||||
|
||||
standAt(p, 'q3');
|
||||
assertEq(K.target()?.anchorId, 'q3', 'standing at the post cleat, the knife is offered');
|
||||
standAt(p, 'tr1b');
|
||||
assertEq(K.target()?.anchorId, 'tr1b', 'and at the tree strop');
|
||||
|
||||
// THE ONE THAT MATTERS. cb1 is the corner about to take the $180 carport, and it is a bracket at
|
||||
// 2.36 m — the same rule that costs a fascia repair two trips and a ladder. You cannot cut there,
|
||||
// which is why the verb releases the WHOLE sail from a corner you can reach.
|
||||
standAt(p, 'cb1');
|
||||
assertEq(K.target(), null,
|
||||
'the carport BEAM is bracket work — a knife cannot reach it, and having no time to fetch the '
|
||||
+ 'ladder is the emergency\'s whole premise');
|
||||
assert(!cuttable(corners[3], YARD.anchor('cb1')), 'cuttable() states the same rule directly');
|
||||
// and the read says WHICH TOOL, rather than going silent — the greyed-prompt rule, S13
|
||||
assert(/ladder/.test(K.read().whyNot ?? ''), 'the refusal names the tool it would need');
|
||||
|
||||
// out of range of everything: still a sail, still no cut
|
||||
p.pos.x = 0; p.pos.z = 0;
|
||||
assertEq(K.target(), null, 'mid-yard is not at a cleat');
|
||||
// up the ladder: both hands are on the rungs
|
||||
standAt(p, 'q3');
|
||||
p.climbY = 2.35;
|
||||
assertEq(K.target(), null, 'you cannot cut from up a ladder');
|
||||
});
|
||||
|
||||
t.test('knife: hold X for the full 1.2 s and the WHOLE sail goes — once', () => {
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
|
||||
const rig = fakeRig(fourCorners());
|
||||
const K = knifeAt(p, rig);
|
||||
standAt(p, 'q3');
|
||||
|
||||
// half a hold is no cut, and that is the deliberation the verb is made of
|
||||
fixedLoop(KNIFE_TUNE.cutSecs * 0.5, DT, (dt, tt) => K.step(dt, tt, true));
|
||||
assertEq(rig.cuts.length, 0, 'half a hold cuts nothing');
|
||||
assert(K.cutting, 'and the radial is running');
|
||||
|
||||
fixedLoop(KNIFE_TUNE.cutSecs * 0.6, DT, (dt, tt) => K.step(dt, 1 + tt, true));
|
||||
assertEq(rig.cuts.length, 1, 'the full hold cuts');
|
||||
assertEq(rig.cuts[0].corners.length, 4,
|
||||
'the WHOLE sail — DESIGN.md line 165, and B\'s cutAway() with no argument');
|
||||
assertEq(rig.cuts[0].reason, 'the carport',
|
||||
'the paperwork carries what it was FOR: the most expensive thing those corners were holding up');
|
||||
|
||||
// one press, one cut. Without the latch the held key re-arms and the ticker fills with refusals.
|
||||
fixedLoop(3, DT, (dt, tt) => K.step(dt, 5 + tt, true));
|
||||
assertEq(rig.cuts.length, 1, 'a held key must not try again every 1.2 s');
|
||||
assertEq(K.read().state, 'none', 'and with no live corner there is nothing left to offer');
|
||||
});
|
||||
|
||||
t.test('knife: letting go, walking off, or being put down all abort the cut', () => {
|
||||
const start = () => {
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
|
||||
const rig = fakeRig(fourCorners());
|
||||
const K = knifeAt(p, rig);
|
||||
standAt(p, 'q3');
|
||||
fixedLoop(KNIFE_TUNE.cutSecs * 0.7, DT, (dt, tt) => K.step(dt, tt, true));
|
||||
assert(K.progress > 0.5, 'mid-cut');
|
||||
return { p, rig, K };
|
||||
};
|
||||
|
||||
const a = start();
|
||||
a.K.step(DT, 1, false); // let go
|
||||
assertEq(a.K.progress, 0, 'releasing X abandons the cut');
|
||||
const b = start();
|
||||
b.p.pos.x += 4; // walk off the cleat
|
||||
b.K.step(DT, 1, true);
|
||||
assertEq(b.K.progress, 0, 'walking away abandons it too');
|
||||
const c = start();
|
||||
c.p.knockdown(1, 1, 0); // the storm votes
|
||||
c.K.step(DT, 1, true);
|
||||
assertEq(c.K.progress, 0, 'a gust that floors you mid-cut takes the cut with it');
|
||||
assertEq(c.p.state, 'knocked',
|
||||
'and the knife did NOT stomp the state the world just set — interact.js owns `busy`, '
|
||||
+ 'nothing else writes it');
|
||||
// ...nor can a cut START while the player is locked in something else
|
||||
const d = start();
|
||||
d.p.setState('busy', 2);
|
||||
fixedLoop(2, DT, (dt, tt) => d.K.step(dt, 3 + tt, true));
|
||||
assertEq(d.rig.cuts.length, 0, 'you cannot cut mid-crank or mid-poke');
|
||||
});
|
||||
|
||||
t.test('knife: the read prints BOTH halves of the trade, and the saves side is priced', () => {
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
|
||||
const rig = fakeRig(fourCorners());
|
||||
const K = knifeAt(p, rig, { getGarden: () => ({ hp: 84 }), secsLeft: () => 52.4 });
|
||||
standAt(p, 'q3');
|
||||
const r = K.read();
|
||||
assertEq(r.state, 'ready');
|
||||
// $205: the carport 180 + the gnome 25 on the two-corner rule. The number the OFFER CARD printed
|
||||
// as "at risk" that morning — same walk, so it cannot disagree with the invoice.
|
||||
assertEq(r.savesTotal, 205, 'the saves side is the collateral the live corners are still holding');
|
||||
assertEq(r.hp, 84, 'and the costs side is the bed');
|
||||
assertEq(r.secsLeft, 52, 'for however long is left of the storm');
|
||||
|
||||
// The trade must survive on the glass, not just in the object — this panel is the thing that was
|
||||
// missing for five sprints, so it gets an assert rather than a screenshot.
|
||||
const panel = createVerbPanel({});
|
||||
panel.update({ target: { id: 'rerig_0' }, usable: true, label: 're-rig corner', holding: false, progress: 0 }, r);
|
||||
const text = panel.el.textContent;
|
||||
assert(text.includes('[E] re-rig corner'), `the hold-E prompt reaches the glass: ${text}`);
|
||||
assert(/CUT THE SAIL LOOSE/.test(text), 'so does the knife');
|
||||
assert(text.includes('$205') && text.includes('the carport'), 'with what it saves, named and priced');
|
||||
assert(/84%/.test(text) && /52s/.test(text), 'and what it costs, in the same breath');
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- the money (D's own filing, fixed)
|
||||
t.test('knife (the money): a corner you CUT is not a corner you are answerable for', () => {
|
||||
// The bug this pins: `_releaseCorner` sets `broken` for a cut exactly as for an overload, and
|
||||
// every ledger surface reads `broken`. So the shipped tree billed the knife on five surfaces at
|
||||
// once — the hardware, the $180 carport you cut the sail TO SAVE, the gnome's two-corner rule,
|
||||
// the clean bonus it voided, and (in week.js) warranty plus standing.
|
||||
const cut = fourCorners();
|
||||
cut.forEach((c) => { c.broken = true; c.cut = true; });
|
||||
const a = answerableFor(cut, null);
|
||||
assertEq(a.lost.length, 4, 'the record still says four corners are down — the sail IS on the lawn');
|
||||
assertEq(a.yours.length, 0, 'and none of them is a failure of yours');
|
||||
assertEq(a.cut.length, 4, 'they are named as cut, so the paper can say who put them there');
|
||||
assertEq(collateralWalk(a.yours, YARD).total, 0,
|
||||
'THE HEADLINE: cutting the sail to save the carport does not then bill you for the carport');
|
||||
|
||||
// The vacuity guard, and it is the whole reason this test can be trusted: the SAME four corners
|
||||
// broken by the storm bill everything. If both cases read $0 the walk is simply not working.
|
||||
const blown = fourCorners();
|
||||
blown.forEach((c) => { c.broken = true; });
|
||||
const b = answerableFor(blown, null);
|
||||
assertEq(b.yours.length, 4, 'an overload is still yours');
|
||||
assertEq(collateralWalk(b.yours, YARD).total, 205, 'and it still costs the carport and the gnome');
|
||||
|
||||
// one cut, one blown: the ledger splits them by name, not by count
|
||||
const mixed = fourCorners();
|
||||
mixed[3].broken = true; // cb1 blew — the carport goes, honestly
|
||||
mixed[0].broken = true; mixed[0].cut = true; // q3 cut afterwards
|
||||
const m = answerableFor(mixed, null);
|
||||
assertEq(m.yours.map((c) => c.anchorId).join(','), 'cb1', 'the one that failed is yours');
|
||||
assertEq(m.cut.map((c) => c.anchorId).join(','), 'q3', 'the one you cut is not');
|
||||
assertEq(collateralWalk(m.yours, YARD).total, 180,
|
||||
'the carport bills (its corner really did let go) and the gnome does not — one corner, not two');
|
||||
});
|
||||
|
||||
t.test('knife (the money): the walk that prices the panel is the walk that bills the invoice', () => {
|
||||
// Not "they agree today" — they are the SAME FUNCTION, injected. This asserts the wire, because a
|
||||
// second copy of the arithmetic is how the board came to advertise $57 against a $114 dispatch.
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
|
||||
const rig = fakeRig(fourCorners());
|
||||
const K = knifeAt(p, rig);
|
||||
standAt(p, 'q3');
|
||||
const promised = K.read();
|
||||
K.step(KNIFE_TUNE.cutSecs + DT, 0, true);
|
||||
assertEq(rig.cuts.length, 1, 'cut');
|
||||
const billed = collateralWalk(answerableFor(rig.corners, null).yours, YARD);
|
||||
assertEq(promised.savesTotal, 205, 'the panel promised $205 off the table');
|
||||
assertEq(billed.total, 0, 'and the invoice honoured it to the cent');
|
||||
// PURE: asking the question must not wreck the yard. (The wreck stays at scoreRun's call site.)
|
||||
let wrecks = 0;
|
||||
collateralWalk(fourCorners(), { ...YARD, anchor: YARD.anchor.bind(YARD),
|
||||
collateralFor: YARD.collateralFor, wreckStructure: () => { wrecks++; } });
|
||||
assertEq(wrecks, 0, 'the walk never swaps a structure for its wreck — the HUD asks it every frame');
|
||||
});
|
||||
|
||||
t.test('knife: the verdict says you cut it — and stands aside when something of yours failed too', () => {
|
||||
const dmg = { hail: 30, rain: 4 };
|
||||
const cut = fourCorners();
|
||||
cut.forEach((c) => { c.broken = true; c.cut = true; });
|
||||
const a = answerableFor(cut, null);
|
||||
|
||||
// The bug: with `lost` empty and hp under the line, verdictFor reached `uncovered` and told a
|
||||
// player who had just cut the sail down that "the hail fell where your sail wasn't". That is the
|
||||
// one sentence the verdict must never say here — it blames placement for a decision.
|
||||
const lostBed = verdictFor({ hp: 31, lost: a.yours, cut: a.cut, win: false, dmg, pondPeak: 0, pondDumped: 0 });
|
||||
assertEq(lostBed.mode, 'cut', `a cut night is its own mode, not '${lostBed.mode}'`);
|
||||
assert(/CUT IT LOOSE/.test(lostBed.verdict), 'and it says so');
|
||||
assert(!/sail wasn/.test(lostBed.verdict), 'never "the hail fell where your sail wasn\'t"');
|
||||
assert(/didn't/.test(lostBed.verdict) || /their property/.test(lostBed.verdict),
|
||||
'it names what the bed bought: their property');
|
||||
|
||||
const heldBed = verdictFor({ hp: 62, lost: a.yours, cut: a.cut, win: true, dmg, pondPeak: 0, pondDumped: 0 });
|
||||
assertEq(heldBed.mode, 'cut-held', 'cut early enough and the bed can still make it');
|
||||
|
||||
// And the composition rule: a corner of YOURS on the ground is the louder fact, so the failure
|
||||
// modes keep speaking and the knife stays a line on the ledger.
|
||||
const mixed = fourCorners();
|
||||
mixed[3].broken = true;
|
||||
mixed[0].broken = true; mixed[0].cut = true;
|
||||
const m = answerableFor(mixed, null);
|
||||
const both = verdictFor({ hp: 20, lost: m.yours, cut: m.cut, win: false, dmg, pondPeak: 0, pondDumped: 0 });
|
||||
assertEq(both.mode, 'corner', 'one corner of yours went: that verdict is true and it wins');
|
||||
assert(/CB1/.test(both.verdict), 'and it names the corner that actually let go');
|
||||
|
||||
// no knife, no change: every pre-existing mode is untouched by the new argument
|
||||
const plain = verdictFor({ hp: 31, lost: [], cut: [], win: false, dmg, pondPeak: 0, pondDumped: 0 });
|
||||
assertEq(plain.mode, 'uncovered', 'a night with no cut still reads exactly as it did');
|
||||
});
|
||||
|
||||
t.test('knife: the shipped emergency has something to CUT FOR — B\'s authored rig, not the placeholder', () => {
|
||||
// The night's whole premise, as DATA, because that is where it was wrong. On the merged tree the
|
||||
// rig was still A's flagged first cut with `cb1` (the carport beam) ARRIVING ALREADY DOWN — which
|
||||
// makes the $180 carport `theirs`, unbillable and unwreckable, and leaves DESIGN.md line 165's
|
||||
// signature decision with no object. Found by playing the night and reading the dispatch card.
|
||||
const em = EMERGENCIES.find((e) => e.id === 'corner_block_emergency');
|
||||
assert(em, 'the shipped emergency is still on the board');
|
||||
const by = (id) => em.emergency.rig.find((c) => c.anchorId === id);
|
||||
assert(by('cb1'), 'the carport beam is in the cowboy\'s rig — it is the bait the client has been '
|
||||
+ 'selling since night 3');
|
||||
assertEq(by('cb1').hw, 'shackle',
|
||||
'a SHACKLE on the 0.22 beam: 704 N holding their roof, over its ceiling t=35..42 (B, measured). '
|
||||
+ 'A carabiner there fails in the first second and there is nothing left to decide');
|
||||
assert(!by('cb1').broken,
|
||||
'AND IT IS FLYING WHEN YOU ARRIVE. A carport corner already on the ground is not a knife '
|
||||
+ 'decision, it is a receipt — this is the assert that stops the placeholder coming back');
|
||||
const down = em.emergency.rig.filter((c) => c.broken).map((c) => c.anchorId);
|
||||
assertEq(down.join(','), 'tr1b',
|
||||
'exactly one corner arrives gone, and it is the 0.88 branch on the cheapest steel');
|
||||
// B's ruling, which is also data in A's file and also had not landed.
|
||||
assertEq(em.pay?.garden, 0,
|
||||
'the garden BONUS is off on a callout (B): you cannot be paid per-HP for a bed you had no prep '
|
||||
+ 'phase to protect. The garden WIN stays, which is what makes the knife cost something');
|
||||
});
|
||||
|
||||
t.test('knife: every rig door the emergency path feature-detects actually EXISTS', () => {
|
||||
// ⚠️ This is the pin for the bug that cost this sprint its arrived-broken corner: main.js called
|
||||
// `rig.breakCorner?.(id)` and B had named the door `failCorner`, so an optional call to a
|
||||
// nonexistent method ran silently, forever, looking exactly like a call. The repo has a name for
|
||||
// it — D's own `rigging.setWorld` lesson, quoted in main.js — and it still happened, because
|
||||
// `?.()` makes a rename invisible. A feature-detect is only safe if something checks the feature.
|
||||
for (const door of ['failCorner', 'cutAway', 'seedClock', 'setFabric']) {
|
||||
assertEq(typeof SailRig.prototype[door], 'function',
|
||||
`SailRig has no ${door}() — main.js feature-detects this door on the emergency path, so a `
|
||||
+ 'rename here is a silent no-op, not an error');
|
||||
}
|
||||
// and the pre-fly argument, which is how the cloth gets into the storm it is standing in
|
||||
assert(/\{\s*at\s*=\s*0/.test(SailRig.prototype.attach.toString()),
|
||||
'attach() no longer takes {at} — the emergency\'s sail would fly the first second of a storm '
|
||||
+ 'it arrived 30s into (measured: clockSkew 30.02)');
|
||||
});
|
||||
|
||||
t.test('knife: warranty and standing do not chase a corner you chose to let go', () => {
|
||||
const def = { baseCurve: [[0, 10]], gusts: { powBase: 5, powRamp: 5 } };
|
||||
const cutRec = () => {
|
||||
const cs = fourCorners();
|
||||
cs.forEach((c) => { c.broken = true; c.cut = true; });
|
||||
return rigRecordFor(cs);
|
||||
};
|
||||
assert(cutRec().every((c) => c.cut && c.brokenAtDawn),
|
||||
'the record is honest: cut AND down at dawn, both true, neither hidden');
|
||||
|
||||
// night 1: cut the lot. night 2: the sheet must book NO warranty off it.
|
||||
const w = createWeek();
|
||||
const s1 = w.settle({ hp: 40, win: false, collateral: [], intactHardwareValue: 0,
|
||||
rigRecord: cutRec(), cutAways: [{ t: 33, corners: ['q3', 'q1', 'tr1b', 'cb1'], reason: 'the carport' }] }, def, 0);
|
||||
assertEq(s1.warranty.length, 0, 'nothing to warranty on the night you cut');
|
||||
assert(!s1.repReasons.some((r) => /down at dawn/.test(r)),
|
||||
`standing is not docked for the cut: ${s1.repReasons.join(' · ')}`);
|
||||
assertEq(s1.repDelta, REP.CLEAN,
|
||||
'a night where nothing FAILED is a clean night — the knife\'s price is the garden, not the star');
|
||||
w.advance();
|
||||
const s2 = w.settle({ hp: 100, win: true, collateral: [], intactHardwareValue: 0, rigRecord: [] }, def, 0);
|
||||
assertEq(s2.warranty.length, 0,
|
||||
'and no free callout tomorrow to go and re-hang a sail you deliberately spent');
|
||||
|
||||
// the vacuity guard: the same four corners BLOWN book four warranty lines and cost the standing
|
||||
const w2 = createWeek();
|
||||
const blownRec = () => { const cs = fourCorners(); cs.forEach((c) => { c.broken = true; }); return rigRecordFor(cs); };
|
||||
const b1 = w2.settle({ hp: 40, win: false, collateral: [], intactHardwareValue: 0, rigRecord: blownRec() }, def, 0);
|
||||
assert(b1.repDelta < 0, 'four blown corners DO cost standing');
|
||||
w2.advance();
|
||||
const b2 = w2.settle({ hp: 100, win: true, collateral: [], intactHardwareValue: 0, rigRecord: [] }, def, 0);
|
||||
assertEq(b2.warranty.length, 4, 'and they DO book four warranty lines — the pin is not vacuous');
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user