Files
wownerogue/test/pvpCaptureHarness.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

228 lines
11 KiB
JavaScript

'use strict';
const vm = require('vm');
const {
CAPTURE_RULESET_IDS,
DEFAULT_SEED,
assertCaptureSafety,
assertCaptureStateEnvironment,
capturePageHtml,
capturePresentation,
captureResultSummary,
captureStateHash,
captureTraceHeader,
createCaptureRoom,
parseArgs,
planBotMove,
validateOptions
} = require('../src/scripts/pvp-capture');
describe('deterministic PvP capture harness', () => {
function options(extra = {}) {
return validateOptions(parseArgs([
'--confirm-local-free-only',
'--no-video',
...extra.args || []
]));
}
test('requires an explicit acknowledgement and can never run in production', () => {
const safe = options();
expect(() => assertCaptureSafety({ NODE_ENV: 'development' }, { ...safe, confirmed: false }))
.toThrow('--confirm-local-free-only');
expect(() => assertCaptureSafety({ NODE_ENV: 'production' }, safe))
.toThrow('NODE_ENV=development or NODE_ENV=test');
expect(() => assertCaptureSafety({ NODE_ENV: 'development', PAYOUTS_ENABLED: 'true' }, safe))
.toThrow('money-enabled environments');
expect(() => assertCaptureSafety({ NODE_ENV: 'test', MATCH_CRYPTO_RACE_ENABLED: '1' }, safe))
.toThrow('MATCH_CRYPTO_RACE_ENABLED');
expect(() => assertCaptureSafety({ NODE_ENV: 'test', PAYMENTS_ENABLED: 'true' }, safe))
.toThrow('PAYMENTS_ENABLED');
expect(() => assertCaptureSafety({ NODE_ENV: 'test', CREDITS_PAYOUT_ENABLED: 'true' }, safe))
.toThrow('CREDITS_PAYOUT_ENABLED');
expect(() => assertCaptureSafety({ NODE_ENV: 'test', WALLET_RPC_URL: 'http://127.0.0.1:18083' }, safe))
.toThrow('wallet-configured environments');
expect(assertCaptureSafety({ NODE_ENV: 'test' }, safe)).toBe(true);
});
test('refuses inherited gameplay tuning so a seed has one authoritative replay', () => {
expect(() => assertCaptureStateEnvironment({ DIFFICULTY_PRESET: 'casino' }))
.toThrow('inherited gameplay tuning: DIFFICULTY_PRESET');
expect(() => assertCaptureSafety({ NODE_ENV: 'test', DUNGEON_WIDTH: '70' }, options()))
.toThrow('inherited gameplay tuning: DUNGEON_WIDTH');
expect(() => assertCaptureSafety({ NODE_ENV: 'development', MONSTER_SPEED: '2' }, options()))
.toThrow('inherited gameplay tuning: MONSTER_SPEED');
expect(assertCaptureStateEnvironment({})).toBe(true);
});
test('accepts only local renderer/capture options and a fixed-width seed', () => {
expect(parseArgs(['--help']).help).toBe(true);
expect(() => validateOptions(parseArgs([
'--confirm-local-free-only', '--mode', '3d'
]))).toThrow('network-loaded renderers are forbidden');
expect(() => validateOptions(parseArgs([
'--confirm-local-free-only', '--seed', '1234'
]))).toThrow('64 hexadecimal');
expect(() => validateOptions(parseArgs([
'--confirm-local-free-only', '--ruleset', 'crypto-race'
]))).toThrow('--ruleset must be one of');
expect(() => validateOptions(parseArgs([
'--confirm-local-free-only', '--camera', 'wallet'
]))).toThrow('--camera must be action or focus');
expect(() => parseArgs(['--url', 'https://example.com'])).toThrow('Unknown option');
});
test('creates every built-in multiplayer ruleset deterministically with no money state', () => {
const config = options();
const first = createCaptureRoom(config);
const second = createCaptureRoom(config);
expect(config.seed).toBe(DEFAULT_SEED);
expect(first.economy).toBe('free');
expect(first.ruleset.id).toBe('race');
expect(first.ruleset.economy.model).toBe('free');
expect(first.entryFeeAtomic).toBe(0);
expect(first.potAtomic).toBe(0);
expect(first.houseFeeAtomic).toBe(0);
expect(Array.from(first.occupants.keys())).toEqual(['bot-1', 'bot-2', 'bot-3', 'bot-4']);
expect(first.seedHash).toBe(second.seedHash);
expect(first.dungeon.map).toEqual(second.dungeon.map);
expect(first.dungeon.exit).toEqual(second.dungeon.exit);
expect(first.dungeon.treasure).toEqual(second.dungeon.treasure);
for (const rulesetId of CAPTURE_RULESET_IDS) {
const room = createCaptureRoom(options({ args: ['--ruleset', rulesetId] }));
expect(room.ruleset.id).toBe(rulesetId);
expect(room.economy).toBe('free');
expect(room.ruleset.economy.model).toBe('free');
expect(room.entryFeeAtomic).toBe(0);
expect(room.potAtomic).toBe(0);
expect(room.houseFeeAtomic).toBe(0);
}
});
test.each(CAPTURE_RULESET_IDS)('%s uses deterministic bot inputs and authoritative state', rulesetId => {
const config = options({ args: ['--ruleset', rulesetId, '--ticks', '30'] });
const rooms = [createCaptureRoom(config), createCaptureRoom(config)];
rooms.forEach(room => room.start());
for (let tick = 0; tick < 30; tick++) {
for (const room of rooms) {
const state = room.toGameState();
for (const botId of room.occupants.keys()) {
const move = planBotMove(botId, state);
if (Math.abs(move.dx) + Math.abs(move.dy) === 1) {
room.queueMove(botId, move.dx, move.dy);
}
}
room.resolveTick();
}
expect(captureStateHash(rooms[0].toGameState()))
.toBe(captureStateHash(rooms[1].toGameState()));
}
});
test('last-alive bots pursue a rival instead of following the race exit', () => {
const move = planBotMove('bot-1', {
tick: 1,
ruleset: { id: 'last-alive' },
visibleTiles: [["'1", "'1", "'1"]],
exit: [0, 0],
players: [
{ id: 'bot-1', x: 0, y: 0, alive: true, finished: false },
{ id: 'bot-2', x: 2, y: 0, alive: true, finished: false }
]
});
expect(move).toEqual({ dx: 1, dy: 0 });
});
test('presentation and final copy are ruleset-correct, including cooperative results', () => {
expect(capturePresentation('last-alive')).toEqual(expect.objectContaining({
title: 'LAST ALIVE', activeNoun: 'STANDING'
}));
expect(captureResultSummary({
ruleset: { id: 'score-attack' }, winnerId: 'bot-2', tickCount: 55
}, [{ id: 'bot-2', name: 'Nyx', score: 912 }])).toEqual(expect.objectContaining({
headline: 'NYX TOPS THE BOARD',
detail: expect.stringMatching(/912 POINTS.*FREE EXHIBITION.*NO CASH PRIZES/)
}));
expect(captureResultSummary({
ruleset: { id: 'coop-escape' }, winnerId: 'bot-1', tickCount: 40
}, [
{ id: 'bot-1', escaped: true },
{ id: 'bot-2', escaped: true }
])).toEqual(expect.objectContaining({
headline: 'TEAM ESCAPED TOGETHER', cooperative: true
}));
expect(captureResultSummary({
ruleset: { id: 'last-alive' }, winnerId: 'bot-2', tickCount: 80
}, [
{ id: 'bot-1', name: 'Rook', alive: true, finished: false },
{ id: 'bot-2', name: 'Nyx', alive: true, finished: false }
])).toEqual(expect.objectContaining({ headline: 'NYX LEADS AT THE BELL', winnerStatus: 'LEADER' }));
expect(captureResultSummary({
ruleset: { id: 'race' }, winnerId: 'bot-1', tickCount: 10, endReason: 'capture_limit'
}, [{ id: 'bot-1', name: 'Rook' }])).toEqual(expect.objectContaining({
headline: 'ROOK LEADS AT THE BELL', winnerStatus: 'LEADER'
}));
expect(captureResultSummary({
ruleset: { id: 'last-alive' }, winnerId: 'bot-1', tickCount: 10, endReason: 'all_dead'
}, [{ id: 'bot-1', name: 'Rook', alive: false }])).toEqual(expect.objectContaining({
headline: 'ROOK SURVIVED LONGEST', winnerStatus: 'LAST OUT'
}));
});
test('capture page uses local assets, a spectator focus camera, and no payment UI', () => {
const html = capturePageHtml();
expect(html).toContain('focusPlayerId:cameraFocus');
expect(html).toContain("cameraMode=query.get('camera')||'action'");
expect(html).toContain('id="roster"');
expect(html).toContain('actionFocusUntil');
expect(html).toContain('function composeCamera');
expect(html).toContain("if(finalWinnerId&&player.id===finalWinnerId)classes.push('winner')");
expect(html).toContain("finalWinnerId=outcome.cooperative?null:(payload.winnerId||null)");
expect(html).toContain("if(finalWinnerId)document.body.classList.add('finale');else document.body.classList.remove('finale')");
expect(html).toContain("if(renderer.name==='iso')return Math.max(cover");
expect(html).toContain('baseFocus.x-previousFocus.x');
expect(html).not.toContain('chosen.x-previousFocus.x');
expect(html).toContain('DETERMINISTIC REPLAY ID');
expect(html).not.toContain('VERIFIED REPLAY');
expect(html).toContain('FREE EXHIBITION • NO CASH PRIZES');
expect(html).toContain('id="roster" role="list"');
expect(html).toContain('id="result" role="status" aria-live="polite" aria-atomic="true"');
expect(html).toContain("slice(0,portrait?3:4)");
expect(html).toContain("cover=Math.max(stage.clientWidth/(canvas.width||stage.clientWidth)");
expect(html).toContain('#result{position:absolute;z-index:30;left:20px;right:20px;bottom:18px');
expect(html).toContain("if(entity.kind==='player')entity.label=null");
expect(html).toContain('controls:false');
expect(html).toContain('/socket.io/socket.io.js');
expect(html).not.toMatch(/https?:\/\/(?!127\.0\.0\.1)/);
expect(html).not.toContain('crypto_race');
expect(html).not.toContain('payment');
const inlineScripts = Array.from(
html.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi),
match => match[1]
);
expect(inlineScripts.length).toBeGreaterThanOrEqual(2);
for (const source of inlineScripts) {
expect(() => new vm.Script(source)).not.toThrow();
}
});
test('parses the camera viewport as replay provenance', () => {
const config = options({ args: ['--viewport', '1080x1920', '--camera', 'focus'] });
const room = createCaptureRoom(config);
const trace = captureTraceHeader(room, capturePresentation(room.ruleset), config);
expect(config.viewport).toEqual({ width: 1080, height: 1920 });
expect(config.camera).toBe('focus');
expect(trace.camera).toEqual({
mode: 'focus',
focus: 'bot-1',
renderer: 'tiles',
viewport: { width: 1080, height: 1920 }
});
});
});