The leagues, phase 2: five bands, pace tiers, locked courts, the members' ground, the promotion ceremony

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-29 12:43:23 +02:00
1 parent 9133c0d351
commit 16bf5b9fed
12 files changed
+406 -71

No files matched your search

+89 -14
View File
@@ -89,7 +89,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { update, forceZombies, startGore, splatPoint, restartRun, leaguePace, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
@@ -110,6 +110,9 @@ const { read: readStats, check: statsCheck } = statsMod;
const DT = 1 / 60;
function freshSplatter() {
restartRun();
// THE LEAGUES: a won cup PROMOTES the profile (by design), which would leak league 2 into every
// test after the tournament walkthrough — each test opens in league 1 with no ceremony pending
prof.prof.lg = 1; prof.prof.ch = 0; prof.takePromo();
G.mode = 'splatter';
G.everTransitioned = true;
setupPoint();
@@ -877,12 +880,23 @@ test('the opponent card is a RAMP: it starts easy and only gets harder', () => {
for (let n = 1; n <= LADDER; n++)
assert.ok(cfg.ZFAMS.includes(matchFoe(n).fam), `rung ${n}'s family is registered in ZFAMS`);
assert.ok(matchFoe(LADDER).h > 1.85, 'the champion stands taller than a stock body');
// the cup's final draws the top rung and nothing else: a band pointing past the ladder would
// wrap through matchFoe's modulo and quietly deal the shambler as THE CHAMPION
const fin = cfg.CUP_ROUNDS[cfg.CUP_ROUNDS.length - 1];
assert.deepEqual(fin.band, [LADDER], 'the final is the top of the ladder');
for (const r of cfg.CUP_ROUNDS) for (const rung of r.band)
assert.ok(rung >= 1 && rung <= LADDER, `${r.name} draws a rung that exists (${rung})`);
// THE LEAGUES: every league's cup draws only from its own band, its final is the band's top
// rung and nothing else, and the five bands tile the whole ladder with no gap and no overlap
let expectLo = 1;
for (const l of cfg.LEAGUES) {
const [lo, hi] = l.band;
assert.equal(lo, expectLo, `${l.name} starts where the league below ended`);
expectLo = hi + 1;
const R = cfg.cupRoundsFor(l.id);
assert.deepEqual(R[R.length - 1].band, [hi], `${l.name}'s final is its top rung`);
assert.equal(R.length, hi - lo + 1, `${l.name} plays one round per rung`);
for (const r of R) for (const rung of r.band)
assert.ok(rung >= lo && rung <= hi, `${l.name} ${r.name} stays in its band (${rung})`);
for (const r of R.slice(0, -1))
assert.ok(!r.band.includes(hi), `${l.name}'s top rung is only ever the final`);
assert.ok(cfg.COURTS.some(c => c.id === l.court), `${l.name}'s court exists`);
}
assert.equal(expectLo, LADDER + 1, 'the five bands cover the ladder exactly');
});
test('the first opponent cannot paint lines: its returns are width-clamped', () => {
@@ -1185,13 +1199,16 @@ test('the gore economy: barrels stop being income, and the late court keeps gett
});
test('THE TOURNAMENT: bands draw, bodies deepen, rounds hold their body, the champion ends it', () => {
const { CUP_ROUNDS } = cfg;
_startMatch(true); G.modal = null; G.modalQ.length = 0;
G.mode = 'splatter';
const d = G.match;
assert.ok(d.cup, 'the cup rides the match machinery');
const CUP_ROUNDS = d.cup.rounds; // THE LEAGUES: the run carries its own rounds
const dealt = [];
for (let round = 0; round < CUP_ROUNDS.length; round++) {
const R = CUP_ROUNDS[round];
assert.ok(!dealt.includes(d.opp), R.name + ' never re-deals a fallen body');
dealt.push(d.opp);
assert.equal(d.cup.round, round, 'on ' + R.name);
assert.ok(R.band.includes(d.opp), R.name + ' drew from its band (rung ' + d.opp + ')');
const zb = oppOf();
@@ -1209,7 +1226,11 @@ test('THE TOURNAMENT: bands draw, bodies deepen, rounds hold their body, the cha
if (round < CUP_ROUNDS.length - 1) passMopBeat(); // the boy, then the next draw walks on
}
assert.ok(G.done && G.done.cup && G.done.won, 'the champion fell: the trophy screen holds');
assert.equal(G.match.felled, CUP_ROUNDS.length, 'four bodies felled, banked as a match run');
assert.equal(G.match.felled, CUP_ROUNDS.length, 'every round felled a body, banked as a match run');
// THE LEAGUES: this cup was league one's, so winning it IS the promotion
assert.equal(prof.prof.lg, 2, 'the cup is the promotion gate');
const pr = prof.takePromo();
assert.ok(pr && pr.to === 2, 'the ceremony is parked for the menu to play');
});
test('THE DRAW: a bracket settled on screen is the bracket that is played', () => {
@@ -1217,20 +1238,21 @@ test('THE DRAW: a bracket settled on screen is the bracket that is played', () =
// showed you settling (main.buildBracket hands its rungs straight to startMatch). The guard is
// the other half: a rung that is not in the round's band falls back to an honest roll rather
// than dealing THE CHAMPION in round one.
const { CUP_ROUNDS, LADDER } = cfg;
const drawn = CUP_ROUNDS.map(r => r.band[r.band.length - 1]);
const { LADDER } = cfg;
const R = cfg.cupRoundsFor(1); // a fresh profile's cup is league one's
const drawn = [3, 1, 2, 4]; // a legal shuffle of the sub-band, the final settled
_startMatch(true, drawn); G.modal = null; G.modalQ.length = 0;
assert.equal(G.match.opp, drawn[0], 'round one walks on the body the screen landed on');
assert.deepEqual(G.match.cup.draw, drawn, 'and the rest of the draw rides with the run');
endMatch();
// a nonsense draw is refused per round, not trusted
_startMatch(true, [LADDER, LADDER, LADDER, LADDER]); G.modal = null; G.modalQ.length = 0;
assert.ok(CUP_ROUNDS[0].band.includes(G.match.opp),
assert.ok(R[0].band.includes(G.match.opp),
'a rung round one cannot contain is rolled instead of dealt');
endMatch();
// ...and no draw at all is the old behaviour, which the #cup back door still uses
_startMatch(true); G.modal = null; G.modalQ.length = 0;
assert.ok(CUP_ROUNDS[0].band.includes(G.match.opp), 'no draw: the round rolls its own');
assert.ok(R[0].band.includes(G.match.opp), 'no draw: the round rolls its own');
endMatch();
});
@@ -1240,7 +1262,6 @@ test('cup rallies never strand the latch: no-crossing points, the fall, and the
// no-fall path re-armed synchronously, and the after-set latch went stale ("basically like
// freeze from before"). The latch is set BEFORE the resolve now (the swallow exempts the
// resolution in progress via G.matchEarly), and willFall mirrors the cup divisor exactly.
const { CUP_ROUNDS } = cfg;
_startMatch(true); G.modal = null; G.modalQ.length = 0;
G.mode = 'splatter';
const d = G.match;
@@ -2193,5 +2214,59 @@ test('PROFILE: zombie names fit the board and its charset, and the profile round
prof.prof.lg = 1; prof.save();
});
test('THE LEAGUES: the arcade opens at its band, climbs inside it, and counts felled bodies', () => {
prof.prof.lg = 2;
assert.ok(_startMatch(), 'the arcade starts');
G.modal = null; G.modalQ.length = 0;
G.mode = 'splatter';
assert.equal(G.league, 2, 'the run wears the profile league');
const d = G.match;
assert.equal(d.opp, 5, "league two opens at its band's bottom rung");
for (let b = 0; b < 6; b++) {
G.interlude.introT = 0;
for (let k = 0; k < 4; k++) { resolveInterlude('P'); G.modal = null; G.modalQ.length = 0; }
for (let i = 0; i < 6 * 60 && G.matchFall; i++) { G.modal = null; G.modalQ.length = 0; update(DT); }
passMopBeat();
}
assert.equal(d.opp, 8, 'the ladder clamps at the top of the band — the cup is the only way up');
assert.equal(d.felled, 6, 'every felled body counts, clamp or no clamp');
endMatch();
});
test('THE LEAGUES: the top league crowns a champion instead of promoting', () => {
prof.prof.lg = 5;
_startMatch(true); G.modal = null; G.modalQ.length = 0;
G.mode = 'splatter';
const d = G.match;
assert.equal(d.cup.rounds.length, 3, 'a three-rung league plays three rounds');
for (let round = 0; round < d.cup.rounds.length; round++) {
const R = d.cup.rounds[round];
G.interlude.introT = 0;
for (let g = 1; g <= R.need; g++)
for (let k = 0; k < 4; k++) { resolveInterlude('P'); G.modal = null; G.modalQ.length = 0; }
for (let i = 0; i < 6 * 60 && G.matchFall; i++) { G.modal = null; G.modalQ.length = 0; update(DT); }
if (round < d.cup.rounds.length - 1) passMopBeat();
}
assert.ok(G.done && G.done.cup, 'the trophy screen holds');
assert.equal(prof.prof.lg, 5, 'there is no league six');
assert.equal(prof.prof.ch, 1, 'the profile is crowned');
const pr = prof.takePromo();
assert.ok(pr && pr.champion, 'the ceremony parked is the champion one');
});
test('THE LEAGUES: the pace tier scales the solo match world and nothing else', () => {
G.match = { opp: 1 }; G.league = 1;
assert.equal(leaguePace(), 1, 'league one IS the shipped game');
G.league = 5;
assert.ok(leaguePace() > 1.2, 'the top league is a visibly faster world');
const climbs = [1, 2, 3, 4, 5].map(l => { G.league = l; return leaguePace(); });
for (let i = 1; i < climbs.length; i++) assert.ok(climbs[i] > climbs[i - 1], 'each league is faster');
G.net = { on: true };
assert.equal(leaguePace(), 1, 'netplay is a human and exempt');
G.net = null; G.match = null; G.league = 5;
assert.equal(leaguePace(), 1, 'outside the match modes the factor is 1 — gore keeps its own director');
G.league = 1;
});
console.log(`\n${passed} tests passed`);
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report