// Cloned by Harpreet Singh on 1 Nov 2019 from World "Complex World" by Starter user
// Please leave this clone trail here.
// ==== Starter World ===============================================================================================
// (c) Ancient Brain Ltd. All rights reserved.
// This code is only for use on the Ancient Brain site.
// This code may be freely copied and edited by anyone on the Ancient Brain site.
// This code may not be copied, re-published or used on any other website.
// To include a run of this code on another website, see the "Embed code" links provided on the Ancient Brain site.
// ==================================================================================================================
// =============================================================================================
// More complex starter World
// 3d-effect Maze World (really a 2-D problem)
// Movement is on a semi-visible grid of squares
//
// This more complex World shows:
// - Skybox
// - Internal maze (randomly drawn each time)
// - Enemy actively chases agent
// - Music/audio
// - 2D world (clone this and set show3d = false)
// - User keyboard control (clone this and comment out Mind actions to see)
// =============================================================================================
// =============================================================================================
// Scoring:
// Bad steps = steps where enemy is within one step of agent.
// Good steps = steps where enemy is further away.
// Score = good steps as percentage of all steps.
//
// There are situations where agent is trapped and cannot move.
// If this happens, you score zero.
// =============================================================================================
// =============================================================================================
// Media Credits:
//https://www.appannie.com/en/apps/ios/app/1344484784/
//https://img.pixers.pics
//https://www.thoughtco.com/thmb/
//https://sophosnews.files.wordpress.com
//
//SKYBOX Graphics: https://threejsfundamentals.org/threejs/lessons/threejs-backgrounds.html
//Audio Files: https://freesound.org/
// =============================================================================================
//************************************** CODE SECTIONS ***************************************************************
/*
All code has been reorganised to make it easily readable.
Didn't partition code to different JS files but organised similar functions in various sections
Code section from top to bottom are as follows:
01. Tweaker Box and global variables declaration/initialisation
02. AB World Key Functions: newrun, getstate, takeaction, endrun, getscore
03. Setup and Utility Functions: loadResources, initScene
04. Functions to draw moving objects: drawAgent, drawEnemy
05. Functions for status checks and agent actions asynchFinished, occupied, updateStatusBefore, updateStatusAfter, badstep, agentBlocked
06. Agent and Enemy moves moveLogicalAgent, moveLogicalEnemy
07. A* search findShortestPath
08. Grid Cells - create object & assign properties for each cell
09. Conversion and Utility functions translate, removeFromArray, heuristic
10. Music and sound effects musicPlay, musicPause, soundAlarm
11. Event handlers ourKeys, keyHandler
*/
//********************************************************************************************************************
// ===================================================================================================================
// === Section 01. Tweaker Box and global variables declaration/initialisation========================================
// ===================================================================================================================
//---- global constants: -------------------------------------------------------
const show3d = true; //Switch between 3d and 2d view (both using Three.js)
const slowmotion = false; //the run is slowed down to 1 second per cycle. default is 100 ms/cycle
const diagonalmoves = false; //allow the enemy chase with diagonal moves.
const tracesearch = false; //for every search trace full path back. memory intensive and likely to throw memory exception
const TEXTURE_WALL = '/uploads/hsingh/brick3.jpg' ;
const TEXTURE_MAZE = '/uploads/hsingh/patternmaze.jpg' ;
const TEXTURE_AGENT = '/uploads/hsingh/smiley2.jpg' ;
const TEXTURE_ENEMY = '/uploads/hsingh/1572624791.png' ;
const MUSIC_BACK = '/uploads/hsingh/ailoop.wav' ;
const SOUND_ALARM = '/uploads/hsingh/jolly-fanfares.wav' ;
const gridsize = 20; // number of squares along side of world
const NOBOXES = Math.trunc((gridsize * gridsize) / 10 ); // density of maze - number of internal boxes
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: -------------------------------
if (slowmotion) AB.clockTick = 1000; else 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.
AB.headerRHS();
AB.headerWidth (300);
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
const SKYBOX_ARRAY = [
"/uploads/hsingh/pos-x.jpg",
"/uploads/hsingh/neg-x.jpg",
"/uploads/hsingh/pos-y.jpg",
"/uploads/hsingh/neg-y.jpg",
"/uploads/hsingh/pos-z.jpg",
"/uploads/hsingh/neg-z.jpg"
];
// ===================================================================================================================
// === End of tweaker's box ==========================================================================================
// ===================================================================================================================
//--- Mind can pick one of these actions -----------------
const ACTION_LEFT = 0;
const ACTION_RIGHT = 1;
const ACTION_UP = 2;
const ACTION_DOWN = 3;
const ACTION_STAYSTILL = 4;
// in initial view, (smaller-larger) on i axis is aligned with (left-right)
// in initial view, (smaller-larger) on j axis is aligned with (away from you - towards you)
var BOXHEIGHT; // 3d or 2d box height
var the_agent, the_enemy;
var wall_texture, agent_texture, enemy_texture, maze_texture;
// enemy and agent position on squares
var ei, ej, ai, aj;
var badsteps;
var goodsteps;
var grid = new Array(gridsize); // can query GRID about whether squares are occupied, will in fact be initialised as a 2D array
//Enemy search move check variables (A*)--------------------------
var openSet = [];
var closedSet = [];
var path = [];
var start, end;
// ===================================================================================================================
// === Section 02. AB World Key Functions=============================================================================
// === newrun, getstate, takeaction, endrun, getscore ================================================================
// ===================================================================================================================
AB.world.newRun = function(){
AB.loadingScreen(); //Ancient brain splash screen that shows up till all graphics and media is loaded and ready to run
AB.runReady = false;
badsteps = 0;
goodsteps = 0;
if (show3d){
BOXHEIGHT = squaresize;
ABWorld.init3d(startRadiusConst, maxRadiusConst, SKYCOLOR);
}else{
BOXHEIGHT = 1;
ABWorld.init2d(startRadiusConst, maxRadiusConst, SKYCOLOR);
}
loadResources(); // aynch file loads
// calls initScene() when it returns
//document.onkeydown = keyHandler;
};
AB.world.getState = function(){
var x = [ai, aj, ei, ej];
return x;
};
AB.world.takeAction = function(a){
updateStatusBefore(a); // show status line before moves
moveLogicalAgent(a);
if ((AB.step % 2) == 0) // slow the enemy down to every nth step -- we can remove this to make enemy more aggressive
moveLogicalEnemy();
if (badstep()) badsteps++; else goodsteps++;
drawAgent();
drawEnemy();
updateStatusAfter(); // show status line after moves
// if agent blocked in, run over
if (agentBlocked()){
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
};
// ===================================================================================================================
// === Section 03. Setup and Utility Functions========================================================================
// === loadResources, initScene ======================================================================================
// ===================================================================================================================
//Functions to set the scene, create agent and enemy and then kick off the play
// asynchronous file loads - call initScene() when all finished
function loadResources(){
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 initScene(){
var i, j, shape, the_cube;
for(i=0; i<gridsize ; i++){
grid[i] = new Array(gridsize ); //Create a 2D grid with rows and columns -- we are keeping same rows and columns
}
//Initialize each grid cell object with default properties (f,g,h etc.)
for(i=0; i<gridsize; i++){
for(j=0; j<gridsize; j++){
grid[i][j] = new GridCell(i, j);
//while creating the grid cell also initialize the side walls
if ((i==0) || (i==gridsize-1) || (j==0) || (j==gridsize-1)){
//MODIFICATION TO ORIGINAL CODE- tracking all states through one object
grid[i][j].external_wall = true; //USED the boolean flags within GridCell to track wall, maze, open or occupied space
shape = new THREE.BoxGeometry (squaresize, BOXHEIGHT, squaresize);
the_cube = new THREE.Mesh(shape);
the_cube.material = new THREE.MeshBasicMaterial({ map: wall_texture });
the_cube.position.copy ( translate(i,j) ); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
ABWorld.scene.add(the_cube);
}
//also set random maze, changed this from original code.
if (Math.random(1)<0.20 && !grid[i][j].external_wall){
grid[i][j].internal_maze_block = true; //USED the boolean flags within GridCell to track wall, maze, open or occupied space
shape = new THREE.BoxGeometry ( squaresize, BOXHEIGHT, squaresize );
the_cube = new THREE.Mesh( shape );
the_cube.material = new THREE.MeshBasicMaterial( { map: maze_texture } );
the_cube.position.copy ( translate(i,j) ); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
ABWorld.scene.add(the_cube);
}
}
}
//Add neighbours for each grid cell as we need to know neighbors for A* search while visiting a node during search
for(var i=0; i<gridsize; i++){
for(var j=0; j<gridsize; j++){
grid[i][j].addNeighbours(grid);
}
}
// set up enemy
// start in random location
do {
i = AB.randomIntAtoB(1,gridsize-2);
j = AB.randomIntAtoB(1,gridsize-2);
} while (occupied(i,j)); // search for empty square
ei = i;
ej = j;
shape = new THREE.BoxGeometry ( squaresize, BOXHEIGHT, squaresize );
the_enemy = new THREE.Mesh(shape);
the_enemy.material = new THREE.MeshBasicMaterial( { map: enemy_texture } );
ABWorld.scene.add(the_enemy);
drawEnemy();
// set up agent
// start in random location
do{
i = AB.randomIntAtoB(1,gridsize-2);
j = AB.randomIntAtoB(1,gridsize-2);
} while (occupied(i,j)); // search for empty square
ai = i;
aj = j;
shape = new THREE.BoxGeometry ( squaresize, BOXHEIGHT, squaresize );
the_agent = new THREE.Mesh( shape );
the_agent.material = new THREE.MeshBasicMaterial( { map: agent_texture } );
ABWorld.scene.add(the_agent);
drawAgent();
// finally skybox
// setting up skybox is simple
// just pass it array of 6 URLs and it does the asych load
ABWorld.scene.background = new THREE.CubeTextureLoader().load (SKYBOX_ARRAY, function(){
ABWorld.render();
AB.removeLoading();
AB.runReady = true; // start the run loop
});
}
// ===================================================================================================================
// === Section 04. Functions to draw moving objects===================================================================
// === drawAgent, drawEnemy ==========================================================================================
// ===================================================================================================================
function drawAgent(){
// given ai, aj, draw it
the_agent.position.copy(translate(ai,aj)); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
ABWorld.follow.copy(the_agent.position); // follow vector = agent position (for camera following agent)
}
function drawEnemy(){
// given ei, ej, draw it
the_enemy.position.copy( translate(ei,ej) ); // translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
ABWorld.lookat.copy( the_enemy.position ); // if camera moving, look back at where the enemy is
}
// ===================================================================================================================
// === Section 05. Functions for status checks and agent actions =====================================================
// === asynchFinished, occupied, updateStatusBefore, updateStatusAfter, badstep, agentBlocked ========================
// ===================================================================================================================
//Are all files loaded to kick off the chase?
function asynchFinished(){
return (wall_texture && agent_texture && enemy_texture && maze_texture);
}
//Check to see if this square is occupied?
// my numbering is 0 to gridsize-1
function occupied ( i, j ){
if ((ei == i) && (ej == j)) return true; // variable objects
if ((ai == i) && (aj == j)) return true;
//MODIFIED BELOW CODE to check for the wall or maze using the grid cell property
if (grid[i][j].external_wall) return true; // fixed objects
if (grid[i][j].internal_maze_block) return true;
return false;
}
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 );
}
// is the enemy within one square of the agent
function badstep(){
return ((Math.abs(ei - ai) < 2 ) && ( Math.abs(ej - aj) < 2));
}
// agent is blocked on all sides, run over
function agentBlocked(){
return (
occupied (ai-1, aj) &&
occupied (ai+1, aj) &&
occupied (ai, aj+1) &&
occupied (ai, aj-1)
);
}
// ===================================================================================================================
// === Section 06. Agent and Enemy moves =============================================================================
// === moveLogicalAgent, moveLogicalEnemy ============================================================================
// ===================================================================================================================
function moveLogicalAgent(a){
// this is called by the infrastructure that gets action a from the Mind
var i = ai;
var j = aj;
if (a == ACTION_LEFT) i--;
else if (a == ACTION_RIGHT) i++;
else if (a == ACTION_UP) j++;
else if (a == ACTION_DOWN) j--;
if (!occupied(i,j)){
ai = i;
aj = j;
}
}
function moveLogicalEnemy(){
//Call A* Search function to trace the shortest path to the agent from enemy
//During first search the path from enemy to agent will take longer to search
//Subsequent moves search will be faster as the enemy is now closely chasing the agent
var winner = findShortestPath();//Search function will return the winning node or null is the search didn't find any path from enemy to agent
var i, j;
if (winner!== undefined){ //Check to ensure we have found a path to the agent
//If search found the path to the agent we want to move enemy close to agent
//We are using the search output to look at the node
//As the winner node is where the agent is and we can not move enemy to agent position which is already occupied
//So we move the enemy to one previous node in the shortest search path
i = winner.previous.i; //go to previous neighbour of the winning node and get i,j values
j = winner.previous.j;
if (tracesearch){ //If we want to trace the full path back to start during each search-- this function is memory intensive
path = [];
var temp = winner;
path.push(temp);
while (temp.previous)
{
//console.log ("loop");
path.push(temp.previous);
temp = temp.previous;
}
}
}
//This check ensures we are not moving to a blocked position
if (!occupied(i,j)){
ei = i;
ej = j;
}
}
// ===================================================================================================================
// === Section 07. A* Search =========================================================================================
// === findShortestPath ==============================================================================================
// ===================================================================================================================
function findShortestPath(){
//Initial arrays to hold open and closed
openSet = [];
closedSet = [];
//With every move agent and enemy position has changed to we need to capture the current location for both
start = grid[ei][ej]; //enemy position
end = grid[ai][aj]; //agent position
openSet.push(start); //this is our starting position to search from. This is the only node we know in beginning.
//iterate through open
while (openSet.length > 0){
// Best next option
var winner = 0;
for (var i = 0; i < openSet.length; i++)
if (openSet[i].f < openSet[winner].f)
winner = i;
var current = openSet[winner];
//if current node is same as agent we have found the target.
if (current === end){
return current;
}
// Best option moves from openSet to closedSet
removeFromArray(openSet, current);
closedSet.push(current);
// Check all the neighbors
var neighbours = current.neighbours;
//--- start of for loop -----------
for (var i = 0; i < neighbours.length; i++)
{
var neighbor = neighbours[i];
// Valid next spot?
if (!closedSet.includes(neighbor) && !neighbor.external_wall && !neighbor.internal_maze_block)
{
var tempG = current.g + heuristic(neighbor, current);
// Is this a better path than before?
var 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 = heuristic(neighbor, end);
neighbor.f = neighbor.g + neighbor.h;
neighbor.previous = current;
}
}
}
//--- end of for loop -----------
}
// --- end still searching -----------------------------
return current;
}
// ===================================================================================================================
// === Section 08. Grid Cells - create object and assign properties for each cell ====================================
// === GridCell ======================================================================================================
// ===================================================================================================================
function GridCell(i, j){
this.i = i;
this.j = j;
this.f = 0; //required to hold f(n)
this.g = 0; //required to hold g(n)
this.h = 0; //required to hold h(n)
this.neighbours = [];
this.previous = undefined;
this.external_wall = false; //sets the specific grid cell as external wall
this.internal_maze_block = false; //sets the specific grid cell as maze block
//add all neighbors of current node
this.addNeighbours = function(grid){
var i = this.i;
var j = this.j;
if (i<gridsize-1){
this.neighbours.push(grid[i+1][j]);
}
if (i>0){
this.neighbours.push(grid[i-1][j]);
}
if (j<gridsize-1){
this.neighbours.push(grid[i][j+1]);
}
if (j>0){
this.neighbours.push(grid[i][j-1]);
}
//addd diagonal neighbours
if (diagonalmoves){
if (i>0 && j>0){
this.neighbours.push(grid[i-1][j-1]);
}
if (i<gridsize-1 && j>0){
this.neighbours.push(grid[i+1][j-1]);
}
if(i>0 && j<gridsize-1){
this.neighbours.push(grid[i-1][j+1]);
}
if(i<gridsize-1 && j<gridsize-1){
this.neighbours.push(grid[i+1][j+1]);
}
}
}
}
// ===================================================================================================================
// === Section 09. Conversion and Utility functions ==================================================================
// === translate, removeFromArray, heuristic =========================================================================
// ===================================================================================================================
// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates
// logically, coordinates are: y=0, x and z all positive (no negative)
// logically my dimensions are all positive 0 to MAXPOS
// to centre everything on origin, subtract (MAXPOS/2) from all dimensions
function translate(i, j){
var v = new THREE.Vector3();
v.y = 0;
v.x = ( i * squaresize ) - ( MAXPOS/2 );
v.z = ( j * squaresize ) - ( MAXPOS/2 );
return v;
}
function removeFromArray(arr, ele){
for (var i=arr.length-1; i>=0; i--){
if (arr[i] == ele){
arr.splice(i, 1);
}
}
}
function heuristic(a,b){
if (diagonalmoves){
return Math.abs(a.i-b.i) + Math.abs(a.j-b.j);
} else {
return AB.distance2D (a.i, a.j, b.i, b.j );
}
//return Math.dist(a.i, a.j, b.i, b.j);
//return Math.abs(a.i-b.i) + Math.abs(a.j-b.j);
}
// ===================================================================================================================
// === Section 10. Music and sound effects ===========================================================================
// === musicPlay, musicPause, soundAlarm =============================================================================
// ===================================================================================================================
var backmusic = AB.backgroundMusic ( MUSIC_BACK );
function musicPlay() { backmusic.play(); }
function musicPause() { backmusic.pause(); }
function soundAlarm()
{
var alarm = new Audio(SOUND_ALARM);
alarm.play(); // play once, no loop
}
// ===================================================================================================================
// === Section 11. Event handlers ====================================================================================
// === ourKeys, keyHandler ===========================================================================================
// ===================================================================================================================
// This is hard to see while the Mind is also moving the agent:
// AB.mind.getAction() and AB.world.takeAction() are constantly running in a loop at the same time
// have to turn off Mind actions to really see user key control
// we will handle these keys:
var OURKEYS = [ 37, 38, 39, 40 ];
function ourKeys ( event ) { return ( OURKEYS.includes ( event.keyCode ) ); }
function keyHandler ( event )
{
if ( ! AB.runReady ) return true; // not ready yet
// if not one of our special keys, send it to default key handling:
if ( ! ourKeys ( event ) ) return true;
// else handle key and prevent default handling:
if ( event.keyCode == 37 ) moveLogicalAgent ( ACTION_LEFT );
if ( event.keyCode == 38 ) moveLogicalAgent ( ACTION_DOWN );
if ( event.keyCode == 39 ) moveLogicalAgent ( ACTION_RIGHT );
if ( event.keyCode == 40 ) moveLogicalAgent ( ACTION_UP );
// when the World is embedded in an iframe in a page, we want arrow key events handled by World and not passed up to parent
event.stopPropagation(); event.preventDefault(); return false;
}