Code viewer for World: A star Practical 1 (clone ...
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!");
    }
}