Files
wownerogue/test/matchLeaderboard.test.js
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

152 lines
5.8 KiB
JavaScript

const MatchLeaderboard = require('../src/network/matchLeaderboard');
const MatchRoom = require('../src/multiplayer/MatchRoom');
const { WIN } = require('../src/game/rulesets');
describe('MatchLeaderboard', () => {
function makeDb() {
const captured = [];
return {
captured,
async withTransaction(fn) {
const client = {
async query(sql, params) {
captured.push({ sql, params });
return { rowCount: 1, rows: [] };
}
};
return await fn(client);
}
};
}
function makeFinishedRoom(economy) {
const room = new MatchRoom({
entrants: {
a: { userId: 1, name: 'A' },
b: { userId: 2, name: 'B' }
},
economy,
seed: '0000000000000000000000000000000000000000000000000000000000000000'
});
room.start();
room.expire('hard_ceiling');
room.finalize();
return room;
}
test('free race inserts synthetic FREE records', async () => {
const db = makeDb();
const lb = new MatchLeaderboard({ db });
const room = makeFinishedRoom('free');
await lb.postMatch(room);
const gamesInserts = db.captured.filter(q => q.sql.includes('INSERT INTO games')) || [];
expect(gamesInserts.length).toBe(2);
expect(gamesInserts[0].params[2]).toBe('FREE');
});
test('prestige race broadcasts without inserting games', async () => {
const emitted = [];
const io = { emit: (e, p) => emitted.push({ e, p }) };
const db = makeDb();
const lb = new MatchLeaderboard({ db, io });
const room = makeFinishedRoom('credits_prestige');
await lb.postMatch(room);
const gamesInserts = db.captured.filter(q => q.sql.includes('INSERT INTO games')) || [];
expect(gamesInserts.length).toBe(0);
expect(emitted.some(e => e.e === 'leaderboard_update' && e.p.board === 'prestige')).toBe(true);
});
test('crypto race inserts PAID_CREDITS records', async () => {
const db = makeDb();
const lb = new MatchLeaderboard({ db });
const room = makeFinishedRoom('crypto_race');
await lb.postMatch(room);
const gamesInserts = db.captured.filter(q => q.sql.includes('INSERT INTO games')) || [];
expect(gamesInserts.length).toBe(2);
expect(gamesInserts[0].params[2]).toBe('PAID_CREDITS');
});
test('last-alive win/loss follows winnerId + placement, never the escaped flag', async () => {
const db = makeDb();
const lb = new MatchLeaderboard({ db });
const room = new MatchRoom({
entrants: { a: { userId: 1 }, b: { userId: 2 } },
economy: 'free',
ruleset: {
id: 'test-last-alive', mode: 'pvp',
entities: { monster: false, pvpCombat: true },
players: { min: 2, max: 2 },
winCondition: { type: WIN.LAST_ALIVE }
},
seed: '0'.repeat(64)
});
room.start();
room._finishPlayer('a', true);
room.winCondition.onExit(room, 'a');
room.finalize();
expect(room.playerStates.get('a').escaped).toBe(true);
expect(room.winnerId).toBe('b');
expect(room.playerStates.get('b').placement).toBe(1);
await lb.postMatch(room);
const inserts = db.captured.filter(q => q.sql.includes('INSERT INTO games'));
const bySocket = new Map(inserts.map(q => [q.params[1], q.params]));
expect(bySocket.get('a').slice(3, 5)).toEqual(['lost', 'match_loss']);
expect(bySocket.get('b').slice(3, 5)).toEqual(['won', 'match_winner']);
});
test('score-attack awards the authoritative score winner even when another entrant escaped', async () => {
const db = makeDb();
const lb = new MatchLeaderboard({ db });
const room = new MatchRoom({
entrants: { a: { userId: 1 }, b: { userId: 2 } },
economy: 'free',
ruleset: {
id: 'test-score', mode: 'race', entities: { monster: false },
players: { min: 2, max: 2 }, winCondition: { type: WIN.HIGH_SCORE }
},
seed: '0'.repeat(64)
});
room.start();
room.playerStates.get('a').escaped = true;
room.playerStates.get('a').finished = true;
room.playerStates.get('a').moves = 200;
room.playerStates.get('b').hasTreasure = true;
room.playerStates.get('b').moves = 1;
room.expire('hard_ceiling');
room.finalize();
expect(room.winnerId).toBe('b');
await lb.postMatch(room);
const inserts = db.captured.filter(q => q.sql.includes('INSERT INTO games'));
const bySocket = new Map(inserts.map(q => [q.params[1], q.params]));
expect(bySocket.get('a')[3]).toBe('lost');
expect(bySocket.get('b')[3]).toBe('won');
});
test('co-op results stay in match history and never enter individual boards', async () => {
const emitted = [];
const db = makeDb();
const lb = new MatchLeaderboard({ db, io: { emit: (e, p) => emitted.push({ e, p }) } });
const room = new MatchRoom({
entrants: { a: { userId: 1 }, b: { userId: 2 } },
economy: 'free',
ruleset: {
id: 'coop-escape', mode: 'coop',
entities: { monster: false }, players: { min: 2, max: 2 },
winCondition: { type: WIN.ALL_ESCAPE }
},
seed: '0'.repeat(64)
});
room.start();
room.expire('hard_ceiling');
room.finalize();
expect(room.winnerId).toBeNull();
await lb.postMatch(room);
expect(db.captured).toHaveLength(0);
expect(emitted).toHaveLength(0);
});
});