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)
|
||||
|| (sim.terrain[k] === TERRAIN.LAND && !sim.occupiedAt(k));
|
||||
|
||||
// State = cell * 9 + dirIndex (0-7 = one of DIRS, 8 = "no prior direction")
|
||||
// Primary cost: new road tiles placed (integer).
|
||||
// Tiebreaker: tiny penalty per direction change so straight/diagonal runs win.
|
||||
// State = cell * 9 + dirIndex (0-7 = DIRS entry, 8 = "no prior direction").
|
||||
// Primary cost: new road tiles. Tiebreaker: tiny turn penalty so straight
|
||||
// and diagonal runs beat zigzags when tile counts are equal.
|
||||
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 dist = new Float64Array(stateCount).fill(Infinity);
|
||||
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;
|
||||
dist[startState] = 0;
|
||||
|
||||
const settled = new Uint8Array(stateCount);
|
||||
const heap = [startState];
|
||||
|
||||
const swap = (a, b) => { const t = heap[a]; heap[a] = heap[b]; heap[b] = t; };
|
||||
const siftUp = (i) => {
|
||||
while (i > 0) {
|
||||
|
|
@ -927,11 +933,15 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
|
|||
const heapPop = () => {
|
||||
const top = heap[0];
|
||||
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); }
|
||||
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) {
|
||||
const state = heapPop();
|
||||
|
|
@ -939,10 +949,16 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
|
|||
settled[state] = 1;
|
||||
|
||||
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);
|
||||
|
||||
for (let di = 0; di < 8; di++) {
|
||||
const [dx, dy] = DIRS[di];
|
||||
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.
|
||||
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;
|
||||
return null; // target unreachable
|
||||
}
|
||||
|
||||
buildDrawPath() {
|
||||
|
|
|
|||
|
|
@ -383,12 +383,15 @@ export class Sim {
|
|||
const nk = keyOf(nx, ny);
|
||||
if (!this.roads.has(nk)) continue;
|
||||
if (dx !== 0 && dy !== 0) {
|
||||
// Crossing rule: if both shared corners carry road, the two diagonals
|
||||
// of this 2x2 block would cross — and the corners already connect the
|
||||
// cells orthogonally — so the diagonal edge is suppressed.
|
||||
// Suppress diagonal if either orthogonal intermediary is a road: either
|
||||
// the X-crossing case (both corners are roads, two diagonals would
|
||||
// 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 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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue