diff --git a/PROJECT.md b/PROJECT.md index cd064cc..a57631f 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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 diff --git a/index.html b/index.html index c52b6ef..45fade0 100644 --- a/index.html +++ b/index.html @@ -854,6 +854,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/beats.js b/src/beats.js index e3a464a..ccfd48b 100644 --- a/src/beats.js +++ b/src/beats.js @@ -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 diff --git a/src/config.js b/src/config.js index 49d7f51..a362988 100644 --- a/src/config.js +++ b/src/config.js @@ -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 = [ diff --git a/test.js b/test.js index 8a1422c..5e7efa7 100644 --- a/test.js +++ b/test.js @@ -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