Code viewer for World: Ch.8. Tidying up.Taking turns.

// The working "Battleship" style Websocket game!
 
// Ch.8
// We put in the final functionality needed to make it a basic game. 

// You can see if another player is online.
// 1st player waits for a 2nd player. 
// When 2 players are online the game starts.
// Turn taking is enforced.  
// First one online gets the first turn.
// Can only have 2 players.
 
 

// === start of code ===========================================================================================================

const gridsize 		= 10;							  	   
const squaresize 	= 100;							 
const MAXPOS 		= gridsize * squaresize;	  

const startRadiusConst	 	= MAXPOS * 2 ;		      
const maxRadiusConst 		= MAXPOS * 10 ;			   
	
const SKYCOLOR 		= 'lightyellow';			      

AB.maxSteps                 = 1000000;         
AB.drawRunControls          = false;
ABWorld.drawCameraControls  = false;          


 const TEXTURE_WALL 	= '/uploads/chapters/stone.png' ;
 const TEXTURE_SHIP 	= '/uploads/chapters/ship.png' ;
 const TEXTURE_FIRE 	= '/uploads/chapters/fire.png' ;
 
// credit:
// https://commons.wikimedia.org/wiki/File:Square_stone_brick_Texture.jpg
// https://commons.wikimedia.org/wiki/File:Regina_Maris.JPG
// https://commons.wikimedia.org/wiki/File:FIRE_01.JPG

var wall_texture, ship_texture, fire_texture; 
var thesea;       


var GRID1  = new Array(gridsize);	    // my grid
var GRID2  = new Array(gridsize);	    // my (initially blank) map of opponent grid 

const GRID_EMPTY = 0;
const GRID_WALL  = 1;
const GRID_SHIP  = 2;
const GRID_BURNT = 3;
const GRID_FAIL  = 4;




	ABWorld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR  ); 

	loadTextures();		 	 
							
    
    
	
function loadTextures()		  // now has 3 textures to load 
{
	var loader1 = new THREE.TextureLoader();
	var loader2 = new THREE.TextureLoader();
	var loader3 = new THREE.TextureLoader();
		
	loader1.load ( TEXTURE_SHIP, function ( texture )  	 
	{
		ship_texture = texture;                  
		if ( ship_texture && wall_texture && fire_texture )	makeEverything();		// if all textures loaded from files  
	});	

	loader2.load ( TEXTURE_WALL, function ( texture )  		
	{
		wall_texture = texture;                  
		if ( ship_texture && wall_texture && fire_texture )	makeEverything();		  
	});	
	
	loader3.load ( TEXTURE_FIRE, function ( texture )  		
	{
		fire_texture = texture;                  
		if ( ship_texture && wall_texture && fire_texture )	makeEverything();		  
	});	
}




//--- two 2D grids  -------------------------------------------------------------------------------

function translateMy ( i, j )			 
// the absolute 3D position of an (i,j) square on my grid is:           
//  width x = (i * squaresize)      
// height y = 0                                
//  depth z = (j * squaresize) 
{
	var v = new THREE.Vector3();
	
	v.x = (i * squaresize) ;   		 
	v.y = 0 ;	
	v.z = (j * squaresize) ;   	
	
	return v;
}


function translateOpponent ( i, j )		
// put 2nd grid beside the 1st grid, offset on the x dimension 
{
	var v = new THREE.Vector3();

	v.x = (i * squaresize) + MAXPOS ;   		 
	v.y = 0 ;	
	v.z = (j * squaresize) ;   	

	return v;
}



//--- translate 3D point to a square in opponent's grid ----------------------------------
// all explained previously 

const minOpponentX = MAXPOS +            (1 * squaresize) - squaresize/2;
const maxOpponentX = MAXPOS + ((gridsize-2) * squaresize) + squaresize/2;

const minOpponentZ =                     (1 * squaresize) - squaresize/2;
const maxOpponentZ =          ((gridsize-2) * squaresize) + squaresize/2;


function point2address ( p )
{
    if ( p.x < minOpponentX ) return null;
    if ( p.x > maxOpponentX ) return null;
    if ( p.z < minOpponentZ ) return null;
    if ( p.z > maxOpponentZ ) return null;
    
    // else we are within the non-wall part of the opponent's grid 
    var i = ( p.x - MAXPOS ) / squaresize;
    var j = p.z / squaresize;
    
    i =  Math.round ( i );
    j =  Math.round ( j );
    return ( new THREE.Vector2 ( i, j ) );
}



	
function makeEverything()		 
{
    makeGrids();
    makeSea();
    makeShips();
    
    // start socket for this World
    // see "Websockets" in Docs 
    // or just select the line below and click "Code Help"

    AB.socketStart();
}


