/**
* A Star with Herding implementation on grid
* himanshu.warekar2@mail.dcu.ie
*//**
* Node for initializing at every GRID position which contains
* the information for A Star.
*/classNode{
constructor(world, idx, jdx){this.world = world;this.GRID = world.GRID;this.idx = idx;this.jdx = jdx;this.columns = world.gridSize;this.rows = world.gridSize;this.squareSize = world.squareSize;this.calculatedTotal =0;// fthis.stepCost =0;// gthis.heuristicEstimate =0;// hthis.neighbours =[];this.previous =null;this.gridWall =false;this.mazeWall =false;this.isEmpty =false;this.tempCube =null;this.Cube=null;}
addNeighbours(){if(this.idx <this.columns -1){this.neighbours.push(this.GRID[this.idx +1][this.jdx]);}if(this.idx >0){this.neighbours.push(this.GRID[this.idx -1][this.jdx]);}if(this.jdx <this.rows -1){this.neighbours.push(this.GRID[this.idx][this.jdx +1]);}if(this.jdx >0){this.neighbours.push(this.GRID[this.idx][this.jdx -1]);}}
setAsGridWall(){this.gridWall =true;}
setAsMazeWall(){this.mazeWall =true;}
setAsEmpty(){this.isEmpty =true;}
isWall(){returnthis.gridWall ||this.mazeWall;}
showTemperoryPath(){this.Shape=new THREE.BoxGeometry(30,30,30);this.Cube=new THREE.Mesh(this.Shape);this.Cube.material =new THREE.MeshBasicMaterial();this.Cube.position.copy(this.translate(this.idx,this.jdx));this.Cube.material.color.setColorName("blue");this.Cube.material.opacity =0.25;ABWorld.scene.add(this.Cube);}
removeTemperoryPath(){ABWorld.scene.remove(this.Cube);this.Shape=null;this.Cube=null;}
showPermanentPath(){this.Shape=new THREE.BoxGeometry(30,30,30);this.Cube=new THREE.Mesh(this.Shape);this.Cube.material =new THREE.MeshBasicMaterial();this.Cube.position.copy(this.translate(this.idx,this.jdx));this.Cube.material.color.setColorName("green");this.Cube.material.opacity =0.25;ABWorld.scene.add(this.Cube);}
removePermanentPath(){ABWorld.scene.remove(this.Cube);this.Shape=null;this.Cube=null;}
showCornerIndicator(){this.Shape=new THREE.BoxGeometry(30,30,30);this.Cube=new THREE.Mesh(this.Shape);this.Cube.material =new THREE.MeshBasicMaterial();this.Cube.position.copy(this.translate(this.idx,this.jdx));this.Cube.material.color.setColorName("orange");this.Cube.material.opacity =0.25;ABWorld.scene.add(this.Cube);}
removeCornerIndicator(){ABWorld.scene.remove(this.Cube);this.Shape=null;this.Cube=null;}
translate(idx, jdx){
let vector =new THREE.Vector3();
vector.y =0;
vector.x = idx *this.world.squareSize -this.world.MAXPOS /2;
vector.z = jdx *this.world.squareSize -this.world.MAXPOS /2;return vector;}}/**
* Analytics to save the totalSteps, goodSteps, badSteps, score, agentBlocked
* for analytics.
*/classAnalytics{
constructor(){this.API_URL ="https://api.airtable.com/v0/appaNU1MlDvrEYsnk/Table%201";this.API_KEY ="a2V5T1BOeXRjUElpV3pCU0c=";}
sendAnalytics(world, totalSteps, goodSteps, badSteps, score, agentBlocked){
let data ={"records":[{"fields":{"world": world,"totalSteps": totalSteps,"goodSteps": goodSteps,"badSteps": badSteps,"score": score,"agentBlocked": agentBlocked
}}]};
$.ajax({
type:"POST",
url:this.API_URL,
contentType:"application/json",
data: JSON.stringify(data),
headers:{"Authorization":"Bearer "+ atob(this.API_KEY)},
success:function(){}});}}/**
* Initialises the Complex world for Herding the Agent into the nearest corner
* using A Star acting like a TSP algorithm.
*/class complexWorld {
constructor(){// ===================================================================================================================// === 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;this.WORLD ="Trapping Agent World"// Switch between 3d and 2d view (both using Three.js)this.show3d =true;this.TEXTURE_WALL ="/uploads/starter/door.jpg";this.TEXTURE_MAZE ="/uploads/starter/latin.jpg";this.TEXTURE_AGENT ="/uploads/starter/pacman.jpg";this.TEXTURE_ENEMY ="/uploads/starter/ghost.3.png";this.MUSIC_BACK ="/uploads/starter/Defense.Line.mp3";this.SOUND_ALARM ="/uploads/starter/air.horn.mp3";// number of squares along side of worldthis.gridSize =50;this.NOBOXES =Math.trunc((this.gridSize *this.gridSize)/3);// density of maze - number of internal boxes// (bug) use trunc or can get a non-integerthis.squareSize =100;// size of square in pixelsthis.MAXPOS =this.gridSize *this.squareSize;// length of one side in pixelsthis.SKYCOLOR =0xddffdd;// a number, not a stringthis.startRadiusConst =this.MAXPOS *0.8;// distance from centre to start the camera atthis.maxRadiusConst =this.MAXPOS *10;// maximum distance from camera we will render things//--- change ABWorld defaults: -------------------------------ABHandler.MAXCAMERAPOS =this.maxRadiusConst;ABHandler.GROUNDZERO =true;// "ground" exists at altitude zerothis.SKYBOX_ARRAY =["/uploads/hrwx/black.jpg","/uploads/hrwx/black.jpg","/uploads/hrwx/black.jpg","/uploads/hrwx/black.jpg","/uploads/hrwx/black.jpg","/uploads/hrwx/black.jpg",];this.ACTION_LEFT =0;this.ACTION_RIGHT =1;this.ACTION_UP =2;this.ACTION_DOWN =3;this.ACTION_STAYSTILL =4;this.GRID_BLANK =0;this.GRID_WALL =1;this.GRID_MAZE =2;this.BOXHEIGHT;// 3d or 2d box heightthis.GRID =newArray(this.gridSize);// can query GRID about whether squares are occupied, will in fact be initialised as a 2D arraythis.theAgent =null;this.theEnemy =null;this.wall_texture =null;this.agent_texture =null;this.enemy_texture =null;this.maze_texture =null;// enemy and agent position on squaresthis.enemyIdx =null;this.enemyJdx =null;this.agentIdx =null;this.agentJdx =null;this.badSteps =null;this.goodSteps =null;this._goodSteps =null;//Used to store good steps information for analytics incase agent is trapped.this.OURKEYS =[37,38,39,40];this.initialized =false;this.path =[];this.trapLocations =[];this.stopCounter =0;// ===================================================================================================================// === End of tweaker's box ==========================================================================================// ===================================================================================================================this.loadResources();this.initMusic();this.initAnalytics();}
loadResources(){this.initWallTexture();this.initAgentTexture();this.initEnemyTexture();this.initMazeTexture();}
initAnalytics(){this.analytics =newAnalytics();}
initWallTexture(){
let me =this;
let wall_texture_loader =new THREE.TextureLoader();
wall_texture_loader.load(this.TEXTURE_WALL,function(thetexture){
thetexture.minFilter = THREE.LinearFilter;
me.wall_texture = thetexture;if(me.resourcesLoaded()) me.initScene();});}
initAgentTexture(){
let me =this;
let agent_texture_loader =new THREE.TextureLoader();
agent_texture_loader.load(this.TEXTURE_AGENT,function(thetexture){
thetexture.minFilter = THREE.LinearFilter;
me.agent_texture = thetexture;if(me.resourcesLoaded()) me.initScene();});}
initEnemyTexture(){
let me =this;
let enemy_texture_loader =new THREE.TextureLoader();
enemy_texture_loader.load(this.TEXTURE_ENEMY,function(thetexture){
thetexture.minFilter = THREE.LinearFilter;
me.enemy_texture = thetexture;if(me.resourcesLoaded()) me.initScene();});}
initMazeTexture(){
let me =this;
let maze_texture_loader =new THREE.TextureLoader();
maze_texture_loader.load(this.TEXTURE_MAZE,function(thetexture){
thetexture.minFilter = THREE.LinearFilter;
me.maze_texture = thetexture;if(me.resourcesLoaded()) me.initScene();});}
initMusic(){this.music = AB.backgroundMusic(this.MUSIC_BACK);}
playMusic(){this.music.play();}
soundAlarm(){
let alarm =newAudio(this.SOUND_ALARM);
alarm.play();}
pauseMusic(){this.music.pause();}
resourcesLoaded(){return(this.wall_texture &&this.agent_texture &&this.enemy_texture &&this.maze_texture
);}
newRun(){
AB.loadingScreen();
AB.runReady =false;this.badSteps =0;this.goodSteps =0;if(this.show3d){this.BOXHEIGHT =this.squareSize;ABWorld.init3d(this.startRadiusConst,this.maxRadiusConst,this.SKYCOLOR);}else{this.BOXHEIGHT =1;ABWorld.init2d(this.startRadiusConst,this.maxRadiusConst,this.SKYCOLOR);}this.loadResources();
document.onkeydown = AB.world.customKeyHandler;}
endRun(){this.pauseMusic();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);}
let score =(this._goodSteps /(AB.step -1))*100;this.analytics.sendAnalytics(this.WORLD, AB.step -1,this._goodSteps,this.badSteps, parseFloat(score.toFixed(2)),this.isAgentBlocked());}
isOccupied(idx, jdx){// variable objectsif(this.enemyIdx === idx &&this.enemyJdx === jdx)returntrue;if(this.agentIdx === idx &&this.agentJdx === jdx)returntrue;if(this.GRID[idx][jdx].isWall())returntrue;returnfalse;}
isBadStep(locationIdx, locationJdx){// is the enemy within one square of the agent
locationIdx = locationIdx ||this.enemyIdx;
locationJdx = locationJdx ||this.enemyJdx;returnMath.abs(locationIdx -this.agentIdx)<2&&Math.abs(locationJdx -this.agentJdx)<2?true:false;}
isAgentBlocked(){// agent is blocked on all sides, run overreturn(this.isOccupied(this.agentIdx -1,this.agentJdx)&&this.isOccupied(this.agentIdx +1,this.agentJdx)&&this.isOccupied(this.agentIdx,this.agentJdx -1)&&this.isOccupied(this.agentIdx,this.agentJdx +1));}
isAgentKindaBlocked(){// agent is blocked on all sides, run over
let blocks =[this.isOccupied(this.agentIdx -1,this.agentJdx),this.isOccupied(this.agentIdx +1,this.agentJdx),this.isOccupied(this.agentIdx,this.agentJdx -1),this.isOccupied(this.agentIdx,this.agentJdx +1)].filter(el => el ===true);return blocks.length ===3?true:false;}
ourKeys(event){returnthis.OURKEYS.includes(event.keyCode);}
initScene(){if(this.initialized)return;this.setupWalls();this.setupMaze();this.setupNeighbours();this.setupTrapLocations();this.setupEnemy();this.setupAgent();this.initialized =true;}
setupWalls(){
let shape =null;
let theCube =null;for(let iCounter =0; iCounter <this.gridSize; iCounter++){this.GRID[iCounter]=newArray(this.gridSize);for(let jCounter =0; jCounter <this.gridSize; jCounter++){if(!iCounter ||
iCounter ===this.gridSize -1||!jCounter ||
jCounter ===this.gridSize -1){
let node =newNode(this, iCounter, jCounter);
node.setAsGridWall();this.GRID[iCounter][jCounter]= node;
shape =new THREE.BoxGeometry(this.squareSize,this.BOXHEIGHT,this.squareSize
);
theCube =new THREE.Mesh(shape);
theCube.material =new THREE.MeshBasicMaterial({
map:this.wall_texture,});
theCube.position.copy(this.translate(iCounter, jCounter));// translate my (i,j) grid coordinates to three.js (x,y,z) coordinatesABWorld.scene.add(theCube);}else{
let node =newNode(this, iCounter, jCounter);
node.setAsEmpty();this.GRID[iCounter][jCounter]= node;}}}}
setupMaze(){
let idx =null;
let jdx =null;
let shape =null;
let theCube =null;for(let count =1; count <=this.NOBOXES; count++){
idx = AB.randomIntAtoB(1,this.gridSize -2);// inner squares are 1 to gridSize-2
jdx = AB.randomIntAtoB(1,this.gridSize -2);
let node =newNode(this, idx, jdx);
node.setAsMazeWall();this.GRID[idx][jdx]= node;
shape =new THREE.BoxGeometry(this.squareSize,this.BOXHEIGHT,this.squareSize
);
theCube =new THREE.Mesh(shape);
theCube.material =new THREE.MeshBasicMaterial({
map:this.maze_texture,});
theCube.position.copy(this.translate(idx, jdx));// translate my (i,j) grid coordinates to three.js (x,y,z) coordinatesABWorld.scene.add(theCube);}}
setupNeighbours(){for(let iCounter =0; iCounter <this.gridSize; iCounter++){for(let jCounter =0; jCounter <this.gridSize; jCounter++){this.GRID[iCounter][jCounter].addNeighbours(this.GRID);}}}
setupEnemy(){// set up enemy// start in random location// search for empty square
let idx =null;
let jdx =null;
let shape =null;do{
idx = AB.randomIntAtoB(1,this.gridSize -2);
jdx = AB.randomIntAtoB(1,this.gridSize -2);}while(this.isOccupied(idx, jdx));this.enemyIdx = idx;this.enemyJdx = jdx;
shape =new THREE.BoxGeometry(this.squareSize,this.BOXHEIGHT,this.squareSize
);this.theEnemy =new THREE.Mesh(shape);this.theEnemy.material =new THREE.MeshBasicMaterial({
map:this.enemy_texture,});ABWorld.scene.add(this.theEnemy);this.drawEnemy();}
setupAgent(){// set up agent// start in random location// search for empty square
let idx =null;
let jdx =null;
let shape =null;do{
idx = AB.randomIntAtoB(1,this.gridSize -2);
jdx = AB.randomIntAtoB(1,this.gridSize -2);}while(this.isOccupied(idx, jdx));// search for empty squarethis.agentIdx = idx;this.agentJdx = jdx;
shape =new THREE.BoxGeometry(this.squareSize,this.BOXHEIGHT,this.squareSize
);this.theAgent =new THREE.Mesh(shape);this.theAgent.material =new THREE.MeshBasicMaterial({
map:this.agent_texture,});ABWorld.scene.add(this.theAgent);this.drawAgent();// finally skybox// setting up skybox is simple// just pass it array of 6 URLs and it does the asych loadABWorld.scene.background =new THREE.CubeTextureLoader().load(this.SKYBOX_ARRAY,function(){ABWorld.render();
AB.removeLoading();
AB.runReady =true;// start the run loop});}
setupTrapLocations(){
let idx =null;
let jdx =null;do{
idx = AB.randomIntAtoB(1,this.gridSize -2);
jdx = AB.randomIntAtoB(1,this.gridSize -2);}while(this.isOccupied(idx, jdx));for(let iCounter =0; iCounter <this.gridSize; iCounter++){for(let jCounter =0; jCounter <this.gridSize; jCounter++){
let node =this.GRID[iCounter][jCounter];if(node.isWall()){continue;}
let canBeTrap = node.neighbours.filter(_node =>!_node.isWall());if(canBeTrap.length ===1){this.resetNodeParameters();this.getPath({
startNode:this.GRID[idx][jdx],
endNode:this.GRID[iCounter][jCounter],
fn:()=>{this.trapLocations.push(this.GRID[iCounter][jCounter]);}});}}}}
translate(idx, jdx){
let vector =new THREE.Vector3();
vector.y =0;
vector.x = idx *this.squareSize -this.MAXPOS /2;
vector.z = jdx *this.squareSize -this.MAXPOS /2;return vector;}
drawEnemy(){// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates// if camera moving, look back at where the enemy isthis.theEnemy.position.copy(this.translate(this.enemyIdx,this.enemyJdx));ABWorld.lookat.copy(this.theEnemy.position);}
drawAgent(){// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates// if camera moving, look back at where the enemy isthis.theAgent.position.copy(this.translate(this.agentIdx,this.agentJdx));ABWorld.follow.copy(this.theAgent.position);}
addPath(node){this.path.push(node);}/**
* Computes manhattan distance
* @param {Array} nodes nodes
* @returns {integer} manhattan distance
*/
heuristic(nodes){
let startNode =null;
let intermediateNode =null;
let endNode =null;if(nodes.length ===2){
startNode = nodes[0];
endNode = nodes[1];return(Math.abs(startNode.idx - endNode.idx)+Math.abs(startNode.jdx - endNode.jdx));}else{
startNode = nodes[0];
intermediateNode = nodes[1];
endNode = nodes[2];returnMath.min(Math.abs(startNode.idx - intermediateNode.idx)+Math.abs(startNode.jdx - intermediateNode.jdx),Math.abs(intermediateNode.idx - endNode.idx)+Math.abs(intermediateNode.jdx - endNode.jdx),);}}
resetNodeParameters(){for(let idx =0; idx <this.gridSize; idx++){for(let jdx =0; jdx <this.gridSize; jdx++){
let node =this.GRID[idx][jdx];if(node &&!node.isWall()){
node.removeTemperoryPath();
node.calculatedTotal =null;
node.stepCost =null;
node.heuristicEstimate =null;
node.previous =null;}}}}/**
* Moves the enemy in the direction of the path returned by A star
* Herds the agent in the location of trap node by maintaining a herding distance
* of one node between the agent and enemy. This gives the agent a possibility to
* escape if stuck.
*/
moveLogicalEnemy(){// move towards agent// put some randomness in so it won't get stuck with barriersthis.resetNodeParameters();
let startNode =this.GRID[this.enemyIdx][this.enemyJdx];
let intermediateNode =this.GRID[this.agentIdx][this.agentJdx];
let endNode =this.getLocationForHerdingAgent();
let currentNode =this.getPath({
startNode: startNode,
intermediateNode: intermediateNode,
endNode: endNode
});
let tempPath =[];
let idx =null;
let jdx =null;
tempPath.push(currentNode);while(currentNode && currentNode.previous){
tempPath.push(currentNode.previous);
currentNode = currentNode.previous;if(!currentNode ||!currentNode.previous){break;}
currentNode.showTemperoryPath();}if(tempPath.length <2){return;}
let nextStep = tempPath[tempPath.length -2];
idx = nextStep.idx;
jdx = nextStep.jdx;if(!this.isOccupied(idx, jdx)){// if no obstacle then move, else just miss a turnif(!this.isBadStep(idx, jdx)||this.isAgentKindaBlocked()||this.stopCounter >10){this.enemyIdx = idx;this.enemyJdx = jdx;this.stopCounter =0;this.addPath(this.GRID[this.enemyIdx][this.enemyJdx]);}else{this.stopCounter +=1;
let node =null;try{this.path.pop();
node =this.path[this.path.length-2];this.enemyIdx = node.idx;this.enemyJdx = node.jdx;}catch(e){return;};}}}/**
* Returns the end node after computing A Star
* @param {JSON} args
* @param {object} startNode start node for A star
* @param {object} intermediateNode intermediate node for A star
* @param {object} endNode end node for A star
* @param {object} fn custom function to run when path is found
* @param {object} returnTotalCost returns only total cost
*
* @returns {object} returns the end node for A star
*/
getPath(args){
let startNode = args.startNode;
let intermediateNode = args.intermediateNode;
let endNode = args.endNode;
let fn = args.fn;
let openNodes =[startNode];
let closedNodes =[];
let currentNode =null;
let totalCost =0;
let returnTotalCost = args.returnTotalCost ||false;
let noPath =true;
let _end = intermediateNode ? intermediateNode : endNode;while(openNodes.length){
let _winner =0;for(let iCounter =0; iCounter < openNodes.length; iCounter++){if(
openNodes[iCounter].calculatedTotal <
openNodes[_winner].calculatedTotal
){
_winner = iCounter;}}
currentNode = openNodes[_winner];// If intermediateNode path is reached viz the agent, assign endNode to _end to// further travel to trap locationif(intermediateNode && currentNode === intermediateNode){
_end = endNode
}// Did I finish?if(currentNode === _end){// console.log("success - found path");
noPath =false;if(fn){
fn();}break;}
openNodes =this.removeFromArray(openNodes, currentNode);
closedNodes.push(currentNode);for(
let iCounter =0;
iCounter < currentNode.neighbours.length;
iCounter++){
let node = currentNode.neighbours[iCounter];if(closedNodes.includes(node)|| node.isWall()){continue;}
let _stepCost = node.stepCost +this.heuristic([node, currentNode]);
let newPath =false;if(openNodes.includes(node)){if((_stepCost < node.stepCost)||(intermediateNode && _end !== intermediateNode)){
node.stepCost = _stepCost;
newPath =true;}}else{
node.stepCost = _stepCost;
newPath =true;
openNodes.push(node);}if(newPath){
node.heuristicEstimate =this.heuristic([node, _end]);
node.calculatedTotal = node.stepCost + node.heuristicEstimate;
totalCost += node.stepCost + node.heuristicEstimate;
node.previous = currentNode;}}}if(noPath){return startNode;}return returnTotalCost ? totalCost : currentNode;}/**
* Finds the nearest trap location within the square coordinates to try
* and trap the agent
* @returns {object} Location of the trap node
*/
getLocationForHerdingAgent(){
let _enemyLocation =this.GRID[this.enemyIdx][this.enemyJdx];
let _agentLocation =this.GRID[this.agentIdx][this.agentJdx];
let _location ={
node:null,
cost:0};
let _cost =null;
let coordinates =this.getCoordinates(_agentLocation);if(this.trapLocations.includes(_agentLocation)){
_location.node = _agentLocation;
_location.cost =0;}else{this.trapLocations.forEach(node =>{if(this.liesWithin(node, coordinates)){
_cost =this.heuristic([_enemyLocation, node, _agentLocation]);if(!_location.cost || _location.cost > _cost){
_location.node = node;
_location.cost = _cost;}}});}
_location.node && _location.node.showCornerIndicator();
_location.node && setTimeout(()=>{
_location.node.removeCornerIndicator();},100);return _location.node || _agentLocation;}
moveLogicalAgent(agent){// this is called by the infrastructure that gets action a from the Mind
let idx =this.agentIdx;
let jdx =this.agentJdx;if(agent ===this.ACTION_LEFT){
idx--;}elseif(agent ===this.ACTION_RIGHT){
idx++;}elseif(agent ===this.ACTION_UP){
jdx++;}elseif(agent ===this.ACTION_DOWN){
jdx--;}if(!this.isOccupied(idx, jdx)){this.agentIdx = idx;this.agentJdx = jdx;}}
renderPath(){for(let node of this.path){
node.showPermanentPath();}}
getScore(){
let steps =(this.goodSteps / AB.maxSteps)*100;
let score =Math.round(steps *100);return score /100;}
getState(){return[this.agentIdx,this.agentJdx,this.enemyIdx,this.enemyJdx];}
updateStatusBefore(agent){// 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
let state =this.getState();
AB.msg(" Step: "+ AB.step +" x = ("+ state.toString()+") a = ("+ agent +") ");}
updateStatusAfter(){// agent and enemy have moved, can calculate score// new state after both have moved
let state =this.getState();
let score =(this.goodSteps / AB.step)*100;
AB.msg(" y = ("+
state.toString()+") <br>"+" Bad steps: "+this.badSteps +" Good steps: "+this.goodSteps +" Score: "+
score.toFixed(2)+"% ",2);}
takeAction(agent){this.updateStatusBefore(agent);this.moveLogicalAgent(agent);if(AB.step %2===0){this.moveLogicalEnemy();}this.isBadStep()?this.badSteps++:this.goodSteps++;this.drawAgent();this.drawEnemy();// show status line after movesthis.updateStatusAfter();// set for analyticsthis._goodSteps =this.goodSteps;if(this.isAgentBlocked()){// if agent blocked in, run over// you score zero as far as database is concerned
AB.abortRun =true;this.goodSteps =0;this.pauseMusic();this.soundAlarm();this.renderPath();}}
getAction(location){
let agentIdx = location[0];
let agentJdx = location[1];
let enemyIdx = location[2];
let enemyJdx = location[3];if(enemyJdx < agentJdx){return AB.randomPick(this.ACTION_UP,
AB.randomPick(this.ACTION_RIGHT,this.ACTION_LEFT));}elseif(enemyJdx > agentJdx){return AB.randomPick(this.ACTION_DOWN,
AB.randomPick(this.ACTION_RIGHT,this.ACTION_LEFT));}elseif(enemyIdx < agentIdx){return AB.randomPick(this.ACTION_RIGHT,
AB.randomPick(this.ACTION_UP,this.ACTION_DOWN));}elseif(enemyIdx > agentIdx){return AB.randomPick(this.ACTION_LEFT,
AB.randomPick(this.ACTION_UP,this.ACTION_DOWN));}else{return AB.randomIntAtoB(0,3);}}
getKeyMap(){return{37:this.ACTION_LEFT,38:this.ACTION_DOWN,39:this.ACTION_RIGHT,40:this.ACTION_UP,};}
removeFromArray(array, element){
let index = array.indexOf(element);if(index !==-1){
array.splice(index,1)}return array;}/**
* Checks if node lies within the boundaries
* @param {object} node node to be checked if it exists in boundaries
* @param {JSON} coordinates boundaries
* @returns {boolean}
*/
liesWithin(node, coordinates){
let topLeft = coordinates.topLeft;
let topRight = coordinates.topRight;
let bottomLeft = coordinates.bottomLeft;
let bottomRight = coordinates.bottomRight;if((node.idx < topRight.idx && node.jdx < topRight.jdx)&&(node.idx > topLeft.idx && node.jdx < topLeft.jdx)&&(node.idx > bottomLeft.idx && node.jdx > bottomLeft.jdx)&&(node.idx < bottomRight.idx && node.jdx > bottomRight.jdx)){returntrue;}returnfalse;}/**
* Computes the coordinates of square which limits the search for the trapLocation
* @param {object} node node for which we want he boundary square
* @returns {JSON} nodes which form the edges of the square
*/
getCoordinates(node){
let distance =5;// topRight
let topRight ={
idx: node.idx + distance,
jdx: node.jdx + distance
}if(topRight.idx >this.gridSize -2){
topRight.idx =this.gridSize -2}elseif(topRight.idx <1){
topRight.idx =1;}if(topRight.jdx >this.gridSize -2){
topRight.jdx =this.gridSize -2}elseif(topRight.jdx <1){
topRight.jdx =1;}// topLeft
let topLeft ={
idx: node.idx - distance,
jdx: node.jdx + distance
}if(topLeft.idx >this.gridSize -2){
topLeft.idx =this.gridSize -2}elseif(topLeft.idx <1){
topLeft.idx =1;}if(topLeft.jdx >this.gridSize -2){
topLeft.jdx =this.gridSize -2}elseif(topLeft.jdx <1){
topLeft.jdx =1;}// bottomLeft
let bottomLeft ={
idx: node.idx - distance,
jdx: node.jdx - distance
}if(bottomLeft.idx >this.gridSize -2){
bottomLeft.idx =this.gridSize -2}elseif(bottomLeft.idx <1){
bottomLeft.idx =1;}if(bottomLeft.jdx >this.gridSize -2){
bottomLeft.jdx =this.gridSize -2}elseif(bottomLeft.jdx <1){
bottomLeft.jdx =1;}
let bottomRight ={
idx: node.idx + distance,
jdx: node.jdx - distance
}if(bottomRight.idx >this.gridSize -2){
bottomRight.idx =this.gridSize -2}elseif(bottomRight.idx <1){
bottomRight.idx =1;}if(bottomRight.jdx >this.gridSize -2){
bottomRight.jdx =this.gridSize -2}elseif(bottomRight.jdx <1){
bottomRight.jdx =1;}return{
topLeft: topLeft,
topRight: topRight,
bottomLeft: bottomLeft,
bottomRight: bottomRight
}}}// Initialises ComplexWorld object and attaches it to window object
window.world =new complexWorld();/**
* Overrides the AB.world functions to run the functions defined in Complex World
*/
AB.world ={
newRun:function(){
world.newRun();},
endRun:function(){
world.endRun();},
getScore:function(){// only called at end - do not use AB.step because it may have just incremented past AB.maxStepsreturn world.getScore();},
getState:function(){return world.getState();},
takeAction:function(agent){
world.takeAction(agent);},
customKeyHandler:function(event){if(!AB.runReady)returntrue;// not ready yet// if not one of our special keys, send it to default key handling:if(!world.ourKeys(event))returntrue;// else handle key and prevent default handling:
let keyMap = world.getKeyMap();
world.moveLogicalAgent(keyMap[event.keyCode]);// 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();returnfalse;},};
AB.mind ={
getAction:function(location){return world.getAction(location);},};