Code viewer for World: Kick Panda - A Star Algorithm
//Student Name : Zhensheng Tan
//Student Number: 19214699


/*[AssignmentComment] 

This is a fancy Chinese style world where a tiger is chasing a panda in the forest of bamboo in Forbidden City! (I don't know why they are there but they exist!)



*/

 /* [AssignmentComment] 
 
 You will read these following metrics on the world, there are explaination later.
 
 OverallOpenSetMin: the minimum openset size of all search runs
 OverallOpenSetMax: the maximum openset size of all search runs
 AverageOpenSet% : all the openset accumulated of all search runs divided by (maze size * search runs), if it's good performnace A*, it should drop very fast. 
 If it's BFS say heuristicModifer = 0, it will be very high.
 
 OverallOpenSetMoveRatioMin: the minimum (openset size / path move) of all search runs
 OverallOpenSetMoveRatioMax: the maximum (openset size / path move) of all search runs
 AverageOpenSetMovieRatio:  all the openset accumulated of all search runs divided by all the path moves accumulated of all search run, e.g. if this is high, it indicates
 the heuristic is busy searching more spaced nodes but the improvement of path move is relatively small, hence the ratio will be high, then we should fine tune to lower this figure. In the BFS, where
 Say heuristicModifer = 0, this will be a very high figure.
 */




// ==== Starter World =========theenemy======================================================================================
// (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.
// ==================================================================================================================


// =============================================================================================
// 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 ======================================================================================== 
// ===================================================================================================================


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.
	

/* [AssignmentComment]
    'heuristicModifier':
    If heuristicModifier = 0, then it's BFS. 
    If heuristicModifier = 1, it's constant heuristic A*.
    If heuristicModifier is (0, 1) and (1, +∞ ) (not including 1 ) it's Weighted A*. 
    e.g. if the heuristic is larger than 1, even though it might be less optimal in each search run, but in overall, it could search less nodes (opensets).
    
    'allowEnemyDiagonalMove': as it is
 */
const heuristicModifier = 0;
const allowEnemyDiagonalMove = true;



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


/* [AssignmentComment]
    These are stats indicator:
    'openset': the number of nodes that has been explored but not close in each search run
    'move': the number of nodes of the selected route in each search run
    Will consider min, max and avg attributes of them later.
 */
var openSetMin = Number.MAX_VALUE;
var openSetMax = Number.MIN_VALUE;
var openSetTotal = 0;
var searchRun = 0;


var openSetMoveRatioMin = Number.MAX_VALUE;
var openSetMoveRatioMax = Number.MIN_VALUE;
var moveTotal = 0;


const show3d = true;						// Switch between 3d and 2d view (both using Three.js) 


/* [AssignmentComment]
    Give credits to these Chinese elements 
 */

//credits: https://pic2.zhimg.com/80/v2-74c0f5a824e76a3e5baf754c28623629_hd.jpg  - Zhihu Ltd. 
const TEXTURE_WALL 	= '/uploads/zhensheng/heuristic_assignment_material_bamboo.jpg' ;
//credits: https://www.thoughtco.com/yin-and-yang-629214  - Thoughtco.com
const TEXTURE_MAZE 	= '/uploads/zhensheng/heuristic_assignment_material_yinyang.png' ;
 
//credits: https://kungfupanda.fandom.com/wiki/Kung_Fu_Panda?file=FightingStylePo.jpg   - Fandom.com
const TEXTURE_AGENT 	= '/uploads/zhensheng/heuristic_assignment_material_panda.png' ;
//credits: https://kungfupanda.fandom.com/wiki/Tai_Lung?file=Tai-Lung-fire-attack.jpg   - Fandom.com
const TEXTURE_ENEMY 	= '/uploads/zhensheng/heuristic_assignment_material_boss.png' ;

 
// credits:
// http://www.dl-sounds.com/royalty-free/defense-line/
// http://soundbible.com/1542-Air-Horn.html 
const MUSIC_BACK  = '/uploads/starter/Defense.Line.mp3' ;
const SOUND_ALARM = '/uploads/starter/air.horn.mp3' ;
	
	
const gridsize = 30;						// number of squares along side of world	 

const NOBOXES =  Math.trunc ( (gridsize * gridsize) / 10 );
		// density of maze - number of internal boxes
		// (bug) use trunc or can get a non-integer 
		
const actualMazeSize = (gridsize -1) * (gridsize -1) - NOBOXES;

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 

/* [AssignmentComment]
    Give credits to these Chinese elements 
 */

//credits to: https://opengameart.org/content/urban-skyboxes
const SKYBOX_ARRAY = [						
                "/uploads/zhensheng/heuristic_assignment_material_forbidden_city_posx.jpg",
                "/uploads/zhensheng/heuristic_assignment_material_forbidden_city_negx.jpg",
                "/uploads/zhensheng/heuristic_assignment_material_forbidden_city_posy.jpg",
                "/uploads/zhensheng/heuristic_assignment_material_forbidden_city_negy.jpg",
                "/uploads/zhensheng/heuristic_assignment_material_forbidden_city_posz.jpg",
                "/uploads/zhensheng/heuristic_assignment_material_forbidden_city_negz.jpg"
];


// ===================================================================================================================
// === End of tweaker's box ==========================================================================================
// ===================================================================================================================


// You will need to be some sort of JavaScript programmer to change things below the 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)


// contents of a grid square

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

var BOXHEIGHT;		// 3d or 2d box height 

var GRID 	= new Array(gridsize);			// can query GRID about whether squares are occupied, will in fact be initialised as a 2D array   

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;


/* [AssignmentComment]
    The temporary A* info of coordinates for each search run for the enemy object.
 */
var chasingGrid;

/* [AssignmentComment]
    Common method / classes starts
*/
// Function to delete element from the array
function removeFromArray(arr, elt) 
{
  // Could use indexOf here instead to be more efficient
  for (var i = arr.length - 1; i >= 0; i--) 
    if (arr[i] == elt) 
      arr.splice(i, 1);
}


function Spot(i, j, chasingGrid) 
{

  // Location
  this.i = i;
  this.j = j;

  // f, g, and h values for A*
  this.f = 0;
  this.g = 0;
  this.h = 0;
  
  this.isWall = (GRID[i][j] === GRID_WALL) || (GRID[i][j] === GRID_MAZE);   //Whether this is not reachable

  // Neighbors
  this.neighbors = [];

  // Where did I come from?
  this.previous = undefined;

  // Figure out who my neighbors are
  this.addNeighbors = function(chasingGrid) 
  {
    var i = this.i;
    var j = this.j;
    
    if (i < gridsize - 1)   this.neighbors.push(chasingGrid[i + 1][j]);
    if (i > 0)          this.neighbors.push(chasingGrid[i - 1][j]);
    if (j < gridsize - 1)   this.neighbors.push(chasingGrid[i][j + 1]);
    if (j > 0)          this.neighbors.push(chasingGrid[i][j - 1]);
    
    if(allowEnemyDiagonalMove) {
        if (i > 0 && j > 0)                 this.neighbors.push(chasingGrid[i - 1][j - 1]);
        if (i < gridsize - 1 && j > 0)          this.neighbors.push(chasingGrid[i + 1][j - 1]);
        if (i > 0 && j < gridsize - 1)          this.neighbors.push(chasingGrid[i - 1][j + 1]);
        if (i < gridsize - 1 && j < gridsize - 1)   this.neighbors.push(chasingGrid[i + 1][j + 1]);
    }
  }
  
}

/* [AssignmentComment]
    Most important part as the A*, if `allowEnemyDiagonalMove` true it is Euclidean distance, else false it is Manhattan distance
*/
function heuristic(a, b) 
{
    if (allowEnemyDiagonalMove) return ( Math.sqrt((a.i - b.i) * (a.i - b.i) +  (a.j - b.j) * (a.j - b.j)) );   
    else return ( Math.abs(a.i - b.i) + Math.abs(a.j - b.j) );
}


	
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] == GRID_WALL ) return true;		// fixed objects	 
 if ( GRID[i][j] == 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;
}

function initScene()		// all file loads have returned 
{
	 var i,j, shape, thecube;
	 
	// set up GRID as 2D array
	 
	 for ( i = 0; i < gridsize ; i++ ) 
		GRID[i] = new Array(gridsize);		 


	// set up walls
	 
	 for ( i = 0; i < gridsize ; i++ ) 
	  for ( j = 0; j < gridsize ; j++ ) 
		if ( ( i==0 ) || ( i==gridsize-1 ) || ( j==0 ) || ( j==gridsize-1 ) )
		{
			GRID[i][j] = GRID_WALL;		 
			shape    = new THREE.BoxGeometry ( squaresize, BOXHEIGHT, squaresize );			 
			thecube  = new THREE.Mesh( shape );
			thecube.material = new THREE.MeshBasicMaterial( { map: wall_texture } );
			
			thecube.position.copy ( translate(i,j) ); 		  	// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 
			ABWorld.scene.add(thecube);
		}
		else 
   			GRID[i][j] = GRID_BLANK;

		
   // 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] = GRID_MAZE ;
		
		shape    = new THREE.BoxGeometry ( squaresize, BOXHEIGHT, squaresize );			 
		thecube  = new THREE.Mesh( shape );
		thecube.material = new THREE.MeshBasicMaterial( { map: maze_texture } );		  

		thecube.position.copy ( translate(i,j) ); 		  	// translate my (i,j) grid coordinates to three.js (x,y,z) coordinates 
		ABWorld.scene.add(thecube);		
	}
	 	 
   
	// 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 );			 
	 theenemy = new THREE.Mesh( shape );
 	 theenemy.material =  new THREE.MeshBasicMaterial( { map: enemy_texture } );
	 ABWorld.scene.add(theenemy);
	 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 );			 
	 theagent = new THREE.Mesh( shape );
	 theagent.material =  new THREE.MeshBasicMaterial( { map: agent_texture } );
	 ABWorld.scene.add(theagent);
	 drawAgent(); 
	 
	 
	 chasingGrid = new Array(gridsize);


/* [AssignmentComment]
    Init the chasing grid coordinates everytime the enemy object will use
 */
  // Making a 2D array
  for (var i = 0; i < gridsize; i++) 
    chasingGrid[i] = new Array(gridsize);

  for (var i = 0; i < gridsize; i++) 
    for (var j = 0; j < gridsize; j++) 
      chasingGrid[i][j] = new Spot(i, j,chasingGrid);

  // All the neighbors
  for (var i = 0; i < gridsize; i++) 
    for (var j = 0; j < gridsize; j++) 
      chasingGrid[i][j].addNeighbors(chasingGrid);


  // 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)
}




// --- take actions -----------------------------------

