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