Code viewer for World: A star (practical 1) - Fai...

// Cloned by AKASH BARIK on 10 Nov 2022 from World "Complex World" by Starter user 
// This is to create an intelligent enemy using A star for finding the right path toward the agent
 



// ==== Starter World =================================================================================================
// This code is designed for use on the Ancient Brain site.
// This code may be freely copied and edited by anyone on the Ancient Brain site.
// To include a working run of this program on another site, see the "Embed code" links provided on Ancient Brain.
// ====================================================================================================================



// =============================================================================================
// More complex starter World 
// 3d-effect Maze World (really a 2-D problem)
// Movement is on a semi-visible grid of squares 
//
// This more complex World shows:
// - Skybox
// - Internal maze (randomly drawn each time)
// - Enemy actively chases agent
// - Music/audio
// - 2D world (clone this and set show3d = false)
// - User keyboard control (clone this and comment out Mind actions to see)
// =============================================================================================


// =============================================================================================
// Scoring:
// Bad steps = steps where enemy is within one step of agent.
// Good steps = steps where enemy is further away. 
// Score = good steps as percentage of all steps.
//
// There are situations where agent is trapped and cannot move.
// If this happens, you score zero.
// =============================================================================================


 





// ===================================================================================================================
// === Start of tweaker's box ======================================================================================== 
// ===================================================================================================================

// The easiest things to modify are in this box.
// You should be able to change things in this box without being a JavaScript programmer.
// Go ahead and change some of these. What's the worst that could happen?


AB.clockTick       = 100;    

  // Speed of run: Step every n milliseconds. Default 100.
  
AB.maxSteps        = 1000;    

  // Length of run: Maximum length of run in steps. Default 1000.

AB.screenshotStep  = 50;   
  
  // Take screenshot on this step. (All resources should have finished loading.) Default 50.



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

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


 const TEXTURE_WALL   = '/uploads/ultron6/venus.png' ;
 const TEXTURE_MAZE   = '/uploads/ultron6/maze_Walls.jpg' ;
 const TEXTURE_AGENT  = '/uploads/ultron6/ghosts.jpg' ;
 const TEXTURE_ENEMY  = '/uploads/ultron6/pacman_v1.jpg' ;

// credits:
// http://commons.wikimedia.org/wiki/File:Old_door_handles.jpg
// https://commons.wikimedia.org/wiki/Category:Pac-Man_icons
// https://commons.wikimedia.org/wiki/Category:Skull_and_crossbone_icons
// http://en.wikipedia.org/wiki/File:Inscription_displaying_apices_(from_the_shrine_of_the_Augustales_at_Herculaneum).jpg

 
  const MUSIC_BACK  = '/uploads/ultron6/spy-jazz-astar.mp3' ;
  const SOUND_ALARM = '/uploads/starter/air.horn.mp3' ;

// credits:
// http://www.dl-sounds.com/royalty-free/defense-line/
// http://soundbible.com/1542-Air-Horn.html 

  
  
const gridsize = 50;            // number of squares along side of world     

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

const squaresize = 100;         // size of square in pixels

const MAXPOS = gridsize * squaresize;   // length of one side in pixels 
  
const SKYCOLOR  = 0xddffdd;       // a number, not a string 

 
const startRadiusConst    = MAXPOS * 0.8 ;    // distance from centre to start the camera at
const maxRadiusConst    = MAXPOS * 10  ;    // maximum distance from camera we will render things  

//--- change ABWorld defaults: -------------------------------

ABHandler.MAXCAMERAPOS  = maxRadiusConst ;

ABHandler.GROUNDZERO    = true;           // "ground" exists at altitude zero



//--- skybox: -------------------------------
// skybox is a collection of 6 files 
// x,y,z positive and negative faces have to be in certain order in the array 
// https://threejs.org/docs/#api/en/loaders/CubeTextureLoader 

// mountain skybox, credit:
// http://stemkoski.github.io/Three.js/Skybox.html
/*
 const SKYBOX_ARRAY = [                    
                "/uploads/starter/dawnmountain-xpos.png",
                "/uploads/starter/dawnmountain-xneg.png",
                "/uploads/starter/dawnmountain-ypos.png",
                "/uploads/starter/dawnmountain-yneg.png",
                "/uploads/starter/dawnmountain-zpos.png",
                "/uploads/starter/dawnmountain-zneg.png"
                ];
*/

// space skybox, credit:
// http://en.spaceengine.org/forum/21-514-1
// x,y,z labelled differently


 const SKYBOX_ARRAY = [                    
                "/uploads/starter/sky_pos_z.jpg",
                "/uploads/starter/sky_neg_z.jpg",
                "/uploads/starter/sky_pos_y.jpg",
                "/uploads/starter/sky_neg_y.jpg",
                "/uploads/starter/sky_pos_x.jpg",
                "/uploads/starter/sky_neg_x.jpg"
                ];
        


// urban photographic skyboxes, credit:
// http://opengameart.org/content/urban-skyboxes

/*
 const SKYBOX_ARRAY = [                    
                "/uploads/starter/posx.jpg",
                "/uploads/starter/negx.jpg",
                "/uploads/starter/posy.jpg",
                "/uploads/starter/negy.jpg",
                "/uploads/starter/posz.jpg",
                "/uploads/starter/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;

var isDiagonalAllowed = false; // Toggle to use Eucllidean distance over manhattan
var aStarList = []; 
var aStarPath = []; 
var openList = []; 
var openListPath = []; 
var closedList = []; 
var closedListPath = []; 
var deadEnd = []; 
var currentNode = null;
var pathColor="white"

  
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(); 


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


function drawAstar()
{
    
    aStarList.forEach(e => {
    shape = new THREE.BoxGeometry ( squaresize, 1, squaresize );       
  path = new THREE.Mesh( shape );
  path.material =  new THREE.MeshBasicMaterial( { color: pathColor } );
  path.material.transparency = true;
  path.material.opacity = 0.9;
  position = translate(e[0], e[1]), 
    position.y = position.y - BOXHEIGHT / 2, 
    path.position.copy(position), 
    ABWorld.scene.add(path), 
    aStarPath.push(path);
  });
}

function drawOpenList()
{
     openList.forEach(e => {
    shape = new THREE.BoxGeometry ( squaresize, 1, squaresize );       
  path = new THREE.Mesh( shape );
  path.material =  new THREE.MeshBasicMaterial( { color: pathColor } );
  path.material.transparent = true;
  path.material.opacity = 0.6;
  position = translate(e.position[0], e.position[1]);
    position.y = position.y - BOXHEIGHT / 2; 
    path.position.copy(position); 
    ABWorld.scene.add(path); 
    aStarPath.push(path);
  });
}

function drawClosedList()
{
       closedList.forEach(e => {
    shape = new THREE.BoxGeometry ( squaresize, 1, squaresize );       
  path = new THREE.Mesh( shape );
  path.material =  new THREE.MeshBasicMaterial( { color: pathColor } );
  path.material.transparent = true;
  path.material.opacity = 0.3;
  position = translate(e.position[0], e.position[1]);
    position.y = position.y - BOXHEIGHT / 2; 
    path.position.copy(position); 
    ABWorld.scene.add(path); 
    aStarPath.push(path);
  }); 
}

function drawAStarPath() 
{
    drawAstar();
    drawOpenList();
    drawClosedList()
}



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

function moveLogicalEnemy()
{ 
// move towards agent 
// put some randomness in so it won't get stuck with barriers 
calculateAStar();
//console.log("A* path is: " + aStarList[0][0] + "," + aStarList[0][1]);
drawAStarPath(aStarList); 

 i = aStarList.at(-1)[0]; 
 j = aStarList.at(-1)[1]; 
  
 
 if ( ! occupied(i,j) )   // if no obstacle then move, else just miss a turn
 {
  ei = i;
  ej = j;
 }
}


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; 

 AB.msg ( " &nbsp; y = (" + y.toString() + ") <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 
}




//Calculate Heuristic based on if diagonal is allowed or not
function calculateHeuristic(i,j) {
  if(isDiagonalAllowed) // If diagonal is allowed or not
  {
      h = Math.sqrt( Math.pow((ai - i), 2) + Math.pow((aj - j), 2)); // If diagonal is allowed use Euclidean algorithm 
  }
  else
  {
      h = Math.abs(i - ai) + Math.abs(j - aj); // If diagonal is not allowed use Manhattan
  }
  
  return h; // h is heuristic for any given node 
}

// Get the next neighbours to explore
function getNeighbours([e, t]) 
{
    let o = [];
    var neighbours = [[e + 1, t], [e - 1, t], [e, t + 1], [e, t - 1]];
    
    if(isDiagonalAllowed)
    {
        neighbours.push([e + 1, t + 1]);
        neighbours.push([e - 1, t + 1]);
        neighbours.push([e - 1, t - 1]);
        neighbours.push([e + 1, t - 1]);
    }
     
     
     return neighbours.forEach(e => {
        !(0 < e[0] < gridsize && 0 < e[1] < gridsize)
         || e.toString() != [ai, aj].toString() && occupied(e[0], e[1])|| 
         closedList.some(t => t.position.toString() == e.toString()) 
         || e.toString() == currentNode.position.toString() 
         || o.push(e);
      }), o;
      
     
  
 }
  

//Creating class to intialize the node with default values
class node {
  constructor(e, t, o) {
    this.position = e;
    this.goalHeuristic = t; 
    this.parentNode = o; 
    this.pathLength = null;
    if (this.pathlength == o) 
    {
        this.pathlength = 0;
    }
    else 
        o.pathLength + 1;
        
    this.cost = this.goalHeuristic + this.pathLength;
  }
}




//Reset the variable before caluclating the next a star path to avoid any issues.

function clearVariable() {
  aStarList = []; 
  openList = []; 
  closedList = []; 
  visited = [];

  aStarPath.forEach(e => {
    ABWorld.scene.remove(e);
  }); 

  openListPath.forEach(e => {
    ABWorld.scene.remove(e);
  }); 

  closedListPath.forEach(e => {
    ABWorld.scene.remove(e);
  }); 

  aStarPath = []; 
  openListPath = []; 
  closedListPath = [];
}

//Calculate the A star path between current poistion for Enemy to reach agent
function calculateAStar() {
  clearVariable();
  const e = calculateHeuristic([ei, ej]); 
  t = [ei, ej]; 
  o = [ai, aj];

  let a = false, 
  i = new node(t, e, null);
  
  for (openList.push(i), iterator = 0, iterationLimit = AB.maxSteps; !a;) 
  {
        if (openList.sort((e, t) => e.cost === t.cost ? e.goalHeuristic < t.goalHeuristic ? -1 : 1 : e.cost < t.cost ? -1 : 1), 
        currentNode = openList.splice(0, 1)[0], 
        closedList.push(currentNode), 
        currentNode.position.toString() === o.toString()){ 
        break;
    }
    
    getNeighbours(currentNode.position).forEach(e => {
      nodesAtPosition = openList.filter(t => t.position.toString() === e.toString()), 
      0 == nodesAtPosition.length ? (newNode = new node(e, calculateHeuristic(e), currentNode), openList.push(newNode)) : (nodeAtPosition = nodesAtPosition[0], nodeAtPosition.pathLength < currentNode.pathLength && (nodeAtPosition.parentNode = currentNode, 
          nodeAtPosition.pathLength = nodeAtPosition.parentNode.pathLength + 1, 
          nodeAtPosition.cost = nodeAtPosition.goalHeuristic + nodeAtPosition.pathLength));
        
    }), iterator++, iterator == iterationLimit && (a = true);
  }
  
  for (iterator = 0; currentNode.position.toString() != t.toString() && (aStarList.push(currentNode.position), currentNode = currentNode.parentNode, iterator++, iterator != iterationLimit);) ;
}