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) {
|
for (const [k, item] of sim.items) {
|
||||||
const cx = cellCx(k); const cy = cellCy(k);
|
const cx = cellCx(k); const cy = cellCy(k);
|
||||||
if (item.type === 'roundabout') {
|
if (item.type === 'roundabout') {
|
||||||
g.fillStyle(this.city.palette.land, 1);
|
const pal = this.city.palette;
|
||||||
g.fillCircle(cx, cy, CELL * 0.16);
|
// Outer ring border
|
||||||
g.lineStyle(5, darken(this.city.palette.roadEdge, 0.7), 1);
|
g.fillStyle(darken(pal.roadEdge, 0.88), 1);
|
||||||
g.strokeCircle(cx, cy, CELL * 0.16);
|
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 {
|
} else {
|
||||||
g.fillStyle(0x2f2f38, 1);
|
g.fillStyle(0x2f2f38, 1);
|
||||||
g.fillRoundedRect(cx - 9, cy - 14, 18, 28, 5);
|
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_MS: 1200, // real traversal time, ignores all traffic
|
||||||
MOTORWAY_MIN_DIST: 4, // portals must be at least this far apart
|
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
|
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,
|
LIGHT_PHASE_MS: 4000,
|
||||||
ROUNDABOUT_CAP: 2,
|
ROUNDABOUT_CAP: 4,
|
||||||
DISPATCH_MS: 500,
|
DISPATCH_MS: 500,
|
||||||
SECOND_CAR_WEEK: 2, // houses gain a second car from this week on
|
SECOND_CAR_WEEK: 2, // houses gain a second car from this week on
|
||||||
COLOR_UNLOCK_WEEKS: [0, 0, 3, 5, 8, 11],
|
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,
|
id: this.nextId++, color: house.color, houseId: house.id,
|
||||||
state: 'idle', path: null, pos: 0, x: xOf(house.k), y: yOf(house.k),
|
state: 'idle', path: null, pos: 0, x: xOf(house.k), y: yOf(house.k),
|
||||||
heading: 0, dwellT: 0, cooldownT: 0, targetId: null,
|
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);
|
this.cars.push(car);
|
||||||
house.carIds.push(car.id);
|
house.carIds.push(car.id);
|
||||||
|
|
@ -894,6 +895,7 @@ export class Sim {
|
||||||
car.state = 'toPickup';
|
car.state = 'toPickup';
|
||||||
car.path = path;
|
car.path = path;
|
||||||
car.pos = 0;
|
car.pos = 0;
|
||||||
|
car.stuckMs = 0;
|
||||||
car.targetId = building.id;
|
car.targetId = building.id;
|
||||||
car.needsReroute = false;
|
car.needsReroute = false;
|
||||||
car.mw = null;
|
car.mw = null;
|
||||||
|
|
@ -918,6 +920,7 @@ export class Sim {
|
||||||
car.cooldownT = TUNE.COOLDOWN_MS;
|
car.cooldownT = TUNE.COOLDOWN_MS;
|
||||||
car.path = null;
|
car.path = null;
|
||||||
car.pos = 0;
|
car.pos = 0;
|
||||||
|
car.stuckMs = 0;
|
||||||
car.mw = null;
|
car.mw = null;
|
||||||
car.needsReroute = false;
|
car.needsReroute = false;
|
||||||
if (house) { car.x = xOf(house.k); car.y = yOf(house.k); }
|
if (house) { car.x = xOf(house.k); car.y = yOf(house.k); }
|
||||||
|
|
@ -936,6 +939,7 @@ export class Sim {
|
||||||
car.state = 'toHome';
|
car.state = 'toHome';
|
||||||
car.path = path;
|
car.path = path;
|
||||||
car.pos = 0;
|
car.pos = 0;
|
||||||
|
car.stuckMs = 0;
|
||||||
car.mw = null;
|
car.mw = null;
|
||||||
car.needsReroute = false;
|
car.needsReroute = false;
|
||||||
this.setCarOcc(car, path[0]);
|
this.setCarOcc(car, path[0]);
|
||||||
|
|
@ -981,6 +985,7 @@ export class Sim {
|
||||||
}
|
}
|
||||||
car.path = path;
|
car.path = path;
|
||||||
car.pos = 0;
|
car.pos = 0;
|
||||||
|
car.stuckMs = 0;
|
||||||
car.mw = null;
|
car.mw = null;
|
||||||
this.setCarOcc(car, path[0]);
|
this.setCarOcc(car, path[0]);
|
||||||
this.syncCarXY(car);
|
this.syncCarXY(car);
|
||||||
|
|
@ -998,7 +1003,18 @@ export class Sim {
|
||||||
const axis = dy === 0 ? 0 : 1;
|
const axis = dy === 0 ? 0 : 1;
|
||||||
return axis === this.lightPhase();
|
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) {
|
headwayLimit(car, want) {
|
||||||
|
|
@ -1031,13 +1047,18 @@ export class Sim {
|
||||||
this.setCarOcc(car, car.path[Math.round(car.pos)]);
|
this.setCarOcc(car, car.path[Math.round(car.pos)]);
|
||||||
}
|
}
|
||||||
this.syncCarXY(car);
|
this.syncCarXY(car);
|
||||||
|
car.stuckMs = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const want = TUNE.CAR_SPEED * (dtMs / 1000);
|
const want = TUNE.CAR_SPEED * (dtMs / 1000);
|
||||||
let allowed = this.headwayLimit(car, want);
|
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 curIdx = Math.round(car.pos);
|
||||||
const boundary = curIdx + 0.5;
|
const boundary = curIdx + 0.5;
|
||||||
let target = car.pos + allowed;
|
let target = car.pos + allowed;
|
||||||
|
|
@ -1053,9 +1074,11 @@ export class Sim {
|
||||||
car.mw = { fromIdx: curIdx, t: 0 };
|
car.mw = { fromIdx: curIdx, t: 0 };
|
||||||
this.setCarOcc(car, null);
|
this.setCarOcc(car, null);
|
||||||
this.syncCarXY(car);
|
this.syncCarXY(car);
|
||||||
|
car.stuckMs = 0;
|
||||||
return;
|
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);
|
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.setCarOcc(car, car.path[Math.min(occIdx, car.path.length - 1)]);
|
||||||
this.syncCarXY(car);
|
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.pos >= car.path.length - 1 - 0.0001) {
|
||||||
if (car.state === 'toPickup') {
|
if (car.state === 'toPickup') {
|
||||||
car.state = 'dwell';
|
car.state = 'dwell';
|
||||||
car.dwellT = TUNE.DWELL_MS;
|
car.dwellT = TUNE.DWELL_MS;
|
||||||
|
car.stuckMs = 0;
|
||||||
} else if (car.state === 'toHome') {
|
} else if (car.state === 'toHome') {
|
||||||
this.parkAtHome(car);
|
this.parkAtHome(car);
|
||||||
car.state = 'cooldown';
|
car.state = 'cooldown';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue