mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
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.
152 lines
5.8 KiB
JavaScript
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);
|
|
});
|
|
});
|