Code viewer for World: Looney Tunes Chase (A* AI)
// This game involves you controlling Bugs Bunny, and the objective is to burrow through the first two floors and reach the Carrot object on floor 1     //
// without being caught by Yosemite Sam. For the best gaming experience, please use the 'Move With' option while playing.                                //
// Yosemite Sam tracks Bugs Bunny's location using an A* Pathfinding Algorithm so I have slowed him down to only move every 3 steps you take in order    //
// to make the game more playable (otherwise it's quite difficult).                                                                                      //
//-------------------------------------------------------------------------------------------------------------------------------------------------------//


const CLOCKTICK = 100;		// move things every 100 milliseconds
const MAXSTEPS = 1000;		// allowed 1000 steps before game is over

const  SCREENSHOT_STEP = 50;    


//-------------------- global constants --------------------//

const GRIDSIZE = 50;											// number of squares along side of world	   
const NOBOXES = Math.trunc ( (GRIDSIZE * GRIDSIZE) / 10 );		// density of maze
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 BLANKCOLOR = SKYCOLOR ;									// make objects this color until texture arrives
const LIGHTCOLOR = 0x000000 ;									// colour of ambient light used later
const STARTRADIUSCONST = MAXPOS * 1 ;							// distance from centre to start the camera at
const SKYBOXCONST = MAXPOS * 3 ;								// where to put skybox 
const MAXRADIUSCONST = MAXPOS * 10  ;							// maximum distance from camera we will render things  


//-------------------- mind can pick on of these actions --------------------//

const ACTION_LEFT 		= 0;		   
const ACTION_RIGHT 		= 1;
const ACTION_UP 		= 2;		 
const ACTION_DOWN 		= 3;
const ACTION_STAYSTILL 	= 4;


//-------------------- contents of a grid square --------------------//

const GRID_BLANK 	= 0;
const GRID_WALL 	= 1;
const GRID_MAZE 	= 2;
const GRID_GROUND   = 3;


//-------------------- random functions --------------------//

function randomfloatAtoB ( A, B ) {
	return ( A + ( Math.random() * (B-A) ) );
}

function randomintAtoB ( A, B )	{
	return  ( Math.round ( randomfloatAtoB ( A, B ) ) );
}


//-------------------- START OF WORLD CLASS --------------------//
 
