const CONFIG = {
CANVAS: {
WIDTH: 900,
HEIGHT: 600
},
GRID: {
MIN_SCALE: 1,
MAX_SCALE: 5,
BASE_COLS: 9,
BASE_ROWS: 6
},
WALLS: {
DENSITY: 0.25
},
COLORS: {
ROAD: '#444444', // Darker gray for asphalt
WALL: '#808080', // Light gray for buildings
BUILDING_COLORS: [ // Various building colors
'#A0A0A0', // Light gray
'#909090', // Medium gray
'#787878', // Dark gray
'#228B22' // Forest green for trees
],
LANE_MARKER: '#ffffff',
DIVIDER: '#ffff00',
PATH_COLORS: [
'rgba(255, 0, 0, 0.3)',
'rgba(128, 0, 128, 0.3)', // Changed from green to purple
'rgba(0, 0, 255, 0.3)',
'rgba(255, 165, 0, 0.3)'
],
COLLISION_HIGHLIGHT: 'rgba(255, 0, 0, 0.5)'
},
CARS: {
EMOJIS: ['🚙', '🚔', '🚘', '🚕'],
COLORS: ['#ff0000', '#800080', '#0000ff', '#ffa500'], // Changed second color to purple
MOVE_SPEED: 0.05,
COLLISION_RADIUS: 1.5,
LOOK_AHEAD_STEPS: 5, // Increased look-ahead steps
RECALCULATION_DELAY: 15
}
};
// Global variables
let grid = [];
let cars = [];
let cols = 0;
let rows = 0;
let cellWidth = 0;
let cellHeight = 0;
class Spot {
constructor(i, j) {
this.i = i;
this.j = j;
this.f = 0;
this.g = 0;
this.h = 0;
this.neighbors = [];
this.previous = null;
this.wall = false;
this.visited = new Set();
this.buildingColor = null;
if ((i % 2 === 0 || j % 2 === 0) && Math.random() < CONFIG.WALLS.DENSITY) {
this.wall = true;
// Randomly assign building colors, with a 20% chance of being green (trees)
this.buildingColor = random() < 0.2 ?
CONFIG.COLORS.BUILDING_COLORS[3] : // Green for trees
random(CONFIG.COLORS.BUILDING_COLORS.slice(0, 3)); // Other building colors
}
}
addNeighbors(grid, cols, rows) {
const { i, j } = this;
// Only orthogonal movements allowed
const directions = [
[0, 1], // right
[1, 0], // down
[0, -1], // left
[-1, 0] // up
];
this.neighbors = directions
.map(([di, dj]) => {
const newI = i + di;
const newJ = j + dj;
if (newI >= 0 && newI < cols && newJ >= 0 && newJ < rows) {
return grid[newI][newJ];
}
return null;
})
.filter(neighbor => neighbor !== null);
}
draw(cellWidth, cellHeight) {
const x = this.i * cellWidth;
const y = this.j * cellHeight;
if (this.wall) {
fill(this.buildingColor);
noStroke();
rect(x, y, cellWidth, cellHeight);
}
this.visited.forEach(carIndex => {
fill(CONFIG.COLORS.PATH_COLORS[carIndex]);
noStroke();
rect(x, y, cellWidth, cellHeight);
});
}
}
class Car {
constructor(spot, color, emoji, index) {
this.currentSpot = spot;
this.startSpot = spot;
this.targetSpot = null;
this.color = color;
this.emoji = emoji;
this.index = index;
this.path = [];
this.currentPathIndex = 0;
this.progress = 0;
this.hasReachedDestination = false;
this.isWaiting = false;
this.waitingTimer = 0;
this.temporaryWalls = new Set();
this.lastRecalculationTime = 0;
}
getCurrentPosition() {
if (!this.path || this.currentPathIndex >= this.path.length - 1) {
return {
x: this.currentSpot.i,
y: this.currentSpot.j
};
}
const current = this.path[this.currentPathIndex];
const next = this.path[this.currentPathIndex + 1];
return {
x: lerp(current.i, next.i, this.progress),
y: lerp(current.j, next.j, this.progress)
};
}
predictFuturePosition(steps) {
if (!this.path || this.currentPathIndex >= this.path.length - steps) {
return this.getCurrentPosition();
}
const futureIndex = Math.min(
this.currentPathIndex + Math.floor(steps),
this.path.length - 1
);
if (steps % 1 === 0) {
return {
x: this.path[futureIndex].i,
y: this.path[futureIndex].j
};
} else {
const current = this.path[futureIndex];
const next = this.path[Math.min(futureIndex + 1, this.path.length - 1)];
const fractionalProgress = steps % 1;
return {
x: lerp(current.i, next.i, fractionalProgress),
y: lerp(current.j, next.j, fractionalProgress)
};
}
}
willCollideWith(otherCar) {
if (this.hasReachedDestination || otherCar.hasReachedDestination) {
return false;
}
const pos1 = this.getCurrentPosition();
const pos2 = otherCar.getCurrentPosition();
// Check current positions
if (dist(pos1.x, pos1.y, pos2.x, pos2.y) < CONFIG.CARS.COLLISION_RADIUS) {
return true;
}
// Check future positions with more precise intervals
for (let step = 0.5; step <= CONFIG.CARS.LOOK_AHEAD_STEPS; step += 0.5) {
const future1 = this.predictFuturePosition(step);
const future2 = otherCar.predictFuturePosition(step);
if (dist(future1.x, future1.y, future2.x, future2.y) < CONFIG.CARS.COLLISION_RADIUS) {
return true;
}
}
return false;
}
handlePotentialCollision(otherCar, grid) {
this.isWaiting = true;
this.waitingTimer = CONFIG.CARS.RECALCULATION_DELAY;
// Add current and future positions of other car as temporary walls
const otherPos = otherCar.getCurrentPosition();
const currentX = Math.floor(otherPos.x);
const currentY = Math.floor(otherPos.y);
// Add surrounding cells as temporary walls to force a wider berth
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const newX = currentX + dx;
const newY = currentY + dy;
if (newX >= 0 && newX < cols && newY >= 0 && newY < rows) {
this.temporaryWalls.add(`${newX},${newY}`);
}
}
}
// Add other car's path positions as temporary walls
if (otherCar.path) {
for (let i = otherCar.currentPathIndex; i < Math.min(otherCar.currentPathIndex + CONFIG.CARS.LOOK_AHEAD_STEPS, otherCar.path.length); i++) {
const pathSpot = otherCar.path[i];
this.temporaryWalls.add(`${pathSpot.i},${pathSpot.j}`);
}
}
}
findPath(grid, target) {
this.targetSpot = target;
this.path = this.aStarSearch(grid, this.currentSpot, target);
this.currentPathIndex = 0;
this.progress = 0;
return this.path !== null;
}
aStarSearch(grid, start, end) {
const openSet = [start];
const closedSet = new Set();
const cameFrom = new Map();
const gScore = new Map();
const fScore = new Map();
gScore.set(start, 0);
fScore.set(start, this.heuristic(start, end));
while (openSet.length > 0) {
let current = this.getLowestFScore(openSet, fScore);
if (current === end) {
return this.reconstructPath(cameFrom, current);
}
openSet.splice(openSet.indexOf(current), 1);
closedSet.add(current);
for (let neighbor of current.neighbors) {
if (closedSet.has(neighbor) ||
neighbor.wall ||
this.temporaryWalls.has(`${neighbor.i},${neighbor.j}`)) {
continue;
}
const tentativeGScore = gScore.get(current) + 1;
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
} else if (tentativeGScore >= gScore.get(neighbor)) {
continue;
}
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
fScore.set(neighbor, gScore.get(neighbor) + this.heuristic(neighbor, end));
}
}
return null;
}
update() {
if (this.hasReachedDestination) {
return false;
}
if (this.isWaiting) {
if (this.waitingTimer > 0) {
this.waitingTimer--;
return true;
}
const currentTime = millis();
if (currentTime - this.lastRecalculationTime > 500) { // Prevent too frequent recalculations
const newPath = this.aStarSearch(grid, this.currentSpot, this.targetSpot);
if (newPath) {
this.path = newPath;
this.currentPathIndex = 0;
this.progress = 0;
this.isWaiting = false;
this.temporaryWalls.clear();
this.lastRecalculationTime = currentTime;
} else {
this.waitingTimer = CONFIG.CARS.RECALCULATION_DELAY;
return true;
}
}
}
if (!this.path || this.currentPathIndex >= this.path.length - 1) {
if (this.currentSpot === this.targetSpot) {
this.hasReachedDestination = true;
}
return false;
}
this.progress += CONFIG.CARS.MOVE_SPEED;
if (this.progress >= 1) {
this.progress = 0;
this.currentPathIndex++;
this.currentSpot = this.path[this.currentPathIndex];
this.currentSpot.visited.add(this.index);
return this.currentPathIndex < this.path.length - 1;
}
return true;
}
heuristic(a, b) {
return Math.abs(a.i - b.i) + Math.abs(a.j - b.j);
}
getLowestFScore(openSet, fScore) {
return openSet.reduce((lowest, spot) =>
(!lowest || fScore.get(spot) < fScore.get(lowest)) ? spot : lowest
);
}
reconstructPath(cameFrom, current) {
const path = [current];
while (cameFrom.has(current)) {
current = cameFrom.get(current);
path.unshift(current);
}
return path;
}
draw(cellWidth, cellHeight) {
const pathColor = CONFIG.COLORS.PATH_COLORS[this.index];
// Draw start spot with car's path color
fill(pathColor);
noStroke();
rect(this.startSpot.i * cellWidth, this.startSpot.j * cellHeight, cellWidth, cellHeight);
// Draw target spot with car's path color
if (this.targetSpot) {
fill(pathColor);
noStroke();
rect(this.targetSpot.i * cellWidth, this.targetSpot.j * cellHeight, cellWidth, cellHeight);
}
let x, y;
if (this.path && this.currentPathIndex < this.path.length - 1) {
const current = this.path[this.currentPathIndex];
const next = this.path[this.currentPathIndex + 1];
x = lerp(current.i * cellWidth, next.i * cellWidth, this.progress);
y = lerp(current.j * cellHeight, next.j * cellHeight, this.progress);
} else {
x = this.currentSpot.i * cellWidth;
y = this.currentSpot.j * cellHeight;
}
if (this.isWaiting) {
fill(CONFIG.COLORS.COLLISION_HIGHLIGHT);
noStroke();
ellipse(x + cellWidth/2, y + cellHeight/2, cellWidth * CONFIG.CARS.COLLISION_RADIUS);
}
textAlign(CENTER, CENTER);
textSize(cellWidth * 0.8);
fill(this.color);
text(this.emoji, x + cellWidth / 2, y + cellHeight / 2);
}
}
function setup() {
const canvas = createCanvas(CONFIG.CANVAS.WIDTH, CONFIG.CANVAS.HEIGHT);
const scale = floor(random(CONFIG.GRID.MIN_SCALE, CONFIG.GRID.MAX_SCALE + 1));
cols = CONFIG.GRID.BASE_COLS * scale;
rows = CONFIG.GRID.BASE_ROWS * scale;
cellWidth = width / cols;
cellHeight = height / rows;
grid = [];
cars = [];
initializeGrid();
initializeCars();
}
function initializeGrid() {
for (let i = 0; i < cols; i++) {
grid[i] = [];
for (let j = 0; j < rows; j++) {
grid[i][j] = new Spot(i, j);
}
}
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
grid[i][j].addNeighbors(grid, cols, rows);
}
}
}
function initializeCars() {
const roadSpots = grid.flat().filter(spot => !spot.wall);
for (let i = 0; i < CONFIG.CARS.EMOJIS.length; i++) {
if (roadSpots.length < 2) break;
const startIndex = floor(random(roadSpots.length));
const startSpot = roadSpots.splice(startIndex, 1)[0];
const endIndex = floor(random(roadSpots.length));
const endSpot = roadSpots.splice(endIndex, 1)[0];
const car = new Car(
startSpot,
CONFIG.CARS.COLORS[i],
CONFIG.CARS.EMOJIS[i],
i
);
startSpot.visited.add(i);
car.findPath(grid, endSpot);
cars.push(car);
}
}
function drawRoadMarkings() {
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const x = i * cellWidth;
const y = j * cellHeight;
// Only draw road markings on non-wall cells
if (!grid[i][j].wall) {
stroke(CONFIG.COLORS.DIVIDER);
strokeWeight(2);
line(x + cellWidth / 2, y + 5, x + cellWidth / 2, y + cellHeight - 5);
stroke(CONFIG.COLORS.LANE_MARKER);
strokeWeight(1);
for (let k = 0; k < cellWidth; k += 15) {
line(x + k, y + 10, x + k + 10, y + 10);
line(x + k, y + cellHeight - 10, x + k + 10, y + cellHeight - 10);
}
}
}
}
}
function draw() {
background(CONFIG.COLORS.ROAD);
drawRoadMarkings();
// Draw grid and visited paths
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
grid[i][j].draw(cellWidth, cellHeight);
}
}
// Check for potential collisions between all pairs of cars
for (let i = 0; i < cars.length; i++) {
for (let j = i + 1; j < cars.length; j++) {
if (cars[i].willCollideWith(cars[j])) {
// The car that's further from its destination should wait and recalculate
const dist1 = cars[i].heuristic(cars[i].currentSpot, cars[i].targetSpot);
const dist2 = cars[j].heuristic(cars[j].currentSpot, cars[j].targetSpot);
if (dist1 > dist2) {
cars[i].handlePotentialCollision(cars[j], grid);
} else {
cars[j].handlePotentialCollision(cars[i], grid);
}
}
}
}
// Update and draw all cars
let allCarsArrived = true;
cars.forEach(car => {
if (!car.hasReachedDestination) {
allCarsArrived = false;
car.update();
}
car.draw(cellWidth, cellHeight);
});
// End simulation when all cars have reached their destinations
if (allCarsArrived) {
noLoop();
console.log("All cars have reached their destinations!");
}
}