Code viewer for World: Complex World (clone by To...
    // Cloned by Tom McAllister on 29 Nov 2020 from World "Complex World" by Starter user 
    // Please leave this clone trail here.

    AB.clockTick = 100;
    AB.maxSteps = 1000;
    AB.screenshotStep = 50;

    const show3d = true;
    const TEXTURE_WALL = '/uploads/tom/green-yellow.jpeg';
    const TEXTURE_MAZE = '/uploads/tom/green-yellow.jpeg';
    const TEXTURE_AGENT = '/uploads/tom/agent.jpg';
    const TEXTURE_ENEMY = '/uploads/tom/enemy.jpg';
    const MUSIC_BACK = '/uploads/starter/Defense.Line.mp3';
    const SOUND_ALARM = '/uploads/starter/air.horn.mp3';
    const gridsize = 50;
    const NOBOXES = Math.trunc((gridsize * gridsize) / 3);
    const diagonal = false;
    const squaresize = 100;
    const MAXPOS = gridsize * squaresize;
    const SKYCOLOR = 0xddffdd;
    const startRadiusConst = MAXPOS * 0.8;
    const maxRadiusConst = MAXPOS * 10;

    ABHandler.MAXCAMERAPOS = maxRadiusConst;
    ABHandler.GROUNDZERO = true;

    const SKYBOX_ARRAY = [
        "/uploads/tom/black.jpeg",
        "/uploads/tom/black.jpeg",
        "/uploads/tom/black.jpeg",
        "/uploads/tom/black.jpeg",
        "/uploads/tom/black.jpeg",
        "/uploads/tom/black.jpeg"
    ];

    //--- Mind can pick one of these actions -----------------

    const ACTION_LEFT = 0;
    const ACTION_RIGHT = 1;
    const ACTION_UP = 2;
    const ACTION_DOWN = 3;
    const ACTION_STAYSTILL = 4;

    const GRID_BLANK = 0;
    const GRID_WALL = 1;
    const GRID_MAZE = 2;
    var BOXHEIGHT; 
    var GRID = new Array(gridsize);
    var SPOTGRID = new Array(gridsize);
    var theagent, theenemy;
    var wall_texture, agent_texture, enemy_texture, maze_texture;
    var ei, ej, ai, aj;

    var badsteps;
    var goodsteps;


    // ADDED THESE
    var openSet = [];
    var closedSet = [];
    var path = [];
    var counter = 0; // WAS FOR DEBUGGING TO MAKE SURE ASTAR WAS CALLED

    function loadResources() // asynchronous file loads - call initScene() when all finished 
    {
        var loader1 = new THREE.TextureLoader();
        var loader2 = new THREE.TextureLoader();
        var loader3 = new THREE.TextureLoader();
        var loader4 = new THREE.TextureLoader();

        loader1.load(TEXTURE_WALL, function(thetexture) {
            thetexture.minFilter = THREE.LinearFilter;
            wall_texture = thetexture;
            if (asynchFinished()) initScene(); // if all file loads have returned 
        });

        loader2.load(TEXTURE_AGENT, function(thetexture) {
            thetexture.minFilter = THREE.LinearFilter;
            agent_texture = thetexture;
            if (asynchFinished()) initScene();
        });

        loader3.load(TEXTURE_ENEMY, function(thetexture) {
            thetexture.minFilter = THREE.LinearFilter;
            enemy_texture = thetexture;
            if (asynchFinished()) initScene();
        });

        loader4.load(TEXTURE_MAZE, function(thetexture) {
            thetexture.minFilter = THREE.LinearFilter;
            maze_texture = thetexture;
            if (asynchFinished()) initScene();
        });

    }


    function asynchFinished() // all file loads returned 
    {
        if (wall_texture && agent_texture && enemy_texture && maze_texture) return true;
        else return false;
    }




    //--- grid system -------------------------------------------------------------------------------
    // my numbering is 0 to gridsize-1

    function occupied(i, j) // is this square occupied
    {
        if ((ei == i) && (ej == j)) return true; // variable objects 
        if ((ai == i) && (aj == j)) return true;

        if (GRID[i][j] == GRID_WALL) return true; // fixed objects   
        if (GRID[i][j] == GRID_MAZE) return true;

        return false;
    }

    // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
    // logically, coordinates are: y=0, x and z all positive (no negative)    
    // logically my dimensions are all positive 0 to MAXPOS
    // to centre everything on origin, subtract (MAXPOS/2) from all dimensions 

    function translate(i, j) {
        var v = new THREE.Vector3();

        v.y = 0;
        v.x = (i * squaresize) - (MAXPOS / 2);
        v.z = (j * squaresize) - (MAXPOS / 2);

        return v;
    }




    function initScene() // all file loads have returned 
    {
        var i, j, shape, thecube;

        // set up GRID as 2D array

        for (i = 0; i < gridsize; i++)
            GRID[i] = new Array(gridsize);


        // set up walls

        for (i = 0; i < gridsize; i++)
            for (j = 0; j < gridsize; j++)
                if ((i == 0) || (i == gridsize - 1) || (j == 0) || (j == gridsize - 1)) {
                    GRID[i][j] = GRID_WALL;
                    shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
                    thecube = new THREE.Mesh(shape);
                    thecube.material = new THREE.MeshBasicMaterial({
                        map: wall_texture
                    });

                    thecube.position.copy(translate(i, j)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 
                    ABWorld.scene.add(thecube);
                }
        else
            GRID[i][j] = GRID_BLANK;


        // set up maze 

        for (var c = 1; c <= NOBOXES; c++) {
            i = AB.randomIntAtoB(1, gridsize - 2); // inner squares are 1 to gridsize-2
            j = AB.randomIntAtoB(1, gridsize - 2);

            GRID[i][j] = GRID_MAZE;

            shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
            thecube = new THREE.Mesh(shape);
            thecube.material = new THREE.MeshBasicMaterial({
                map: maze_texture
            });

            thecube.position.copy(translate(i, j)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 
            ABWorld.scene.add(thecube);
        }


        // set up enemy 
        // start in random location

        do {
            i = AB.randomIntAtoB(1, gridsize - 2);
            j = AB.randomIntAtoB(1, gridsize - 2);
        }
        while (occupied(i, j)); // search for empty square 

        ei = i;
        ej = j;

        shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
        theenemy = new THREE.Mesh(shape);
        theenemy.material = new THREE.MeshBasicMaterial({
            map: enemy_texture
        });
        ABWorld.scene.add(theenemy);
        drawEnemy();



        // set up agent 
        // start in random location

        do {
            i = AB.randomIntAtoB(1, gridsize - 2);
            j = AB.randomIntAtoB(1, gridsize - 2);
        }
        while (occupied(i, j)); // search for empty square 

        ai = i;
        aj = j;

        shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
        theagent = new THREE.Mesh(shape);
        theagent.material = new THREE.MeshBasicMaterial({
            map: agent_texture
        });
        ABWorld.scene.add(theagent);
        drawAgent();


        // finally skybox 
        // setting up skybox is simple 
        // just pass it array of 6 URLs and it does the asych load 

        ABWorld.scene.background = new THREE.CubeTextureLoader().load(SKYBOX_ARRAY, function() {
            ABWorld.render();

            AB.removeLoading();

            AB.runReady = true; // start the run loop
        });



        // CREATE A SEPARATE GRID, COMPOSED OF SPOT OBJECTS, PURELY FOR A* FUNCTION
        for (i = 0; i < gridsize; i++)
            SPOTGRID[i] = new Array(gridsize);

        for (var i = 0; i < gridsize; i++) {
            for (var j = 0; j < gridsize; j++) {
                SPOTGRID[i][j] = new Spot(i, j);
            }
        }

        for (var i = 0; i < gridsize; i++) {
            for (var j = 0; j < gridsize; j++) {
                SPOTGRID[i][j].addNeighbors(SPOTGRID);
            }
        }


        // WAS HERE FOR DEBUGGING
        console.log(GRID)
        console.log(SPOTGRID)


    }

    // DATA STRUCTURE SO A* CAN BE CALCULATED
    function Spot(i, j) {

        // Location
        this.i = i;
        this.j = j;

        // f, g, and h values for A*
        this.f = 0;
        this.g = 0;
        this.h = 0;

        // Neighbors
        this.neighbors = [];

        // Where did I come from?
        this.previous = undefined;

        this.occupied = occupied(this.i, this.j); // CALLS OCCUPIED FUNCTION WITH GRID COORDINATES TO VERIFY IF WALL OR MAZE

        // Figure out who my neighbors are
        this.addNeighbors = function(grid) {
            var i = this.i;
            var j = this.j;

            if (i < gridsize - 1) this.neighbors.push(grid[i + 1][j]);
            if (i > 0) this.neighbors.push(grid[i - 1][j]);
            if (j < gridsize - 1) this.neighbors.push(grid[i][j + 1]);
            if (j > 0) this.neighbors.push(grid[i][j - 1]);

            if (diagonal)
            // diagonals are also neighbours:
            {
                if (i > 0 && j > 0) this.neighbors.push(grid[i - 1][j - 1]);
                if (i < gridsize - 1 && j > 0) this.neighbors.push(grid[i + 1][j - 1]);
                if (i > 0 && j < gridsize - 1) this.neighbors.push(grid[i - 1][j + 1]);
                if (i < gridsize - 1 && j < gridsize - 1) this.neighbors.push(grid[i + 1][j + 1]);
            }

        }

    }

    // --- draw moving objects -----------------------------------


    function drawEnemy() // given ei, ej, draw it 
    {
        theenemy.position.copy(translate(ei, ej)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 

        ABWorld.lookat.copy(theenemy.position); // if camera moving, look back at where the enemy is  
    }


    function drawAgent() // given ai, aj, draw it 
    {
        theagent.position.copy(translate(ai, aj)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 

        ABWorld.follow.copy(theagent.position); // follow vector = agent position (for camera following agent)
    }




    // --- take actions -----------------------------------

    function moveLogicalEnemy() {
        aStar(); // THIS IS A SOME EXCESS REDIRECTION BUT WANTED TO MAKE THE A* STAND ALONE
    }



    function aStar() {


        openSet = [];
        closedSet = [];


        // BECAUSE A* IS CALLED REPEATEDLY, NEED TO RESET PATHS
        for (var i = 0; i < gridsize; i++) {
            for (var j = 0; j < gridsize; j++) {
                SPOTGRID[i][j].previous = undefined;
            }
        }

        counter = counter + 1;
        console.log("A * called: " + counter);

        var start = SPOTGRID[ei][ej];
        var end = SPOTGRID[ai][aj];

        console.log("AI:" + ai + " AJ: " + aj);
        console.log("START: " + start.i + " " + start.j);
        console.log("END: " + end.i + " " + end.j);


        openSet.push(start);

        var finished = false; // FLAG THAT IS USED TO BREAK WHILE LOOP EXECUTION EVEN WHEN OPENSET IS NOT EMPTY(ie WHEN END == CURRENT)

        while (openSet.length > 0) {
            var winner = 0;

            for (var i = 0; i < openSet.length; i++) {
                if (openSet[i].f < openSet[winner].f) {
                    winner = i;
                }
            }

            var current = openSet[winner];

            // Did I finish?
            if (current === end) {
                finished = true;
                console.log("SUCCESS - FOUND PATH!!!!!!!"); // DEBUGGING
            }

            removeFromArray(openSet, current);
            closedSet.push(current);

            var neighbors = current.neighbors;


            //--- start of for loop -----------
            for (var i = 0; i < neighbors.length; i++) {
                var neighbor = neighbors[i];


                // Valid next spot?
                if (!closedSet.includes(neighbor) && !neighbor.occupied) {
                    var tempG = current.g + heuristic(neighbor, current);

                    // Is this a better path than before?
                    var newPath = false;
                    if (openSet.includes(neighbor)) {
                        if (tempG < neighbor.g) {
                            neighbor.g = tempG;
                            newPath = true;
                        }
                    } else {
                        neighbor.g = tempG;
                        newPath = true;
                        openSet.push(neighbor);
                    }

                    // Yes, it's a better path
                    if (newPath) {
                        neighbor.h = heuristic(neighbor, end);
                        neighbor.f = neighbor.g + neighbor.h;
                        neighbor.previous = current;
                    }
                }
            }

            if (finished) {
                path = [];
                var temp = current;
                path.push(temp);

                while (temp.previous) {
                    path.push(temp.previous);
                    temp = temp.previous;
                }

                console.log("PRINTING PATH"); // WAS HERE FOR DEBUGGING
                for (var i = 0; i < path.length; i++) {
                    var spot = path[i];
                    console.log("i:" + spot.i + " j:" + spot.j);
                }


                if (path.length > 2) {
                    if (!SPOTGRID[path[path.length - 2].i][path[path.length - 2].j].occupied) {

                        console.log("Returning the next enemy spot") // AGAIN, MORE DEBUGGING
                        ei = path[path.length - 2].i;
                        ej = path[path.length - 2].j;
                    }
                }
                drawPath(path);
                return;

            }

        }

        if (!finished) {
            console.log("NO PATH EXISTS");
            return;

        }

    }


function drawPath(path) {
    var points = [];
    for (var i = 0; i < path.length; i++) {
        points.push(translate(path[i].i, path[i].j));
    }

    var material = new THREE.LineBasicMaterial({
        color: 0xffffff,
        linewidth: 40,
        linecap: 'round'
    });
    var geometry = new THREE.BufferGeometry().setFromPoints(points);

    var line = new THREE.Line(geometry, material);
    line.name = "path";
    ABWorld.scene.add(line);
}




    function moveLogicalAgent(a) // this is called by the infrastructure that gets action a from the Mind 
    {
        var i = ai;
        var j = aj;

        if (a == ACTION_LEFT) i--;
        else if (a == ACTION_RIGHT) i++;
        else if (a == ACTION_UP) j++;
        else if (a == ACTION_DOWN) j--;

        if (!occupied(i, j)) {
            ai = i;
            aj = j;
        }
    }




    // --- key handling --------------------------------------------------------------------------------------
    // This is hard to see while the Mind is also moving the agent:
    // AB.mind.getAction() and AB.world.takeAction() are constantly running in a loop at the same time 
    // have to turn off Mind actions to really see user key control 

    // we will handle these keys: 

    var OURKEYS = [37, 38, 39, 40];

    function ourKeys(event) {
        return (OURKEYS.includes(event.keyCode));
    }


    function keyHandler(event) {
        if (!AB.runReady) return true; // not ready yet 

        // if not one of our special keys, send it to default key handling:

        if (!ourKeys(event)) return true;

        // else handle key and prevent default handling:

        if (event.keyCode == 37) moveLogicalAgent(ACTION_LEFT);
        if (event.keyCode == 38) moveLogicalAgent(ACTION_DOWN);
        if (event.keyCode == 39) moveLogicalAgent(ACTION_RIGHT);
        if (event.keyCode == 40) moveLogicalAgent(ACTION_UP);

        // when the World is embedded in an iframe in a page, we want arrow key events handled by World and not passed up to parent 

        event.stopPropagation();
        event.preventDefault();
        return false;
    }




    // --- score: -----------------------------------


    function badstep() // is the enemy within one square of the agent
    {
        if ((Math.abs(ei - ai) < 2) && (Math.abs(ej - aj) < 2)) return true;
        else return false;
    }


    function agentBlocked() // agent is blocked on all sides, run over
    {
        return (occupied(ai - 1, aj) &&
            occupied(ai + 1, aj) &&
            occupied(ai, aj + 1) &&
            occupied(ai, aj - 1));
    }


    function updateStatusBefore(a)
    // this is called before anyone has moved on this step, agent has just proposed an action
    // update status to show old state and proposed move 
    {
        var x = AB.world.getState();
        ABWorld.scene.remove(ABWorld.scene.getObjectByName("path"));

        AB.msg(" Step: " + AB.step + " &nbsp; x = (" + x.toString() + ") &nbsp; a = (" + a + ") ");
    }


    function updateStatusAfter() // agent and enemy have moved, can calculate score
    {
        // new state after both have moved

        var y = AB.world.getState();
        var score = (goodsteps / AB.step) * 100;
        //ABWorld.scene.remove(ABWorld.scene.getObjectByName("astarpath"));

        AB.msg(" &nbsp; y = (" + y.toString() + ") <br>" +
            " Bad steps: " + badsteps +
            " &nbsp; Good steps: " + goodsteps +
            " &nbsp; Score: " + score.toFixed(2) + "% ", 2);
    }




    AB.world.newRun = function() {
        AB.loadingScreen();

        AB.runReady = false;

        badsteps = 0;
        goodsteps = 0;


        if (show3d) {
            BOXHEIGHT = squaresize;
            ABWorld.init3d(startRadiusConst, maxRadiusConst, SKYCOLOR);
        } else {
            BOXHEIGHT = 1;
            ABWorld.init2d(startRadiusConst, maxRadiusConst, SKYCOLOR);
        }


        loadResources(); // aynch file loads        
        // calls initScene() when it returns 

        document.onkeydown = keyHandler;

    };



    AB.world.getState = function() {
        var x = [ai, aj, ei, ej];
        return (x);
    };



    AB.world.takeAction = function(a) {


        updateStatusBefore(a); // show status line before moves 

        moveLogicalAgent(a);

        if ((AB.step % 2) == 0) {
            console.log("MOVING LOGICAL ENEMY");
            moveLogicalEnemy();
        }


        if (badstep()) badsteps++;
        else goodsteps++;

        drawAgent();
        drawEnemy();
        updateStatusAfter(); // show status line after moves  


        if (agentBlocked()) // if agent blocked in, run over 
        {
            AB.abortRun = true;
            goodsteps = 0; // you score zero as far as database is concerned             
            musicPause();
            soundAlarm();
        }

    };



    AB.world.endRun = function() {
        musicPause();
        if (AB.abortRun) AB.msg(" <br> <font color=red> <B> Agent trapped. Final score zero. </B> </font>   ", 3);
        else AB.msg(" <br> <font color=green> <B> Run over. </B> </font>   ", 3);
    };


    AB.world.getScore = function() {
        // only called at end - do not use AB.step because it may have just incremented past AB.maxSteps

        var s = (goodsteps / AB.maxSteps) * 100; // float like 93.4372778 
        var x = Math.round(s * 100); // 9344
        return (x / 100); // 93.44
    };




    // --- music and sound effects ----------------------------------------

    var backmusic = AB.backgroundMusic(MUSIC_BACK);

    function musicPlay() {
        backmusic.play();
    }

    function musicPause() {
        backmusic.pause();
    }


    function soundAlarm() {
        var alarm = new Audio(SOUND_ALARM);
        alarm.play(); // play once, no loop 
    }




    // HELPER FUNCTION USED IN A*
    function removeFromArray(arr, elt) {
        // Could use indexOf here instead to be more efficient
        for (var i = arr.length - 1; i >= 0; i--)
            if (arr[i] == elt)
                arr.splice(i, 1);
    }


    //=== heuristic ===========================
    // this must be always optimistic - real time will be this or longer

    function heuristic(a, b) {
        const y = new THREE.Vector2(a.i, a.j);
        const z = new THREE.Vector2(b.i, b.j);

        if (diagonal) return (y.distanceTo(z));

        // 2D distance
        // dist is a P5 function

        else return (Math.abs(y.x - z.x) + Math.abs(y.y - z.y));

        // else not diagonal, can only go across and down
        // so this is optimistic
        // not this is not optimistic if we can do diagonal move
    }