// Cloned by Venkatraman Palani on 12 Nov 2023 from Mind "Trapping Agent" by Himanshu Warekar
// Please leave this clone trail here.
/**
* 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.
*/
class Node {
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; // f
this.stepCost = 0; // g
this.heuristicEstimate = 0; // h
this.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() {
return this.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.
*/
class Analytics {
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 world
this.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-integer
this.squareSize = 100; // size of square in pixels
this.MAXPOS = this.gridSize * this.squareSize; // length of one side in pixels
this.SKYCOLOR = 0xddffdd; // a number, not a string
this.startRadiusConst = this.MAXPOS * 0.8; // distance from centre to start the camera at
this.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 zero
this.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 height
this.GRID = new Array(this.gridSize); // can query GRID about whether squares are occupied, will in fact be initialised as a 2D array
this.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 squares
this.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 = new Analytics();
}
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 = new Audio(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 objects
if (this.enemyIdx === idx && this.enemyJdx === jdx) return true;
if (this.agentIdx === idx && this.agentJdx === jdx) return true;
if (this.GRID[idx][jdx].isWall()) return true;
return false;
}
isBadStep(locationIdx, locationJdx) {
// is the enemy within one square of the agent
locationIdx = locationIdx || this.enemyIdx;
locationJdx = locationJdx || this.enemyJdx;
return Math.abs(locationIdx - this.agentIdx) < 2 &&
Math.abs(locationJdx - this.agentJdx) < 2
? true
: false;
}
isAgentBlocked() {
// agent is blocked on all sides, run over
return (
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) {
return this.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] = new Array(this.gridSize);
for (let jCounter = 0; jCounter < this.gridSize; jCounter++) {
if (
!iCounter ||
iCounter === this.gridSize - 1 ||
!jCounter ||
jCounter === this.gridSize - 1
) {
let node = new Node(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) coordinates
ABWorld.scene.add(theCube);
} else {
let node = new Node(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 = new Node(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) coordinates
ABWorld.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 square
this.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 load
ABWorld.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 is
this.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 is
this.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];
return Math.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 barriers
this.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 turn
if (!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 location
if (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--;
} else if (agent === this.ACTION_RIGHT) {
idx++;
} else if (agent === this.ACTION_UP) {
jdx++;
} else if (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 moves
this.updateStatusAfter();
// set for analytics
this._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)
);
} else if (enemyJdx > agentJdx) {
return AB.randomPick(
this.ACTION_DOWN,
AB.randomPick(this.ACTION_RIGHT, this.ACTION_LEFT)
);
} else if (enemyIdx < agentIdx) {
return AB.randomPick(
this.ACTION_RIGHT,
AB.randomPick(this.ACTION_UP, this.ACTION_DOWN)
);
} else if (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)
) {
return true;
}
return false;
}
/**
* 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
} else if (topRight.idx < 1) {
topRight.idx = 1;
}
if (topRight.jdx > this.gridSize - 2) {
topRight.jdx = this.gridSize -2
} else if (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
} else if (topLeft.idx < 1) {
topLeft.idx = 1;
}
if (topLeft.jdx > this.gridSize - 2) {
topLeft.jdx = this.gridSize -2
} else if (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
} else if (bottomLeft.idx < 1) {
bottomLeft.idx = 1;
}
if (bottomLeft.jdx > this.gridSize - 2) {
bottomLeft.jdx = this.gridSize -2
} else if (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
} else if (bottomRight.idx < 1) {
bottomRight.idx = 1;
}
if (bottomRight.jdx > this.gridSize - 2) {
bottomRight.jdx = this.gridSize -2
} else if (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.maxSteps
return world.getScore();
},
getState: function () {
return world.getState();
},
takeAction: function (agent) {
world.takeAction(agent);
},
customKeyHandler: function (event) {
if (!AB.runReady) return true; // not ready yet
// if not one of our special keys, send it to default key handling:
if (!world.ourKeys(event)) return true;
// 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();
return false;
},
};
AB.mind = {
getAction: function (location) {
return world.getAction(location);
},
};