Code viewer for World: A* Showcase Hunter and Pra...
// Cloned by Deniss Strods on 11 Nov 2020 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 edited 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;

// 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/nikolif2/wall.png";
const TEXTURE_AGENT = "/uploads/starter/pacman.jpg";
const TEXTURE_ENEMY = "/uploads/starter/ghost.3.png";

// 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 = 50; // number of squares along side of world
const QUADRANT_DIVISION = Math.floor(gridsize / 5); // size of the zones for logical agent to move around
const ALLOW_DIAGONAL_MOVES = false; // allow moves diagonally for both pray and hunter


// game dry run options
const EMULATE_GAME = false; //enable game emulation
const EMULATE_GAME_COUNT = 100; //how many games to excecute
const EMULATED_GAME_STEPS = 500; //how many moves to make
const SMART_AGENT = true; //enable enhanced agent algorithm


const NOBOXES = Math.trunc((gridsize * gridsize) / 3);
// 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

// 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",
];

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

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

class GridItem {
  constructor(positionX, positionY, itemType) {
    this.positionX = positionX;
    this.positionY = positionY;
    this.itemType = itemType;
    this.g = 0;
    this.h = 0;
    this.f = 0;
    this.previousGridItem = undefined;
    this.neighbors = [];
    this.visited = false;
  }
}

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

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

const AGENT_GRID = new Array(gridsize);

let quadrants = [];

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].itemType == GRID_WALL) return true; // fixed objects
  if (GRID[i][j].itemType == 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;
}

// Author Deniss Strods
// Generate quadrants for the pray agent
const createQuadrants = (grid) => {

  const quadrantDividor = grid.length / QUADRANT_DIVISION;

  const quadrants = [];

  for (let x = 1; x <= quadrantDividor; x++) {
    for (let y = 1; y <= quadrantDividor; y++) {
      quadrants.push({
        xStart: (x - 1) * QUADRANT_DIVISION,
        xEnd: x * QUADRANT_DIVISION,
        yStart: (y - 1) * QUADRANT_DIVISION,
        yEnd: y * QUADRANT_DIVISION,
      });
    }
  }

  return quadrants;
};


// extracted and refactord from https://ancientbrain.com/world.php?world=2738418227
const getGirdItemNeighbours = (gridItem, grid) => {
  const neighbors = [];

  const { positionX: i, positionY: j } = gridItem;

  const rangeBorder = gridsize - 1;

  if (i < rangeBorder) neighbors.push(grid[i + 1][j]);
  if (i > 0) neighbors.push(grid[i - 1][j]);
  if (j < rangeBorder) neighbors.push(grid[i][j + 1]);
  if (j > 0) neighbors.push(grid[i][j - 1]);
  
  if (ALLOW_DIAGONAL_MOVES){
        if (i > 0 && j > 0) neighbors.push(grid[i - 1][j - 1]);
        if (i < rangeBorder && j > 0) neighbors.push(grid[i + 1][
            j - 1
        ]);
        if (i > 0 && j < gridsize - 1) neighbors.push(grid[i - 1][
            j + 1
        ]);
        if (i < rangeBorder && j < rangeBorder) neighbors.push(
            grid[i + 1][j + 1]);
  }

  return neighbors;
};

// Author Deniss Strods
// allowing simulated game move
const dryRunMove = (step, smartAgent) => {
  moveLogicalAgent(AB.mind.getAction([ ai, aj, ei, ej ]), true, smartAgent);
  if (step % 2 == 0)
    // slow the enemy down to every nth step
    moveLogicalEnemy(true);

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

  if (agentBlocked()) {
    goodsteps = 0; // you score zero as far as database is concerned
    return true;
  }
  return false;
};

// Author Deniss Strods
// allowing simulated game setup
const dryRunSetup = () => {

  quadrants = createQuadrants(GRID);
  badsteps = 0;
  goodsteps = 0;

  for (let i = 0; i < gridsize; i++) {
    GRID[i] = [...new Array(gridsize).keys()].map((j) => {
      const itemType =
        i === 0 || i === gridsize - 1 || j === 0 || j === gridsize - 1
          ? GRID_WALL
          : GRID_BLANK;

      return new GridItem(i, j, itemType);
    });
    AGENT_GRID[i] = [...new Array(gridsize).keys()].map((j) => {
      const itemType =
        i === 0 || i === gridsize - 1 || j === 0 || j === gridsize - 1
          ? GRID_WALL
          : GRID_BLANK;
      return new GridItem(i, j, itemType);
    });
  }

  // All the neighbors
  for (let i = 0; i < gridsize; i++) {
    for (let j = 0; j < gridsize; j++) {
      const gridItem = GRID[i][j];
      gridItem.neighbors = getGirdItemNeighbours(gridItem, GRID);
      const agentGridItem = AGENT_GRID[i][j];
      agentGridItem.neighbors = getGirdItemNeighbours(
        agentGridItem,
        AGENT_GRID
      );
    }
  }
  
  let i, j;
  // set up maze
  for (let 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].itemType = GRID_MAZE;
    AGENT_GRID[i][j].itemType = GRID_MAZE;
  }

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

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

// Author Deniss Strods
// excecute simulated game
const emulateGame = (gameSteps, smartAgent) => {

    dryRunSetup();

    for (var i = 0; i < gameSteps; i++) {
        const agentBlocked = dryRunMove(i, smartAgent);

        if( agentBlocked ){
            return {status: 'BLOCKED', badsteps, goodsteps, steps: i + 1};
        }
    }
    return {status: 'RAN_OUT_OF_STEPS', badsteps, goodsteps, steps: gameSteps};
}

// Refactored to allow emulated games
function initScene() {
    
    if (EMULATE_GAME){
        let gamesDraw = 0;
        let gamesBlocked = 0;
        for (var i = 0; i < EMULATE_GAME_COUNT; i++) {
            const game = emulateGame(EMULATED_GAME_STEPS, SMART_AGENT);
            const {status, badsteps, goodsteps, steps} = game;
            if(status === 'BLOCKED'){
                gamesBlocked++;
            } else {
                gamesDraw++;
            }
            console.log(`Game ${i+1}: `, `${status}, bad-steps:${badsteps}, good-steps:${goodsteps}, total-steps:${steps}`);
        }
        console.log(`Total games: ${EMULATE_GAME_COUNT}, pray-blocked:${gamesBlocked}, gamse-draw:${gamesDraw}`);
        AB.abortRun = true;
    }
    
  // all file loads have returned
  var i, j;

  // initialize quadrants
  quadrants = createQuadrants(GRID);
  // set up GRID as 2D array
  const drawTheCube = (i, j, material) => {
    const shape = new THREE.BoxGeometry(squaresize, BOXHEIGHT, squaresize);
    const thecube = new THREE.Mesh(shape);
    thecube.material = new THREE.MeshBasicMaterial({
      map: material,
    });

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

    return thecube;
  };

  for (i = 0; i < gridsize; i++) {
    GRID[i] = [...new Array(gridsize).keys()].map((j) => {
      const itemType =
        i === 0 || i === gridsize - 1 || j === 0 || j === gridsize - 1
          ? GRID_WALL
          : GRID_BLANK;

      if (itemType === GRID_WALL) {
        drawTheCube(i, j, wall_texture);
      }

      return new GridItem(i, j, itemType);
    });
    AGENT_GRID[i] = [...new Array(gridsize).keys()].map((j) => {
      const itemType =
        i === 0 || i === gridsize - 1 || j === 0 || j === gridsize - 1
          ? GRID_WALL
          : GRID_BLANK;
      return new GridItem(i, j, itemType);
    });
  }

  // All the neighbors
  for (var i = 0; i < gridsize; i++) {
    for (var j = 0; j < gridsize; j++) {
      const gridItem = GRID[i][j];
      gridItem.neighbors = getGirdItemNeighbours(gridItem, GRID);
      const agentGridItem = AGENT_GRID[i][j];
      agentGridItem.neighbors = getGirdItemNeighbours(
        agentGridItem,
        AGENT_GRID
      );
    }
  }

  // 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].itemType = GRID_MAZE;
    AGENT_GRID[i][j].itemType = GRID_MAZE;
    drawTheCube(i, j, maze_texture);
  }

  // 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;
  theenemy = drawTheCube(i, j, enemy_texture);
  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;
  theagent = drawTheCube(i, j, agent_texture);
  drawAgent();

  // add floor and utilize already existing image on ancient brain
  const floorTexture = new THREE.ImageUtils.loadTexture(
    "uploads/smythc44/floor.jpg"
  );
  floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
  floorTexture.repeat.set(10, 10);
  const squareSide = squaresize * gridsize;
  const floorMaterial = new THREE.MeshBasicMaterial({
    map: floorTexture,
    side: THREE.DoubleSide,
  });
  const floorGeometry = new THREE.PlaneGeometry(squareSide, squareSide, 0, 0);
  const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  const initialFloorPosition = (squaresize / 2) * -1;
  floor.position.y = initialFloorPosition;
  floor.position.x = initialFloorPosition;
  floor.position.z = initialFloorPosition;
  floor.rotation.x = Math.PI / 2;
  ABWorld.scene.add(floor);

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