function makeGrids()
{
	var i,j, shape, thecube,   position, lookat;
	shape    = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );			 

	for ( i = 0; i < gridsize ; i++ )  
	{
	  GRID1[i] = new Array(gridsize);		// make each element an array of size "gridsize"
	  GRID2[i] = new Array(gridsize);
	  
	  for ( 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;
		    
			thecube = new THREE.Mesh( shape );
			thecube.material = new THREE.MeshBasicMaterial( { map: wall_texture } );
			position = translateMy(i,j);                
			thecube.position.copy ( position ); 		
			ABWorld.scene.add(thecube);
			
			thecube = new THREE.Mesh( shape );
			thecube.material = new THREE.MeshBasicMaterial( { map: wall_texture } );
			position = translateOpponent(i,j);                
			thecube.position.copy ( position ); 		
			ABWorld.scene.add(thecube);
		}
		else 
		{
		    GRID1[i][j] = GRID_EMPTY;
		    GRID2[i][j] = GRID_EMPTY;
		}
	  }
	}

// position and "lookat" of camera explained previously:

    position = new THREE.Vector3 ( MAXPOS,   MAXPOS * 1.2,   MAXPOS * 1.3 );
    lookat   = translateMy ( gridsize,  gridsize/2  );
    ABWorld.cameraCustom ( position, lookat );         
}

 
function makeSea()      
{
  var shape = new THREE.PlaneGeometry ( MAXPOS * 2,  MAXPOS  );      // make a "sea" plane for 2 grids side by side   
  thesea = new THREE.Mesh ( shape );
  thesea.material    = new THREE.MeshBasicMaterial ( { color: 'lightblue', side: THREE.DoubleSide } );
  thesea.rotation.set ( Math.PI / 2, 0, 0 );        // rotate it 90 degrees   
  
  // "position" of sea explained previously
  thesea.position.set ( MAXPOS - squaresize/2,      - squaresize * 0.4 ,      MAXPOS/2 - squaresize/2 );  

  ABWorld.scene.add ( thesea );
}

 
function makeShips()    
// make randomised ships in my grid
{
	 var  p, i, j, shape, thecube, position;
	 shape = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );	
	 
	 for ( p = 1; p <=5 ; p++ )    
	 {
    	 thecube = new THREE.Mesh( shape );
     	 thecube.material =  new THREE.MeshBasicMaterial( { map: ship_texture } );
     	 i = AB.randomIntAtoB ( 1, gridsize-2 );    // positions 0 and (gridsize-1) are walls 
     	 j = AB.randomIntAtoB ( 1, gridsize-2 ); 
     	 position = translateMy ( i, j );        
     	 thecube.position.copy ( position ); 
    	 ABWorld.scene.add(thecube);

		 GRID1[i][j] = GRID_SHIP;
	 }
}
 


// Mouse click all explained previously:

ABHandler.initMouseDrag = function ( x, y )  
{ 
    trySquare ( x, y );      
    ABHandler.initCameraDrag ( x, y );  
};


function trySquare ( x, y )
{
    if ( ABWorld.hitsObject ( x, y, thesea ) )
    {
        var p = ABWorld.hitsObjectPoint ( x, y, thesea );  
        var a = point2address ( p );
        
        if ( a )
        {
            // we have a valid click 
            // send "a" to opponent as our guess 
            sendGuess ( a );    
        }
    }
}




//--- Websockets functions --------------------------------------------------------------------------------

// turn taking 
// global variables to keep track of what is happening:

var gameon  = false;         // does any game exist right now 
var allowed = false;         // am I allowed in the game (includes when I am 1st player waiting for game to exist)
var myturn  = false;         // is it my turn



// AB.socketUserlist is triggered when we connect, and any time another user arrives or leaves.
AB.socketUserlist = function ( a ) 
{   
//    console.log ( "Got user list: " );
//    console.log ( JSON.stringify ( a, null, ' ' ) );

    if ( a.length == 1 )    // just me here  
    {
        AB.msg ( "We are 1st player. Waiting for 2nd player. We will get 1st turn. ");
        gameon = false;     // gameon = false for 1st player 
        allowed = true;     // allowed = true for 1st player
        myturn = true;      // myturn = true for 1st player
        return;
    }    
    
    if ( a.length == 2 )    
    // 2 players here 
    // Note BOTH players will get message saying 2 players here.
    // Only the first one got the message saying 1 player here. So only the first one got myturn = true 
    {
        AB.msg ("2 players online. Game on. The first player to join gets the first turn. ");
        gameon = true;      // gameon = true for first 2 players
        allowed = true;     // allowed = true for first 2 players
                            // myturn = true for 1st player, not for 2nd 
        return;
    }
    
    AB.msg ( "More than 2 players have joined! Only the first 2 players can use the game.");
    // players who joined 3rd or later will not get gameon = true or allowed = true 
};



