Files
wownerogue/test/tavernMatchBridge.test.js
T
jwinterm 0f68eec26b feat: real-time multiplayer race mode (free, prestige, crypto)
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.
2026-07-09 13:26:52 -04:00

69 lines
2.4 KiB
JavaScript

const TavernMatchBridge = require('../src/network/tavernMatchBridge');
describe('TavernMatchBridge', () => {
function makeManager() {
const rooms = new Map();
return {
rooms,
onTick: jest.fn(),
onFinish: jest.fn()
};
}
function makeBridge({ rooms = [], io = null } = {}) {
const manager = makeManager();
for (const r of rooms) manager.rooms.set(r.id, r);
const emitted = [];
const bridge = new TavernMatchBridge({
matchManager: manager,
io: io || { to: () => ({ emit: (e, p) => emitted.push({ e, p }) }) },
debugManager: null
});
bridge.enabled = true;
return { bridge, manager, emitted };
}
function makeRoom(id = 'm1') {
return {
id,
economy: 'free',
tickCount: 5,
status: 'active',
occupants: new Map([['a', { id: 'a', x: 1, y: 2, name: 'A', avatar: 'default', facing: 'down' }]]),
playerStates: new Map([['a', { alive: true, finished: false, escaped: false, hasTreasure: false, placement: null }]]),
monster: { x: 3, y: 4 },
treasure: null,
dungeon: { exit: [10, 10] },
winnerId: null,
seedHash: 'deadbeef'
};
}
test('forwards public match tick to tavern room', () => {
const room = makeRoom('m1');
const { bridge, manager, emitted } = makeBridge({ rooms: [room] });
bridge.initialize();
manager.onTick('m1', { tick: 5, events: [] });
expect(emitted.some(e => e.e === 'tavern_match_tick' && e.p.matchId === 'm1')).toBe(true);
});
test('forwards match end to tavern room', async () => {
const room = makeRoom('m2');
room.status = 'finished';
room.winnerId = 'a';
const { bridge, manager, emitted } = makeBridge({ rooms: [room] });
bridge.initialize();
await manager.onFinish(room);
expect(emitted.some(e => e.e === 'tavern_match_end' && e.p.matchId === 'm2')).toBe(true);
});
test('getActiveMatches returns public snapshots', () => {
const room = makeRoom('m3');
const { bridge } = makeBridge({ rooms: [room] });
const list = bridge.getActiveMatches();
expect(list.length).toBe(1);
expect(list[0].matchId).toBe('m3');
expect(list[0].players.length).toBe(1);
});
});