// Author Deniss Strods
const drawTheSphere = (i, j, color) => {
  const greenMaterial = new THREE.MeshBasicMaterial({
    color,
    transparent: true,
    opacity: 0.5,
  });
  const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(40, 32, 16),
    greenMaterial
  );

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

  return sphere;
};

// Author Deniss Strods
// eucledian distance + enhancment, extracted and refactord from https://ancientbrain.com/world.php?world=2738418227
const heuristicFunction = (startX, startY, endX, endY) => {
  // return ( Math.abs(startX - endX) + Math.abs(startY - endY) )

  // enhanced heuristics func
  return (
    Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2)) *
    (Math.abs(startX - endX) + Math.abs(startY - endY))
  );
};

// extracted and refactord some items from https://ancientbrain.com/world.php?world=2738418227
// however a lot oh pieces were introduced by myself, Deniss Strods
const aStarShortestPathFinder = (
  grid,
  victemX,
  victemY,
  hunterX,
  hunterY,
  hunterPath = false
) => {
    
  // cleaning data structure before A* comences
  grid.forEach((row) => {
    row.forEach((item) => {
      item.g = 0;
      item.h = 0;
      item.f = 0;
      item.previousGridItem = undefined;
      item.visited = false;
    });
  });    
    
  const removeFromArray = (arr, elt) => {
    const eltIndex = arr.indexOf(elt);
    if (eltIndex !== -1) {
      arr.splice(eltIndex, 1);
    }
  };

  const start = grid[hunterX][hunterY];
  const end = grid[victemX][victemY];

  const openSet = [start];
  const closedSet = [];

  let path = [];
  while (openSet.length) {
    let winner = 0;
    for (let i = 0; i < openSet.length; i++) {
      if (openSet[i].f < openSet[winner].f) {
        winner = i;
      }
    }
    const current = openSet[winner];

    if (current === end) {
      var temp = current;
      path.push(temp);
      while (temp.previousGridItem) {
        path.push(temp.previousGridItem);
        temp = temp.previousGridItem;
      }
      path = path.filter((item) => item !== start && item !== end);

      break;
    }

    // Best option moves from openSet to closedSet
    removeFromArray(openSet, current);
    closedSet.push(current);

    current.neighbors.forEach((neighbor) => {
      const { positionX, positionY } = neighbor;
      const isForAgent = !hunterPath
        ? false
        : positionX === ei && positionY === ej;
      
      if (!closedSet.includes(neighbor) && !neighbor.itemType && !isForAgent) {
        const tempG =
          current.g +
          heuristicFunction(
            positionX,
            positionY,
            current.positionX,
            current.positionY
          );

        // Is this a better path than before?
        let 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 = heuristicFunction(
            positionX,
            positionY,
            end.positionX,
            end.positionY
          );
          neighbor.f = neighbor.g + neighbor.h;
          neighbor.previousGridItem = current;
        }
      }
    });
  }

  return { closedSet, path };
};

// --- take actions -----------------------------------
let drawnSelectedNodes = [];
let closedSet, path;

function moveLogicalEnemy(isDryRun = false) {

  // Calculating path using A*
  const { closedSet, path } = aStarShortestPathFinder(GRID, ai, aj, ei, ej);
  const whereToGo = path[path.length - 1];

  // Dry run is for simulated gamse, if simulated game do not draw anything
  if(!isDryRun){
    //removing previous points on screen
    drawnSelectedNodes.forEach((item) => {
        ABWorld.scene.remove(item);
        item.geometry.dispose();
        item.material.dispose();
      });
      //drawing points on screen
      drawnSelectedNodes = closedSet.map((item) => {
        if (path.includes(item)) {
          return drawTheSphere(item.positionX, item.positionY, 0xff0000);
        }
        return drawTheSphere(item.positionX, item.positionY, 0x00ff00);
      });
  }

  if (whereToGo) {
    const { positionX, positionY } = whereToGo;
    ei = positionX;
    ej = positionY;
  }
}

// getting middle point of the segment
const quadrantMidlePoint = (endX, startX, endY, startY) => {
  const x = Math.floor((endX + startX) / 2);
  const y = Math.floor((endY + startY) / 2);

  return { x, y };
};

