Zombie tennis: one game per opponent, blood only
Triage failed the only test that matters — "i think the normal player does not grasp this" — and the lesson across four drill designs is that every invented rule needs teaching, while the rules the scene already implies need none. So the new mode is the game's own oldest rule: tennis. MATCH TEST in the back room (#match). One game per opponent, real 15/30/40 scoring. Built on top of the duel machinery: G.match runs a permanent interlude, which is already what makes serving tennis-like, hides the rack, quiets the spawn director and wakes racketAI. The one change is in resolveInterlude — a settled point feeds matchPoint instead of ending anything. About 90 lines for a whole mode, because the deepest system in the repo was already built. The score is the body: a quarter of its pool per point won, floored at 1, so a love game walks the racket zombie through every wound stage and the game point fells it. Its points draw your blood — hp--, eaten at zero, matchHeal 2 per game won is how you last. Regicide is inherited: kill the body mid-game and the game banks by hand. The timing is drawn, per Oliver's ask: an approach ring around the incoming ball that kisses a fixed inner ring exactly at the sweet contact distance (gold inside the band), and an apex band beside the toss spanning the same 0.12m keyPressed scores as perfect, with a tick riding the live ball. Both cues use the mechanics' own numbers, so the picture cannot lie. Verified by autopilot: 7 games in 90 simulated seconds, opponents 1..8, pools 4/6/8/10, wounds tracking points exactly. 36 tests, 15 seeds clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
be191dba82
commit
0620e97d6c
9 files changed
+298
-16
No files matched your search
@@ -83,7 +83,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
|
||||
// ---- game modules (real code) ----------------------------------------------
|
||||
const { G, P, score, ball, setupPoint } = await import('./src/state.js');
|
||||
const { update, forceZombies, splatPoint, restartRun, flickTarget } = await import('./src/gameplay.js');
|
||||
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill } = await import('./src/beats.js');
|
||||
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch } = await import('./src/beats.js');
|
||||
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
|
||||
// player would, so the tests step a live court
|
||||
const startDrill = (...a) => { const r = _startDrill(...a); G.modal = null; G.modalQ.length = 0; return r; };
|
||||
@@ -498,5 +498,74 @@ test('endDrill hands the court back', () => {
|
||||
assert.ok(startDuel() !== false && G.interlude, 'and an ordinary mode can have the court again');
|
||||
});
|
||||
|
||||
// ---- zombie tennis: one game per opponent, blood only ------------------------------------------
|
||||
|
||||
const startMatch = () => { const r = _startMatch(); G.modal = null; G.modalQ.length = 0; return r; };
|
||||
const oppOf = () => zombies.find(z => z.variant === 'r');
|
||||
|
||||
test('a match owns the court like every other mode', () => {
|
||||
assert.ok(startMatch(), 'a match starts on a clean court');
|
||||
assert.ok(G.interlude, 'and runs on a permanent interlude — that is the design');
|
||||
startDuel(); // interlude guard catches it
|
||||
forceZombies();
|
||||
assert.equal(G.horde, null, 'no horde into a match');
|
||||
startBossFight(1);
|
||||
assert.equal(G.bossFight, 0, 'no boss into a match');
|
||||
assert.equal(startDrill(), false, 'no drill into a match');
|
||||
assert.equal(startMatch(), false, 'no second match');
|
||||
endMatch();
|
||||
assert.ok(startDrill(), 'endMatch hands the court back');
|
||||
});
|
||||
|
||||
test('a point won tears a wound stage off the body; only the game point fells it', () => {
|
||||
startMatch();
|
||||
const zb = oppOf();
|
||||
assert.equal(zb.hpMax, TUNE.matchOppHp, 'opponent one carries the base pool');
|
||||
for (let k = 1; k <= 3; k++) {
|
||||
resolveInterlude('P');
|
||||
assert.ok(zombies.includes(zb), 'point ' + k + ': still standing');
|
||||
assert.ok(zb.hp < zb.hpMax, '...and visibly wounded (' + zb.hp + '/' + zb.hpMax + ')');
|
||||
}
|
||||
assert.equal(zb.hp, 1, 'three points in: one stage left');
|
||||
resolveInterlude('P'); // the game point
|
||||
assert.ok(!zombies.includes(zb), 'the game-winning point IS the kill');
|
||||
const next = oppOf();
|
||||
assert.ok(next && next !== zb, 'a fresh opponent shambled on');
|
||||
assert.equal(next.hpMax, TUNE.matchOppHp + TUNE.matchOppHpPer, 'with a deeper pool');
|
||||
assert.equal(G.runGames, 1, 'the ladder banked the game');
|
||||
assert.deepEqual(score.pts, [0, 0], 'and the points reset');
|
||||
});
|
||||
|
||||
test('their points draw blood, and the fourth is the run', () => {
|
||||
startMatch();
|
||||
G.hp = TUNE.playerHp;
|
||||
for (let k = 1; k <= 3; k++) {
|
||||
resolveInterlude('O');
|
||||
assert.equal(G.hp, TUNE.playerHp - k, 'point ' + k + ' cost a wound');
|
||||
assert.notEqual(G.mode, 'eaten');
|
||||
}
|
||||
resolveInterlude('O');
|
||||
assert.equal(G.mode, 'eaten', 'blood only — and it runs out the way it always does');
|
||||
});
|
||||
|
||||
test('a game won heals, so a clean player can go the distance', () => {
|
||||
startMatch();
|
||||
G.hp = 1;
|
||||
for (let k = 0; k < 4; k++) resolveInterlude('P');
|
||||
assert.equal(G.hp, Math.min(G.hpMax, 1 + TUNE.matchHeal), 'the game paid its heal');
|
||||
});
|
||||
|
||||
test('regicide: killing the body mid-game wins the game outright', () => {
|
||||
startMatch();
|
||||
const zb = oppOf();
|
||||
resolveInterlude('P'); // 15-0, so the score is mid-game
|
||||
const games = score.games[0];
|
||||
zombies.length = 0; // the body is destroyed by a shot, not by the score
|
||||
resolveInterlude('P'); // killZombie's exit lands here with the court empty
|
||||
assert.equal(score.games[0], games + 1, 'the game banks');
|
||||
assert.deepEqual(score.pts, [0, 0], 'the part-played points reset');
|
||||
assert.ok(oppOf(), 'and the next opponent is on');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report
|
||||
Reference in new issue
Block a user