Code viewer for World: World
const BLOCKPUNISH = 50
const [ACTION_LEFT, ACTION_RIGHT, ACTION_UP, ACTION_DOWN, ACTION_STAYSTILL] = [0, 1, 2, 3, 4]
AB.clockTick = 5
AB.maxSteps = 2000
AB.screenshotStep = 50
const TEXTURE_WALL 	= '/uploads/starter/door.jpg'
const TEXTURE_MAZE 	= '/uploads/starter/latin.jpg'
const TEXTURE_AGENT = '/uploads/starter/pacman.jpg'
const TEXTURE_ENEMY = '/uploads/starter/ghost.3.png'
const SOUND_ALARM = '/uploads/starter/air.horn.mp3'
const gridsize = 20
const BOXDENSITY = 0.1
const NOBOXES = Math.trunc(gridsize * gridsize * BOXDENSITY)
const squaresize = 100
const MAXPOS = gridsize * squaresize
const SKYCOLOR 	= 0xddffdd
const startRadiusConst = MAXPOS * 0.8
const skyboxConst = MAXPOS * 2
const maxRadiusConst = MAXPOS * 10
ABHandler.MAXCAMERAPOS = skyboxConst * 0.6
ABHandler.GROUNDZERO = true

const downmountain = [ 'xpos', 'xneg', 'ypos', 'yneg', 'zpos', 'zneg' ].map(name => 'dawnmountain-' + name + '.png')
const stsky = [ 'posx', 'negx', 'posy', 'negy', 'posz', 'negz' ].map(name => 'st.' + name + '.jpg')
const SKYBOX_ARRAY = downmountain.map(name => '/uploads/starter/' + name)

const [GRID_BLANK, GRID_WALL, GRID_MAZE] = [0, 1, 2]

const soundAlarm = () => new Audio(SOUND_ALARM).play()

function World() { 
    var BOXHEIGHT  = squaresize;
    
    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 stuckfor = 0;       // how long the enemy has been stuck - if stuck for a while start making random moves 
    
     	var self = this;						// needed for private fn to call public fn  
    
     
    
    const getTreeTex = url => new Promise(whenDone => {
        new THREE.TextureLoader().load(url, texture => whenDone(texture))
    })
	
    function loadResources() {
        const res = [TEXTURE_WALL, TEXTURE_AGENT, TEXTURE_ENEMY, TEXTURE_MAZE]
    	Promise.all(res.concat(SKYBOX_ARRAY).map(url => getTreeTex(url)))
    	.then(textures => {
    	    textures.forEach(texture => texture.minFilter = THREE.LinearFilter);
            
            [wall_texture, agent_texture, enemy_texture, maze_texture] = textures.slice(0, 4)
            textures.slice(4).forEach((texture, i) => SKYBOX_ARRAY[i] = texture)
            initScene()
    	})
    }

    
    //--- 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() {
    	 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
    	 resetEnemyPos();
    	 
    	 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()
        var skyGeometry = new THREE.CubeGeometry ( skyboxConst, skyboxConst, skyboxConst )
        var skyMaterial = new THREE.MeshFaceMaterial ( Array(6).fill(0).map((_, i) => new THREE.MeshBasicMaterial({map: SKYBOX_ARRAY[i], side: THREE.BackSide})))
        ABWorld.scene.add(new THREE.Mesh(skyGeometry, skyMaterial))
        ABWorld.render()
        AB.removeLoading()
        ABRun.runReady = true
    }
    
    function resetEnemyPos()
    {
         var i,j;	 
         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;
    }
    
    // --- 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 getEnemyAction()
    { 
    // move towards agent 
     
      // after n failed moves, start making random moves 
      if ( stuckfor > 2 ) return  ( AB.randomIntAtoB (0,3) );
    
    
    if ( AB.randomBoolean() )
    {
         if ( ej < aj ) 	return (   ACTION_UP ); 
    	 if ( ej > aj ) 	return (   ACTION_DOWN ); 
    }
    else
    {
    	 if ( ei < ai ) 	return (   ACTION_RIGHT ); 
    	 if ( ei > ai ) 	return (   ACTION_LEFT ); 
    }
    
     	return  ( AB.randomIntAtoB (0,3) );
    }
    
    function moveLogicalEnemy( a )			 
    { 
     var i = ei;
     var j = ej;		 
    
          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) ) 
      stuckfor++;
     else
     {
      stuckfor = 0;
      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;
     }
    }

    // --- score: -----------------------------------
    const badstep = () => Math.abs(ei - ai) < 2 && Math.abs(ej - aj) < 2
    const agentBlocked = () => occupied(ai - 1, aj) && occupied(ai + 1, aj) && occupied(ai, aj + 1) && occupied(ai, aj - 1)
    const updateStatusBefore = a => $('#user_span1').html(`Step: ${ABRun.step} &nbsp; x = (${this.getState()}) &nbsp; a = (${a}) `)
    const updateStatusAfter = () => $("#user_span2").html(` &nbsp; y = (${this.getState()}) <BR> Bad steps: ${badsteps} &nbsp; Good steps: ${goodsteps} &nbsp; Score: ${(( goodsteps / ABRun.step ) * 100).toFixed(2)}% `)
    
    //--- public functions / interface / API ----------------------------------------------------------
    
    this.newRun = () => {
        AB.loadingScreen()
    	ABRun.runReady = false
    	badsteps = 0
    	goodsteps = 0
	    ABWorld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR  )
    	loadResources()
    }
    
    this.getState = () => [ ai, aj, ei, ej ]
    
    this.takeAction = a => {
        updateStatusBefore(a);
        moveLogicalAgent(a);
        ABRun.step % 2 === 0 && moveLogicalEnemy(getEnemyAction());
        badstep() ? badsteps++ : goodsteps++;
        drawAgent();
        drawEnemy();
        updateStatusAfter()
        if (agentBlocked()) {
            badsteps = badsteps + BLOCKPUNISH
            ABRun.step = ABRun.step + BLOCKPUNISH
            $('#user_span3').html(' <br> <font color=red> <B> Agent blocked. ' + BLOCKPUNISH + ' extra bad steps and reset enemy. </B> </font> ')
            soundAlarm()
            resetEnemyPos()
        }
    }
    this.endRun = () => $('#user_span3').html(' <br> <font color=green> <B> Run over. </B> </font> ')
    this.getScore = () => (( goodsteps / AB.maxSteps ) * 100).toFixed(2)
}