function moveLogicalEnemy()
{ 

// Open and closed set
var openSet = [];
var closedSet = [];
var openSetCount = 0;

/* [AssignmentComment]
   Start point is the enemy, end point is the agent
 */
var start = chasingGrid[ei][ej];
var end = chasingGrid[ai][aj];

  start.isWall    = false;
  end.isWall      = false;

  // openSet starts with beginning only
  openSet.push(start);
  
  var current;


    // --- begin still searching -----------------------------
  while (openSet.length > 0) 
  {
     /* [AssignmentComment] Try to accumulate the open set size */
     openSetCount++;

    // Best next option
    //var winner = openSet.length - 1;
    var winner = 0;
    
    for (var i = 0; i < openSet.length; i++) 
      if (openSet[i].f < openSet[winner].f) 
        winner = i;
        
    var current = openSet[winner];

    // Did I finish?
    if (current === end) 
    {
      break;
    }

    // Best option moves from openSet to closedSet
    removeFromArray(openSet, current);
    closedSet.push(current);

    // Check all the neighbors
    var neighbors = current.neighbors;
    
    //--- start of for loop -----------
    for (var i = 0; i < neighbors.length; i++) 
    {
      var neighbor = neighbors[i];

      // Valid next spot?
      if (!closedSet.includes(neighbor) && !neighbor.isWall) 
      {
        /* [AssignmentComment] Here the heuristicModifier could be tweaked */
        var tempG = current.g + heuristicModifier * 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) 
        {
          /* [AssignmentComment] Here the heuristicModifier could be tweaked */
          neighbor.h = heuristicModifier * heuristic(neighbor, end);
          neighbor.f = neighbor.g + neighbor.h;
          neighbor.previous = current;
        }
      }
    }
  }
  
  var moveSize = 1;
  if(current !== end) {
      console.log("No A* Path Found, Stay here!");
  } else {
      /* [AssignmentComment] This loop calculate backward from the current node back to the second node, i.e. the next node the enemy should take */
      var temp = current;
      while (temp.previous !== start) 
      {
        /* [AssignmentComment] Calculate the length of the path, i.e. the move number of this search run */
        moveSize++;
        temp = temp.previous;
      }
      /* [AssignmentComment] Update enemy only if it's not an obstable */
      if(!occupied(temp.i, temp.j)) {
          ei = temp.i;
          ej = temp.j;
      }
  }
  
  /* [AssignmentComment] Only accumulate sum data or update min/max data when it's a good step for a agent and how our enemy chase up using A* */
  if(!badstep()) {
      openSetMin = Math.min(openSetMin,openSetCount);
      openSetMax = Math.max(openSetMax,openSetCount);
      openSetTotal += openSetCount;
      searchRun++;
      
      openSetMoveRatioMin = Math.min(openSetMoveRatioMin,openSetCount/moveSize);
      openSetMoveRatioMax = Math.max(openSetMoveRatioMax,openSetCount/moveSize);
      moveTotal += moveSize;
  }
}


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;
 }
}




// --- 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 	);   
    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;
}





// --- 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 + " &nbsp; x = (" + x.toString() + ") &nbsp; 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; 
 var everCalcuationTotal = actualMazeSize * searchRun;
 
 
 /* [AssignmentComment] 
 OverallOpenSetMin: the minimum openset size of all search runs
 OverallOpenSetMax: the maximum openset size of all search runs
 AverageOpenSet% : all the openset accumulated of all search runs divided by (maze size * search runs), if it's good performnace A*, it should drop very fast. 
 If it's BFS say heuristicModifer = 0, it will be very high.
 
 OverallOpenSetMoveRatioMin: the minimum (openset size / path move) of all search runs
 OverallOpenSetMoveRatioMax: the maximum (openset size / path move) of all search runs
 AverageOpenSetMovieRatio:  all the openset accumulated of all search runs divided by all the path moves accumulated of all search run, e.g. if this is high, it indicates
 the heuristic is busy searching more spaced nodes but the improvement of path move is relatively small, hence the ratio will be high, then we should fine tune to lower this figure. In the BFS, where
 Say heuristicModifer = 0, this will be a very high figure.
 */
  

 AB.msg ( " &nbsp; y = (" + y.toString() + ") <br>" +
        " OverallOpenSetMin% (" + (100*openSetMin/actualMazeSize).toFixed(2) + ") &nbsp; OveraOpenSetMax% (" + (100*openSetMax/actualMazeSize).toFixed(2)  + ") " +
        " &nbsp; AverageOpenSet% (" + (openSetTotal * 100 / everCalcuationTotal).toFixed(2) + ") <br> "+
        " OverallOpenSetMoveRatioMin (" + openSetMoveRatioMin.toFixed(2) + ") &nbsp; OverallOpenSetMoveRatioMax (" + openSetMoveRatioMax.toFixed(3) + ") " +
        " &nbsp; AverageOpenSetMovieRatio (" + (openSetTotal / moveTotal).toFixed(2) + ")  <br>" +
		" Bad steps: " + badsteps + "&nbsp; Good steps: " + goodsteps + " &nbsp; 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);

  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 ----------------------------------------

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 
}