The cup climbs: easy to hard, deterministic — the shuffle retires with its screen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-29 16:49:36 +02:00
1 parent dbfd085493
commit a5a681f9f2
5 files changed
+47 -57

No files matched your search

+11
View File
@@ -491,6 +491,17 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
## Session log: THE CUP CLIMBS — the shuffle retires too (2026-08-29, same day, STAGING ONLY)
Oliver, straight after the draw screen went: "warum sollten wir noch mischen? wäre es nicht gut
von leicht nach schwer zu spielen?" — right again. Without the screen the shuffle was invisible
novelty, and a bad roll paired the strongest challenger with the one-game round one, INVERTING
the curve the whole game is tuned around ("starts easy, gets harder" is literally test-pinned).
`cupRoundsFor` is deterministic now: one band position per round, ascending, the final the top —
and the setup's league panel is thereby an exact preview of the tournament order. `cupDraw`
collapsed to a read; `cup.draw`/`cup.played` are gone (startMatch keeps the dead `draw` parameter
for call-site arity only). The old draw test became the climb-order pin.
## Session log: THE DRAW SCREEN RETIRES (2026-08-29, same day, STAGING ONLY)
Oliver: "the draw ist doch jetzt eigentlich obsolet, oder?" — yes. The screen was built for the
+1 -1
View File
@@ -854,6 +854,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div>
</div>
</div>
<script type="module" src="src/main.js?v=329"></script>
<script type="module" src="src/main.js?v=330"></script>
</body>
</html>
+8 -22
View File
@@ -354,13 +354,12 @@ export function watchdog(dt) {
// the match mode ends. Regicide is inherited for free: smashing the ball into the body still wounds
// it, and killing it outright wins the game early (killZombie fires resolveInterlude('P'), which
// lands in matchGameWon via the empty-court check).
// `draw` is an optional pre-settled list of rungs, one per cup round — null (the normal case
// since the bracket screen was deleted, 2026-08-29) means "roll each round as it comes". The
// parameter stays: the suite uses it to pin the validation below.
// `train` (2026-08-28) is a RUNG NUMBER and turns the run into the TRAINING COURT: that one
// opponent, one game at a time, nothing banked. The ladder is parked — d.opp never advances, a
// felled body walks straight back on, and bleeding out costs a modal instead of the run. All of
// it rides the ordinary match machinery behind the d.train flag; see matchGameWon / matchPoint.
// `draw` is DEAD weight kept only for call-site arity — the cup runs easy to hard now
// (cupRoundsFor is deterministic, 2026-08-29) and nothing reads it.
export function startMatch(cup = false, draw = null, train = 0) {
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return false;
// THE LEAGUES: the profile's league is the world (band, pace tier, court — see PROJECT.md).
@@ -376,7 +375,7 @@ export function startMatch(cup = false, draw = null, train = 0) {
// on (a rung DRAWN from the round's band — cupDraw sets d.opp, so every ladder dial downstream
// scales off the drawn rung untouched), how deep the body's pool is (matchOpponent multiplies
// by the round's `need`), and where a won game goes (cupGameWon). See config.cupRoundsFor.
if (cup) { G.match.cup = { round: 0, g: 0, draw: draw || null, rounds: cupRoundsFor(G.league) }; cupDraw(); }
if (cup) { G.match.cup = { round: 0, g: 0, rounds: cupRoundsFor(G.league) }; cupDraw(); }
G.matchFall = null;
G.hp = G.hpMax = TUNE.matchPlayerHp; // a real match is longer than four mistakes — the gore
// mode's 4 was "too weak" here, felt not calculated
@@ -412,25 +411,12 @@ export function startMatch(cup = false, draw = null, train = 0) {
return true;
}
// The round's rung, drawn from its band. d.opp carries it so matchFoe, the ladder speed dials and
// the felled arithmetic all read the drawn rung with no cup-awareness of their own.
// A PASSED DRAW IS THE DRAW THAT IS PLAYED (it once arrived from the bracket screen; the suite
// still passes one) — this reads it off rather than rolling again.
// Guarded on the band all the same: a saved or hand-passed draw that names a rung this round does
// not contain falls back to an honest roll rather than dealing the champion in round one.
// The round's opponent. No draw and no roll any more: the rounds ARE the band, ascending, so
// this simply reads the round's one position off (cupRoundsFor carries the order). d.opp carries
// it so matchFoe, the ladder speed dials and the felled arithmetic stay cup-unaware.
function cupDraw() {
const cup = G.match.cup, R = cup.rounds[cup.round];
const drawn = cup.draw && cup.draw[cup.round];
// WITHOUT REPLACEMENT: a league band is 3-4 rungs and every non-final round carries the whole
// sub-band, so the run remembers who has already fallen (cup.played) and never deals a body
// twice — a repeat would read as the bracket screen lying. The final's band is one rung and
// sails through untouched.
cup.played = cup.played || [];
const pool = R.band.filter(r => !cup.played.includes(r));
cup.rung = R.band.includes(drawn) && !cup.played.includes(drawn) ? drawn
: (pool.length ? pool : R.band)[Math.floor(Math.random() * (pool.length ? pool : R.band).length)];
cup.played.push(cup.rung);
G.match.opp = cup.rung;
const cup = G.match.cup;
G.match.opp = cup.rung = cup.rounds[cup.round].band[0];
}
// Every point starts like a real one: the server behind his line (setupPoint's tennis branch) and
+14 -14
View File
@@ -917,22 +917,22 @@ export const leagueOf = lg => LEAGUES[Math.min(LEAGUES.length, Math.max(1, lg |
export const leagueOfRung = n => LEAGUES.find(l => n <= l.band[1]) || LEAGUES[LEAGUES.length - 1];
// The tournament, PER LEAGUE. Same shape CUP_ROUNDS always had (name/band/need) — round one is a
// single game so a run opens fast, and the drawn body's pool still scales with `need`
// (matchOpponent). The rounds ride ON the cup object (beats.startMatch stamps them), so every
// consumer reads cup.rounds and the full-ladder constant is gone.
// A league band is 3-4 rungs, so the draw is WITHOUT REPLACEMENT: every non-final round carries
// the whole sub-band and the run deals each body at most once (cup.played / buildBracket's used
// set), the final is always the band's top rung — "only the finale is settled", still. A 4-rung
// league is four rounds (a shuffle of three challengers), a 3-rung league is three.
// single game so a run opens fast, and the body's pool still scales with `need` (matchOpponent).
// The rounds ride ON the cup object (beats.startMatch stamps them).
// EASY TO HARD, DETERMINISTIC (2026-08-29, Oliver: "warum sollten wir noch mischen?"): each round
// is exactly one position of the band, ascending, the final the band's top. The shuffle was the
// deleted draw screen's ceremony — without the screen it only ever risked pairing the strongest
// challenger with the shortest round, inverting the curve the whole game is tuned around. The
// setup's league panel is now, incidentally, an exact preview of the tournament order.
export const cupRoundsFor = lg => {
const [lo, hi] = leagueOf(lg).band;
const open = [];
for (let r = lo; r < hi; r++) open.push(r);
const names = open.length >= 3 ? ['ROUND ONE', 'QUARTER-FINAL', 'SEMI-FINAL']
: ['ROUND ONE', 'SEMI-FINAL'];
return open.map((_, i) => ({ name: names[i] || 'ROUND ' + (i + 1), band: open.slice(),
need: i ? 2 : 1 }))
.concat([{ name: 'THE FINAL', band: [hi], need: 2 }]);
const names = hi - lo >= 3 ? ['ROUND ONE', 'QUARTER-FINAL', 'SEMI-FINAL']
: ['ROUND ONE', 'SEMI-FINAL'];
const rounds = [];
for (let r = lo; r < hi; r++)
rounds.push({ name: names[rounds.length] || 'ROUND ' + (rounds.length + 1),
band: [r], need: rounds.length ? 2 : 1 });
return rounds.concat([{ name: 'THE FINAL', band: [hi], need: 2 }]);
};
export const TOURS = [
+13 -20
View File
@@ -1233,29 +1233,22 @@ test('THE TOURNAMENT: bands draw, bodies deepen, rounds hold their body, the cha
assert.ok(pr && pr.to === 2, 'the ceremony is parked for the menu to play');
});
test('THE DRAW: a passed draw is validated per round, honoured, or honestly re-rolled', () => {
// The screen would be worse than nothing if it dealt a different tournament from the one it just
// 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 { 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(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
test('THE CUP CLIMBS: rounds run easy to hard, deterministic, the final the top of the band', () => {
// the shuffle retired with the draw screen (Oliver: "warum sollten wir noch mischen?") — a bad
// roll paired the strongest challenger with the shortest round, inverting the tuned curve
for (const l of cfg.LEAGUES) {
const R = cfg.cupRoundsFor(l.id);
const order = R.map(r => (assert.equal(r.band.length, 1, 'one settled body per round'), r.band[0]));
for (let i = 1; i < order.length; i++)
assert.ok(order[i] > order[i - 1], `${l.name}: round ${i + 1} is the harder body`);
assert.equal(order[0], l.band[0], `${l.name} opens on its easiest`);
assert.equal(order[order.length - 1], l.band[1], `${l.name} ends on its final`);
}
// ...and the run reads that order off exactly
_startMatch(true); G.modal = null; G.modalQ.length = 0;
assert.ok(R[0].band.includes(G.match.opp), 'no draw: the round rolls its own');
assert.equal(G.match.opp, 1, 'round one walks on the easiest body');
endMatch();
});
test('cup rallies never strand the latch: no-crossing points, the fall, and the mid-round game', () => {
// Oliver's second lock: in the quarter-final, matchFallWillPlay still divided by 4 while
// matchPoint divided by 4*need — it promised a fall on a point that crossed nothing, the