diff --git a/public/src/games/minimotorways/MiniMotorwaysGame.js b/public/src/games/minimotorways/MiniMotorwaysGame.js index 437a65e..450809d 100644 --- a/public/src/games/minimotorways/MiniMotorwaysGame.js +++ b/public/src/games/minimotorways/MiniMotorwaysGame.js @@ -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() { diff --git a/public/src/games/minimotorways/MiniMotorwaysLogic.js b/public/src/games/minimotorways/MiniMotorwaysLogic.js index ca51aee..184314c 100644 --- a/public/src/games/minimotorways/MiniMotorwaysLogic.js +++ b/public/src/games/minimotorways/MiniMotorwaysLogic.js @@ -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 }); }