mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-31 03:58:32 +01:00
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.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,29 @@ describe('Provably fair: deterministic dungeon generation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dungeon reachability (Phase 3.2)', () => {
|
||||
test('generated dungeons always have a reachable exit (many seeds)', () => {
|
||||
const cfg = { primaryFloor: "'1", secondaryFloor: "'2" };
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const seed = (i.toString(16).padStart(2, '0')).repeat(32);
|
||||
const d = DungeonGenerator.regenerateFromSeed(seed, 'WOW');
|
||||
expect(DungeonGenerator.isReachable(d.map, d.entrance, d.exit, cfg)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('isReachable returns false when the target is walled off', () => {
|
||||
const cfg = { primaryFloor: "'1", secondaryFloor: "'2" };
|
||||
// 3x3 all-floor map, but wall off the target corner.
|
||||
const map = [
|
||||
["'1", "'1", "'1"],
|
||||
["'1", "'1", '#'],
|
||||
["'1", '#', "'1"] // (2,2) is isolated by walls at (2,1) and (1,2)
|
||||
];
|
||||
expect(DungeonGenerator.isReachable(map, [0, 0], [2, 2], cfg)).toBe(false);
|
||||
expect(DungeonGenerator.isReachable(map, [0, 0], [1, 0], cfg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provably fair: deterministic monster movement', () => {
|
||||
// A small open room so the monster always has somewhere to wander.
|
||||
function buildDungeon() {
|
||||
|
||||
Reference in New Issue
Block a user