mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 22:58:32 +02:00
Adds a complete block-cadence multiplayer race mode alongside solo and tavern.
Disabled by default; enable with MATCH_ENABLED=true.
Highlights:
- New shared engine: MatchRoom, MatchEngine, MatchState (transport-agnostic, unit-tested).
- Per-economy queues: free, credits_prestige, crypto_race with atomic escrow/refund.
- MatchScheduler drains queues on every new block and starts 2–32 player races.
- MatchManager handles Socket.IO lifecycle, server-authoritative movement, reconnect grace,
forfeit/AFK handling, and end-of-match persistence.
- Crypto races use race-entry tickets (grants.race_entries) to avoid on-chain refunds; winner
payout is queued through the existing batch payout/retry service.
- Leaderboard integration: free → Pleb, prestige → /api/leaderboard?board=prestige,
crypto → Hall of Champions.
- Tavern integration: Queue for Race panel, active race list, and live spectator rendering.
- Dedicated /match.html client with economy selector, race HUD, and post-match scoreboard.
- Migration 022 adds matches, match_entrants, match_events, match_queue_entries,
race_entry_transactions, users.race_entries, and payouts.match_id.
Docs:
- New docs/MATCH_MODE.md with full architecture, config, events, and operational notes.
- Updated README.md, docs/TAVERN_AND_MULTIPLAYER.md, docs/TODO.md, src/.env.example,
and src/.env.
Tests: 40 new unit tests across 7 suites, all passing.
Known follow-ups for real-world validation:
- Full integration test with Postgres + Socket.IO.
- Server boot check with MATCH_ENABLED=false (default) to ensure solo/tavern remain unaffected.
- Browser render-kit verification in /match.html and tavern spectator.
- Crypto-race payout end-to-end with real wallet-rpc.
68 lines
2.6 KiB
JavaScript
68 lines
2.6 KiB
JavaScript
const MatchPayoutService = require('../src/network/matchPayoutService');
|
|
const MatchRoom = require('../src/multiplayer/MatchRoom');
|
|
|
|
describe('MatchPayoutService', () => {
|
|
function makeDb() {
|
|
const captured = [];
|
|
return {
|
|
captured,
|
|
async query(sql, params) {
|
|
if (sql.includes('SELECT pot_atomic')) return { rows: [{ pot_atomic: '20000', house_fee_atomic: '1000' }] };
|
|
if (sql.includes('SELECT id, payout_address')) return { rows: [{ id: 1, payout_address: 'WALLET_ADDRESS' }] };
|
|
return { rows: [], rowCount: 0 };
|
|
},
|
|
async withTransaction(fn) {
|
|
const client = {
|
|
async query(sql, params) {
|
|
captured.push({ sql, params });
|
|
if (sql.includes('SELECT id FROM payouts')) return { rows: [] };
|
|
return { rows: [{ id: 99 }], rowCount: 1 };
|
|
}
|
|
};
|
|
return await fn(client);
|
|
}
|
|
};
|
|
}
|
|
|
|
function makeRoom() {
|
|
const room = new MatchRoom({
|
|
economy: 'crypto_race',
|
|
entrants: { a: { userId: 1 }, b: { userId: 2 } },
|
|
seed: '0000000000000000000000000000000000000000000000000000000000000000'
|
|
});
|
|
room.start();
|
|
room.playerStates.get('a').escaped = true;
|
|
room.winnerId = 'a';
|
|
room.status = 'finished';
|
|
room.finalize();
|
|
return room;
|
|
}
|
|
|
|
test('collectEntryTickets locks pot on match row', async () => {
|
|
const db = makeDb();
|
|
const service = new MatchPayoutService({ db });
|
|
const room = makeRoom();
|
|
await service.collectEntryTickets(room, [
|
|
{ userId: 1, socketId: 'a' },
|
|
{ userId: 2, socketId: 'b' }
|
|
]);
|
|
const updateMatch = db.captured.find(q => q.sql.includes('UPDATE matches'));
|
|
expect(updateMatch).toBeTruthy();
|
|
expect(room.potAtomic).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('payoutWinner inserts one pending payout for winner', async () => {
|
|
const db = makeDb();
|
|
const service = new MatchPayoutService({ db });
|
|
const room = makeRoom();
|
|
room.entryFeeAtomic = 10000;
|
|
room.potAtomic = 20000;
|
|
room.houseFeeAtomic = 1000;
|
|
await service.payoutWinner(room);
|
|
const insert = db.captured.find(q => q.sql.includes('INSERT INTO payouts'));
|
|
expect(insert).toBeTruthy();
|
|
expect(insert.params[3]).toBe('19000'); // winner amount
|
|
expect(insert.params[5]).toBe('match_winner');
|
|
});
|
|
});
|