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:
parent
fc7f5f83ca
commit
41818f7a4a
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue