refactor(minimotorways): improve roundabout visuals and traffic flow

- Redesign roundabout rendering with concentric rings (outer border, road
  surface, inner border, central island) for clearer visual distinction
- Increase ROUNDABOUT_CAP from 2 to 4 to allow more cars simultaneously
- Add stuck detection (stuckMs) with FORCE_ADVANCE_MS (6s) timeout to
  prevent cars from remaining permanently blocked
- Implement "don't block the box" logic: cars won't enter bare junctions
  if their exit cell is occupied, reducing gridlock
This commit is contained in:
Brian Fertig 2026-06-19 20:38:56 -06:00
parent fc7f5f83ca
commit 41818f7a4a
2 changed files with 48 additions and 9 deletions

View File

@ -511,10 +511,19 @@ export default class MiniMotorwaysGame extends Phaser.Scene {
for (const [k, item] of sim.items) {
const cx = cellCx(k); const cy = cellCy(k);
if (item.type === 'roundabout') {
g.fillStyle(this.city.palette.land, 1);
g.fillCircle(cx, cy, CELL * 0.16);
g.lineStyle(5, darken(this.city.palette.roadEdge, 0.7), 1);
g.strokeCircle(cx, cy, CELL * 0.16);
const pal = this.city.palette;
// Outer ring border
g.fillStyle(darken(pal.roadEdge, 0.88), 1);
g.fillCircle(cx, cy, CELL * 0.34);
// Ring road surface (covers road cap circles drawn beneath)
g.fillStyle(pal.road, 1);
g.fillCircle(cx, cy, CELL * 0.31);
// Inner ring border
g.fillStyle(darken(pal.roadEdge, 0.82), 1);
g.fillCircle(cx, cy, CELL * 0.18);
// Central island
g.fillStyle(pal.land, 1);
g.fillCircle(cx, cy, CELL * 0.14);
} else {
g.fillStyle(0x2f2f38, 1);
g.fillRoundedRect(cx - 9, cy - 14, 18, 28, 5);

View File

@ -45,8 +45,9 @@ export const TUNE = {
MOTORWAY_MS: 1200, // real traversal time, ignores all traffic
MOTORWAY_MIN_DIST: 4, // portals must be at least this far apart
CONGESTION_COST: 0.4, // A* edge penalty per car on the target cell
FORCE_ADVANCE_MS: 6000, // after this long stuck, car overrides canEnterCell
LIGHT_PHASE_MS: 4000,
ROUNDABOUT_CAP: 2,
ROUNDABOUT_CAP: 4,
DISPATCH_MS: 500,
SECOND_CAR_WEEK: 2, // houses gain a second car from this week on
COLOR_UNLOCK_WEEKS: [0, 0, 3, 5, 8, 11],
@ -859,7 +860,7 @@ export class Sim {
id: this.nextId++, color: house.color, houseId: house.id,
state: 'idle', path: null, pos: 0, x: xOf(house.k), y: yOf(house.k),
heading: 0, dwellT: 0, cooldownT: 0, targetId: null,
needsReroute: false, mw: null, occK: null,
needsReroute: false, mw: null, occK: null, stuckMs: 0,
};
this.cars.push(car);
house.carIds.push(car.id);
@ -894,6 +895,7 @@ export class Sim {
car.state = 'toPickup';
car.path = path;
car.pos = 0;
car.stuckMs = 0;
car.targetId = building.id;
car.needsReroute = false;
car.mw = null;
@ -918,6 +920,7 @@ export class Sim {
car.cooldownT = TUNE.COOLDOWN_MS;
car.path = null;
car.pos = 0;
car.stuckMs = 0;
car.mw = null;
car.needsReroute = false;
if (house) { car.x = xOf(house.k); car.y = yOf(house.k); }
@ -936,6 +939,7 @@ export class Sim {
car.state = 'toHome';
car.path = path;
car.pos = 0;
car.stuckMs = 0;
car.mw = null;
car.needsReroute = false;
this.setCarOcc(car, path[0]);
@ -981,6 +985,7 @@ export class Sim {
}
car.path = path;
car.pos = 0;
car.stuckMs = 0;
car.mw = null;
this.setCarOcc(car, path[0]);
this.syncCarXY(car);
@ -998,7 +1003,18 @@ export class Sim {
const axis = dy === 0 ? 0 : 1;
return axis === this.lightPhase();
}
return occ === 0;
// Bare junction: only enter if empty, and don't block the box —
// hold outside if our exit is itself a bare occupied junction.
if (occ > 0) return false;
if (car.path && car.path.length > 0) {
const curIdx = Math.round(car.pos);
if (curIdx + 2 < car.path.length) {
const exitK = car.path[curIdx + 2];
if (this.connCount(exitK) >= 3 && !this.items.has(exitK)
&& (this.cellOcc.get(exitK) || 0) > 0) return false;
}
}
return true;
}
headwayLimit(car, want) {
@ -1031,13 +1047,18 @@ export class Sim {
this.setCarOcc(car, car.path[Math.round(car.pos)]);
}
this.syncCarXY(car);
car.stuckMs = 0;
return;
}
const want = TUNE.CAR_SPEED * (dtMs / 1000);
let allowed = this.headwayLimit(car, want);
if (allowed <= 0.0001) return;
if (allowed <= 0.0001) {
car.stuckMs = (car.stuckMs ?? 0) + dtMs;
return;
}
const prevPos = car.pos;
const curIdx = Math.round(car.pos);
const boundary = curIdx + 0.5;
let target = car.pos + allowed;
@ -1053,9 +1074,11 @@ export class Sim {
car.mw = { fromIdx: curIdx, t: 0 };
this.setCarOcc(car, null);
this.syncCarXY(car);
car.stuckMs = 0;
return;
}
if (!this.canEnterCell(car, nextK, fromK)) target = boundary - 0.01;
const forceThrough = (car.stuckMs ?? 0) >= TUNE.FORCE_ADVANCE_MS;
if (!forceThrough && !this.canEnterCell(car, nextK, fromK)) target = boundary - 0.01;
}
car.pos = Math.min(target, car.path.length - 1);
@ -1063,10 +1086,17 @@ export class Sim {
this.setCarOcc(car, car.path[Math.min(occIdx, car.path.length - 1)]);
this.syncCarXY(car);
if (car.pos > prevPos + 0.0001) {
car.stuckMs = 0;
} else {
car.stuckMs = (car.stuckMs ?? 0) + dtMs;
}
if (car.pos >= car.path.length - 1 - 0.0001) {
if (car.state === 'toPickup') {
car.state = 'dwell';
car.dwellT = TUNE.DWELL_MS;
car.stuckMs = 0;
} else if (car.state === 'toHome') {
this.parkAtHome(car);
car.state = 'cooldown';