Code viewer for World: Practical 1 Submission - A...

// Cloned by Vanisri on 2 Nov 2024 from World "A star" by "Coding Train" project 
// Please leave this clone trail here.
 
let cols, rows;
let grid = [];
let cars = [];
let cellSize = 50;
let obstacles = [];

// canvas size 
const cw = 700;
const ch = 700; 
 

function setup() 
{
    //slower frame rate to see how it is working 
    // MH edit
    frameRate (1);
    
    createCanvas(cw, ch);
    cols = width / cellSize;    //14 Columns
    rows = height / cellSize;   //14 Rows
    background(220);
    
    //Draw Road structure
    drawGrid();
    
    // Initialize the grid
    for (let i = 0; i < cols; i++) {
        grid[i] = [];
        for (let j = 0; j < rows; j++) {
            grid[i][j] = 0; // empty cell
        }
    }
    
    // MH edit
    const NumObstacles = 0;
    
    // Vanisri: Add 15 random obstacles to the grid
    for (let i = 0; i < NumObstacles ; i++) {
        let x = floor(random(cols));
        let y = floor(random(rows));
        grid[x][y] = 1;
        obstacles.push([x, y]);
    }

    // Vanisri: Create multiple cars with random start and goal positions
    for (let i = 0; i < 4; i++) {
        let start = randomPosition();
        let goal;
        do {
            goal = randomPosition();
        } while (arraysEqual(start, goal));

        cars.push(new Car(i, start, goal, color(random(255), random(255), random(255))));
    }
    
    //Draw walls/obstructions
    drawWalls();
}

// Utility function to generate a random position
function randomPosition() {
    return [floor(random(cols)), floor(random(rows))];
}

function draw()
// the search goes on over many timesteps 
// each timestep, check one more square and draw current partial solution 
{
    let occupiedPositions = cars.map(car => `${car.position}`);//Fetches the current position of all cars. Eg. positions=["2,3", "5,4", "1,1"]
    for (let car of cars) {
        if (!car.path.length || arraysEqual(car.position, car.goal)) {
            car.recalculatePath();
        } else {
            if (!occupiedPositions.includes(`${car.path[1]}`)) {
                //If car's next move doesn't have another car there, then move forward with calculated path (car.path[1])
                car.move();
            } else {
                //If car's next move has another car there, then Recalculate path
                car.recalculatePath();//If car's next move has another car there, then Recalculate path
            }
        }
        car.draw();
    }
}

//Sanidhya: Draws Black grids and borders to create a road like view.
function drawGrid() { 
    
    //Black Grids and Border
    stroke("black");
    strokeWeight(3);
    for (let i = 0; i < cols; i++) {
        for (let j = 0; j < rows; j++) { 
            noFill();
            rect(i * cellSize, j * cellSize, cellSize, cellSize); 
        }
    }
    
    //White Dotted Lines
    stroke("white");
    strokeWeight(3);
    
    for (let i = 0; i <= cellSize*cols; i+=cellSize) { 
        var interval=25;
        var interval2 = 50;
        for (let j = 0; j <= cellSize*rows; j+=cellSize) { 
            noFill();
            line(i, interval, j,interval);
            line(i, interval2, j,interval2);
            setLineDash([5, 5]);
            interval+=50;
            interval2+=50;
        }
    }
}

// Utility function to draw dotted lines
function setLineDash(list) {
  drawingContext.setLineDash(list);
}

//Sanidhya: Draws walls. Used Emoticons to denote Obstructions.
function drawWalls() { 
    obstructions=["🌲","🧱","🌳","🏢","🪵","🏨","🏭","🚶","🤰","🪨"];
    for (let i = 0; i < cols; i++) {
        for (let j = 0; j < rows; j++) { 
            if (grid[i][j] === 1) {
                fill('black');
                stroke('black');
                rect(i * cellSize, j * cellSize, cellSize, cellSize); //cellsize=50
                textSize(34);
                text(random(obstructions), i * cellSize, j * cellSize, cellSize, cellSize);
            } else {
                noFill();
            }
        }
    }
}

