Code viewer for World: Complex World (clone by Ni...

// Cloned by Nithin Sai K J on 18 Oct 2023 from World "Complex World" by Starter user 
// Please leave this clone trail here.




// ==== Starter World =================================================================================================
// This code is designed for use on the Ancient Brain site.
// This code may be freely copied and by anyone on the Ancient Brain site.
// To include a working run of this program on another site, see the "Embed code" links provided on Ancient Brain.
// ====================================================================================================================



// =============================================================================================
// More complex starter World 
// 3d-effect Maze World (really a 2-D problem)
// Movement is on a semi-visible grid of squares 
//
// This more complex World shows:
// - Skybox
// - Internal maze (randomly drawn each time)
// - Enemy actively chases agent
// - Music/audio
// - 2D world (clone this and set show3d = false)
// - User keyboard control (clone this and comment out Mind actions to see)
// =============================================================================================


// =============================================================================================
// Scoring:
// Bad steps = steps where enemy is within one step of agent.
// Good steps = steps where enemy is further away. 
// Score = good steps as percentage of all steps.
//
// There are situations where agent is trapped and cannot move.
// If this happens, you score zero.
// =============================================================================================








// ===================================================================================================================
// === Start of tweaker's box ======================================================================================== 
// ===================================================================================================================

// The easiest things to modify are in this box.
// You should be able to change things in this box without being a JavaScript programmer.
// Go ahead and change some of these. What's the worst that could happen?


AB.clockTick = 100; // 200;

// Speed of run: Step every n milliseconds. Default 100.

AB.maxSteps = 1000;

// Length of run: Maximum length of run in steps. Default 1000.

AB.screenshotStep = 5;

// Take screenshot on this step. (All resources should have finished loading.) Default 50.



//---- global constants: -------------------------------------------------------

const show3d = true;						// Switch between 3d and 2d view (both using Three.js) 

// following textures from: Freepik.com
// const TEXTURE_WALL = '/uploads/nithinsai/ancient_wall.jpg';
const TEXTURE_WALL = '/uploads/nithinsai/minecraft_brick.jpg';
const TEXTURE_MAZE = '/uploads/nithinsai/minecraft_brick.jpg';
const TEXTURE_AGENT = '/uploads/nithinsai/crying.png';
const TEXTURE_ENEMY = '/uploads/nithinsai/rick.jpg';
const TEXTURE_FLOOR = '/uploads/nithinsai/grass_texture.jpg';

const MUSIC_BACK = '/uploads/nithinsai/winning-elevation.mp3';
const SOUND_ALARM = '/uploads/starter/air.horn.mp3';

// credits:
// https://pixabay.com/music/main-title-winning-elevation-111355/
// http://soundbible.com/1542-Air-Horn.html 


var PATH_COLOR = 0x000000;              // color of the path

const gridsize = 40;					// number of squares along side of world

const HEURISTIC_APPROACH_TYPE = 2;
/*
HEURISTIC APPROACH TYPE
0 = high constant heuristic of 1000
1 = breadth first search heuristic of 0
2 = an A* heuristic of Manhattan distance
3 = an A* heuristic of Euclidean distance
4 = an A* heuristic of Chebyshev distance
*/

const NOBOXES = Math.trunc((gridsize * gridsize) / 4);      // 5
// density of maze - number of internal boxes
// (bug) use trunc or can get a non-integer 

const squaresize = 20;					// size of square in pixels

const MAXPOS = gridsize * squaresize;	// length of one side in pixels 

const SKYCOLOR = 0x000000;				// a number, not a string 

const DARK_GAME =  false// enable to test a dark game

const startRadiusConst = MAXPOS * 0.8;	// distance from centre to start the camera at
const maxRadiusConst = MAXPOS * 10;		// maximum distance from camera we will render things  



//--- change ABWorld defaults: -------------------------------

ABHandler.MAXCAMERAPOS = maxRadiusConst;

ABHandler.GROUNDZERO = true;						// "ground" exists at altitude zero



//--- skybox: -------------------------------


// Image from: https://www.cleanpng.com/png-skybox-multiplication-sign-android-multiplication-1687670
const SKYBOX_ARRAY = [
    "/uploads/nithinsai/neg_x_dark.png",
    "/uploads/nithinsai/pos_x_dark.png",
    "/uploads/nithinsai/pos_y_darkk.png",
    "/uploads/nithinsai/neg_y_dark.png",
    "/uploads/nithinsai/pos_z_dark.png",
    "/uploads/nithinsai/neg_z_dark.png"
];




// ===================================================================================================================
// === End of tweaker's box ==========================================================================================
// ===================================================================================================================


// You will need to be some sort of JavaScript programmer to change things below the tweaker's box.









//--- 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;

// in initial view, (smaller-larger) on i axis is aligned with (left-right)
// in initial view, (smaller-larger) on j axis is aligned with (away from you - towards you)


// contents of a grid square

const GRID_BLANK = 0;
const GRID_WALL = 1;
const GRID_MAZE = 2;


var totalStatesExplored = 0;  // global variable to keep track of the total states explored
var totalPossibleSteps = 0;  // global variable to keep track of the total possible steps


var BOXHEIGHT;		// 3d or 2d box height 

var GRID = new Array(gridsize);			// can query GRID about whether squares are occupied, will in fact be initialised as a 2D array   

var theagent, theenemy;

var wall_texture, agent_texture, enemy_texture, maze_texture, floor_texture;


// enemy and agent position on squares
var ei, ej, ai, aj;

var badsteps;
var goodsteps;

const heuristicMap = {
    0: "Bad heuristic: h 1000",
    1: "BFS: h 0",
    2: "A* with Manhattan Distance",
    3: "A* with Euclidean Distance",
    4: "A* with Chebyshev Distance"
};


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();
    var loader5 = 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();
    });

    // code by Nithin Sai Kirumani Jagadish - added floor texture

    loader5.load(TEXTURE_FLOOR, function (thetexture) {
        thetexture.minFilter = THREE.LinearFilter;
        floor_texture = thetexture;
        if (asynchFinished()) initScene();
    });


    // end code by Nithin Sai Kirumani Jagadish  - added floor texture

}


