Code viewer for World: Car Pathfinding Simulation
//Made by Abhishek Malaviya
//Leave this trail here if you clone it
// Update v1.3 features:
// 1) Pathfinding A* Algorithm (Cars will find a fastest route and/or reroute if collision is detected )
// 2) Collision Avoidance and Incremental Waiting (Cars will wait for traffic to clear and if it doesnt, it will reroute after a set frame[partially works])
// 3) Error Prevention: Range Error Handling (Only partially works)
// 4) Makes sure when cars load into the world, it has a start point, end point and a path (only partially works, might be a cause of RangeError)
// 5) Added cars with retry logic to avoid infinite loops if stuck at a point

// Update v1.5: 
// Solved Range Error (Cars spawned before and outside the grid, when grid was loaded, cars couldn't find a way inside the grid)
// Cars Reroute when stuck in traffic (works partially)
// Added wait times for car being stuck in traffic and retrying after a set frames(works partially)
// Added debugging tools (console logs) to check proper working of code.
// Will need more debugging and trial methods to make it work properly
// To fix these issues, I am thinking of making each car as a separate class, havings its own wait periods and rerouting logic which could fix these issues. 
// Will try to update it in V2.0 where it will be a complete redesign of the code, grid(graphically), and the cars(graphically).
// I have mentioned the resources I have used and referred them in my documentation.

// Disclaimer: I have used Inspect Element's AI(Gemini I think) for debugging and understanding the core of a problem but the entire code is NOT made from GeminiAI or ChatGPT



const diagonal = true;
const cw = 1000;
const ch = 700;
const numCars = 4;

let cols = 20, rows = 15;
let w, h;
const grid = [];
const cars = [];

const wallAmount = 0.4;
const backcolor = 'white';
const wallcolor = 'black';
const carColors = ['red', 'green', 'blue', 'yellow'];

class Spot {
    constructor(i, j) {
        this.i = i;
        this.j = j;
        this.f = 0;
        this.g = 0;
        this.h = 0;
        this.neighbors = [];
        this.previous = undefined;

        const isStreetRow = random() < 0.5;
        const isStreetCol = random() < 0.5;
        this.wall = !(isStreetRow || isStreetCol);
    }

    show(col) {
        fill(this.wall ? wallcolor : col || backcolor);
        noStroke();
        rect(this.i * w, this.j * h, w, h);
    }

    addNeighbors(grid) {
        let { i, j } = this;
        if (i < cols - 1) this.neighbors.push(grid[i + 1][j]);
        if (i > 0) this.neighbors.push(grid[i - 1][j]);
        if (j < rows - 1) this.neighbors.push(grid[i][j + 1]);
        if (j > 0) this.neighbors.push(grid[i][j - 1]);
    }
}

class Car {
    constructor(start, end, color) {
        this.start = start;
        this.end = end;
        this.path = [];
        this.current = start;
        this.color = color;
        this.recalculatePath();
        this.waitingForPath = false;
        this.waitTurns = 0;
        this.maxWaitTurns = Math.floor(Math.random() * 3) + 1;
        this.visited = new Set();
        this.waitTimer = 0;
        this.maxWaitTime = Math.max(1, Math.floor(this.waitTimer / 2)); // Decrease wait time based on waiting duration
    }

    recalculatePath() {
        console.log(`Car starting at (${this.start.i}, ${this.start.j}) trying to reach (${this.end.i}, ${this.end.j})`);

        if (!this.visited) {
            this.visited = new Set();
        }

        if (this.start.i === this.end.i && this.start.j === this.end.j) {
            console.error("Invalid path: Start and end positions are the same.");
            this.path = [];
            this.waitingForPath = true;
            return;
        }

        let openSet = [this.start];
        let closedSet = new Set();
        this.path = [];
        this.start.g = 0;
        this.start.f = heuristic(this.start, this.end);

        let pathFound = false;

        while (openSet.length > 0) {
            let winner = openSet.reduce((best, spot, idx) => spot.f < openSet[best].f ? idx : best, 0);
            let current = openSet[winner];

            if (current === this.end) {
                let temp = current;
                this.path = [];
                let loopCounter = 0;

                while (temp) {
                    if (!temp.previous || temp.previous === this.start) {
                        this.path.push(temp);
                        break;
                    }
                    this.path.push(temp);
                    temp = temp.previous;

                    loopCounter++;
                    if (loopCounter > 1000) {
                        console.error("Possible infinite loop detected while constructing path!");
                        break;
                    }
                }

                this.path.reverse();
                pathFound = true;
                console.log(`Path found for car! Path length: ${this.path.length}`);
                return;
            }

            openSet = openSet.filter((spot) => spot !== current);
            closedSet.add(current);

            for (let neighbor of current.neighbors) {
                let isCarBlocking = cars.some(car => car.current === neighbor && car !== this);

                if (neighbor && !closedSet.has(neighbor) && !neighbor.wall && !isCarBlocking && !this.visited.has(neighbor)) {
                    let tempG = current.g + 1;

                    if (!openSet.includes(neighbor)) {
                        openSet.push(neighbor);
                    } else if (tempG >= neighbor.g) {
                        continue;
                    }

                    neighbor.g = tempG;
                    neighbor.h = heuristic(neighbor, this.end);
                    neighbor.f = neighbor.g + neighbor.h;
                    neighbor.previous = current;

                    this.visited.add(neighbor);
                }
            }
        }

        if (!pathFound) {
            console.log(`No path found for car from (${this.start.i}, ${this.start.j}) to (${this.end.i}, ${this.end.j}). Car is waiting.`);
            this.waitingForPath = true;
            this.path = [];
        }
    }

