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.
100 lines
4.0 KiB
JavaScript
100 lines
4.0 KiB
JavaScript
/**
|
|
* Server-side fog of war.
|
|
*
|
|
* `Game.getState()` used to put the absolute entrance/exit/treasure coordinates AND the monster's
|
|
* exact position into every `game_start` / `game_update` frame, relying entirely on the browser
|
|
* renderer to decline to draw them. Anyone reading the socket payload in devtools could walk
|
|
* straight to the treasure and the exit from move 0: in a paid mode, a direct attack on the payout,
|
|
* and the same frame is forwarded verbatim to spectators.
|
|
*
|
|
* Concealment is now enforced where it is authoritative: the server. Discovery is STICKY so a
|
|
* feature you have found stays on your map after you walk away, matching the client's notion of
|
|
* explored memory.
|
|
*/
|
|
|
|
const Game = require('../src/game/game');
|
|
|
|
const KEYS = ['DUNGEON_LEVELS', 'DIFFICULTY_PRESET', 'CRYPTO_TYPE'];
|
|
const SAVE = {};
|
|
beforeEach(() => {
|
|
KEYS.forEach(k => { SAVE[k] = process.env[k]; delete process.env[k]; });
|
|
process.env.DIFFICULTY_PRESET = 'normal';
|
|
});
|
|
afterEach(() => {
|
|
KEYS.forEach(k => { if (SAVE[k] === undefined) delete process.env[k]; else process.env[k] = SAVE[k]; });
|
|
});
|
|
|
|
const user = () => ({ id: 0, username: 'test', endGame() {} });
|
|
|
|
/** Force a point into (or out of) the player's current FOV. */
|
|
function setVisible(game, point, visible) {
|
|
const [x, y] = Array.isArray(point) ? point : [point.x, point.y];
|
|
if (visible) {
|
|
game.visibleTiles[y] = game.visibleTiles[y] || {};
|
|
game.visibleTiles[y][x] = '.';
|
|
} else if (game.visibleTiles[y]) {
|
|
delete game.visibleTiles[y][x];
|
|
}
|
|
}
|
|
|
|
describe('server-side fog of war', () => {
|
|
test('undiscovered exit and treasure are withheld from the state frame', () => {
|
|
process.env.DUNGEON_LEVELS = '1';
|
|
const game = new Game('sock', user(), {});
|
|
// Nothing but the spawn room is in view at game start.
|
|
setVisible(game, game.dungeon.exit, false);
|
|
if (game.dungeon.treasure) setVisible(game, game.dungeon.treasure, false);
|
|
|
|
const state = game.getState();
|
|
expect(state.exit).toBeNull();
|
|
expect(state.treasure).toBeNull();
|
|
// The dungeon itself still knows; only the wire frame is redacted.
|
|
expect(game.dungeon.exit).toEqual(expect.any(Array));
|
|
});
|
|
|
|
test('a feature is disclosed once seen, and STAYS disclosed after it leaves view', () => {
|
|
process.env.DUNGEON_LEVELS = '1';
|
|
const game = new Game('sock', user(), {});
|
|
const exit = game.dungeon.exit;
|
|
|
|
expect(game.getState().exit).toBeNull();
|
|
|
|
setVisible(game, exit, true);
|
|
expect(game.getState().exit).toEqual(exit);
|
|
|
|
// Walk away: explored memory must persist, or a found staircase would vanish off the map.
|
|
setVisible(game, exit, false);
|
|
expect(game.getState().exit).toEqual(exit);
|
|
});
|
|
|
|
test('the monster is only reported while it is actually in view', () => {
|
|
process.env.DUNGEON_LEVELS = '1';
|
|
const game = new Game('sock', user(), {});
|
|
expect(game.monster).toBeTruthy();
|
|
|
|
setVisible(game, game.monster, false);
|
|
expect(game.getState().monster).toBeNull();
|
|
|
|
setVisible(game, game.monster, true);
|
|
const seen = game.getState().monster;
|
|
expect(seen).toBeTruthy();
|
|
expect(seen.x).toBe(game.monster.x);
|
|
expect(seen.y).toBe(game.monster.y);
|
|
|
|
// Unlike features, the monster moves: concealment is NOT sticky, or you could track the
|
|
// hunter through walls for the rest of the run.
|
|
setVisible(game, game.monster, false);
|
|
expect(game.getState().monster).toBeNull();
|
|
});
|
|
|
|
test('discovery survives repeated getState calls without re-checking visibility', () => {
|
|
process.env.DUNGEON_LEVELS = '1';
|
|
const game = new Game('sock', user(), {});
|
|
setVisible(game, game.dungeon.entrance, true);
|
|
expect(game.getState().entrance).toEqual(game.dungeon.entrance);
|
|
setVisible(game, game.dungeon.entrance, false);
|
|
expect(game.getState().entrance).toEqual(game.dungeon.entrance);
|
|
expect(game.getState().entrance).toEqual(game.dungeon.entrance);
|
|
});
|
|
});
|