function sendGuess ( a )
// send my guess (a 2D point) to the other player 
{
  if ( ! allowed )      // 3rd user trying to play - not allowed  
  {
      AB.msg ( "Game already has 2 players. You are not allowed play."); 
      return;            
  }
  
  if ( ! gameon )      // 1st user waiting for game - keep waiting 
  {
      AB.msg ( "Game has not started."); 
      return;            
  }

  var data = 
  {
    guess: a
  };
  
  if ( myturn )
  {
     AB.msg ("My turn. My guess is sent.");
     AB.socketOut ( data );   
     myturn = false;          
  }
  else 
    AB.msg ( "Not my turn. No guess sent.");
}  
  
  

function sendReply ( i, j, hit )
// tell the other player if their guess i,j was a hit or not 
// it is only called by AB.socketIn - that will do all the condition checking, setting of myturn, etc.
{
  var data = 
  {
    reply: [ i, j, hit ]        // send an array of the 3 pieces of data 
  };
  
  AB.socketOut ( data ); 
}



// receive data on Websocket

AB.socketIn = function(data)
{
    var i,j, shape, position, thecube, hit;

  if ( ! allowed )     // if not allowed, incoming messages are ignored 
  {
      AB.msg ("I am not in the game. Incoming messages ignored.");
      return;            
  }
  
  if ( ! gameon )   // should never happen that there are incoming but no game is on! 
  {
      AB.msg ( "There is incoming. But no game is on!"); 
      return;            
  }
  
	shape = new THREE.BoxGeometry ( squaresize, squaresize, squaresize );	
    thecube = new THREE.Mesh( shape );
	
    if ( data.guess )     // if this field exists - opponent making a guess 
    {
        AB.msg ("Opponent makes a guess and I tell them hit/miss.");
        i = data.guess.x;   
        j = data.guess.y;   
        
        if ( ( GRID1[i][j] == GRID_SHIP ) || ( GRID1[i][j] == GRID_BURNT ) )    // if ship or burnt (already burnt this spot)
        {
            GRID1[i][j] = GRID_BURNT;       // opponent success
         	thecube.material = new THREE.MeshBasicMaterial( { map: fire_texture } );
            position = translateMy ( i, j );        // cube goes onto my grid 
         	sendReply ( i, j, true );               // tell the opponent what happened
        }        
        else
        {
            GRID1[i][j] = GRID_FAIL;       // opponent failure 
         	thecube.material = new THREE.MeshBasicMaterial ( { color: 'navy' } );
            position = translateMy ( i, j );          
         	sendReply ( i, j, false );
        }
        
        // now it is my turn 
        myturn = true;    
    }
      
    
    if ( data.reply )     // if this field exists - opponent reply to my guess 
    {
        AB.msg ("Received opponent reply to my guess.");
        i = data.reply[0];   
        j = data.reply[1];
        hit = data.reply[2]; 
        
        if ( hit )
        {
            GRID2[i][j] = GRID_BURNT;       // my success
         	thecube.material = new THREE.MeshBasicMaterial( { map: fire_texture } );
            position = translateOpponent ( i, j );        // cube goes onto my map of opponent grid 
        }        
        else
        {
            GRID2[i][j] = GRID_FAIL;       // my failure 
         	thecube.material = new THREE.MeshBasicMaterial ( { color: 'navy' } );
            position = translateOpponent ( i, j );          
        }     
        
        // now it is their turn 
        myturn = false;         // in fact no need - we already set this when we sent the message 
    }
    
    thecube.position.copy ( position ); 
    ABWorld.scene.add(thecube);            
};  
  
  
// === end of code ===========================================================================================================




// Exercises:
// Add sound effects when hit/miss. Search for "audio" in Docs.

// Hacking exercise:  
// You can type almost any JS code into the console. So a user can redefine JS code while the run is in progress.
// Show how a user can redefine variables to join a game.
// Show how a user can redefine functions to declare that an incoming guess is a miss even when it is a hit!
// This shows how web browser based games are often easy to cheat in.
// More seriously, many hacking exploits use this kind of thing (code entered in console) and sites need to be aware of it.

// Other hacking exercise:
// Can you really stop a 3rd person joining? Can a 3rd person send messages to the Websocket anyway to disrupt a game?
// Investigate AB.socketStart ( password ) to prevent this   


// Outcomes:  
// Learn how code can try to synchronise two different browsers. 
// Learn the issues with identifying the 1st user to run the code versus the 2nd user to run the code, and 3rd, etc.
// This is "distributed computing", core to making the Internet work:
// https://en.wikipedia.org/wiki/Distributed_computing
// If you think it is hard to get your head around, don't worry. It is! 

// If they do the hacking exercises:
// Learn how hard it is to stop cheating in web games in the browser. 
// Learn how to enter arbitrary code in the console and how this can be exploited for hacking.