function asynchFinished()		 // all file loads returned 
{
    if (wall_texture && agent_texture && enemy_texture && maze_texture && floor_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;
    var edgesMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });

    // set up GRID as 2D array

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


    // set up walls
    // code edit by Nithin Sai - small edits to add an alternate version of game

    var wallMaterial;

    if (DARK_GAME) {
        wallMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
    }

    else {
        wallMaterial = new THREE.MeshBasicMaterial({ map: wall_texture });
        wallMaterial.color.set(0x765656);
    }
    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);
                let edgesMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
                var edges = new THREE.EdgesGeometry(shape);
                if (DARK_GAME) {
                    var edgesMesh = new THREE.LineSegments(edges, edgesMaterial);
                    thecube.add(edgesMesh);
                }
                thecube.material = wallMaterial;

                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
    var mazeMaterial;

    if (DARK_GAME) {
        mazeMaterial = new THREE.MeshBasicMaterial({ color: 0x555555 });
    }
    else {
        mazeMaterial = new THREE.MeshBasicMaterial({ map: maze_texture });
        mazeMaterial.color.set(0xaaaaaa);
    }
    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);
        let edges = new THREE.EdgesGeometry(shape);
        if (DARK_GAME) {
            let edgesMesh = new THREE.LineSegments(edges, edgesMaterial);
            thecube.add(edgesMesh);
        }
        thecube.material = mazeMaterial;

        thecube.position.copy(translate(i, j)); 		  	// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 
        ABWorld.scene.add(thecube);
    }
    
    // total number of places on the GRID that are vacant
    totalPossibleSteps = GRID.flat().filter(cell => cell === GRID_BLANK).length;

    // 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);

    if (DARK_GAME) {
        theenemy.material = new THREE.MeshBasicMaterial({ color: 0xffffff });
        let edgesMesh = new THREE.LineSegments(edges, edgesMaterial);
        theenemy.add(edgesMesh);
    } else {
        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);
    shapeCircle = new THREE.SphereGeometry(squaresize / 2, squaresize / 2, squaresize / 2);
    theagent = new THREE.Mesh(shape);
    if(DARK_GAME) {
        theagent.material = new THREE.MeshBasicMaterial({ color: 0x000000 });
    } else {
        theagent.material = new THREE.MeshBasicMaterial({ map: agent_texture });
    }
    ABWorld.scene.add(theagent);
    drawAgent();
    
    
    // end code edit by Nithin Sai - small edits to add an alternate version of game


    // set up the floor
    // code by Nithin Sai Kirumani Jagadish - added floor to the game

    var floorGrid = new Array(gridsize);
    for (i = 0; i < gridsize; i++) {
        floorGrid[i] = new Array(gridsize);
    }

    var floorMaterial;

    if (DARK_GAME) {
        floorMaterial = new THREE.MeshBasicMaterial({ color: 0xbbbbbb });
    }
    else {

        floorMaterial = new THREE.MeshBasicMaterial({ map: floor_texture });
        floorMaterial.color.set(0x77aa88);
    }

    for (i = 0; i < gridsize; i++) {
        for (j = 0; j < gridsize; j++) {
            shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
            thecube = new THREE.Mesh(shape);
            let edges = new THREE.EdgesGeometry(shape);
            if (DARK_GAME) {
                let edgesMesh = new THREE.LineSegments(edges, edgesMaterial);
                thecube.add(edgesMesh);
            }
            thecube.material = floorMaterial;
            thecube.position.copy(translate(i, j));
            thecube.position.y = -BOXHEIGHT;
            ABWorld.scene.add(thecube);
            floorGrid[i][j] = thecube;
        }
    }
    // end code by Nithin Sai Kirumani Jagadish  - added floor to the game



    // 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
    });

}





// --- 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)
}

// code by Nithin Sai Kirumani Jagadish - draw path and remove path (as box in grid)

const pathCubes = [];

var finalPathLength;

function drawPath(path) {
    if (path.length < 1) { return; }
    const l = path.length;
    finalPathLength = path.length;
    for (var i = 2; i < l; i++) {
        var point = path[i];
        var shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
        var thecube = new THREE.Mesh(shape);
        // opacity based on distance from agent
        var opacity = 1 - (i / l);
        if(DARK_GAME) {
            PATH_COLOR = 0xffffff;
            thecube.material = new THREE.MeshBasicMaterial({ color: PATH_COLOR, transparent: true, opacity: opacity });
        } else {
            thecube.material = new THREE.MeshBasicMaterial({ map: enemy_texture, transparent: true, opacity: opacity });
        }
        thecube.position.copy(translate(point.i, point.j));
        ABWorld.scene.add(thecube);
        pathCubes.push(thecube);
    }
}


function removePath() {
    for (var i = 0; i < pathCubes.length; i++) {
        var cube = pathCubes[i];
        ABWorld.scene.remove(cube);
    }
}

// end code by Nithin Sai Kirumani Jagadish - draw path and remove path (as box in grid)

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

// code by Nithin Sai - added heuristic, g and total cost functions for A*

// function heuristic(a, b) {
//     // test: bad constant heuristic
//     // return 1000;
    
//     // if Breadth First Search is enabled, h = 0
//     if (useBFS) return 0;
    
//     // not diagonal
//     // can only go across and down 
//     // so this is optimistic
//     //
//     // this is not optimistic if we allow diagonal moves
//     if (manhattanDistance)
//         return (Math.abs(a.i - b.i) + Math.abs(a.j - b.j));
//     return (Math.sqrt((a.i - b.i) ** 2 + (a.j - b.j) ** 2));
// }

function heuristic(a, b) {
    switch (HEURISTIC_APPROACH_TYPE) {
        case 0:
            // Scenario 1: High Constant Heuristic
            return 1000;
        case 1:
            // Scenario 2: BFS Heuristic
            return 0;
        case 2:
            // Scenario 3: Manhattan Distance
            return Math.abs(a.i - b.i) + Math.abs(a.j - b.j);
        case 3:
            // Scenario 4: Euclidean Distance
            return Math.sqrt((a.i - b.i) ** 2 + (a.j - b.j) ** 2);
        case 4:
            // Scenario 5: Chebyshev Distance
            return Math.max(Math.abs(a.i - b.i), Math.abs(a.j - b.j));
        default:
            // Default: High Constant Heuristic
            return 1000;
    }
}

function gfn(a, b) {
    // enable BFS if needed
    if (HEURISTIC_APPROACH_TYPE === 1) return 0;

    // assuming each movement has a cost of 1
    return 1;
}

function totalCost(a, b) {
    return (heuristic(a, b) + gfn(a, b));
}
// code by Nithin Sai - added heuristic, g and total cost functions for A*

// Function to delete element from the array
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);
}



// code by Nithin Sai Kirumani Jagadish - calculate A star

function calculateAStarPath() {
    var path = [];
    var openSet = [];
    var closedSet = [];
    var start = { i: ei, j: ej };
    var end = { i: ai, j: aj };
    openSet.push(start);
    totalStatesExplored = 0;

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

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

        var current = openSet[winner];

        if (current.i === end.i && current.j === end.j) {
            var temp = current;
            path.push(temp);

            while (temp.previous) {
                path.push(temp.previous);
                temp = temp.previous;
            }
            
            totalStatesExplored += closedSet.length;
            return path.reverse();
        }

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

        var neighbors = [];
        if (current.i < gridsize - 1) neighbors.push({ i: current.i + 1, j: current.j });
        if (current.i > 0) neighbors.push({ i: current.i - 1, j: current.j });
        if (current.j < gridsize - 1) neighbors.push({ i: current.i, j: current.j + 1 });
        if (current.j > 0) neighbors.push({ i: current.i, j: current.j - 1 });

        for (let i = 0; i < neighbors.length; i++) {
            var neighbor = neighbors[i];

            if (GRID[neighbor.i][neighbor.j] === GRID_BLANK) {
                if (!closedSet.some((closedNode) => closedNode.i === neighbor.i && closedNode.j === neighbor.j)) {
                    var tempG = current.g + 1; // assume a cost of 1 to move to a neighbor

                    var newPath = false;
                    if (!openSet.some((openNode) => openNode.i === neighbor.i && openNode.j === neighbor.j)) {
                        openSet.push(neighbor);
                        newPath = true;
                    } else if (tempG < neighbor.g) {
                        newPath = true;
                    }

                    if (newPath) {
                        neighbor.g = tempG;
                        neighbor.h = heuristic(neighbor, end);
                        neighbor.f = neighbor.g + neighbor.h;
                        neighbor.previous = current;
                    }
                }
            }
        }
    }

    // no path found
    return [];
}

// end code by Nithin Sai Kirumani Jagadish - calculate A star

// code by Nithin Sai Kirumani Jagadish - calculate AO star

function heuristicAO(node, end, observedCost) {
    // A more advanced heuristic with dynamic adaptation
    // Adjust the weight based on the observed cost
    var weight = 1.0 + 0.2 * observedCost;
    return weight * (Math.abs(node.i - end.i) + Math.abs(node.j - end.j));
}

function calculateAOStarPath() {
    var path = [];
    var openSet = [];
    var closedSet = [];
    var start = { i: ei, j: ej };
    var end = { i: ai, j: aj };
    openSet.push(start);
    totalStatesExplored = 0;
    var observedCost = 0; // Initialize the observed cost

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

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

        var current = openSet[winner];

        if (current.i === end.i && current.j === end.j) {
            var temp = current;
            path.push(temp);

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

            totalStatesExplored += closedSet.length;
            return path.reverse();
        }

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

        var neighbors = [];
        if (current.i < gridsize - 1) neighbors.push({ i: current.i + 1, j: current.j });
        if (current.i > 0) neighbors.push({ i: current.i - 1, j: current.j });
        if (current.j < gridsize - 1) neighbors.push({ i: current.i, j: current.j + 1 });
        if (current.j > 0) neighbors.push({ i: current.i, j: current.j - 1 });

        for (let i = 0; i < neighbors.length; i++) {
            var neighbor = neighbors[i];

            if (GRID[neighbor.i][neighbor.j] === GRID_BLANK) {
                if (!closedSet.some((closedNode) => closedNode.i === neighbor.i && closedNode.j === neighbor.j)) {
                    var tempG = current.g + 1;

                    var newPath = false;
                    if (!openSet.some((openNode) => openNode.i === neighbor.i && openNode.j === neighbor.j)) {
                        openSet.push(neighbor);
                        newPath = true;
                    } else if (tempG < neighbor.g) {
                        newPath = true;
                    }

                    if (newPath) {
                        neighbor.g = tempG;
                        neighbor.h = heuristicAO(neighbor, end, observedCost); // Pass the observed cost to the heuristic
                        neighbor.f = neighbor.g + neighbor.h;
                        neighbor.previous = current;
                    }
                }
            }
        }

        observedCost += 1; // Increment the observed cost (assuming a cost of 1 to move to a neighbor)
    }

    // no path found
    return [];
}

// end code by Nithin Sai Kirumani Jagadish - calculate AO star

function moveLogicalEnemy() {

    var path = calculateAStarPath();
    if (path.length > 1) {
        var next = path[1];
        if (!occupied(next.i, next.j)) {
            ei = next.i;
            ej = next.j;

            removePath();
            drawPath(path);
        }
    }
    else {
        console.log("No path found");
    }
}


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 
{
    // code by Nithin Sai - prettier outputs
    var x = AB.world.getState();
    const tableHtml = `
        ${heuristicMap[HEURISTIC_APPROACH_TYPE]}
        <hr>
        <table>
            <tr>
                <td>Step:</td>
                <td>${AB.step}</td>
            </tr>
            <tr>
                <td>x:</td>
                <td>${x.toString()}</td>
            </tr>
            <tr>
                <td>a:</td>
                <td>${a}</td>
            </tr>
        </table>
    `;
    AB.msg(tableHtml);
    // end code by Nithin Sai - prettier outputs
}

var score;
let cumulativeSum = 0;
let averageStepsHistory = [];

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

    var y = AB.world.getState();
    score = (goodsteps / AB.step) * 100;
    
    // code by Nithin Sai - prettier outputs
    
    let currentAverageSteps = (totalStatesExplored/totalPossibleSteps) * 100;
    averageStepsHistory.push(currentAverageSteps);
    
    cumulativeSum += currentAverageSteps;
    
    let runningAverageSteps = cumulativeSum / averageStepsHistory.length;

    const tableHtml = `
        <hr>
        <table>
            <tr>
                <td>y:</td>
                <td>${y}</td>
            </tr>
            <tr>
                <td>Bad steps:</td>
                <td>${badsteps}</td>
            </tr>
            <tr>
                <td>Good steps:</td>
                <td>${goodsteps}</td>
            </tr>
            <tr>
                <td>Score:</td>
                <td>${score.toFixed(2)}%</td>
            </tr>
            <tr>
                <td>Total States:</td>
                <td>${totalPossibleSteps}</td>
            </tr>
            <tr>
                <td>Path length (current step):</td>
                <td>${finalPathLength}</td>
            </tr>
            <tr>
                <td>Total States Explored (current step):</td>
                <td>${totalStatesExplored}</td>
            </tr>
            <tr>
                <td>Average States Explored (current step):</td>
                <td>${currentAverageSteps.toFixed(2)}%</td>
            </tr>
            <tr>
                <td>Running Average of States Explored (life time):</td>
                <td>${runningAverageSteps.toFixed(2)}%</td>
            </tr>
        </table>
    `;
    AB.msg(tableHtml, 2);
    
    // end code by Nithin Sai - prettier outputs
}





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)		// slow the enemy down to every nth step
        moveLogicalEnemy();


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

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


    if (agentBlocked())			// if agent blocked in, run over 
    {
        if(!DARK_GAME) {
            theagent.material = new THREE.MeshBasicMaterial({ map: enemy_texture });
        } else {
            theenemy.material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
        }
        AB.abortRun = true;
        goodsteps = 0;			// you score zero as far as database is concerned 			 
        musicPause();
        soundAlarm();
    }

};


AB.world.endRun = function () {
    musicPause();
    // code edit by Nithin Sai - changed final score
    if (AB.abortRun) AB.msg(" <br> <font color=red> <B> Agent trapped. Final score " + score.toFixed(2) + ". </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 
}