Refactor codebase
This commit is contained in:
103
src/engine/world/pathfinding.ts
Normal file
103
src/engine/world/pathfinding.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { World, Vec2 } from "../../core/types";
|
||||
import { key } from "../../core/utils";
|
||||
import { manhattan } from "../../core/math";
|
||||
import { inBounds, isWall, isBlocked, idx } from "./world-logic";
|
||||
|
||||
/**
|
||||
* Simple 4-dir A* pathfinding.
|
||||
* Returns an array of positions INCLUDING start and end. If no path, returns [].
|
||||
*
|
||||
* Exploration rule:
|
||||
* - You cannot path THROUGH unseen tiles.
|
||||
* - You cannot path TO an unseen target tile.
|
||||
*/
|
||||
export function findPathAStar(w: World, seen: Uint8Array, start: Vec2, end: Vec2, options: { ignoreBlockedTarget?: boolean } = {}): Vec2[] {
|
||||
if (!inBounds(w, end.x, end.y)) return [];
|
||||
if (isWall(w, end.x, end.y)) return [];
|
||||
|
||||
// If not ignoring target block, fail if blocked
|
||||
if (!options.ignoreBlockedTarget && isBlocked(w, end.x, end.y)) return [];
|
||||
|
||||
if (seen[idx(w, end.x, end.y)] !== 1) return [];
|
||||
|
||||
const open: Vec2[] = [start];
|
||||
const cameFrom = new Map<string, string>();
|
||||
|
||||
const gScore = new Map<string, number>();
|
||||
const fScore = new Map<string, number>();
|
||||
|
||||
const startK = key(start.x, start.y);
|
||||
gScore.set(startK, 0);
|
||||
fScore.set(startK, manhattan(start, end));
|
||||
|
||||
const inOpen = new Set<string>([startK]);
|
||||
|
||||
const dirs = [
|
||||
{ x: 1, y: 0 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 0, y: -1 }
|
||||
];
|
||||
|
||||
while (open.length > 0) {
|
||||
// Pick node with lowest fScore
|
||||
let bestIdx = 0;
|
||||
let bestF = Infinity;
|
||||
for (let i = 0; i < open.length; i++) {
|
||||
const k = key(open[i].x, open[i].y);
|
||||
const f = fScore.get(k) ?? Infinity;
|
||||
if (f < bestF) {
|
||||
bestF = f;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
const current = open.splice(bestIdx, 1)[0];
|
||||
const currentK = key(current.x, current.y);
|
||||
inOpen.delete(currentK);
|
||||
|
||||
if (current.x === end.x && current.y === end.y) {
|
||||
// Reconstruct path
|
||||
const path: Vec2[] = [end];
|
||||
let k = currentK;
|
||||
while (cameFrom.has(k)) {
|
||||
const prevK = cameFrom.get(k)!;
|
||||
const [px, py] = prevK.split(",").map(Number);
|
||||
path.push({ x: px, y: py });
|
||||
k = prevK;
|
||||
}
|
||||
path.reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
for (const d of dirs) {
|
||||
const nx = current.x + d.x;
|
||||
const ny = current.y + d.y;
|
||||
if (!inBounds(w, nx, ny)) continue;
|
||||
if (isWall(w, nx, ny)) continue;
|
||||
|
||||
// Exploration rule: cannot path through unseen (except start)
|
||||
if (!(nx === start.x && ny === start.y) && seen[idx(w, nx, ny)] !== 1) continue;
|
||||
|
||||
// Avoid walking through other actors (except standing on start, OR if it is the target and we ignore block)
|
||||
const isTarget = nx === end.x && ny === end.y;
|
||||
if (!isTarget && !(nx === start.x && ny === start.y) && isBlocked(w, nx, ny)) continue;
|
||||
|
||||
const nK = key(nx, ny);
|
||||
const tentativeG = (gScore.get(currentK) ?? Infinity) + 1;
|
||||
|
||||
if (tentativeG < (gScore.get(nK) ?? Infinity)) {
|
||||
cameFrom.set(nK, currentK);
|
||||
gScore.set(nK, tentativeG);
|
||||
fScore.set(nK, tentativeG + manhattan({ x: nx, y: ny }, end));
|
||||
|
||||
if (!inOpen.has(nK)) {
|
||||
open.push({ x: nx, y: ny });
|
||||
inOpen.add(nK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user