// Author Deniss Strods
// Adjusting points, if mid point is not empty
const checkMidPointAndAdjust = ({ x, y }, agentGrid) => {
  const currentSquareOcupied = !agentGrid[x][y].itemType;

  if (currentSquareOcupied) {
    return { x, y };
  }

  const scanArea = QUADRANT_DIVISION;
  const gridLength = agentGrid.length;
  
  // Adjusting if we have reached one of the walls
  const adjustedX = gridLength - x > 0 ? x : x + gridLength - x;
  const adjustedY = gridLength - y > 0 ? y : y + gridLength - y;
  
  // Iterating and searching for empty square
  for (let xInd = 0; xInd < scanArea; xInd++) {
    for (let yInd = 0; yInd < scanArea; yInd++) {
      const calcX = xInd + adjustedX;
      const calcY = yInd + adjustedY;
      const square = agentGrid[calcX][calcY];
      if (square && !square.itemType) {
        return { x: calcX, y: calcY };
      }
    }
  }
};

// Author Deniss Strods
// Calculating amd moving pray agent
const calculateDestenationPath = (
  agentX,
  agentY,
  enemyX,
  enemyY,
  quadrants,
  agentGrid
) => {
  let destenationQuadrant;
  let bestHeuristicsSoFar = 100;

  // Creating sorted list of quadrants, sorting by heuristics, best last
  const sortedQuadrants = quadrants
    .map((quadrant) => {
      // Calculating mid point of the quadrant  
      const { xEnd, xStart, yEnd, yStart } = quadrant;
      let quadrantMidPoint = quadrantMidlePoint(xEnd, xStart, yEnd, yStart);
      // Adjusting mid point. In case if its a wall, scanning trough the quadrant to get another suitable point
      quadrantMidPoint = checkMidPointAndAdjust(quadrantMidPoint, agentGrid);

      const { path } = aStarShortestPathFinder(
        agentGrid,
        quadrantMidPoint.x,
        quadrantMidPoint.y,
        agentX,
        agentY,
        true
      );
      let closestDistanceToTheEnemy = 100000;
      path.forEach((item) => {
        const { positionX, positionY } = item;
        const heuristics = heuristicFunction(
          positionX,
          positionY,
          enemyX,
          enemyY
        );
        if (closestDistanceToTheEnemy > heuristics) {
          closestDistanceToTheEnemy = heuristics;
        }
      });
      const distanceToTheEnemy =
        heuristicFunction(
          quadrantMidPoint.x,
          quadrantMidPoint.y,
          enemyX,
          enemyY
        ) / 1000;
      const calculatedClosestDistanceOnPath = (closestDistanceToTheEnemy / 100 * 2)
      //heuristics for quadrants
      const heuristics =
        distanceToTheEnemy + calculatedClosestDistanceOnPath;
        
      return {
        quadrant,
        calculatedClosestDistanceOnPath,
        distanceToTheEnemy,
        heuristics: path.length < 1 ? -1 : heuristics,
        path,
      };
    })
    .sort((a, b) =>
      a.heuristics > b.heuristics ? 1 : b.heuristics > a.heuristics ? -1 : 0
    );

  return sortedQuadrants[sortedQuadrants.length - 1].path;
};

const getAgentPath = (agentX, agentY, enemyX, enemyY, quadrants, agentGrid) => {
  return calculateDestenationPath(
    agentX,
    agentY,
    enemyX,
    enemyY,
    quadrants,
    agentGrid
  );
};

// collection to track selected 3D nodes
let drawnSelectedNodesAgent = [];

function moveLogicalAgent(a, isDryRun = false, smartAgent = true) {
  // this is called by the infrastructure that gets action a from the Mind
  var i = ai;
  var j = aj;
  
  if(!smartAgent){
            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;
           }
           
    return;
  }

  const path = getAgentPath(ai, aj, ei, ej, quadrants, AGENT_GRID);

  if(!isDryRun){
    drawnSelectedNodesAgent.forEach((item) => {
        ABWorld.scene.remove(item);
        item.geometry.dispose();
        item.material.dispose();
      });
    
      drawnSelectedNodesAgent = path.map((item) => {
        return drawTheSphere(item.positionX, item.positionY, 0xffc300);
      });
  }

  const whereToGo = path[path.length - 1];

  if (whereToGo && path.length !== 1) {
    const { positionX, positionY } = whereToGo;
    ai = positionX;
    aj = positionY;
  }
}

// --- 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, false, SMART_AGENT);
  if (event.keyCode == 38) moveLogicalAgent(ACTION_DOWN, false, SMART_AGENT);
  if (event.keyCode == 39) moveLogicalAgent(ACTION_RIGHT, false, SMART_AGENT);
  if (event.keyCode == 40) moveLogicalAgent(ACTION_UP, false, SMART_AGENT);

  // 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, false, SMART_AGENT);

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

const 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
}