Files
wownerogue/test/monsterCollision.test.js
T
jwinterm 7d3456f107 fix: dungeon reachability guard + per-substep monster collision
3.2 Reachability: a dungeon whose exit is unreachable from the entrance is
paid-but-unwinnable. generate() now BFS-checks entrance->exit and regenerates
if unreachable. Retries are deterministic (per-attempt ROT seed = seedInt +
attempt; the seeded rng stream keeps advancing), so verify-regeneration
replays the same attempts and reproduces the exact dungeon — the provably-fair
determinism from Phase 0.2 is preserved. The single-pass body is extracted to
_generateOnce and isReachable does the flood-fill.

3.3 Monster collision: with movesPerPlayerMove > 1 (the casino preset's 1.5x
speed) the monster takes multiple steps per turn, but collision was only
checked after the loop — so it could step onto the player's tile and off again
between checks, phasing through and missing the catch (player-favorable, erodes
the house edge). Collision is now checked after each sub-step.

Adds reachability tests (provablyFair.test.js) and monsterCollision.test.js.
Random block timing remains the untouched core mechanic. Full suite: 20 suites
/ 153 tests.
2026-05-31 14:08:45 -04:00

55 lines
2.0 KiB
JavaScript

/**
* Monster collision tests (Phase 3.3).
*
* With movesPerPlayerMove > 1 (e.g. the casino preset's 1.5x speed) the monster takes
* multiple steps per turn. Collision must be checked after EACH sub-step, or the monster
* can step onto the player's tile and off again between checks, phasing through the player
* and missing the catch (a player-favorable bug that lowers the house edge).
*/
const Game = require('../src/game/game');
function openGame() {
// Default-size dungeon so the digger generates validly; we then override the map and
// positions with a known fully-open grid so the scenario is deterministic.
const game = new Game('sock-test', { id: 1 });
const FL = "'1";
game.dungeon.map = Array.from({ length: 7 }, () => Array(7).fill(FL));
game.gameConfig.primaryFloor = FL;
game.gameConfig.secondaryFloor = "'2";
game.gameState = 'active';
game.seededRNG = () => 0; // 0 < chaseAggressiveness => always chase
return game;
}
describe('Monster collision with multi-step movement', () => {
test('2-step monster adjacent to the player catches it (no phasing through)', () => {
const game = openGame();
game.difficultyConfig.monster.movesPerPlayerMove = 2;
game.difficultyConfig.monster.chaseAggressiveness = 1.0;
game.monsterMoveAccumulator = 0;
game.player.moveTo(3, 3);
game.monster.moveTo(3, 4); // directly adjacent
game.monster.lastKnownPlayerX = null;
game.monster.lastKnownPlayerY = null;
const res = game.moveMonster();
expect(game.gameState).toBe('lost');
expect(res.event).toBe('monster_caught');
});
test('single-step monster moving onto the player still catches (sanity)', () => {
const game = openGame();
game.difficultyConfig.monster.movesPerPlayerMove = 1;
game.difficultyConfig.monster.chaseAggressiveness = 1.0;
game.monsterMoveAccumulator = 0;
game.player.moveTo(3, 3);
game.monster.moveTo(3, 4);
game.moveMonster();
expect(game.gameState).toBe('lost');
});
});