    move() {
        if (this.waitingForPath) {
            this.waitTurns++;
            this.waitTimer++;

            if (this.waitTurns >= this.maxWaitTurns || this.waitTimer >= this.maxWaitTime) {
                console.log(`Car at (${this.current.i}, ${this.current.j}) finished waiting. Recalculating path.`);
                this.recalculatePath();

                let next = this.path[0];
                let isNextPositionOccupied = cars.some(car => car.current === next && car !== this);

                if (!isNextPositionOccupied && this.path.length > 0) {
                    this.waitingForPath = false;
                    this.waitTurns = 0;
                    this.waitTimer = 0;
                } else {
                    console.log("Car cannot proceed, still blocked.");
                }
            }
            return;
        }

        if (this.path.length > 0) {
            let next = this.path[0];
            let isNextPositionOccupied = cars.some(car => car.current === next && car !== this);

            if (!isNextPositionOccupied) {
                this.current = next;
                this.path.shift();
                this.waitTurns = 0;
            } else {
                console.log(`Car at (${this.current.i}, ${this.current.j}) is waiting for the path to clear.`);
                this.waitingForPath = true;
                this.waitTurns = 0;
                this.waitTimer = 0;
            }
        }
    }

    show() {
        fill(this.color);
        noStroke();
        rect(this.current.i * w, this.current.j * h, w, h);
    }

    drawPath() {
        noFill();
        stroke(this.color);
        beginShape();
        for (let spot of this.path) {
            vertex(spot.i * w + w / 2, spot.j * h + h / 2);
        }
        endShape();
    }
}

function initializeGrid() {
    for (let i = 0; i < cols; i++) {
        grid[i] = new Array(rows);
        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);
        }
    }
}

function initializeCars() {
    let start = grid[0][0];
    let end = grid[cols - 1][rows - 1];
    for (let i = 0; i < numCars; i++) {
        let car = new Car(start, end, carColors[i]);
        cars.push(car);
    }
}


function setup() {
    createCanvas(cw, ch);
    frameRate(60);
    w = width / cols;
    h = height / rows;
    initializeGrid();
    initializeCars();
}



function draw() {
    
    
    background(backcolor);

    for (let i = 0; i < cols; i++) {
        for (let j = 0; j < rows; j++) {
            grid[i][j].show();
        }
    }

    for (let car of cars) {
        car.move();
        car.show();
        car.drawPath();
    }
}

function heuristic(a, b) {
    return dist(a.i, a.j, b.i, b.j);
}

function setup() {
    frameRate(2);
    createCanvas(cw, ch);
    cols = 20;
    rows = 15;
    w = width / cols;
    h = height / rows;

    // Create the grid
    for (let i = 0; i < cols; i++) {
        grid[i] = new Array(rows);
        for (let j = 0; j < rows; j++) {
            grid[i][j] = new Spot(i, j);
        }
    }

    // Add neighbors to each spot
    for (let i = 0; i < cols; i++) {
        for (let j = 0; j < rows; j++) {
            grid[i][j].addNeighbors(grid);
        }
    }

    // Add cars with retry logic to avoid infinite loops
    for (let i = 0; i < numCars; i++) {
        let start, end;
        let retryCount = 0;  // Initialize retryCount for each car
        const maxRetries = 100;  // Maximum number of retries to avoid infinite loops

        // Ensure the start point is valid and not a wall or occupied by another car
        do {
            start = grid[floor(random(cols))][floor(random(rows))];
            retryCount++;
            if (retryCount > maxRetries) {
                console.log("Failed to find valid start point after max retries");
                break;  // Exit loop if we've tried too many times
            }
        } while (start.wall || cars.some(car => car.current === start));  // Check if spot is a wall or occupied

        retryCount = 0;  // Reset retryCount for the end point

        // Ensure the end point is valid, not a wall, and not the same as the start or occupied by another car
        do {
            end = grid[floor(random(cols))][floor(random(rows))];
            retryCount++;
            if (retryCount > maxRetries) {
                console.log("Failed to find valid end point after max retries");
                break;  // Exit loop if we've tried too many times
            }
        } while (end.wall || end === start || cars.some(car => car.end === end));  // Check for wall, start, or occupied

        // Set start and end to be walkable for this car
        start.wall = end.wall = false;

        // Assign a unique car color
        let color = carColors[i % carColors.length];
        cars.push(new Car(start, end, color));
    }
}


function draw() {
    try {
        background(backcolor);

        // Draw all grid spots
        grid.flat().forEach((spot) => spot.show());

        cars.forEach((car, idx) => {
            car.drawPath();

            let nextPosition = car.path.length > 1 ? car.path[1] : null;

            if (nextPosition) {
                let blockingCar = cars.find((otherCar, otherIdx) =>
                    otherIdx !== idx && otherCar.current === nextPosition
                );

                if (blockingCar) {
                    car.waiting = true;
                } else {
                    car.move();
                }
            }

            car.show();
        });
    } catch (error) {
        if (error instanceof RangeError) {
            console.error("RangeError detected. Refreshing the page.");
            alert("RangeError occurred. Refreshing the page to fix the issue.");
            location.reload();
        } else {
            console.error(error);
            throw error;  // re-throw other errors
        }
    }
    
     

}