function World() { 
	var GRID1 	= new Array(GRIDSIZE);			// grid 1, 2 and 3 represent the 3 floors
	var GRID2 	= new Array(GRIDSIZE);			// in the game and can be used to check if
	var GRID3 	= new Array(GRIDSIZE);			// a square is occupied etc.

	var UNDERSIDE 	= new Array(GRIDSIZE);			// used as the bottom of the floor on which objects and characters stand

	var FLOOR_1_WALLS 	= new Array ( 4 * GRIDSIZE );			// wall for floors
	var FLOOR_2_WALLS 	= new Array ( 4 * GRIDSIZE );			// 1, 2 and 3
	var FLOOR_3_WALLS 	= new Array ( 4 * GRIDSIZE );			// respectively

	var FLOOR_1_MAZE 	= new Array ( NOBOXES );			// maze for floors
	var FLOOR_2_MAZE 	= new Array ( NOBOXES );			// 1, 2 and 3
	var FLOOR_3_MAZE 	= new Array ( NOBOXES );			// respectively

	var FLOOR_1_GROUND = new Array (GRIDSIZE * GRIDSIZE);			// ground for floors
	var FLOOR_2_GROUND = new Array (GRIDSIZE * GRIDSIZE);			// 1, 2 and 3
	var FLOOR_3_GROUND = new Array (GRIDSIZE * GRIDSIZE);			// respectively

	var theagent, theenemy, thecamera, thecarrot, topfloorburrow, midfloorburrow;			// will be used to represent the object after which they are named
	  
	var enemyFloor  = 1950;			// the floor on which the enemy is initialised -> 1950 is the 3rd floor
	var agentFloor = 1950;			// the floor on which the agent is initialised

	var ei, ej;			// enemy position on squares
	var ai, aj;			// agent position on squares
	var ci, cj;			// camera position on squares (will be behind and above Bugs Bunny)
	var ti, tj;			// carrot position on squares
	var b1i, b1j;		// top floor burrow position on squares
	var b2i, b2j;		// middle floor burrow position on squares

	// booleans used to identify what direction Bugs Bunny is facing at any given time
    var bugsLookingForward = true;
    var bugsLookingBack = false;
    var bugsLookingLeft = false;
    var bugsLookingRight = false;
    
	// booleans used to identify what direction Yosemite Sam is facing at any given time
    var samLookingForward = true;
    var samLookingBack = false;
    var samLookingLeft = false;
    var samLookingRight = false;
    
	// will be used as array of size 2 marking the coordinates of Bugs Bunny and Yosemite Sam in Pathfinding Algorithm
    var currentAgentPosition;
    var currentEnemyPosition;
    
	var step;			// number of steps in game so far

 	var self = this;			// needed for private fn to call public fn
	
	
	// initialise the 3 floors
	function initGrid() {
		for (var i = 0; i < GRIDSIZE ; i++) {
			GRID1[i] = new Array(GRIDSIZE);	
			GRID2[i] = new Array(GRIDSIZE);
			GRID3[i] = new Array(GRIDSIZE);

			for (var j = 0; j < GRIDSIZE ; j++) {
				GRID1[i][j] = GRID_BLANK ;
				GRID2[i][j] = GRID_BLANK ;
				GRID3[i][j] = GRID_BLANK ;
			}
		}
	}

	// initialise the bottom of the 3 floors
	function initUnderside() {
		for (var i = 0; i < GRIDSIZE ; i++) {
			UNDERSIDE[i] = new Array(GRIDSIZE);		// each element is an array 
			for (var j = 0; j < GRIDSIZE ; j++) {
				UNDERSIDE[i][j] = GRID_BLANK ;
			}
		}
	}

	// checks if given coordinates are currently occupied by an object
	function occupied ( i, j ) {
		if ( GRID1[i][j] == GRID_WALL)
			return true;
		if ( GRID1[i][j] == GRID_MAZE && agentFloor == -50) 
			return true;

		if ( GRID2[i][j] == GRID_WALL) 
			return true;
		if ( GRID2[i][j] == GRID_MAZE && agentFloor == 950) 
			return true;

		if ( GRID3[i][j] == GRID_WALL) 
			return true;
		if ( GRID3[i][j] == GRID_MAZE && agentFloor == 1950) 
			return true;

		return false;
	}
	
	// similar to above, but doesn't consider where the agent is, this method prevents the burrows/carrot being places in same position as a maze object
	function cannotPlaceObject(i,j) {
	    if ( GRID1[i][j] == GRID_WALL)
			return true;
		if ( GRID1[i][j] == GRID_MAZE) 
			return true;

		if ( GRID2[i][j] == GRID_WALL) 
			return true;
		if ( GRID2[i][j] == GRID_MAZE && agentFloor) 
			return true;

		if ( GRID3[i][j] == GRID_WALL) 
			return true;
		if ( GRID3[i][j] == GRID_MAZE && agentFloor) 
			return true;

		return false;
	}

	// checks to see if Bugs Bunny is standing on a Burrow object and can move down to the next floor
	function canBurrow ()	{
		if ( b1i == ai && b1j == aj && agentFloor == 1950) return true;		// fixed objects
		if ( b2i == ai && b2j == aj  && agentFloor == 950) return true;
		
		return false;
	} 

	// translates objects to Three.js coordinates
	function translate ( x ) {
		return ( x - ( MAXPOS/2 ) );
	}


	// initialises the skybox.		credit:	 http://korzonrocknet.deviantart.com/art/Skybox-446554846
	function initSkybox() {
		var materialArray = [
			( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/seansinnott/skybox_right.jpg" ), side: THREE.BackSide } ) ),
			( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/seansinnott/skybox_left.jpg" ), side: THREE.BackSide } ) ),
			( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/seansinnott/skybox_top.jpg" ), side: THREE.BackSide } ) ),
			( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/seansinnott/skybox_bottom.jpg" ), side: THREE.BackSide } ) ),
			( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/seansinnott/skybox_near.jpg" ), side: THREE.BackSide } ) ),
			( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/seansinnott/skybox_far.jpg" ), side: THREE.BackSide } ) ),
		];

		var skyGeometry = new THREE.CubeGeometry ( SKYBOXCONST, SKYBOXCONST, SKYBOXCONST );	
		var skyMaterial = new THREE.MeshFaceMaterial ( materialArray );
		var theskybox = new THREE.Mesh ( skyGeometry, skyMaterial );
		threeworld.scene.add( theskybox );
	}

	//-------------------- START OF LOAD TEXTURES & OBJECTS --------------------//
	function loadTextures() {
	    
		// load in Bugs Bunny object.		credit:  https://www.models-resource.com/wii/looneytunesacmearsenal/model/9874/
	    var m = new THREE.MTLLoader();
	    m.setTexturePath ( "/uploads/seansinnott/" );
	    m.setPath        ( "/uploads/seansinnott/" );
	    m.load( "bugs.mtl", function( materials ) {
 
    		materials.preload();
    		var o = new THREE.OBJLoader();
    		o.setMaterials ( materials );
    		o.setPath ( "/uploads/seansinnott/" );
    		o.load( "bugs.obj", function ( object ) {
    			addagent ( object );
		    } );
	    } );
	    
		// load in Yosemite Sam object.			credit:  https://www.models-resource.com/wii/looneytunesacmearsenal/model/15925/
	    var n = new THREE.MTLLoader();
	    n.setTexturePath ( "/uploads/seansinnott/" );
	    n.setPath        ( "/uploads/seansinnott/" );
	    n.load( "yosemite.mtl", function( materials ) {
 
    		materials.preload();
    		var p = new THREE.OBJLoader();
    		p.setMaterials ( materials );
    		p.setPath ( "/uploads/seansinnott/" );
    		p.load( "yosemite.obj", function ( object ) {
    			addenemy ( object );
		    } );
	    } );
	    
		// load in carrot object.		credit:  https://www.models-resource.com/custom_edited/mariocustoms/model/8638/
	    var s = new THREE.MTLLoader();
	    s.setTexturePath ( "/uploads/seansinnott/" );
	    s.setPath        ( "/uploads/seansinnott/" );
	    s.load( "carrot.mtl", function( materials ) {
 
    		materials.preload();
    		var t = new THREE.OBJLoader();
    		t.setMaterials ( materials );
    		t.setPath ( "/uploads/seansinnott/" );
    		t.load( "carrot.obj", function ( object ) {
    			addcarrot ( object );
		    } );
	    } );
	    
		// load in both burrow objects.		credit:  https://www.models-resource.com/pc_computer/harrypottertheprisonerofazkaban/model/14837/
	    var x = new THREE.MTLLoader();
	    x.setTexturePath ( "/uploads/seansinnott/" );
	    x.setPath        ( "/uploads/seansinnott/" );
	    x.load( "burrow.mtl", function( materials ) {
 
    		materials.preload();
    		var y = new THREE.OBJLoader();
    		y.setMaterials ( materials );
    		y.setPath ( "/uploads/seansinnott/" );
    		y.load( "burrow.obj", function ( object ) {
    			addmidburrow ( object );
		    } );
	    } );	    
	    var q = new THREE.MTLLoader();
	    q.setTexturePath ( "/uploads/seansinnott/" );
	    q.setPath        ( "/uploads/seansinnott/" );
	    q.load( "burrow.mtl", function( materials ) {
 
    		materials.preload();
    		var r = new THREE.OBJLoader();
    		r.setMaterials ( materials );
    		r.setPath ( "/uploads/seansinnott/" );
    		r.load( "burrow.obj", function ( object ) {
    			addtopburrow ( object );
		    } );
	    } );
	    	    
	    // load in wall texture.		credit: https://freestocktextures.com/texture/wood-weathered-plank,421.html
		var loader1 = new THREE.TextureLoader();
		loader1.load ( '/uploads/seansinnott/fence.jpg',		function ( thetexture ) {			 
			thetexture.minFilter = THREE.LinearFilter;
			paintWalls ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
		} ); 

		// load in maze texture. 		credit:  https://freestocktextures.com/texture/leaves-plant-nature,686.html
		var loader2 = new THREE.TextureLoader();
		loader2.load ( '/uploads/seansinnott/bush.jpg',		function ( thetexture ) {			 
			thetexture.minFilter = THREE.LinearFilter;
			paintMaze ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
		} ); 

		// load in ground texture, used same ground as in skybox above.		credit:  http://korzonrocknet.deviantart.com/art/Skybox-446554846
		var loader3 = new THREE.TextureLoader();
		loader3.load ( '/uploads/seansinnott/skybox_bottom.jpg',		function ( thetexture ) {			 
			thetexture.minFilter = THREE.LinearFilter;
			paintGround ( new THREE.MeshBasicMaterial( { map: thetexture } ) );
		} );
	}
	//-------------------- END OF LOAD TEXTURES & OBJECTS --------------------//
	
	// add Bugs Bunny object into world
	function addagent ( object ) {
    	object.scale.multiplyScalar ( 20 );    	   
    	theagent = object;
    	theagent.rotateY(2 * (Math.PI / 2));
    	threeworld.scene.add( theagent ); 
    }
    
	// add Yosemite Sam object into world
    function addenemy ( object ) {
    	object.scale.multiplyScalar ( 30 );    	   
    	theenemy = object;
    	theenemy.rotateY(2 * (Math.PI / 2));
    	threeworld.scene.add( theenemy ); 
    }

	// add Carrot object into world
    function addcarrot ( object ) {
    	object.scale.multiplyScalar ( 2 );    	   
    	thecarrot = object;
    	thecarrot.rotateY(2 * (Math.PI / 2));
    	threeworld.scene.add( thecarrot ); 
    }
    
	// add Top Burrow object into world
    function addtopburrow ( object ) {
    	object.scale.multiplyScalar ( 1 );    	   
    	topfloorburrow = object;
    	threeworld.scene.add( topfloorburrow ); 

    }
    
	// add Middle Burrow object into world
    function addmidburrow ( object ) {
    	object.scale.multiplyScalar ( 1 );    	   
    	midfloorburrow = object;
    	threeworld.scene.add( midfloorburrow ); 
    }
    

	//-------------------- START OF WALL FUNCTIONS --------------------//
	
	//initialise the walls logically
	function initLogicalWalls() {
		for (var i = 0; i < GRIDSIZE ; i++) 
			for (var j = 0; j < GRIDSIZE ; j++) 
				if ( ( i==0 ) || ( i==GRIDSIZE-1 ) || ( j==0 ) || ( j==GRIDSIZE-1 ) ) {
					GRID1[i][j] = GRID_WALL ;
					GRID2[i][j] = GRID_WALL ;
					GRID3[i][j] = GRID_WALL ;
				}
	}

	// initialise the walls in terms of Three.js
	function initThreeWalls() {
		var t = 0;
		for (var i = 0; i < GRIDSIZE ; i++) {
			for (var j = 0; j < GRIDSIZE ; j++) {
				if ( GRID1[i][j] == GRID_WALL ) {
					var shape    = new THREE.BoxGeometry( SQUARESIZE, SQUARESIZE, SQUARESIZE );			 
					var thecube1  = new THREE.Mesh( shape );
					var thecube2  = new THREE.Mesh( shape );
					var thecube3  = new THREE.Mesh( shape );

					thecube1.material.color.setHex( BLANKCOLOR  );	
					thecube2.material.color.setHex( BLANKCOLOR  );
					thecube3.material.color.setHex( BLANKCOLOR  );

					thecube1.position.x = translate ( i * SQUARESIZE );
					thecube1.position.z = translate ( j * SQUARESIZE );   	
					thecube1.position.y =  0;

					thecube2.position.x = translate ( i * SQUARESIZE );
					thecube2.position.z = translate ( j * SQUARESIZE );   	
					thecube2.position.y =  1000;

					thecube3.position.x = translate ( i * SQUARESIZE );
					thecube3.position.z = translate ( j * SQUARESIZE );   	
					thecube3.position.y =  2000;

					threeworld.scene.add(thecube1);
					threeworld.scene.add(thecube2);
					threeworld.scene.add(thecube3);

					FLOOR_1_WALLS[t] = thecube1;
					FLOOR_2_WALLS[t] = thecube2;
					FLOOR_3_WALLS[t] = thecube3;
					t++;
				}
			}
		}
	}

	// paint the walls
	function paintWalls ( material ) {
		for ( var i = 0; i < FLOOR_1_WALLS.length; i++ ) { 
			if ( FLOOR_1_WALLS[i] ) {
				FLOOR_1_WALLS[i].material = material;
				FLOOR_2_WALLS[i].material = material;
				FLOOR_3_WALLS[i].material = material;
			}
		}
	}
	//-------------------- END OF WALL FUNCTIONS --------------------//
	
	
	//-------------------- START OF GROUND FUNCTIONS --------------------//
	
	//initialise the ground logically
	function initLogicalGround() {
		for (var i = 0; i < GRIDSIZE ; i++) 
			for (var j = 0; j < GRIDSIZE ; j++) 
				UNDERSIDE[i][j] = GRID_GROUND;   
	}

	// initialise the ground in terms of Three.js
	function initThreeGround() {
		var t = 0;
		for (var i = 0; i < GRIDSIZE ; i++) {
			for (var j = 0; j < GRIDSIZE ; j++) {
				if ( UNDERSIDE[i][j] == GRID_GROUND ) {
					var shape    = new THREE.BoxGeometry( SQUARESIZE, SQUARESIZE, SQUARESIZE );			 
					var thecube1  = new THREE.Mesh( shape );
					var thecube2  = new THREE.Mesh( shape );
					var thecube3  = new THREE.Mesh( shape );

					thecube1.material.color.setHex( BLANKCOLOR  );	
					thecube2.material.color.setHex( BLANKCOLOR  );
					thecube3.material.color.setHex( BLANKCOLOR  );

					thecube1.position.x = translate ( i * SQUARESIZE );
					thecube1.position.z = translate ( j * SQUARESIZE );   	
					thecube1.position.y =  -100;

					thecube2.position.x = translate ( i * SQUARESIZE );
					thecube2.position.z = translate ( j * SQUARESIZE );   	
					thecube2.position.y =  900;

					thecube3.position.x = translate ( i * SQUARESIZE );
					thecube3.position.z = translate ( j * SQUARESIZE );   	
					thecube3.position.y =  1900;

					threeworld.scene.add(thecube1);
					threeworld.scene.add(thecube2);
					threeworld.scene.add(thecube3);

					FLOOR_1_GROUND[t] = thecube1;
					FLOOR_2_GROUND[t] = thecube2;
					FLOOR_3_GROUND[t] = thecube3;
					t++;
				}
			}
		}
	}

	// paint the ground
	function paintGround ( material ) {
		for ( var i = 0; i < FLOOR_1_GROUND.length; i++ ) { 
			if ( FLOOR_1_GROUND[i] )  FLOOR_1_GROUND[i].material = material;
			if ( FLOOR_2_GROUND[i] )  FLOOR_2_GROUND[i].material = material;
			if ( FLOOR_3_GROUND[i] )  FLOOR_3_GROUND[i].material = material;
		}
	}
	//-------------------- END OF GROUND FUNCTIONS --------------------//
	
	
	//-------------------- START OF MAZE FUNCTIONS --------------------//
	
	// initialise the maze logically, all three floors have a different randomised maze
	function initLogicalMaze() {
		for ( var c=1 ; c <= NOBOXES ; c++ ) {
			var i = randomintAtoB(1,GRIDSIZE-2);
			var j = randomintAtoB(1,GRIDSIZE-2);
			GRID1[i][j] = GRID_MAZE ;
			
			var m = randomintAtoB(1,GRIDSIZE-2);
			var n = randomintAtoB(1,GRIDSIZE-2);
			GRID2[m][n] = GRID_MAZE ;
			
			var t = randomintAtoB(1,GRIDSIZE-2);
			var u = randomintAtoB(1,GRIDSIZE-2);
			GRID3[t][u] = GRID_MAZE ;
		}
	}

	// initialise the maze in terms of Three.js, need separate for loops to ensure that all maze objects on all floors can be recognised and painted etc.
	function initThreeMaze() {
		var t = 0;
		var u = 0;
		var v = 0;
		for (var i = 0; i < GRIDSIZE ; i++) {
			for (var j = 0; j < GRIDSIZE ; j++) {
				if ( GRID1[i][j] == GRID_MAZE ) {
					var shape    = new THREE.BoxGeometry( SQUARESIZE, SQUARESIZE, SQUARESIZE );			 
					var thecube1  = new THREE.Mesh( shape );
					thecube1.material.color.setHex( BLANKCOLOR  );	
					thecube1.position.x = translate ( i * SQUARESIZE );
					thecube1.position.z = translate ( j * SQUARESIZE );   	
					thecube1.position.y =  0;
					threeworld.scene.add(thecube1);
					FLOOR_1_MAZE[t] = thecube1;
					t++; 
				}
	        }
        }
		for (var m = 0; m < GRIDSIZE ; m++) {
			for (var n = 0; n < GRIDSIZE ; n++) {	   
				if ( GRID2[m][n] == GRID_MAZE ) {
					var shape    = new THREE.BoxGeometry( SQUARESIZE, SQUARESIZE, SQUARESIZE );			 
					var thecube2  = new THREE.Mesh( shape );
					thecube2.material.color.setHex( BLANKCOLOR  );	
					thecube2.position.x = translate ( m * SQUARESIZE );   	
					thecube2.position.z = translate ( n * SQUARESIZE );   	
					thecube2.position.y =  1000;
					threeworld.scene.add(thecube2);
					FLOOR_2_MAZE[u] = thecube2;
					u++; 
				}
			}
        }
		for (var x = 0; x < GRIDSIZE ; x++) {
			for (var y = 0; y < GRIDSIZE ; y++) {	   
				if ( GRID3[x][y] == GRID_MAZE ) {
					var shape    = new THREE.BoxGeometry( SQUARESIZE, SQUARESIZE, SQUARESIZE );			 
					var thecube3  = new THREE.Mesh( shape );
					thecube3.material.color.setHex( BLANKCOLOR  );	
					thecube3.position.x = translate ( x * SQUARESIZE );
					thecube3.position.z = translate ( y * SQUARESIZE );   	
					thecube3.position.y =  2000;
					threeworld.scene.add(thecube3);
					FLOOR_3_MAZE[v] = thecube3;
					v++; 
				}
			}
        }
	}

	// paint the maze
	function paintMaze ( material )	{
		for ( var i = 0; i < FLOOR_1_MAZE.length; i++ ) { 
			if ( FLOOR_1_MAZE[i] )  FLOOR_1_MAZE[i].material = material;
			if ( FLOOR_2_MAZE[i] )  FLOOR_2_MAZE[i].material = material;
			if ( FLOOR_3_MAZE[i] )  FLOOR_3_MAZE[i].material = material;
		}
	}
	//-------------------- END OF MAZE FUNCTIONS --------------------//
	

	//-------------------- START OF AGENT (BUGS BUNNY) FUNCTIONS --------------------//

	// used to draw Bugs Bunny into the world at a certain position
	function drawAgent() {
	    if (theagent) {
    		var x = translate ( ai * SQUARESIZE );   	
    		var z = translate ( aj * SQUARESIZE );   	
    		var y =  agentFloor;
    
    		theagent.position.x = x;
    		theagent.position.y = y;
    		theagent.position.z = z;
            threeworld.lookat.copy ( theagent.position );
	    }
		
	}
	
	// initialise Bugs Bunny logically at a random position
	function initLogicalAgent() {
		var i, j;
		do {
			i = randomintAtoB(1,GRIDSIZE-2);
			j = randomintAtoB(1,GRIDSIZE-2);
		} while ( cannotPlaceObject(i,j) );

		ai = i;
		aj = j;
		currentAgentPosition = [ai,aj];			// remember the position Bugs is currently at
	}
    
	// moves Bugs Bunny around the world and also rotates him to face the direction that he is currently moving in
	function moveLogicalAgent( a ) { 
		var i = ai;
		var j = aj;	
		
		// if moving left, face the correct direction and move
		if ( a == ACTION_LEFT ) { 
		    if(bugsLookingForward) {
		        theagent.rotateY(1 * (Math.PI / 2));
		        bugsLookingLeft = true;
		        bugsLookingForward = false;
		    }
		    else if(bugsLookingBack) {
		        theagent.rotateY(3 * (Math.PI / 2));
		        bugsLookingLeft = true;
		        bugsLookingBack = false;
		    }
		    else if(bugsLookingRight) {
		        theagent.rotateY(2 * (Math.PI / 2));
		        bugsLookingLeft = true;
		        bugsLookingRight = false;
		    }
			i--;
		}
		
		// if moving right, face the correct direction and move
		else if ( a == ACTION_RIGHT ) {
		    if(bugsLookingForward) {
		        theagent.rotateY(3 * (Math.PI / 2));
		        bugsLookingRight = true;
		        bugsLookingForward = false;
		    }
		    else if(bugsLookingBack) {
		        theagent.rotateY(1 * (Math.PI / 2));
		        bugsLookingRight = true;
		        bugsLookingBack = false;
		    }
		    else if(bugsLookingLeft) {
		        theagent.rotateY(2 * (Math.PI / 2));
		        bugsLookingRight = true;
		        bugsLookingLeft = false;
		    }
			i++;
		}
		
		// if moving forward, face the correct direction and move
		else if ( a == ACTION_UP ) {
		    if(bugsLookingRight) {
		        theagent.rotateY(3 * (Math.PI / 2));
		        bugsLookingBack = true;
		        bugsLookingRight = false;
		    }
		    else if(bugsLookingForward) {
		        theagent.rotateY(2 * (Math.PI / 2));
		        bugsLookingBack = true;
		        bugsLookingForward = false;
		    }
		    else if(bugsLookingLeft) {
		        theagent.rotateY(1 * (Math.PI / 2));
		        bugsLookingBack = true;
		        bugsLookingLeft = false;
		    }
		    
			j++;
		}
		
		// if moving backward, face the correct direction and move
		else if ( a == ACTION_DOWN ) {
		    if(bugsLookingRight) {
		        theagent.rotateY(1 * (Math.PI / 2));
		        bugsLookingForward = true;
		        bugsLookingRight = false;
		    }
		    else if(bugsLookingBack) {
		        theagent.rotateY(2 * (Math.PI / 2));
		        bugsLookingForward = true;
		        bugsLookingBack = false;
		    }
		    else if(bugsLookingLeft) {
		        theagent.rotateY(3 * (Math.PI / 2));
		        bugsLookingForward = true;
		        bugsLookingLeft = false;
		    }
			j--;
		}

		if ( ! occupied(i,j) ) { 		// as long as the desired position isn't occupied
			// set new position for Bugs
			ai = i;
			aj = j;
			
			// remember the position
			currentAgentPosition = [ai,aj];
			
			// set the new camera position to follow Bugs
			ci = i;
			cj = j+4;
		}
	}

	// move Bugs Bunny down to the next floor through a burrow, and make Yosemite Sam follow him to the next floor
	function moveAgentDown() {
		if(agentFloor != -50 && canBurrow()) { 			// as long as Bugs is standing on a burrow and isn't on the bottom floor where there is nowhere else to burrow to
			agentFloor -= 1000;
			drawAgent(); 			// draw Bugs on the next floor down
			playBugsBurrowing(); 			// play burrowing sound effect
			moveEnemyDown(); 			// move Yosemite Sam down too
		}
	}
	//-------------------- END OF AGENT (BUGS BUNNY) FUNCTIONS --------------------//


	//-------------------- START OF ENEMY (YOSEMITE SAM) FUNCTIONS --------------------//

	// used to draw Yosemite Sam into the world at a certain position
	function drawEnemy() {
	    if (theenemy) {
    		var x = translate ( ei * SQUARESIZE );   	
    		var z = translate ( ej * SQUARESIZE );   	
    		var y =  enemyFloor;	
    
    		theenemy.position.x = x;
    		theenemy.position.y = y;
    		theenemy.position.z = z;
	    }
		  
	}
	
	//initialise Yosemite Sam logically at a certain position
	function initLogicalEnemy() {
		var i, j;
		do {
			i = randomintAtoB(1,GRIDSIZE-2);
			j = randomintAtoB(1,GRIDSIZE-2);
		} while ( cannotPlaceObject(i,j) );

		ei = i;
		ej = j;
		currentEnemyPosition = [ei,ej];			// remember the position Yosemite is currently at
	}


	//-------------------- START OF A-STAR ALGORITHM PATHFINDING --------------------//
	// credit for A-Star Algorithm:			mainly 2d version here:		http://buildnewgames.com/astar/	
	//										another 2d version here:	http://www.briangrinstead.com/blog/astar-search-algorithm-in-javascript
	//										this tutorial:				http://www.redblobgames.com/pathfinding/a-star/introduction.html
	//										and this video:				https://www.youtube.com/watch?v=-L-WgKMFuhE
	
	
	function findPath (startingPosition, finishingPosition) {
		var worldSize = GRIDSIZE * GRIDSIZE;	
		
		// Manhattan Distance is the distance between two points measured along axes at right angles, so no diagonal movement allowed
		function manhattanDistance(Point,Goal) {
			return Math.abs(Point.x - Goal.x) + Math.abs(Point.y - Goal.y);		
		}
		
		var distanceFunction = manhattanDistance;
		
		// this function returns an array of every available empty position North, South, East or West of where you are now
		function Neighbours(x,y) {
			var N = y-1;
			var S = y+1;
			var E = x+1;
			var W = x-1;
			myN = N > -1 && !occupied(x,N);
			myS = S < GRIDSIZE && !occupied(x,S);
			myE = E < GRIDSIZE && !occupied(E,y);
			myW = W > -1 && !occupied(W,y);
			result = [];
			if(myN)
				result.push({x:x, y:N});
			if(myE)
				result.push({x:E, y:y});
			if(myS)
				result.push({x:x, y:S});
			if(myW)
				result.push({x:W, y:y});
			
			return result;
		}
		// this function returns a node object that contains possible next routes and their respective costs
		function Node(Parent, Point) {
			var newNode = {			
				Parent:Parent,								// pointer to another Node object			
				value:Point.x + (Point.y * GRIDSIZE),		// array index of this Node			
				x:Point.x,									// the x coordinates of this Node
				y:Point.y,									// the y coordinates of this node			
				f:0,										// f is the cost to get to this Node from Yosemite's current position			
				g:0											// g is the cost to get from this Node to Bugs' curent position
			};

			return newNode;
		}
		
		// this function finds the best path from Yosemite Sam's position to Bugs Bunny's position using the A* Algorithm
		function calculateBestPath() {
			var	pathStart = Node(null, {x:startingPosition[0], y:startingPosition[1]}); // a node from the start coordinates
			var pathEnd = Node(null, {x:finishingPosition[0], y:finishingPosition[1]}); // a node from the end coordinates
					
			var AStar = new Array(worldSize);			// an array that will contain all positions in the world
			var Open = [pathStart];			// a list of currently open nodes
			var Closed = [];			// a list of closed nodes
			var result = []; 			// the array that will be returned
			var myNeighbours;			// a reference to a node that is nearby
			var currentNode;			// a reference to a node that we are now considering taking
			var currentPath;			// a reference to a node that starts the current path we are considering
			
			// temporary variables used in the calculations
			var length, max, min, a, b;
			
			while(length = Open.length) {				// iterate through the open list until there is nothing left		    
				max = worldSize;
				min = -1;
				
				// get the max and min
				for(a = 0; a < length; a++) {
					if(Open[a].f < max) {
						max = Open[a].f;
						min = a;
					}
				}
				
				currentNode = Open.splice(min, 1)[0];			// remove the next node from the open array and put it in currentNode
				
				// is it the destination node?
				if( currentNode.value === pathEnd.value ) {			// if we have reached our destination node (Bugs Bunny's position)
					currentPath = currentNode;
					do {
						result.push([currentPath.x, currentPath.y]);
					} while (currentPath = currentPath.Parent);
					AStar = Closed = Open = [];			// clear the arrays
					result.reverse();			// reverse the result so as not to return a list from finish to start instead of start to finish
				}
				else {			// otherwise, it was not our destination			    
					myNeighbours = Neighbours(currentNode.x, currentNode.y);				// find which nearby nodes are available to move to
					for(a = 0, b = myNeighbours.length; a < b; a++) {				// test each neighbouring node that hasn't been tested already				
						currentPath = Node(currentNode, myNeighbours[a]);				// add to the current path we are considering
						if (!AStar[currentPath.value]) {
							currentPath.g = currentNode.g + distanceFunction(myNeighbours[a], currentNode);			// cost of this current route so far
							currentPath.f = currentPath.g + distanceFunction(myNeighbours[a], pathEnd);			// cost of the current route all the way to the destination (Bugs' position)
							Open.push(currentPath);			// remember this new path by placing it in the open array
							AStar[currentPath.value] = true;			// mark this node as having been visited already so as not to have to check it again
						}
					}
				}
			} // keep iterating while open is not empty
			return result;
		}
		return calculateBestPath();
	}
	//-------------------- END OF A-STAR ALGORITHM PATHFINDING --------------------//

	// moves Yosemite Sam around the world chasing Bugs Bunny according to the A* Pathfinding Algorithm
	// and also rotates him to face the direction that he is currently moving in
    function findBestPathToAgent() {
        var newPathArray = [];
        var i, j;
        newPathArray = (findPath(currentEnemyPosition,currentAgentPosition)); 			// find the best path to Bugs' position and store it in newPathArray
        
		// store the first best possible move to make to reach bugs in the i and j coordinates
		i = newPathArray[1][0];
        j = newPathArray[1][1];
        
		// if moving left, face the correct direction and move
        if ( i < ei && j == ej ) { 
		    if(samLookingForward) {
		        theenemy.rotateY(1 * (Math.PI / 2));
		        samLookingLeft = true;
		        samLookingForward = false;
		    }
		    else if(samLookingBack) {
		        theenemy.rotateY(3 * (Math.PI / 2));
		        samLookingLeft = true;
		        samLookingBack = false;
		    }
		    else if(samLookingRight) {
		        theenemy.rotateY(2 * (Math.PI / 2));
		        samLookingLeft = true;
		        samLookingRight = false;
		    }
		}
		
		// if moving right, face the correct direction and move
		else if ( i > ei && j == ej ) {
		    if(samLookingForward) {
		        theenemy.rotateY(3 * (Math.PI / 2));
		        samLookingRight = true;
		        samLookingForward = false;
		    }
		    else if(samLookingBack) {
		        theenemy.rotateY(1 * (Math.PI / 2));
		        samLookingRight = true;
		        samLookingBack = false;
		    }
		    else if(samLookingLeft) {
		        theenemy.rotateY(2 * (Math.PI / 2));
		        samLookingRight = true;
		        samLookingLeft = false;
		    }
		}
		
		// if moving forward, face the correct direction and move
		else if ( i == ei && j > ej ) {
		    if(samLookingRight) {
		        theenemy.rotateY(3 * (Math.PI / 2));
		        samLookingBack = true;
		        samLookingRight = false;
		    }
		    else if(samLookingForward) {
		        theenemy.rotateY(2 * (Math.PI / 2));
		        samLookingBack = true;
		        samLookingForward = false;
		    }
		    else if(samLookingLeft) {
		        theenemy.rotateY(1 * (Math.PI / 2));
		        samLookingBack = true;
		        samLookingLeft = false;
		    }
		}
		
		// if moving backward, face the correct direction and move
		else if ( i == ei && j < ej ) {
		    if(samLookingRight) {
		        theenemy.rotateY(1 * (Math.PI / 2));
		        samLookingForward = true;
		        samLookingRight = false;
		    }
		    else if(samLookingBack) {
		        theenemy.rotateY(2 * (Math.PI / 2));
		        samLookingForward = true;
		        samLookingBack = false;
		    }
		    else if(samLookingLeft) {
		        theenemy.rotateY(3 * (Math.PI / 2));
		        samLookingForward = true;
		        samLookingLeft = false;
		    }

		}
        
		// set new position for Yosemite Sam
        ei = i;
        ej = j;		
		
        currentEnemyPosition = [ei,ej];			// remember the position
    }

	// move Yosemite Sam down to the next floor	to follow Bugs as long as he isn't already on the bottom floor
	function moveEnemyDown() {
		if(enemyFloor != -50) {
			
			// pick a random position to start off in on the next floor down
		    var i, j;
		    do {
			i = randomintAtoB(1,GRIDSIZE-2);
			j = randomintAtoB(1,GRIDSIZE-2);
		    } while ( cannotPlaceObject(i,j) );  	  // search for empty square 

			// set the new position
	    	ei = i;
		    ej = j;
			
			// remember the position
		    currentEnemyPosition = [ei,ej];
			
			// set the new floor
			enemyFloor -= 1000;
			
			// draw Yosemite Sam in his new position on the next floor
			drawEnemy();
		}
	}
	//-------------------- END OF ENEMY (YOSEMITE SAM) FUNCTIONS --------------------//


	//-------------------- CAMERA FUNCTIONS --------------------//
	
	// draw the camera object that will follow Bugs Bunny
	function drawCamera() {
		var x = translate ( ci * SQUARESIZE );   	
		var z = translate ( cj * SQUARESIZE );   	
		var y =  agentFloor+350;			// set the camera above Bugs Bunny
		
		thecamera.position.x = x;
		thecamera.position.y = y;
		thecamera.position.z = z;
		threeworld.scene.add(thecamera);
        
		threeworld.follow.copy ( thecamera.position );
	}

	// initialise the camera to be 4 steps behind Bugs
    function initLogicalCamera() {    
    		ci = ai;
    		cj = aj+4;
    }
    	
	// initialise the camera in terms of Three.js to avoid errors, basically just make it an invisible box and draw it
	function initThreeCamera() {
		var shape    = new THREE.BoxGeometry( 0, 0, 0 );			 
		thecamera = new THREE.Mesh( shape );
	
		drawCamera();    	    
	}
	
	
	//-------------------- CARROT FUNCTIONS --------------------//
	
	// draw the carrot object on the bottom floor
	function drawCarrot() {
	    if (thecarrot) {
    		var x = translate ( ti * SQUARESIZE );   	
    		var z = translate ( tj * SQUARESIZE );   	
    		var y =  -50;	
    
    		thecarrot.position.x = x;
    		thecarrot.position.y = y;
    		thecarrot.position.z = z;
	    }		  
	}
	
	//initialise the carrot logically in a random position
    function initLogicalCarrot() {
		var i, j;
		do {
			i = randomintAtoB(1,GRIDSIZE-2);
			j = randomintAtoB(1,GRIDSIZE-2);
		} while ( cannotPlaceObject(i,j) );
		
		// set the position of the carrot
		ti = i;
		tj = j;
	}
	
	
	//-------------------- BURROW FUNCTIONS --------------------//
	
	// draw the burrow on the top floor
	function drawTopBurrow() {
	    if (topfloorburrow) {
    		var x = translate ( b1i * SQUARESIZE );   	
    		var z = translate ( b1j * SQUARESIZE );   	
    		var y =  1950;	
    
    		topfloorburrow.position.x = x;
    		topfloorburrow.position.y = y;
    		topfloorburrow.position.z = z;
	    }
    }
    
	// draw the burrow on the middle floor
    function drawMidBurrow() {
	    if (midfloorburrow) {
    		var x = translate ( b2i * SQUARESIZE );   	
    		var z = translate ( b2j * SQUARESIZE );   	
    		var y =  950;	
    
    		midfloorburrow.position.x = x;
    		midfloorburrow.position.y = y;
    		midfloorburrow.position.z = z;
	    }
	}
	
	//initialise the top burrow logically in a random position
    function initLogicalTopBurrow() {
		var i, j;
		do {
			i = randomintAtoB(1,GRIDSIZE-2);
			j = randomintAtoB(1,GRIDSIZE-2);
		} while ( cannotPlaceObject(i,j) );
		
		// set the position of the top burrow
		b1i = i;
		b1j = j;
	}

	//initialise the middle burrow logically in a random position
    function initLogicalMidBurrow() {
		var i, j;
		do {
			i = randomintAtoB(1,GRIDSIZE-2);
			j = randomintAtoB(1,GRIDSIZE-2);
		} while ( cannotPlaceObject(i,j) );
		
		// set the position of the middle burrow
		b2i = i;
		b2j = j;
	}
	

	
	// this function handles user controls to move Bugs around the world
	function keyHandler(e)	{	
		if (e.keyCode == 37)
			moveLogicalAgent ( ACTION_LEFT );
		if (e.keyCode == 38)
			moveLogicalAgent ( ACTION_DOWN );
		if (e.keyCode == 39)
			moveLogicalAgent ( ACTION_RIGHT );
		if (e.keyCode == 40)
			moveLogicalAgent ( ACTION_UP );
		if (e.keyCode == 83)
			moveAgentDown();
	}
	
	
	//-------------------- GENERAL GAME FUNCTIONS, RULES, INTERFACE ETC. --------------------//	

	// checks if Yosemite Sam has caught Bugs
	function bugsCaught() {
		return (ai == ei && aj == ej);		
	}
	// checks if Bugs has reached the carrot
	function reachedCarrot() {
	    return (ai == ti && aj == tj);
	}
	
	// returns the floor that bugs is currently on
	function getBugsFloor() {
	    var printFloor;
		if (agentFloor == 1950) {
		    printFloor = 3;
		}
		else if (agentFloor == 950) {
		    printFloor = 2;
		}
		else {
		    printFloor = 1;
		}
		return printFloor;
	}
	
	// this is called before anyone has moved on this step, agent has just proposed an action
	function updateStatusBefore(a) {
		// show the current step and floor
		var status = " <b> Step:  " + step + " Floor: <b> " + getBugsFloor(); 
		$("#user_span3").html( status );
	}

	// new state after both have moved
	function updateStatusAfter() {
		var status = "..... Click 'Move With' for best gaming experience"; 
		$("#user_span4").html( status );
		var status = "..... Burrow to Floor 1 and Get the Carrot without being Caught to win ..... Press 's' to burrow"; 
		$("#user_span5").html( status );
	}
	
	this.endCondition;			// if set to true the run will end. 

	this.newRun = function() {
		this.endCondition = false;
		step = 0;
        
		// whether graphical run or not, initialise all of these:
		initGrid();
		initUnderside();
		initLogicalWalls(); 
		initLogicalMaze();
		initLogicalAgent();
		initLogicalGround();
		initLogicalEnemy();
		initLogicalCamera();
		initLogicalCarrot();
		initLogicalTopBurrow();
		initLogicalMidBurrow();
		
		// for graphical runs initialise these:
		if ( true  ) {
			threeworld.init3d ( STARTRADIUSCONST, MAXRADIUSCONST, SKYCOLOR  );
            
			// add in ambient light to the world
            var ambient = new THREE.AmbientLight();
    		threeworld.scene.add( ambient );
	   	    var thelight = new THREE.DirectionalLight ( LIGHTCOLOR, 3 );
	  	    thelight.position.set ( STARTRADIUSCONST, STARTRADIUSCONST, STARTRADIUSCONST );
	   	    threeworld.scene.add(thelight);
	   	    
			initSkybox();
			initMusic();
			initThreeWalls(); 
			initThreeMaze();
			initThreeGround();
			initThreeCamera();			
			loadTextures();	

			document.onkeydown = keyHandler;	 
		}
	};

	this.getState = function() {
		var x = [ ai, aj, ei, ej ];
		return ( x );  
	};


this.nextStep = function()		 
{
 var a = 4;

		step++;
		if ( true  )
			updateStatusBefore(a);			// show status line before moves 
		moveLogicalAgent(a);
		if ( ( step % 3 ) == 0 )		// slow the enemy down to every 3rd step the user takes
			findBestPathToAgent();			// chase Bugs

		if ( true  ) {
			
			// draw all of these:
			drawAgent();
			drawEnemy();
			drawCamera();
			drawCarrot();
			drawTopBurrow();
			drawMidBurrow();
			
			updateStatusAfter();			// show status line after moves  
		}

		// if Bugs Bunny has been caught
		if ( bugsCaught() ) {
			this.endCondition = true;
			if ( true  ) {
				musicPause();			// pause the music
				playBugsLoses();			// play soundbite of Bugs being upset
			}
		}
		
		// if Bugs has reached the carrot
		if ( reachedCarrot() ) {
			this.endCondition = true;
			if ( true  ) {
				musicPause();			// pause the music
				playBugsWins();				// play soundbite of Bugs being happy
			}
		}		
	};

	this.endRun = function() {
		if ( true  ) {
			musicPause();		
			if ( this.endCondition ) {
				
				// if Bugs was caught, print this message to the screen:
			    if( bugsCaught() )
				    $("#user_span6").html( "<br>  &nbsp; <font color=red> <B> You've been caught! You Reached floor " + getBugsFloor() + "</B> </font>");
				
				// if Bugs reached the carrot, print this message to the screen:
			    if( reachedCarrot() )
			        $("#user_span6").html( "<br>  &nbsp; <font color=red> <B> You Got The Carrot! You Win! </B> </font>");
			}
			
			// otherwise, the user ran our of steps, so print this message:
			else
				$("#user_span6").html( "<br> &nbsp; <font color=red> <B> You Ran out of steps! . </B> </font>   "  );
		}
	};
}

//-------------------- END OF WORLD CLASS --------------------//

//-------------------- MUSIC AND SOUND EFFECTS --------------------//

// credit:  http://junglevibe23.net/tracks/barber_of_seville.html
function initMusic() {
	// put music element in one of the spans
	var x = "<audio  id=theaudio  src=/uploads/seansinnott/barberofseville.mp3   autoplay loop> </audio>" ;
	$("#user_span1").html( x );
} 
 
function musicPlay() {
 	document.getElementById('theaudio').play();
}

function musicPause() {
 	document.getElementById('theaudio').pause();
}

// credit:  http://www.nonstick.com/bugs-bunny-sounds/
function playBugsWins() {
 	var x = "<audio    src=/uploads/seansinnott/bugswins.mp3   autoplay  > </audio>";
  	$("#user_span2").html( x );
}

// credit:  http://www.nonstick.com/bugs-bunny-sounds/
function playBugsLoses() {
 	var x = "<audio    src=/uploads/seansinnott/bugsloses.mp3   autoplay  > </audio>";
  	$("#user_span2").html( x );
}

// credit:  http://www.sounddogs.com/previews/2213/mp3/349472_SOUNDDOGS__di.mp3
function playBugsBurrowing() {
 	var x = "<audio    src=/uploads/seansinnott/burrowing.mp3   autoplay  > </audio>";
  	$("#user_span2").html( x );
}