The host names the stakes: classic tennis, or the death match

Three chips under VS A FRIEND — the host's pick rides the room and the
start message rebuilds both courts on pairing. In the death match your
opponent IS the zombie: their lost points tear its wound stages, their
won points draw your blood, and bleeding out crosses the wire as the
other phone's win. The same receiver-is-judge verdicts drive the match
machinery through resolveInterlude, so the fall, the mop beat and the
burst come along unmodified. New: gushProne — the game-winning body
bleeds out on the ground, from prone-height anchors, until it bursts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 18:02:44 +02:00
1 parent 5e80c4e69d
commit c0b59eee90
10 files changed
+241 -26

No files matched your search

+77 -1
View File
@@ -89,7 +89,7 @@ const { mirror, netMsg, netTest } = await import('./src/net.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; };
const { zombies, spawnZombie, pellets } = await import('./src/zombies.js');
const { zombies, spawnZombie, pellets, parts } = await import('./src/zombies.js');
const { killPlayer } = await import('./src/zombies.js');
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
const cfg = await import('./src/config.js');
@@ -1272,6 +1272,82 @@ test('netplay: a serve fault is the receiver\'s call, and re-arms the server\'s
assert.ok(!netTest.sent.some(m => m.t === 'fault'), 'applying a fault does not echo one back');
});
// ---- NETPLAY DEATH MATCH: the host's pick rides the room's start message, and the match
// machinery is driven by the same receiver-is-judge verdicts classic uses — routed through
// resolveInterlude instead of pointTo, so wounds, blood, the fall and the burst all come along.
function freshNetDeath(side) {
restartRun();
G.classic = true; // the waiting court, exactly as the menu builds it
assert.ok(startNetVs(), 'net vs takes a clean court');
netTest.sent = [];
netMsg({ t: 'start', side, death: true });
}
test('netplay death match: the host\'s pick rebuilds the court on pairing', () => {
freshNetDeath('B');
assert.ok(G.net && G.net.ready && G.net.death, 'the wire survives the mode swap');
assert.equal(G.mode, 'splatter');
assert.ok(G.match && G.interlude, 'the match machinery owns the court');
assert.ok(zombies.some(z => z.variant === 'r'), 'the body is on court');
assert.equal(score.server, 'O', 'the host serves first here too');
assert.equal(G.modal, null, 'no popup may eat the first remote serve');
});
test('netplay death match: their verdicts wound the body, mine cost blood — no echo', () => {
freshNetDeath('A');
const rz = zombies.find(z => z.variant === 'r');
const hp0 = rz.hp;
netMsg({ t: 'pt', win: 'you' }); // their phone judged: my ball beat them
assert.ok(rz.hp < hp0, 'my point tears at the body');
assert.ok(!netTest.sent.some(m => m.t === 'pt'), 'applying a verdict does not echo one back');
const blood = G.hp;
netMsg({ t: 'pt', win: 'me' }); // their phone judged: their ball beat me
assert.equal(G.hp, blood - 1, 'their point draws my blood');
});
test('netplay death match: the receiver judges — an untouched winner draws blood and crosses the wire', () => {
freshNetDeath('B');
netMsg({ t: 'shot', x: 0, y: 1.5, z: 21.77, vx: 0, vy: 3, vz: -16, g: 9.2, pow: 0.6, serve: false });
const blood = G.hp;
for (let i = 0; i < 900 && ball.active; i++) update(DT);
assert.ok(!ball.active, 'the rally resolved locally — this phone was the judge');
assert.equal(G.hp, blood - 1, 'their untouched winner draws my blood');
const pt = netTest.sent.find(m => m.t === 'pt');
assert.ok(pt && pt.win === 'you', 'and the verdict went to their phone');
});
test('netplay death match: bleeding out is said on the wire, and the other phone wins on it', () => {
freshNetDeath('B');
G.hp = 1;
netMsg({ t: 'pt', win: 'me' }); // the point that empties me
assert.equal(G.mode, 'eaten', 'my run ends as every match death does');
assert.ok(netTest.sent.some(m => m.t === 'dead'), 'the death is said out loud');
freshNetDeath('A'); // ...and the winner's phone, receiving it
score.games[0] = 3;
netMsg({ t: 'dead' });
assert.ok(G.done && G.done.won, 'the last one standing gets the win ending');
assert.equal(G.match.felled, 3, 'felled = the games that actually burst the body');
});
test('netplay death match: the same friend walks back on — no roster climb', () => {
freshNetDeath('A');
const opp0 = G.match.opp;
for (let i = 0; i < 4; i++) netMsg({ t: 'pt', win: 'you' }); // four points = the game = the burst
assert.equal(G.match.opp, opp0, 'the opponent number holds through the game win');
for (let i = 0; i < 60 * 25 && !zombies.some(z => z.variant === 'r'); i++) update(DT);
assert.ok(zombies.some(z => z.variant === 'r'), 'the body returns after the mop beat');
assert.equal(G.match.opp, opp0, 'as the same opponent, not the roster\'s next rung');
});
test('death match: the game-winning body bleeds out on the ground before it bursts', () => {
freshNetDeath('A');
const rz = zombies.find(z => z.variant === 'r');
G.matchFall = { t: 0.3, from: 1, vx: 0, x0: rz.x, kill: true }; // landed, prone, pre-burst
const before = parts.length;
for (let i = 0; i < 20; i++) update(DT);
assert.ok(parts.length > before, 'the prone hold gushes (gushProne), where bleed() is suppressed');
});
// stats.js ships its own fixture-based check — the period filter, the mode split and the name
// normalisation that has to agree with server.py. It was exported and never called by anything.
test("stats.js's own check", () => { assert.equal(statsCheck(), 'ok'); });