fix: improve road pathfinding junction quality in Mini Motorways
- Add stop-at-road mode to A*: snap to nearest road cell on approach instead of routing along the road to the cursor, producing clean T- and L-junctions without diagonal shortcuts from unexpected angles - Tighten diagonal suppression rule: block diagonals when EITHER orthogonal intermediary is a road (previously required BOTH), preventing spurious diagonal shortcuts at T-junctions that would cause perpendicular approaches to fan out to three connections - Refactor A* path reconstruction into shared helper and add early termination to avoid post-search state scanning
This commit is contained in:
parent
44b837676c
commit
fc7f5f83ca
|
|
@ -890,21 +890,27 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
|
||||||
const isPassable = (k) => sim.roads.has(k)
|
const isPassable = (k) => sim.roads.has(k)
|
||||||
|| (sim.terrain[k] === TERRAIN.LAND && !sim.occupiedAt(k));
|
|| (sim.terrain[k] === TERRAIN.LAND && !sim.occupiedAt(k));
|
||||||
|
|
||||||
// State = cell * 9 + dirIndex (0-7 = one of DIRS, 8 = "no prior direction")
|
// State = cell * 9 + dirIndex (0-7 = DIRS entry, 8 = "no prior direction").
|
||||||
// Primary cost: new road tiles placed (integer).
|
// Primary cost: new road tiles. Tiebreaker: tiny turn penalty so straight
|
||||||
// Tiebreaker: tiny penalty per direction change so straight/diagonal runs win.
|
// and diagonal runs beat zigzags when tile counts are equal.
|
||||||
const ND = 9;
|
const ND = 9;
|
||||||
const TURN_COST = 0.001; // << 1, so never overrides tile-count optimality
|
const TURN_COST = 0.001;
|
||||||
const stateCount = WORLD_W * WORLD_H * ND;
|
const stateCount = WORLD_W * WORLD_H * ND;
|
||||||
const dist = new Float64Array(stateCount).fill(Infinity);
|
const dist = new Float64Array(stateCount).fill(Infinity);
|
||||||
const par = new Int32Array(stateCount).fill(-1);
|
const par = new Int32Array(stateCount).fill(-1);
|
||||||
|
const settled = new Uint8Array(stateCount);
|
||||||
|
|
||||||
|
// When the cursor is on an existing road and we're not starting from one,
|
||||||
|
// treat ANY road cell as the goal instead of routing along the road to the
|
||||||
|
// cursor. The nearest road cell on a straight/diagonal approach always wins
|
||||||
|
// (0 turns), so you get a clean T- or L-junction rather than a diagonal
|
||||||
|
// shortcut that sneaks up to the road from an unexpected angle.
|
||||||
|
const stopAtRoad = sim.roads.has(to) && !sim.roads.has(from);
|
||||||
|
|
||||||
const startState = from * ND + 8;
|
const startState = from * ND + 8;
|
||||||
dist[startState] = 0;
|
dist[startState] = 0;
|
||||||
|
|
||||||
const settled = new Uint8Array(stateCount);
|
|
||||||
const heap = [startState];
|
const heap = [startState];
|
||||||
|
|
||||||
const swap = (a, b) => { const t = heap[a]; heap[a] = heap[b]; heap[b] = t; };
|
const swap = (a, b) => { const t = heap[a]; heap[a] = heap[b]; heap[b] = t; };
|
||||||
const siftUp = (i) => {
|
const siftUp = (i) => {
|
||||||
while (i > 0) {
|
while (i > 0) {
|
||||||
|
|
@ -927,11 +933,15 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
|
||||||
const heapPop = () => {
|
const heapPop = () => {
|
||||||
const top = heap[0];
|
const top = heap[0];
|
||||||
const last = heap.pop();
|
const last = heap.pop();
|
||||||
// Only replace root if heap still has elements — avoids the single-element
|
|
||||||
// infinite loop where heap.pop() empties the array then heap[0]=last refills it.
|
|
||||||
if (heap.length > 0) { heap[0] = last; siftDown(0); }
|
if (heap.length > 0) { heap[0] = last; siftDown(0); }
|
||||||
return top;
|
return top;
|
||||||
};
|
};
|
||||||
|
const reconstruct = (state) => {
|
||||||
|
const path = [];
|
||||||
|
let s = state;
|
||||||
|
while (s !== -1) { path.unshift((s / ND) | 0); s = par[s]; }
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
while (heap.length > 0) {
|
while (heap.length > 0) {
|
||||||
const state = heapPop();
|
const state = heapPop();
|
||||||
|
|
@ -939,10 +949,16 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
|
||||||
settled[state] = 1;
|
settled[state] = 1;
|
||||||
|
|
||||||
const cur = (state / ND) | 0;
|
const cur = (state / ND) | 0;
|
||||||
const curDir = state % ND;
|
|
||||||
const cd = dist[state];
|
|
||||||
|
|
||||||
|
// Terminate: reached specific target, or (in road-stop mode) any road cell.
|
||||||
|
if (cur !== from && (cur === to || (stopAtRoad && sim.roads.has(cur)))) {
|
||||||
|
return reconstruct(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cd = dist[state];
|
||||||
|
const curDir = state % ND;
|
||||||
const cx = xOf(cur); const cy = yOf(cur);
|
const cx = xOf(cur); const cy = yOf(cur);
|
||||||
|
|
||||||
for (let di = 0; di < 8; di++) {
|
for (let di = 0; di < 8; di++) {
|
||||||
const [dx, dy] = DIRS[di];
|
const [dx, dy] = DIRS[di];
|
||||||
const nx = cx + dx; const ny = cy + dy;
|
const nx = cx + dx; const ny = cy + dy;
|
||||||
|
|
@ -962,18 +978,7 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the cheapest arrival direction at the destination.
|
return null; // target unreachable
|
||||||
let bestState = -1; let bestDist = Infinity;
|
|
||||||
for (let di = 0; di < ND; di++) {
|
|
||||||
const s = to * ND + di;
|
|
||||||
if (dist[s] < bestDist) { bestDist = dist[s]; bestState = s; }
|
|
||||||
}
|
|
||||||
if (bestState === -1 || bestDist === Infinity) return null;
|
|
||||||
|
|
||||||
const path = [];
|
|
||||||
let s = bestState;
|
|
||||||
while (s !== -1) { path.unshift((s / ND) | 0); s = par[s]; }
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDrawPath() {
|
buildDrawPath() {
|
||||||
|
|
|
||||||
|
|
@ -383,12 +383,15 @@ export class Sim {
|
||||||
const nk = keyOf(nx, ny);
|
const nk = keyOf(nx, ny);
|
||||||
if (!this.roads.has(nk)) continue;
|
if (!this.roads.has(nk)) continue;
|
||||||
if (dx !== 0 && dy !== 0) {
|
if (dx !== 0 && dy !== 0) {
|
||||||
// Crossing rule: if both shared corners carry road, the two diagonals
|
// Suppress diagonal if either orthogonal intermediary is a road: either
|
||||||
// of this 2x2 block would cross — and the corners already connect the
|
// the X-crossing case (both corners are roads, two diagonals would
|
||||||
// cells orthogonally — so the diagonal edge is suppressed.
|
// cross) or the T-junction case (one corner is road — traffic already
|
||||||
|
// flows through that cell, so the diagonal shortcut is spurious and
|
||||||
|
// would make a perpendicular approach fan out to three connections
|
||||||
|
// instead of one clean junction).
|
||||||
const cornerA = keyOf(x + dx, y);
|
const cornerA = keyOf(x + dx, y);
|
||||||
const cornerB = keyOf(x, y + dy);
|
const cornerB = keyOf(x, y + dy);
|
||||||
if (this.roads.has(cornerA) && this.roads.has(cornerB)) continue;
|
if (this.roads.has(cornerA) || this.roads.has(cornerB)) continue;
|
||||||
}
|
}
|
||||||
out.push({ k: nk, cost: dx !== 0 && dy !== 0 ? SQRT2 : 1 });
|
out.push({ k: nk, cost: dx !== 0 && dy !== 0 ? SQRT2 : 1 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue