Code viewer for World: Complex World Enemy using ...
// Cloned by Thomas Mc Cann on 3 Nov 2019 from World "Complex World" by Starter user
// Please leave this clone trail here.

// ==== Starter World ===============================================================================================
// (c) Ancient Brain Ltd. All rights reserved.
// This code is only for use on the Ancient Brain site.
// This code may be freely copied and edited by anyone on the Ancient Brain site.
// This code may not be copied, re-published or used on any other website.
// To include a run of this code on another website, see the "Embed code" links provided on the Ancient Brain site.
// ==================================================================================================================

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

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

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

const TEXTURE_WALL = "/uploads/starter/door.jpg";
const TEXTURE_MAZE = "/uploads/starter/latin.jpg";
const TEXTURE_AGENT = "/uploads/starter/pacman.jpg";
const TEXTURE_ENEMY = "/uploads/midnightsky/brain1.jpg";

// credits:
// http://commons.wikimedia.org/wiki/File:Old_door_handles.jpg
// https://commons.wikimedia.org/wiki/Category:Pac-Man_icons
// https://commons.wikimedia.org/wiki/Category:Skull_and_crossbone_icons
// http://en.wikipedia.org/wiki/File:Inscription_displaying_apices_(from_the_shrine_of_the_Augustales_at_Herculaneum).jpg

// const MUSIC_BACK  = '/uploads/starter/Defense.Line.mp3' ;
const SOUND_ALARM = "/uploads/starter/air.horn.mp3";

// credits:
// http://www.dl-sounds.com/royalty-free/defense-line/
// http://soundbible.com/1542-Air-Horn.html

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

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

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

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

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

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: -------------------------------
// skybox is a collection of 6 files
// x,y,z positive and negative faces have to be in certain order in the array
// https://threejs.org/docs/#api/en/loaders/CubeTextureLoader

// mountain skybox, credit:
// http://stemkoski.github.io/Three.js/Skybox.html

const SKYBOX_ARRAY = [
  "/uploads/starter/dawnmountain-xpos.png",
  "/uploads/starter/dawnmountain-xneg.png",
  "/uploads/starter/dawnmountain-ypos.png",
  "/uploads/starter/dawnmountain-yneg.png",
  "/uploads/starter/dawnmountain-zpos.png",
  "/uploads/starter/dawnmountain-zneg.png"
];

// space skybox, credit:
// http://en.spaceengine.org/forum/21-514-1
// x,y,z labelled differently

/*
 const SKYBOX_ARRAY = [										 
                "/uploads/starter/sky_pos_z.jpg",
                "/uploads/starter/sky_neg_z.jpg",
                "/uploads/starter/sky_pos_y.jpg",
                "/uploads/starter/sky_neg_y.jpg",
                "/uploads/starter/sky_pos_x.jpg",
                "/uploads/starter/sky_neg_x.jpg"
                ];
*/

// urban photographic skyboxes, credit:
// http://opengameart.org/content/urban-skyboxes

/*
 const SKYBOX_ARRAY = [										 
                "/uploads/starter/posx.jpg",
                "/uploads/starter/negx.jpg",
                "/uploads/starter/posy.jpg",
                "/uploads/starter/negy.jpg",
                "/uploads/starter/posz.jpg",
                "/uploads/starter/negz.jpg"
                ];
*/

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

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

var badsteps;
var goodsteps;

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

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

//*********************************************************************************************************************************
//*********************************************************************************************************************************
//
//  THOMAS MC CANN - CA686FL Foundations of Artificial Intelligence - Heuristic Search: Assignment 1
//
//*********************************************************************************************************************************
//*********************************************************************************************************************************

// Enemy Maze Mind 2D Array
// Required to evaluate each maze position and assign f(n), g(n) and h(n) values.
// to assign f(n), g(n) and h(n) values to each Maze position, to add neighbouring squares
var enemysMazeMap = new Array(gridsize);

// MazePosion object constructor that take in i,j vales that correspond to the complex world GRID values/positions
function MazePosition(i, j) {
  // i and j hold the MazePosition location
  this.i = i;
  this.j = j;

  // Initialise each MazePosition's f(n),g(n) and h(n)
  // as part of the A* Alogortim

  this.f = 0; // f(n) = g(n) + h(n)
  this.g = 0; // g(n) - distance from start
  this.h = 0; // h(n) - reseult of the heuristic evaluation function that calculates the curents positon from the goal ( target / good guy).
  // A MazePosition object keep a record of the MazePosition postion object before it
  // in the path which A* calcultes.
  this.cameFrom = null;
  // Array to hold each MazePosition objects neighbours
  this.neighbours = [];
  // If the MazePosition is discovered to be a wall or part of the maze then 'this.wall' will be set to true as discovered during game time
  this.wall = false;
}

// Create empty enemysMazeMap array gridsize X gridsize
for (let i = 0; i < enemysMazeMap.length; i++) {
  enemysMazeMap[i] = new Array(gridsize);
} // end of for loop

// Initialise each MazePosition in the enemysMazeMap 2D array
for (let i = 0; i < gridsize; i++) {
  for (let j = 0; j < gridsize; j++) {
    enemysMazeMap[i][j] = new MazePosition(i, j);
    // Adding the maze perimeter/boundary walls to limit search area
    // (I hope this is not cheating as neither good guy or bad guy use this area)
    // NOTE: Decided against this and GRID may not be initialised. However I migth add this later.
    //   if(GRID[i][j] == GRID_WALL){
    //       enemysMazeMap[i][j].wall = true;
    //   }
  }
}

// Function to add each MazePosition's neighbours
MazePosition.prototype.addNeigbours = function(gridsize) {
  var i = this.i;
  var j = this.j;

  // The order in which neighbours are added will
  // affect the A star's alogrithm (in this example)
  // as nieghbours are evaluated by the default order in which
  // they were added to each spots/node neighbour array.

  // The enemy will only be able to move to positions
  // verticall and horizontally as the good guy can only move UP, DOWN, LEFT, RIGHT
  // Only vertical and horizontal neighbours will be added

  // This could be modified to factor in walls and only add neigbours to
  // Maze positions within the boundaries walls of the maze.
  // However in the interest of fairness in this exercise we will
  // let the enemy discover the walls during the Path finding (A*)
  // and add them to the enemysMazeMap as walls as discoverd at runtime.

  if (i < gridsize - 1) {
    this.neighbours.push(enemysMazeMap[i + 1][j]);
  }
  if (i > 0) {
    this.neighbours.push(enemysMazeMap[i - 1][j]);
  }

  if (j < gridsize - 1) {
    this.neighbours.push(enemysMazeMap[i][j + 1]);
  }
  if (j > 0) {
    this.neighbours.push(enemysMazeMap[i][j - 1]);
  }
}

// Used to partially reset each MazePsotion before the end of moveLogicalEnemy()
function resetEnemyMazeMapPositionValues() {
  for (let i = 0; i < gridsize; i++) {
    for (let j = 0; j < gridsize; j++) {
      enemysMazeMap[i][j].cameFrom = null;
      enemysMazeMap[i][j].neighbours = [];
      enemysMazeMap[i][j].f = 0; // f(n) = g(n) + h(n)
      enemysMazeMap[i][j].g = 0; // g(n) - distance from start
      enemysMazeMap[i][j].h = 0; // h(n)

      // NOTE: We will not reset the boolean wall variable
      // NOTE: i an j will also remain as is.
    }
  } // end outter for loop
}

// Add each MazePositions neighbours from the enemysMazeMap
for (let i = 0; i < gridsize; i++) {
  for (let j = 0; j < gridsize; j++) {
    enemysMazeMap[i][j].addNeigbours(enemysMazeMap);
  }
} // end of for loop

// A* heuristic evalution function to calculate the distance between the enemy and target
// This function can be used to calculate both the euclidean and manhattan distance
// between x,y positions in a grid system.
function heuristic(enemyPosition, targetPosition, euclidean = false) {
  // The raw Euclidean distance as a heurstic
  // Best when we can move both vertically and diagonally
  // It is the straight line distance between points
  if (euclidean) {
    return AB.distance2D(
      enemyPosition.i,
      enemyPosition.j,
      targetPosition.i,
      targetPosition.j
    );
  }
  // The Manhattan (Taxi cab) distance as a heurstic
  // Best when we can only move vertically and horizontally
  else {
    return (
      Math.abs(enemyPosition.i - targetPosition.i) +
      Math.abs(enemyPosition.j - targetPosition.j)
    );
  }
}

// Utility function to remove a element from an array
// Start at the back of the array
// If we reomve an itme at th front hten all
// elements shift forward (along with their indexes).
function RemoveFromArray(arr, ele) {
  arr.splice(arr.indexOf(ele), 1);
}

// Objects to hold the enemy start and target positions which will be
// updated during each call of moveLogicalEnemy() by the world
var enemyStartPos = null;
var targetPos = null;

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

//*********************************************************************************************************************************
//*********************************************************************************************************************************
//
//  THOMAS MC CANN - CA686FL Foundations of Artificial Intelligence - Heuristic Search: Assignment 1
//
//*********************************************************************************************************************************
//*********************************************************************************************************************************

function moveLogicalEnemy() {
  let path = [];
    
    console.log("CONTENT OF ENENMY MAZE");
    console.log(enemysMazeMap)    
      
  // Start of A* Algorithm

  // Arrays to hold open and closed sets od postions in the maze during A* Algorithm
  var openSet = [];
  var closedSet = [];

  // Get the current positions of the enemy and the good guy
  enemyStartPos = enemysMazeMap[ei][ej];
  targetPos = enemysMazeMap[ai][aj];
  
  console.log("enemyStartPos is a type of: ");
  console.log(typeof enemyStartPos);
  console.log(Object.keys(enemyStartPos));

  console.log(enemyStartPos);
  console.log("Enemey in GRID: i = " + ei + " j = " + ej); // All good
  console.log(targetPos);
  console.log("Target in GRID: i = " + ai + " j = " + aj); // All good

  // Push the maze postion of the enemy to the openSet
  openSet.push(enemyStartPos);
  
  console.log("Content of openSet before WHILE loop: ");
  console.log(openSet);         // For Debug
  console.log("OPENSET LENGHT : " + openSet.length);

  

  // I ACCIDENTLY DELETED SOME CODE WITHOUT REALISING AND THEN HAD TO TRY REPLICATE IT ONCE I DISCOVERED WHAT HAD HAPPENED. 
  // I HAVE SOMEHOW MADE THINGS WORSE :-(
  // SOMETHING BAD IS NOW HAPPENING IN THE MAIN WHILE LOOP!!! IT KEPT CRASHING ANCIENT BRAIN.
  // TRYING TO DEBUG IN ANCIENT BRAIN BUT HAVING DIFFICULTIES
  
  // While OpenSet has members continue
  while (openSet.length > 0) {
    console.log("OPENSET INSIDE START OF WHILE LOOP");     // For Debug
    console.log(openSet);       // For Debug
    
    
   

    // Evaluate the node with the lowest f
    var indexOfLowestFInOpenSet = 0;
    for (let i = 0; i < openSet.length; i++) {
      // If the f at index i is less than the other f
      if (openSet[i].f < openSet[indexOfLowestFInOpenSet].f) {
        // The index of the array member with
        // the lowest f value (Note: f is g + h )
        indexOfLowestFInOpenSet = i;
      }
    }
    
    console.log("INSIDE WHILE LOOP INDEX OF OPENSET WITH LOWEST f VALUE" + indexOfLowestFInOpenSet);
    

    // Get the MazePostion in the openSet with current lowest f(n)
    var current = openSet[indexOfLowestFInOpenSet];
    console.log("OPENSET AFTER: ");   // For Debug
    console.log(openSet);       // For Debug

    
     // Something bad happens after this.
    console.log("CURRENT NODE IS SET TO:");
    console.log(current);
    console.log("TARGET NODE IS SET TO:");
    console.log(targetPos);
    break;
    

    if (current === targetPos) {
      console.log("FOUND OPTIMAL PATH TO THE TARGET");

      // Clear and create a new path
      path = [];
      var temp = current;
      while (temp !== null) {
        path.push(temp);
        temp = temp.cameFrom;
        break;
      }

      console.log("Showing optimal path found to target:")
      console.log(path);
      
    } else {
      // Otherwsie remove the spot/node/element from the openSet
      // and push it on to the closed set
      RemoveFromArray(openSet, current);
      console.log("AFTER ITEM REMOVED FROM OPEN SET"); // SEEMS EMPTY
      
      closedSet.push(current);      // For Debug

    console.log("OPENSET AFTER ITEM REMOVED FROM OPEN SET"); // SEEMS EMPTY
    console.log(openSet);         // For Debug
    console.log("CLOSED SET NOW HAS"); // 
    console.log(closedSet);         // For Debug
    
        
        
        
      // Get the current spots elements
      var neighbours = current.neighbours;
      
      console.log("NEIGHBOURS OF CURRENT:");
      console.log(neighbours);
      
     

      // Evaluate neighbours
      for (let i = 0; i < neighbours.length; i++) {
        var neighbour = neighbours[i];
        
        console.log("CURENT NEIGHBOUR BEING EXAMINED");
        console.log(neighbour);
        
        
        if (
          GRID [neighbour.i] [neighbour.j] == GRID_WALL ||
          GRID [neighbour.i] [neighbour.j] == GRID_MAZE
        ) {
          // If our inspection of the path show it is a wall or part of the maze then we do not want to
          // include it in our path.
          neighbour.wall = true;
          console.log("Found a wall");
        }
        
        
       
        
        // If the neighbour is not part of the closed set or is has been registered as a wall in then
        if (!closedSet.includes(neighbour) && !neighbour.wall) {
          // tentative g(n) value if g(n) does not already
          // have a lower g(n) value. (Or no g(n) value at all.)

          
          // var tempG = current.g + heuristic(neighbour, current); <- Seems to increase h(n) way too much
          // Under certain conditions this seems more effective/faster
          
          // Each move is only one cell away from the last cell so added
          // 1 to the distance form start is adequate and will keep h(n) lower.
          // May not work as well under some other game parameters
          var tempG = current.g + 1;

            console.log("TEMPG");
            console.log(tempG);
            

          var newPath = false;
          if (openSet.includes(neighbour)) {
            if (tempG < neighbour.g) {
              neighbour.g = tempG;
              // This g is lower then we have a new path!
              newPath = true;
            }
          } else {
            // If this MazePosition is not already on the openSet push it on to the openSet
            // Use the tentative tempG as its new g value
            neighbour.g = tempG;
            openSet.push(neighbour);
            // We have a new curent more optimal path sp far
            newPath = true;
          }

          console.log("NEWPATH" + newPath);
          
           

          // Only recalculate the h(n) and f(n) for this neighbour if it was a new path
          if (newPath) {
            // Calculate h
            neighbour.h = heuristic(neighbour, targetPos);
            // f(n) = g(n) + h(n)
            neighbour.f = neighbour.g + neighbour.h;
            // Keep track of where we came from
            neighbour.cameFrom = current;

            console.log(current);
          }
        } // end -- if(openSet.includes(neighbour))
      } // end for loop of checking neighbours
    }
  } // end while loop

  // Things to do before we exit the moveLogicalEnemy function.
  // console.log("Showing optimal path found to target:");
  //console.log(path);

  // Move the enemy on step along the best path.
  // ei = path[0].i;
  // ej = path[0].j;
  // console.log("Enemy currently at i: " + ei);
  // console.log("Enemy currently at j: " + ej);
  // console.log(path);
  // console.log("Next move for enemy is to j: " + path[0]);

  // if (!occupied(path[0].i,path[0].j) )
  //     {
  //     ei = i;
  //     ej = j;
  // }

  // resetEnemyMazeMapPositionValues();
} // end of moveLogicalEnemy

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

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