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
13 changes: 9 additions & 4 deletions src/app/game/agents/PathFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,24 @@ export default class PathFinder {
return false;
}

// TODO: handle grid edges
// Bounds-check the row AND col before indexing the matrix. A neighbor off the top/bottom edge has
// no row entry (matrix[-1] is undefined), so indexing it before this guard threw a TypeError.
const isValid = this.field.isValidPosition(neighbor.row, neighbor.col);
if (!isValid || !destination) {
return false;
}

const neighborTile = matrix[neighbor.row]![neighbor.col];
if (!neighborTile || !destination) {
if (!neighborTile) {
return false;
}

const isValid = this.field.isValidPosition(neighbor.row, neighbor.col);
const isRoad = (neighborTile instanceof Road);
// Every cell of the destination structure shares the same anchor identifier, so this lets A* step
// through a building's footprint to reach its anchor cell from an adjacent road.
const isDestination = (neighborTile.getIdentifier() === `${destination.row}-${destination.col}`);

return ( isValid && (isRoad || isDestination) );
return (isRoad || isDestination);
});

if (!validNeighbors) {
Expand Down
42 changes: 34 additions & 8 deletions test/agents/pathFinder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ describe('PathFinder A*', () => {
expect(() => pathFinder.findPath({ row: 1, col: 0 }, { row: 1, col: 2 })).not.toThrow();
});

// Documents a live edge-case limitation (acknowledged by the `// TODO: handle grid edges` comment in
// PathFinder.getValidNeighbors): a north/south neighbor lookup indexes `field.matrix[row]` BEFORE
// `isValidPosition` is consulted. A real Field's matrix only has entries for rows [0, rows), so probing
// the out-of-bounds north neighbor of a row-0 start (or the south neighbor of the last row) throws
// instead of being filtered out like an out-of-bounds column is. This pins the CURRENT behavior; it is
// not a desired outcome, and this test will need updating if the underlying bug is ever fixed.
test('a start on the grid top edge crashes probing the out-of-bounds north neighbor (documents a known edge-case bug)', () => {
// A north/south neighbor lookup used to index `field.matrix[row]` BEFORE `isValidPosition` was
// consulted. A real Field's matrix only has entries for rows [0, rows), so probing the out-of-bounds
// north neighbor of a row-0 start (or the south neighbor of the last row) threw a TypeError instead of
// being filtered out like an out-of-bounds column is. The fix bounds-checks the row before indexing;
// these two tests use UNPADDED matrices (only real rows exist, exactly like a real Field's top/bottom
// edge) to prove the edge is now filtered and the search still routes normally.
test('a start on the grid top edge filters the out-of-bounds north neighbor and still finds a path', () => {
const road00 = new Road(0, 0, null);
const road01 = new Road(0, 1, null);
const road02 = new Road(0, 2, null);
Expand All @@ -211,6 +211,32 @@ describe('PathFinder A*', () => {
};
const pathFinder = new PathFinder(field as unknown as Field);

expect(() => pathFinder.findPath({ row: 0, col: 0 }, { row: 0, col: 2 })).toThrow();
let path: Tile[] = [];
expect(() => { path = pathFinder.findPath({ row: 0, col: 0 }, { row: 0, col: 2 }); }).not.toThrow();
expect(path).toEqual([road01, road02]);
});

test('a start on the grid bottom edge filters the out-of-bounds south neighbor and still finds a path', () => {
// A 3-row road grid where only rows 0..2 exist (no row 3), just like a real Field's bottom edge.
// Starting on the last row (row 2) probes a south neighbor on row 3, which must be filtered.
const roads: { [row: number]: { [col: number]: Road } } = {};
for (let row = 0; row < 3; row++) {
roads[row] = {};
for (let col = 0; col < 3; col++) {
roads[row]![col] = new Road(row, col, null);
}
}
const matrix = roads as unknown as { [row: number]: { [col: number]: Tile } };
const field: FakeField = {
matrix: matrix as unknown as Field['matrix'],
isValidPosition: (row: number, col: number) => row >= 0 && row < 3 && col >= 0 && col < 3,
getTile: (row: number, col: number) => matrix[row]?.[col] ?? null,
};
const pathFinder = new PathFinder(field as unknown as Field);

let path: Tile[] = [];
expect(() => { path = pathFinder.findPath({ row: 2, col: 0 }, { row: 2, col: 2 }); }).not.toThrow();
// Straight along the bottom row: (2,0) -> (2,1) -> (2,2).
expect(path).toEqual([roads[2]![1], roads[2]![2]]);
});
});
Loading