// 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 +
" x = (" +
x.toString() +
") 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(
" y = (" +
y.toString() +
") <br>" +
" Bad steps: " +
badsteps +
" Good steps: " +
goodsteps +
" 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
}