Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/engine/atlas/battlefield.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,28 @@ describe('robustness across many seeds', () => {
expect(field.zones?.npc.length).toBeGreaterThan(0);
}
});

it('NEVER turns interior floor into rock, across 60 seeds', () => {
// The bug this pins: connectivity pruning used to treat a shut door as a
// barrier, so any room the spanning tree happened to gate with a closed
// door was declared unreachable and filled with solid stone. A single seed
// can easily roll all-open doors and look fine, which is how it shipped.
for (let i = 0; i < 60; i += 1) {
const field = compileBattlefield({ seed: seedFor(`interior-${i}`), archetype: 'interior' });
expect(field.solid ?? []).toEqual([]);
expect(openHexes(field).length).toBe(field.dims.cols * field.dims.rows);
}
});

it('keeps every room reachable even when every door is shut', () => {
for (let i = 0; i < 30; i += 1) {
const field = compileBattlefield({ seed: seedFor(`shut-${i}`), archetype: 'interior' });
const allShut: Battlefield = {
...field,
doors: Object.fromEntries(Object.keys(field.doors ?? {}).map((k) => [k, 'locked' as const])),
};
const total = allShut.dims.cols * allShut.dims.rows;
expect(reachableFrom(openHexes(allShut)[0], allShut).size).toBe(total);
}
});
});
17 changes: 11 additions & 6 deletions src/engine/atlas/battlefield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,27 @@ function borders(dims: GridDims): { a: HexCoord; b: HexCoord; dir: number }[] {
}

/**
* Hexes reachable from `start`, honouring walls, doors, and solid cells.
* Used to prove the generated arena has no isolated pockets.
* Hexes **structurally** reachable from `start` — blocked by walls and solid
* terrain, but **not** by a shut door.
*
* A closed door is a door: someone opens it. Treating it as a barrier here would
* make the generator declare every room behind one "stranded" and fill it with
* rock, which is exactly the bug this comment replaces — a tavern whose back
* rooms turned to solid stone because the spanning tree happened to roll a
* closed door. Moment-to-moment passability is a different question, and the
* engine already answers it via `compileTerrain` + `reachableHexes`.
*/
export function reachableFrom(start: HexCoord, field: Battlefield): Set<string> {
const walls = new Set(field.walls ?? []);
const doors = new Map(Object.entries(field.doors ?? {}).map(([k, v]) => [Number(k), v]));
const doors = new Set(Object.keys(field.doors ?? {}).map(Number));
const solid = new Set(field.solid ?? []);
const seen = new Set<string>([hexKey(start)]);
const queue: HexCoord[] = [start];
while (queue.length) {
const c = queue.pop()!;
for (let d = 0; d < 6; d += 1) {
const id = edgeId(c, d);
if (walls.has(id)) continue;
const door = doors.get(id);
if (door !== undefined && door !== 'open') continue;
if (walls.has(id) && !doors.has(id)) continue;
const n = { q: c.q + HEX_DIRECTIONS[d].q, r: c.r + HEX_DIRECTIONS[d].r };
const k = hexKey(n);
if (seen.has(k) || !inBounds(n, dims_(field)) || solid.has(k)) continue;
Expand Down
Loading