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

113 lines
4.6 KiB
JavaScript

// The single-player render-kit migration hinges on sceneFromGameState consuming the CLIENT state
// shape: sparse {y:{x:v}} tile maps (vs the server's arrays). Guards that adapter + the RK alias.
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function loadSceneKit() {
const ctx = { console };
ctx.window = ctx;
vm.createContext(ctx);
vm.runInContext(fs.readFileSync(path.join(__dirname, '../html/js/render/sceneModel.js'), 'utf8'), ctx);
return ctx.RK;
}
const F = "'1"; // primary-floor char
describe('SP client-state → Scene adapter', () => {
const RK = loadSceneKit();
test('RK.sceneFromGameState is exposed directly (not only under RK.scene)', () => {
expect(typeof RK.sceneFromGameState).toBe('function');
expect(RK.sceneFromGameState).toBe(RK.scene.sceneFromGameState);
});
test('sparse {y:{x:v}} maps yield correct dims, kinds, and entities', () => {
const state = {
visibleTiles: { 0: { 0: '#', 1: F, 2: '>' }, 1: { 0: F, 1: F, 2: '#' } },
exploredTiles: { 0: { 0: '#', 1: F, 2: '>' }, 1: { 0: F, 1: F, 2: '#' } },
player: { x: 1, y: 1, facing: 'down' }, monster: { x: 0, y: 0 },
exit: [2, 0], treasure: [1, 1], items: {}, lighting: {}
};
const scene = RK.sceneFromGameState(state, { cryptoType: 'WOW' });
expect(scene.cols).toBe(3);
expect(scene.rows).toBe(2);
expect(scene.isDungeon).toBe(true);
expect(scene.grid[0]).toEqual(['wall', 'floor', 'exit']);
expect(scene.grid[1]).toEqual(['floor', 'floor', 'wall']);
const kinds = scene.entities.map(e => e.kind);
expect(kinds).toContain('player');
expect(kinds).toContain('monster');
expect(kinds).toContain('feature'); // exit + treasure
const player = scene.entities.find(e => e.kind === 'player');
expect(player).toMatchObject({ x: 1, y: 1 });
});
test('unexplored cells render as dark, not floor', () => {
const state = {
visibleTiles: { 0: { 0: F } }, // only (0,0) seen
exploredTiles: { 0: { 0: F } },
player: { x: 0, y: 0 }, items: {}, lighting: {}
};
const scene = RK.sceneFromGameState(state, {});
expect(scene.grid[0][0]).toBe('floor');
});
test('multiplayer players[] become racers and an explicit socket id owns the camera', () => {
const state = {
visibleTiles: [[F, F, F], [F, F, F]],
exploredTiles: [[F, F, F], [F, F, F]],
players: [
{ id: 'rival', x: 0, y: 0, name: 'Rival', alive: true },
{ id: 'mine', x: 2, y: 1, name: 'Me', avatar: 'char-ranger', alive: true, placement: 2 }
],
entrance: [0, 0], exit: [2, 1], monster: null, lighting: {}
};
// String second args were used by the original match client; keep them backward compatible.
const scene = RK.sceneFromGameState(state, 'mine');
const racers = scene.entities.filter(e => e.kind === 'player');
expect(racers).toHaveLength(2);
expect(racers.find(e => e.id === 'mine')).toMatchObject({
x: 2, y: 1, label: 'Me', avatar: 'char-ranger', you: true, cameraTarget: true, placement: 2
});
expect(racers.find(e => e.id === 'rival')).toMatchObject({ you: false, cameraTarget: false });
});
test('server you flag is honored and spectator camera falls back to a live racer', () => {
const base = {
visibleTiles: [[F, F, F]], exploredTiles: [[F, F, F]],
entrance: [0, 0], exit: [2, 0], monster: null, lighting: {}
};
const marked = RK.sceneFromGameState({
...base,
players: [{ id: 'a', x: 0, y: 0 }, { id: 'b', x: 1, y: 0, you: true }]
}, {});
expect(marked.entities.find(e => e.id === 'b')).toMatchObject({ you: true, cameraTarget: true });
const spectator = RK.sceneFromGameState({
...base,
players: [
{ id: 'dead', x: 0, y: 0, alive: false },
{ id: 'live', x: 1, y: 0, alive: true },
{ id: 'done', x: 2, y: 0, alive: true, finished: true }
]
}, { viewerId: 'spectator' });
expect(spectator.entities.find(e => e.cameraTarget).id).toBe('live');
expect(spectator.entities.some(e => e.you)).toBe(false);
});
test('match treasure object shape renders until it has a carrier', () => {
const base = {
visibleTiles: [[F, F]], exploredTiles: [[F, F]],
players: [{ id: 'a', x: 0, y: 0 }], monster: null, lighting: {}
};
const loose = RK.sceneFromGameState({ ...base, treasure: { x: 1, y: 0, carrierId: null } }, {});
expect(loose.entities.some(e => e.id === 'treasure')).toBe(true);
const carried = RK.sceneFromGameState({ ...base, treasure: { x: 1, y: 0, carrierId: 'a' } }, {});
expect(carried.entities.some(e => e.id === 'treasure')).toBe(false);
});
});