//Vanisri: Car class to store details of each of the 4 cars
class Car {
    constructor(id, start, goal, col) {
        this.id = id;
        this.position = start;
        this.goal = goal;
        this.color = col;
        this.path = [];
        this.drawSourceGrid();
        this.drawDestinationGrid();
        this.recalculatePath();
    }

    //Calls aStar function to recalculate the path.
    recalculatePath() {
        this.path = aStar(this.position, this.goal);
    }

    move() {
        if (this.path.length > 1) {
            this.position = this.path[1]; // Move to the next step
            this.path.shift();
        }
    }

    //Displays the path of car moved
    draw() {
        noFill();
        stroke(this.color);
        strokeWeight(3);
        rect(this.position[0] * cellSize, this.position[1] * cellSize, cellSize, cellSize); 
        textSize(34);
        text("🚗", this.position[0] * cellSize,this.position[1] * cellSize, cellSize, cellSize);
    }
    
    //To highlight the source of each car
    drawSourceGrid(){
        fill(this.color);
        rect(this.position[0] * cellSize, this.position[1] * cellSize, cellSize, cellSize); 
    }
    
    //To highlight the destination of each car
    drawDestinationGrid() { 
        stroke(this.color);
        strokeWeight(3);
        rect(this.goal[0] * cellSize, this.goal[1] * cellSize, cellSize, cellSize); 
    }
    
}

// https://github.com/qiao/PathFinding.js/blob/master/src/finders/AStarFinder.js
// https://youtu.be/aKYlikFAV4k
// https://briangrinstead.com/blog/astar-search-algorithm-in-javascript/
// Referenced all 3 sources above for the aStar function to find the shortest path
function aStar(start, goal) {
    let openList = [];
    let closedList = new Set();
    let gScore = {};
    let fScore = {};
    let cameFrom = {};

    gScore[`${start}`] = 0;
    fScore[`${start}`] = heuristic(start, goal);
    openList.push(start);

    while (openList.length > 0) {
        let current = openList.reduce((a, b) => (fScore[a] < fScore[b] ? a : b));

        if (arraysEqual(current, goal)) {
            return reconstructPath(cameFrom, current);
        }

        openList = openList.filter(node => !arraysEqual(node, current));
        closedList.add(`${current}`);

        for (let neighbor of getNeighbors(current)) {
            if (closedList.has(`${neighbor}`) || grid[neighbor[0]][neighbor[1]] === 1 || cars.some(car => arraysEqual(car.position, neighbor))) {
                continue;
            }

            let tentativeGScore = gScore[`${current}`] + 1;
            if (tentativeGScore < (gScore[`${neighbor}`] || Infinity)) {
                cameFrom[`${neighbor}`] = current;
                gScore[`${neighbor}`] = tentativeGScore;
                fScore[`${neighbor}`] = tentativeGScore + heuristic(neighbor, goal);

                if (!openList.some(n => arraysEqual(n, neighbor))) {
                    openList.push(neighbor);
                }
            }
        }
    }

    return []; // Return empty if no path found

    function heuristic([x1, y1], [x2, y2]) {
        return abs(x1 - x2) + abs(y1 - y2);
    }

    function reconstructPath(cameFrom, current) {
        let path = [current];
        while (cameFrom[`${current}`]) {
            current = cameFrom[`${current}`];
            path.unshift(current);
        }
        return path;
    }

    function getNeighbors([x, y]) {
        let neighbors = [
            [x + 1, y], [x - 1, y],
            [x, y + 1], [x, y - 1]
        ];
        return neighbors.filter(([nx, ny]) => nx >= 0 && nx < cols && ny >= 0 && ny < rows);
    }
}

// Utility function to check if 2 positions(arrays) are same
function arraysEqual(a, b) {
    return a[0] === b[0] && a[1] === b[1];
}