(function() { // Battleboat // Bill Mei, 2014 // MIT License // Thanks to Nick Berry for the inspiration // http://www.datagenetics.com/blog/december32011/ // TODO: Add a toggle that visualizes the probability grid via heatmap // (scale a color via max and 0). The toggle only works once the user has // finished placing their ships, or she can cheat easily by placing her ships // outside the regions with the highest probability. console.log("%cHi! Thanks for checking out this game.%c Please be nice and don't " + "hack the Stats object, I'm using Google Analytics to collect info about " + "the AI's win/loss percentage in order to improve the bot, so if you do " + "look around, I kindly ask that you don't give it bad data. Thanks :)", "font-weight: bold; font-family: Tahoma, Helvetica, Arial, sans-serif;", ""); console.log("Also, if you want to try stuff out, run %csetDebug(true);%c in the " + "console before doing anything. You'll also get access to some cool features.", "background: #000; color: #0f0; padding: 2px 5px; border-radius: 2px;", ""); // Global Constants var CONST = {}; CONST.AVAILABLE_SHIPS = ['carrier', 'battleship', 'destroyer', 'submarine', 'patrolboat']; // You are player 0 and the computer is player 1 // The virtual player is used for generating temporary ships // for calculating the probability heatmap CONST.HUMAN_PLAYER = 0; CONST.COMPUTER_PLAYER = 1; CONST.VIRTUAL_PLAYER = 2; // Possible values for the parameter `type` (string) CONST.CSS_TYPE_EMPTY = 'empty'; CONST.CSS_TYPE_SHIP = 'ship'; CONST.CSS_TYPE_MISS = 'miss'; CONST.CSS_TYPE_HIT = 'hit'; CONST.CSS_TYPE_SUNK = 'sunk'; // Grid code: CONST.TYPE_EMPTY = 0; // 0 = water (empty) CONST.TYPE_SHIP = 1; // 1 = undamaged ship CONST.TYPE_MISS = 2; // 2 = water with a cannonball in it (missed shot) CONST.TYPE_HIT = 3; // 3 = damaged ship (hit shot) CONST.TYPE_SUNK = 4; // 4 = sunk ship // TODO: Make this better OO code. CONST.AVAILABLE_SHIPS should be an array // of objects rather than than two parallel arrays. Or, a better // solution would be to store "USED" and "UNUSED" as properties of // the individual ship object. // These numbers correspond to CONST.AVAILABLE_SHIPS // 0) 'carrier' 1) 'battleship' 2) 'destroyer' 3) 'submarine' 4) 'patrolboat' // This variable is only used when DEBUG_MODE === true. Game.usedShips = [CONST.UNUSED, CONST.UNUSED, CONST.UNUSED, CONST.UNUSED, CONST.UNUSED]; CONST.USED = 1; CONST.UNUSED = 0; // Game Statistics function Stats(){ this.shotsTaken = 0; this.shotsHit = 0; this.totalShots = parseInt(localStorage.getItem('totalShots'), 10) || 0; this.totalHits = parseInt(localStorage.getItem('totalHits'), 10) || 0; this.gamesPlayed = parseInt(localStorage.getItem('gamesPlayed'), 10) || 0; this.gamesWon = parseInt(localStorage.getItem('gamesWon'), 10) || 0; this.uuid = localStorage.getItem('uuid') || this.createUUID(); if (DEBUG_MODE) { this.skipCurrentGame = true; } } Stats.prototype.incrementShots = function() { this.shotsTaken++; }; Stats.prototype.hitShot = function() { this.shotsHit++; }; Stats.prototype.wonGame = function() { this.gamesPlayed++; this.gamesWon++; if (!DEBUG_MODE) { ga('send', 'event', 'gameOver', 'win', this.uuid); } }; Stats.prototype.lostGame = function() { this.gamesPlayed++; if (!DEBUG_MODE) { ga('send', 'event', 'gameOver', 'lose', this.uuid); } }; // Saves the game statistics to localstorage, also uploads where the user placed // their ships to Google Analytics so that in the future I'll be able to see // which cells humans are disproportionately biased to place ships on. Stats.prototype.syncStats = function() { if(!this.skipCurrentGame) { var totalShots = parseInt(localStorage.getItem('totalShots'), 10) || 0; totalShots += this.shotsTaken; var totalHits = parseInt(localStorage.getItem('totalHits'), 10) || 0; totalHits += this.shotsHit; localStorage.setItem('totalShots', totalShots); localStorage.setItem('totalHits', totalHits); localStorage.setItem('gamesPlayed', this.gamesPlayed); localStorage.setItem('gamesWon', this.gamesWon); localStorage.setItem('uuid', this.uuid); } else { this.skipCurrentGame = false; } var stringifiedGrid = ''; for (var x = 0; x < Game.size; x++) { for (var y = 0; y < Game.size; y++) { stringifiedGrid += '(' + x + ',' + y + '):' + mainGame.humanGrid.cells[x][y] + ';\n'; } } if (!DEBUG_MODE) { ga('send', 'event', 'humanGrid', stringifiedGrid, this.uuid); } }; // Updates the sidebar display with the current statistics Stats.prototype.updateStatsSidebar = function() { var elWinPercent = document.getElementById('stats-wins'); var elAccuracy = document.getElementById('stats-accuracy'); elWinPercent.innerHTML = this.gamesWon + " of " + this.gamesPlayed; elAccuracy.innerHTML = Math.round((100 * this.totalHits / this.totalShots) || 0) + "%"; }; // Reset all game vanity statistics to zero. Doesn't reset your uuid. Stats.prototype.resetStats = function(e) { // Skip tracking stats until the end of the current game or else // the accuracy percentage will be wrong (since you are tracking // hits that didn't start from the beginning of the game) Game.stats.skipCurrentGame = true; localStorage.setItem('totalShots', 0); localStorage.setItem('totalHits', 0); localStorage.setItem('gamesPlayed', 0); localStorage.setItem('gamesWon', 0); localStorage.setItem('showTutorial', true); Game.stats.shotsTaken = 0; Game.stats.shotsHit = 0; Game.stats.totalShots = 0; Game.stats.totalHits = 0; Game.stats.gamesPlayed = 0; Game.stats.gamesWon = 0; Game.stats.updateStatsSidebar(); }; Stats.prototype.createUUID = function(len, radix) { /*! Math.uuid.js (v1.4) http://www.broofa.com mailto:robert@broofa.com Copyright (c) 2010 Robert Kieffer Dual licensed under the MIT and GPL licenses. */ var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''), uuid = [], i; radix = radix || chars.length; if (len) { // Compact form for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; } else { // rfc4122, version 4 form var r; // rfc4122 requires these characters uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; uuid[14] = '4'; // Fill in random data. At i==19 set the high bits of clock sequence as // per rfc4122, sec. 4.1.5 for (i = 0; i < 36; i++) { if (!uuid[i]) { r = 0 | Math.random()*16; uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r]; } } } return uuid.join(''); }; // Game manager object // Constructor function Game(size) { Game.size = size; this.shotsTaken = 0; this.createGrid(); this.init(); } Game.size = 10; // Default grid size is 10x10 Game.gameOver = false; // Checks if the game is won, and if it is, re-initializes the game Game.prototype.checkIfWon = function() { if (this.computerFleet.allShipsSunk()) { alert('Congratulations, you win!'); Game.gameOver = true; Game.stats.wonGame(); Game.stats.syncStats(); Game.stats.updateStatsSidebar(); this.showRestartSidebar(); } else if (this.humanFleet.allShipsSunk()) { alert('Yarr! The computer sank all your ships. Try again.'); Game.gameOver = true; Game.stats.lostGame(); Game.stats.syncStats(); Game.stats.updateStatsSidebar(); this.showRestartSidebar(); } }; // Shoots at the target player on the grid. // Returns {int} Constants.TYPE: What the shot uncovered Game.prototype.shoot = function(x, y, targetPlayer) { var targetGrid; var targetFleet; if (targetPlayer === CONST.HUMAN_PLAYER) { targetGrid = this.humanGrid; targetFleet = this.humanFleet; } else if (targetPlayer === CONST.COMPUTER_PLAYER) { targetGrid = this.computerGrid; targetFleet = this.computerFleet; } else { // Should never be called console.log("There was an error trying to find the correct player to target"); } if (targetGrid.isDamagedShip(x, y)) { return null; } else if (targetGrid.isMiss(x, y)) { return null; } else if (targetGrid.isUndamagedShip(x, y)) { // update the board/grid targetGrid.updateCell(x, y, 'hit', targetPlayer); // IMPORTANT: This function needs to be called _after_ updating the cell to a 'hit', // because it overrides the CSS class to 'sunk' if we find that the ship was sunk targetFleet.findShipByCoords(x, y).incrementDamage(); // increase the damage this.checkIfWon(); return CONST.TYPE_HIT; } else { targetGrid.updateCell(x, y, 'miss', targetPlayer); this.checkIfWon(); return CONST.TYPE_MISS; } }; // Creates click event listeners on each one of the 100 grid cells Game.prototype.shootListener = function(e) { var self = e.target.self; // Extract coordinates from event listener var x = parseInt(e.target.getAttribute('data-x'), 10); var y = parseInt(e.target.getAttribute('data-y'), 10); var result = null; if (self.readyToPlay) { result = self.shoot(x, y, CONST.COMPUTER_PLAYER); // Remove the tutorial arrow if (gameTutorial.showTutorial) { gameTutorial.nextStep(); } } if (result !== null && !Game.gameOver) { Game.stats.incrementShots(); if (result === CONST.TYPE_HIT) { Game.stats.hitShot(); } // The AI shoots iff the player clicks on a cell that he/she hasn't // already clicked on yet self.robot.shoot(); } else { Game.gameOver = false; } }; // Creates click event listeners on each of the ship names in the roster Game.prototype.rosterListener = function(e) { var self = e.target.self; // Remove all classes of 'placing' from the fleet roster first var roster = document.querySelectorAll('.fleet-roster li'); for (var i = 0; i < roster.length; i++) { var classes = roster[i].getAttribute('class') || ''; classes = classes.replace('placing', ''); roster[i].setAttribute('class', classes); } // Move the highlight to the next step if (gameTutorial.currentStep === 1) { gameTutorial.nextStep(); } // Set the class of the target ship to 'placing' Game.placeShipType = e.target.getAttribute('id'); document.getElementById(Game.placeShipType).setAttribute('class', 'placing'); Game.placeShipDirection = parseInt(document.getElementById('rotate-button').getAttribute('data-direction'), 10); self.placingOnGrid = true; }; // Creates click event listeners on the human player's grid to handle // ship placement after the user has selected a ship name Game.prototype.placementListener = function(e) { var self = e.target.self; if (self.placingOnGrid) { // Extract coordinates from event listener var x = parseInt(e.target.getAttribute('data-x'), 10); var y = parseInt(e.target.getAttribute('data-y'), 10); // Don't screw up the direction if the user tries to place again. var successful = self.humanFleet.placeShip(x, y, Game.placeShipDirection, Game.placeShipType); if (successful) { // Done placing this ship self.endPlacing(Game.placeShipType); // Remove the helper arrow if (gameTutorial.currentStep === 2) { gameTutorial.nextStep(); } self.placingOnGrid = false; if (self.areAllShipsPlaced()) { var el = document.getElementById('rotate-button'); el.addEventListener(transitionEndEventName(),(function(){ el.setAttribute('class', 'hidden'); if (gameTutorial.showTutorial) { document.getElementById('start-game').setAttribute('class', 'highlight'); } else { document.getElementById('start-game').removeAttribute('class'); } }),false); el.setAttribute('class', 'invisible'); } } } }; // Creates mouseover event listeners that handles mouseover on the // human player's grid to draw a phantom ship implying that the user // is allowed to place a ship there Game.prototype.placementMouseover = function(e) { var self = e.target.self; if (self.placingOnGrid) { var x = parseInt(e.target.getAttribute('data-x'), 10); var y = parseInt(e.target.getAttribute('data-y'), 10); var classes; var fleetRoster = self.humanFleet.fleetRoster; for (var i = 0; i < fleetRoster.length; i++) { var shipType = fleetRoster[i].type; if (Game.placeShipType === shipType && fleetRoster[i].isLegal(x, y, Game.placeShipDirection)) { // Virtual ship fleetRoster[i].create(x, y, Game.placeShipDirection, true); Game.placeShipCoords = fleetRoster[i].getAllShipCells(); for (var j = 0; j < Game.placeShipCoords.length; j++) { var el = document.querySelector('.grid-cell-' + Game.placeShipCoords[j].x + '-' + Game.placeShipCoords[j].y); classes = el.getAttribute('class'); // Check if the substring ' grid-ship' already exists to avoid adding it twice if (classes.indexOf(' grid-ship') < 0) { classes += ' grid-ship'; el.setAttribute('class', classes); } } } } } }; // Creates mouseout event listeners that un-draws the phantom ship // on the human player's grid as the user hovers over a different cell Game.prototype.placementMouseout = function(e) { var self = e.target.self; if (self.placingOnGrid) { for (var j = 0; j < Game.placeShipCoords.length; j++) { var el = document.querySelector('.grid-cell-' + Game.placeShipCoords[j].x + '-' + Game.placeShipCoords[j].y); classes = el.getAttribute('class'); // Check if the substring ' grid-ship' already exists to avoid adding it twice if (classes.indexOf(' grid-ship') > -1) { classes = classes.replace(' grid-ship', ''); el.setAttribute('class', classes); } } } }; // Click handler for the Rotate Ship button Game.prototype.toggleRotation = function(e) { // Toggle rotation direction var direction = parseInt(e.target.getAttribute('data-direction'), 10); if (direction === Ship.DIRECTION_VERTICAL) { e.target.setAttribute('data-direction', '1'); Game.placeShipDirection = Ship.DIRECTION_HORIZONTAL; } else if (direction === Ship.DIRECTION_HORIZONTAL) { e.target.setAttribute('data-direction', '0'); Game.placeShipDirection = Ship.DIRECTION_VERTICAL; } }; // Click handler for the Start Game button Game.prototype.startGame = function(e) { var self = e.target.self; var el = document.getElementById('roster-sidebar'); var fn = function() {el.setAttribute('class', 'hidden');}; el.addEventListener(transitionEndEventName(),fn,false); el.setAttribute('class', 'invisible'); self.readyToPlay = true; // Advanced the tutorial step if (gameTutorial.currentStep === 3) { gameTutorial.nextStep(); } el.removeEventListener(transitionEndEventName(),fn,false); }; // Click handler for Restart Game button Game.prototype.restartGame = function(e) { e.target.removeEventListener(e.type, arguments.callee); var self = e.target.self; document.getElementById('restart-sidebar').setAttribute('class', 'hidden'); self.resetFogOfWar(); self.init(); }; // Debugging function used to place all ships and just start Game.prototype.placeRandomly = function(e){ e.target.removeEventListener(e.type, arguments.callee); e.target.self.humanFleet.placeShipsRandomly(); e.target.self.readyToPlay = true; document.getElementById('roster-sidebar').setAttribute('class', 'hidden'); this.setAttribute('class', 'hidden'); }; // Ends placing the current ship Game.prototype.endPlacing = function(shipType) { document.getElementById(shipType).setAttribute('class', 'placed'); // Mark the ship as 'used' Game.usedShips[CONST.AVAILABLE_SHIPS.indexOf(shipType)] = CONST.USED; // Wipe out the variable when you're done with it Game.placeShipDirection = null; Game.placeShipType = ''; Game.placeShipCoords = []; }; // Checks whether or not all ships are done placing // Returns boolean Game.prototype.areAllShipsPlaced = function() { var playerRoster = document.querySelectorAll('.fleet-roster li'); for (var i = 0; i < playerRoster.length; i++) { if (playerRoster[i].getAttribute('class') === 'placed') { continue; } else { return false; } } // Reset temporary variables Game.placeShipDirection = 0; Game.placeShipType = ''; Game.placeShipCoords = []; return true; }; // Resets the fog of war Game.prototype.resetFogOfWar = function() { for (var i = 0; i < Game.size; i++) { for (var j = 0; j < Game.size; j++) { this.humanGrid.updateCell(i, j, 'empty', CONST.HUMAN_PLAYER); this.computerGrid.updateCell(i, j, 'empty', CONST.COMPUTER_PLAYER); } } // Reset all values to indicate the ships are ready to be placed again Game.usedShips = Game.usedShips.map(function(){return CONST.UNUSED;}); }; // Resets CSS styling of the sidebar Game.prototype.resetRosterSidebar = function() { var els = document.querySelector('.fleet-roster').querySelectorAll('li'); for (var i = 0; i < els.length; i++) { els[i].removeAttribute('class'); } if (gameTutorial.showTutorial) { gameTutorial.nextStep(); } else { document.getElementById('roster-sidebar').removeAttribute('class'); } document.getElementById('rotate-button').removeAttribute('class'); document.getElementById('start-game').setAttribute('class', 'hidden'); if (DEBUG_MODE) { document.getElementById('place-randomly').removeAttribute('class'); } }; Game.prototype.showRestartSidebar = function() { var sidebar = document.getElementById('restart-sidebar'); sidebar.setAttribute('class', 'highlight'); // Deregister listeners var computerCells = document.querySelector('.computer-player').childNodes; for (var j = 0; j < computerCells.length; j++) { computerCells[j].removeEventListener('click', this.shootListener, false); } var playerRoster = document.querySelector('.fleet-roster').querySelectorAll('li'); for (var i = 0; i < playerRoster.length; i++) { playerRoster[i].removeEventListener('click', this.rosterListener, false); } var restartButton = document.getElementById('restart-game'); restartButton.addEventListener('click', this.restartGame, false); restartButton.self = this; }; // Generates the HTML divs for the grid for both players Game.prototype.createGrid = function() { var gridDiv = document.querySelectorAll('.grid'); for (var grid = 0; grid < gridDiv.length; grid++) { gridDiv[grid].removeChild(gridDiv[grid].querySelector('.no-js')); // Removes the no-js warning for (var i = 0; i < Game.size; i++) { for (var j = 0; j < Game.size; j++) { var el = document.createElement('div'); el.setAttribute('data-x', i); el.setAttribute('data-y', j); el.setAttribute('class', 'grid-cell grid-cell-' + i + '-' + j); gridDiv[grid].appendChild(el); } } } }; // Initializes the Game. Also resets the game if previously initialized Game.prototype.init = function() { this.humanGrid = new Grid(Game.size); this.computerGrid = new Grid(Game.size); this.humanFleet = new Fleet(this.humanGrid, CONST.HUMAN_PLAYER); this.computerFleet = new Fleet(this.computerGrid, CONST.COMPUTER_PLAYER); this.robot = new AI(this); Game.stats = new Stats(); Game.stats.updateStatsSidebar(); // Reset game variables this.shotsTaken = 0; this.readyToPlay = false; this.placingOnGrid = false; Game.placeShipDirection = 0; Game.placeShipType = ''; Game.placeShipCoords = []; this.resetRosterSidebar(); // Add a click listener for the Grid.shoot() method for all cells // Only add this listener to the computer's grid var computerCells = document.querySelector('.computer-player').childNodes; for (var j = 0; j < computerCells.length; j++) { computerCells[j].self = this; computerCells[j].addEventListener('click', this.shootListener, false); } // Add a click listener to the roster var playerRoster = document.querySelector('.fleet-roster').querySelectorAll('li'); for (var i = 0; i < playerRoster.length; i++) { playerRoster[i].self = this; playerRoster[i].addEventListener('click', this.rosterListener, false); } // Add a click listener to the human player's grid while placing var humanCells = document.querySelector('.human-player').childNodes; for (var k = 0; k < humanCells.length; k++) { humanCells[k].self = this; humanCells[k].addEventListener('click', this.placementListener, false); humanCells[k].addEventListener('mouseover', this.placementMouseover, false); humanCells[k].addEventListener('mouseout', this.placementMouseout, false); } var rotateButton = document.getElementById('rotate-button'); rotateButton.addEventListener('click', this.toggleRotation, false); var startButton = document.getElementById('start-game'); startButton.self = this; startButton.addEventListener('click', this.startGame, false); var resetButton = document.getElementById('reset-stats'); resetButton.addEventListener('click', Game.stats.resetStats, false); var randomButton = document.getElementById('place-randomly'); randomButton.self = this; randomButton.addEventListener('click', this.placeRandomly, false); this.computerFleet.placeShipsRandomly(); }; // Grid object // Constructor function Grid(size) { this.size = size; this.cells = []; this.init(); } // Initialize and populate the grid Grid.prototype.init = function() { for (var x = 0; x < this.size; x++) { var row = []; this.cells[x] = row; for (var y = 0; y < this.size; y++) { row.push(CONST.TYPE_EMPTY); } } }; // Updates the cell's CSS class based on the type passed in Grid.prototype.updateCell = function(x, y, type, targetPlayer) { var player; if (targetPlayer === CONST.HUMAN_PLAYER) { player = 'human-player'; } else if (targetPlayer === CONST.COMPUTER_PLAYER) { player = 'computer-player'; } else { // Should never be called console.log("There was an error trying to find the correct player's grid"); } switch (type) { case CONST.CSS_TYPE_EMPTY: this.cells[x][y] = CONST.TYPE_EMPTY; break; case CONST.CSS_TYPE_SHIP: this.cells[x][y] = CONST.TYPE_SHIP; break; case CONST.CSS_TYPE_MISS: this.cells[x][y] = CONST.TYPE_MISS; break; case CONST.CSS_TYPE_HIT: this.cells[x][y] = CONST.TYPE_HIT; break; case CONST.CSS_TYPE_SUNK: this.cells[x][y] = CONST.TYPE_SUNK; break; default: this.cells[x][y] = CONST.TYPE_EMPTY; break; } var classes = ['grid-cell', 'grid-cell-' + x + '-' + y, 'grid-' + type]; document.querySelector('.' + player + ' .grid-cell-' + x + '-' + y).setAttribute('class', classes.join(' ')); }; // Checks to see if a cell contains an undamaged ship // Returns boolean Grid.prototype.isUndamagedShip = function(x, y) { return this.cells[x][y] === CONST.TYPE_SHIP; }; // Checks to see if the shot was missed. This is equivalent // to checking if a cell contains a cannonball // Returns boolean Grid.prototype.isMiss = function(x, y) { return this.cells[x][y] === CONST.TYPE_MISS; }; // Checks to see if a cell contains a damaged ship, // either hit or sunk. // Returns boolean Grid.prototype.isDamagedShip = function(x, y) { return this.cells[x][y] === CONST.TYPE_HIT || this.cells[x][y] === CONST.TYPE_SUNK; }; // Fleet object // This object is used to keep track of a player's portfolio of ships // Constructor function Fleet(playerGrid, player) { this.numShips = CONST.AVAILABLE_SHIPS.length; this.playerGrid = playerGrid; this.player = player; this.fleetRoster = []; this.populate(); } // Populates a fleet Fleet.prototype.populate = function() { for (var i = 0; i < this.numShips; i++) { // loop over the ship types when numShips > Constants.AVAILABLE_SHIPS.length var j = i % CONST.AVAILABLE_SHIPS.length; this.fleetRoster.push(new Ship(CONST.AVAILABLE_SHIPS[j], this.playerGrid, this.player)); } }; // Places the ship and returns whether or not the placement was successful // Returns boolean Fleet.prototype.placeShip = function(x, y, direction, shipType) { var shipCoords; for (var i = 0; i < this.fleetRoster.length; i++) { var shipTypes = this.fleetRoster[i].type; if (shipType === shipTypes && this.fleetRoster[i].isLegal(x, y, direction)) { this.fleetRoster[i].create(x, y, direction, false); shipCoords = this.fleetRoster[i].getAllShipCells(); for (var j = 0; j < shipCoords.length; j++) { this.playerGrid.updateCell(shipCoords[j].x, shipCoords[j].y, 'ship', this.player); } return true; } } return false; }; // Places ships randomly on the board // TODO: Avoid placing ships too close to each other Fleet.prototype.placeShipsRandomly = function() { var shipCoords; for (var i = 0; i < this.fleetRoster.length; i++) { var illegalPlacement = true; // Prevents the random placement of already placed ships if(this.player === CONST.HUMAN_PLAYER && Game.usedShips[i] === CONST.USED) { continue; } while (illegalPlacement) { var randomX = Math.floor(Game.size * Math.random()); var randomY = Math.floor(Game.size * Math.random()); var randomDirection = Math.floor(2*Math.random()); if (this.fleetRoster[i].isLegal(randomX, randomY, randomDirection)) { this.fleetRoster[i].create(randomX, randomY, randomDirection, false); shipCoords = this.fleetRoster[i].getAllShipCells(); illegalPlacement = false; } else { continue; } } if (this.player === CONST.HUMAN_PLAYER && Game.usedShips[i] !== CONST.USED) { for (var j = 0; j < shipCoords.length; j++) { this.playerGrid.updateCell(shipCoords[j].x, shipCoords[j].y, 'ship', this.player); Game.usedShips[i] = CONST.USED; } } } }; // Finds a ship by location // Returns the ship object located at (x, y) // If no ship exists at (x, y), this returns null instead Fleet.prototype.findShipByCoords = function(x, y) { for (var i = 0; i < this.fleetRoster.length; i++) { var currentShip = this.fleetRoster[i]; if (currentShip.direction === Ship.DIRECTION_VERTICAL) { if (y === currentShip.yPosition && x >= currentShip.xPosition && x < currentShip.xPosition + currentShip.shipLength) { return currentShip; } else { continue; } } else { if (x === currentShip.xPosition && y >= currentShip.yPosition && y < currentShip.yPosition + currentShip.shipLength) { return currentShip; } else { continue; } } } return null; }; // Finds a ship by its type // Param shipType is a string // Returns the ship object that is of type shipType // If no ship exists, this returns null. Fleet.prototype.findShipByType = function(shipType) { for (var i = 0; i < this.fleetRoster.length; i++) { if (this.fleetRoster[i].type === shipType) { return this.fleetRoster[i]; } } return null; }; // Checks to see if all ships have been sunk // Returns boolean Fleet.prototype.allShipsSunk = function() { for (var i = 0; i < this.fleetRoster.length; i++) { // If one or more ships are not sunk, then the sentence "all ships are sunk" is false. if (this.fleetRoster[i].sunk === false) { return false; } } return true; }; // Ship object // Constructor function Ship(type, playerGrid, player) { this.damage = 0; this.type = type; this.playerGrid = playerGrid; this.player = player; switch (this.type) { case CONST.AVAILABLE_SHIPS[0]: this.shipLength = 5; break; case CONST.AVAILABLE_SHIPS[1]: this.shipLength = 4; break; case CONST.AVAILABLE_SHIPS[2]: this.shipLength = 3; break; case CONST.AVAILABLE_SHIPS[3]: this.shipLength = 3; break; case CONST.AVAILABLE_SHIPS[4]: this.shipLength = 2; break; default: this.shipLength = 3; break; } this.maxDamage = this.shipLength; this.sunk = false; } // Checks to see if the placement of a ship is legal // Returns boolean Ship.prototype.isLegal = function(x, y, direction) { // first, check if the ship is within the grid... if (this.withinBounds(x, y, direction)) { // ...then check to make sure it doesn't collide with another ship for (var i = 0; i < this.shipLength; i++) { if (direction === Ship.DIRECTION_VERTICAL) { if (this.playerGrid.cells[x + i][y] === CONST.TYPE_SHIP || this.playerGrid.cells[x + i][y] === CONST.TYPE_MISS || this.playerGrid.cells[x + i][y] === CONST.TYPE_SUNK) { return false; } } else { if (this.playerGrid.cells[x][y + i] === CONST.TYPE_SHIP || this.playerGrid.cells[x][y + i] === CONST.TYPE_MISS || this.playerGrid.cells[x][y + i] === CONST.TYPE_SUNK) { return false; } } } return true; } else { return false; } }; // Checks to see if the ship is within bounds of the grid // Returns boolean Ship.prototype.withinBounds = function(x, y, direction) { if (direction === Ship.DIRECTION_VERTICAL) { return x + this.shipLength <= Game.size; } else { return y + this.shipLength <= Game.size; } }; // Increments the damage counter of a ship // Returns Ship Ship.prototype.incrementDamage = function() { this.damage++; if (this.isSunk()) { this.sinkShip(false); // Sinks the ship } }; // Checks to see if the ship is sunk // Returns boolean Ship.prototype.isSunk = function() { return this.damage >= this.maxDamage; }; // Sinks the ship Ship.prototype.sinkShip = function(virtual) { this.damage = this.maxDamage; // Force the damage to exceed max damage this.sunk = true; // Make the CSS class sunk, but only if the ship is not virtual if (!virtual) { var allCells = this.getAllShipCells(); for (var i = 0; i < this.shipLength; i++) { this.playerGrid.updateCell(allCells[i].x, allCells[i].y, 'sunk', this.player); } } }; /** * Gets all the ship cells * * Returns an array with all (x, y) coordinates of the ship: * e.g. * [ * {'x':2, 'y':2}, * {'x':3, 'y':2}, * {'x':4, 'y':2} * ] */ Ship.prototype.getAllShipCells = function() { var resultObject = []; for (var i = 0; i < this.shipLength; i++) { if (this.direction === Ship.DIRECTION_VERTICAL) { resultObject[i] = {'x': this.xPosition + i, 'y': this.yPosition}; } else { resultObject[i] = {'x': this.xPosition, 'y': this.yPosition + i}; } } return resultObject; }; // Initializes a ship with the given coordinates and direction (bearing). // If the ship is declared "virtual", then the ship gets initialized with // its coordinates but DOESN'T get placed on the grid. Ship.prototype.create = function(x, y, direction, virtual) { // This function assumes that you've already checked that the placement is legal this.xPosition = x; this.yPosition = y; this.direction = direction; // If the ship is virtual, don't add it to the grid. if (!virtual) { for (var i = 0; i < this.shipLength; i++) { if (this.direction === Ship.DIRECTION_VERTICAL) { this.playerGrid.cells[x + i][y] = CONST.TYPE_SHIP; } else { this.playerGrid.cells[x][y + i] = CONST.TYPE_SHIP; } } } }; // direction === 0 when the ship is facing north/south // direction === 1 when the ship is facing east/west Ship.DIRECTION_VERTICAL = 0; Ship.DIRECTION_HORIZONTAL = 1; // Tutorial Object // Constructor function Tutorial() { this.currentStep = 0; // Check if 'showTutorial' is initialized, if it's uninitialized, set it to true. this.showTutorial = localStorage.getItem('showTutorial') !== 'false'; } // Advances the tutorial to the next step Tutorial.prototype.nextStep = function() { var humanGrid = document.querySelector('.human-player'); var computerGrid = document.querySelector('.computer-player'); switch (this.currentStep) { case 0: document.getElementById('roster-sidebar').setAttribute('class', 'highlight'); document.getElementById('step1').setAttribute('class', 'current-step'); this.currentStep++; break; case 1: document.getElementById('roster-sidebar').removeAttribute('class'); document.getElementById('step1').removeAttribute('class'); humanGrid.setAttribute('class', humanGrid.getAttribute('class') + ' highlight'); document.getElementById('step2').setAttribute('class', 'current-step'); this.currentStep++; break; case 2: document.getElementById('step2').removeAttribute('class'); var humanClasses = humanGrid.getAttribute('class'); humanClasses = humanClasses.replace(' highlight', ''); humanGrid.setAttribute('class', humanClasses); this.currentStep++; break; case 3: computerGrid.setAttribute('class', computerGrid.getAttribute('class') + ' highlight'); document.getElementById('step3').setAttribute('class', 'current-step'); this.currentStep++; break; case 4: var computerClasses = computerGrid.getAttribute('class'); document.getElementById('step3').removeAttribute('class'); computerClasses = computerClasses.replace(' highlight', ''); computerGrid.setAttribute('class', computerClasses); document.getElementById('step4').setAttribute('class', 'current-step'); this.currentStep++; break; case 5: document.getElementById('step4').removeAttribute('class'); this.currentStep = 6; this.showTutorial = false; localStorage.setItem('showTutorial', false); break; default: break; } }; // AI Object // Optimal battleship-playing AI // Constructor function AI(gameObject) { this.gameObject = gameObject; this.virtualGrid = new Grid(Game.size); this.virtualFleet = new Fleet(this.virtualGrid, CONST.VIRTUAL_PLAYER); this.probGrid = []; // Probability Grid this.initProbs(); this.updateProbs(); } AI.PROB_WEIGHT = 5000; // arbitrarily big number // how much weight to give to the opening book's high probability cells AI.OPEN_HIGH_MIN = 20; AI.OPEN_HIGH_MAX = 30; // how much weight to give to the opening book's medium probability cells AI.OPEN_MED_MIN = 15; AI.OPEN_MED_MAX = 25; // how much weight to give to the opening book's low probability cells AI.OPEN_LOW_MIN = 10; AI.OPEN_LOW_MAX = 20; // Amount of randomness when selecting between cells of equal probability AI.RANDOMNESS = 0.1; // AI's opening book. // This is the pattern of the first cells for the AI to target AI.OPENINGS = [ {'x': 7, 'y': 3, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 6, 'y': 2, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 3, 'y': 7, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 2, 'y': 6, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 6, 'y': 6, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 3, 'y': 3, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 5, 'y': 5, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, {'x': 4, 'y': 4, 'weight': getRandom(AI.OPEN_LOW_MIN, AI.OPEN_LOW_MAX)}, // {'x': 9, 'y': 5, 'weight': getRandom(AI.OPEN_MED_MIN, AI.OPEN_MED_MAX)}, // {'x': 0, 'y': 4, 'weight': getRandom(AI.OPEN_MED_MIN, AI.OPEN_MED_MAX)}, // {'x': 5, 'y': 9, 'weight': getRandom(AI.OPEN_MED_MIN, AI.OPEN_MED_MAX)}, // {'x': 4, 'y': 0, 'weight': getRandom(AI.OPEN_MED_MIN, AI.OPEN_MED_MAX)}, {'x': 0, 'y': 8, 'weight': getRandom(AI.OPEN_MED_MIN, AI.OPEN_MED_MAX)}, {'x': 1, 'y': 9, 'weight': getRandom(AI.OPEN_HIGH_MIN, AI.OPEN_HIGH_MAX)}, {'x': 8, 'y': 0, 'weight': getRandom(AI.OPEN_MED_MIN, AI.OPEN_MED_MAX)}, {'x': 9, 'y': 1, 'weight': getRandom(AI.OPEN_HIGH_MIN, AI.OPEN_HIGH_MAX)}, {'x': 9, 'y': 9, 'weight': getRandom(AI.OPEN_HIGH_MIN, AI.OPEN_HIGH_MAX)}, {'x': 0, 'y': 0, 'weight': getRandom(AI.OPEN_HIGH_MIN, AI.OPEN_HIGH_MAX)} ]; // Scouts the grid based on max probability, and shoots at the cell // that has the highest probability of containing a ship AI.prototype.shoot = function() { var maxProbability = 0; var maxProbCoords; var maxProbs = []; // Add the AI's opening book to the probability grid for (var i = 0; i < AI.OPENINGS.length; i++) { var cell = AI.OPENINGS[i]; if (this.probGrid[cell.x][cell.y] !== 0) { this.probGrid[cell.x][cell.y] += cell.weight; } } for (var x = 0; x < Game.size; x++) { for (var y = 0; y < Game.size; y++) { if (this.probGrid[x][y] > maxProbability) { maxProbability = this.probGrid[x][y]; maxProbs = [{'x': x, 'y': y}]; // Replace the array } else if (this.probGrid[x][y] === maxProbability) { maxProbs.push({'x': x, 'y': y}); } } } maxProbCoords = Math.random() < AI.RANDOMNESS ? maxProbs[Math.floor(Math.random() * maxProbs.length)] : maxProbs[0]; var result = this.gameObject.shoot(maxProbCoords.x, maxProbCoords.y, CONST.HUMAN_PLAYER); // If the game ends, the next lines need to be skipped. if (Game.gameOver) { Game.gameOver = false; return; } this.virtualGrid.cells[maxProbCoords.x][maxProbCoords.y] = result; // If you hit a ship, check to make sure if you've sunk it. if (result === CONST.TYPE_HIT) { var humanShip = this.findHumanShip(maxProbCoords.x, maxProbCoords.y); if (humanShip.isSunk()) { // Remove any ships from the roster that have been sunk var shipTypes = []; for (var k = 0; k < this.virtualFleet.fleetRoster.length; k++) { shipTypes.push(this.virtualFleet.fleetRoster[k].type); } var index = shipTypes.indexOf(humanShip.type); this.virtualFleet.fleetRoster.splice(index, 1); // Update the virtual grid with the sunk ship's cells var shipCells = humanShip.getAllShipCells(); for (var _i = 0; _i < shipCells.length; _i++) { this.virtualGrid.cells[shipCells[_i].x][shipCells[_i].y] = CONST.TYPE_SUNK; } } } // Update probability grid after each shot this.updateProbs(); }; // Update the probability grid AI.prototype.updateProbs = function() { var roster = this.virtualFleet.fleetRoster; var coords; this.resetProbs(); // Probabilities are not normalized to fit in the interval [0, 1] // because we're only interested in the maximum value. // This works by trying to fit each ship in each cell in every orientation // For every cell, the more legal ways a ship can pass through it, the more // likely the cell is to contain a ship. // Cells that surround known 'hits' are given an arbitrarily large probability // so that the AI tries to completely sink the ship before moving on. // TODO: Think about a more efficient implementation for (var k = 0; k < roster.length; k++) { for (var x = 0; x < Game.size; x++) { for (var y = 0; y < Game.size; y++) { if (roster[k].isLegal(x, y, Ship.DIRECTION_VERTICAL)) { roster[k].create(x, y, Ship.DIRECTION_VERTICAL, true); coords = roster[k].getAllShipCells(); if (this.passesThroughHitCell(coords)) { for (var i = 0; i < coords.length; i++) { this.probGrid[coords[i].x][coords[i].y] += AI.PROB_WEIGHT * this.numHitCellsCovered(coords); } } else { for (var _i = 0; _i < coords.length; _i++) { this.probGrid[coords[_i].x][coords[_i].y]++; } } } if (roster[k].isLegal(x, y, Ship.DIRECTION_HORIZONTAL)) { roster[k].create(x, y, Ship.DIRECTION_HORIZONTAL, true); coords = roster[k].getAllShipCells(); if (this.passesThroughHitCell(coords)) { for (var j = 0; j < coords.length; j++) { this.probGrid[coords[j].x][coords[j].y] += AI.PROB_WEIGHT * this.numHitCellsCovered(coords); } } else { for (var _j = 0; _j < coords.length; _j++) { this.probGrid[coords[_j].x][coords[_j].y]++; } } } // Set hit cells to probability zero so the AI doesn't // target cells that are already hit if (this.virtualGrid.cells[x][y] === CONST.TYPE_HIT) { this.probGrid[x][y] = 0; } } } } }; // Initializes the probability grid for targeting AI.prototype.initProbs = function() { for (var x = 0; x < Game.size; x++) { var row = []; this.probGrid[x] = row; for (var y = 0; y < Game.size; y++) { row.push(0); } } }; // Resets the probability grid to all 0. AI.prototype.resetProbs = function() { for (var x = 0; x < Game.size; x++) { for (var y = 0; y < Game.size; y++) { this.probGrid[x][y] = 0; } } }; AI.prototype.metagame = function() { // Inputs: // Proximity of hit cells to edge // Proximity of hit cells to each other // Edit the probability grid by multiplying each cell with a new probability weight (e.g. 0.4, or 3). Set this as a CONST and make 1-CONST the inverse for decreasing, or 2*CONST for increasing }; // Finds a human ship by coordinates // Returns Ship AI.prototype.findHumanShip = function(x, y) { return this.gameObject.humanFleet.findShipByCoords(x, y); }; // Checks whether or not a given ship's cells passes through // any cell that is hit. // Returns boolean AI.prototype.passesThroughHitCell = function(shipCells) { for (var i = 0; i < shipCells.length; i++) { if (this.virtualGrid.cells[shipCells[i].x][shipCells[i].y] === CONST.TYPE_HIT) { return true; } } return false; }; // Gives the number of hit cells the ships passes through. The more // cells this is, the more probable the ship exists in those coordinates // Returns int AI.prototype.numHitCellsCovered = function(shipCells) { var cells = 0; for (var i = 0; i < shipCells.length; i++) { if (this.virtualGrid.cells[shipCells[i].x][shipCells[i].y] === CONST.TYPE_HIT) { cells++; } } return cells; }; // Global constant only initialized once var gameTutorial = new Tutorial(); // Start the game var mainGame = new Game(10); })(); // Array.prototype.indexOf workaround for IE browsers that don't support it // From MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (searchElement, fromIndex) { var k; // 1. Let O be the result of calling ToObject passing // the this value as the argument. if (this === null || this === undefined) { throw new TypeError('"this" is null or not defined'); } var O = Object(this); // 2. Let lenValue be the result of calling the Get // internal method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; // 4. If len is 0, return -1. if (len === 0) { return -1; } // 5. If argument fromIndex was passed let n be // ToInteger(fromIndex); else let n be 0. var n = +fromIndex || 0; if (Math.abs(n) === Infinity) { n = 0; } // 6. If n >= len, return -1. if (n >= len) { return -1; } // 7. If n >= 0, then Let k be n. // 8. Else, n<0, Let k be len - abs(n). // If k is less than 0, then let k be 0. k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); // 9. Repeat, while k < len while (k < len) { var kValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the // HasProperty internal method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then // i. Let elementK be the result of calling the Get // internal method of O with the argument ToString(k). // ii. Let same be the result of applying the // Strict Equality Comparison Algorithm to // searchElement and elementK. // iii. If same is true, return k. if (k in O && O[k] === searchElement) { return k; } k++; } return -1; }; } // Array.prototype.map workaround for IE browsers that don't support it // From MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.io/#x15.4.4.19 if (!Array.prototype.map) { Array.prototype.map = function(callback, thisArg) { var T, A, k; if (this == null) { throw new TypeError(" this is null or not defined"); } // 1. Let O be the result of calling ToObject passing the |this| // value as the argument. var O = Object(this); // 2. Let lenValue be the result of calling the Get internal // method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; // 4. If IsCallable(callback) is false, throw a TypeError exception. // See: http://es5.github.com/#x9.11 if (typeof callback !== "function") { throw new TypeError(callback + " is not a function"); } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. if (arguments.length > 1) { T = thisArg; } // 6. Let A be a new array created as if by the expression new Array(len) // where Array is the standard built-in constructor with that name and // len is the value of len. A = new Array(len); // 7. Let k be 0 k = 0; // 8. Repeat, while k < len while (k < len) { var kValue, mappedValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the HasProperty internal // method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { // i. Let kValue be the result of calling the Get internal // method of O with argument Pk. kValue = O[k]; // ii. Let mappedValue be the result of calling the Call internal // method of callback with T as the this value and argument // list containing kValue, k, and O. mappedValue = callback.call(T, kValue, k, O); // iii. Call the DefineOwnProperty internal method of A with arguments // Pk, Property Descriptor // { Value: mappedValue, // Writable: true, // Enumerable: true, // Configurable: true }, // and false. // In browsers that support Object.defineProperty, use the following: // Object.defineProperty(A, k, { // value: mappedValue, // writable: true, // enumerable: true, // configurable: true // }); // For best browser support, use the following: A[k] = mappedValue; } // d. Increase k by 1. k++; } // 9. return A return A; }; } // Browser compatability workaround for transition end event names. // From modernizr: http://stackoverflow.com/a/9090128 function transitionEndEventName() { var i, undefined, el = document.createElement('div'), transitions = { 'transition':'transitionend', 'OTransition':'otransitionend', // oTransitionEnd in very old Opera 'MozTransition':'transitionend', 'WebkitTransition':'webkitTransitionEnd' }; for (i in transitions) { if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) { return transitions[i]; } } } // Returns a random number between min (inclusive) and max (exclusive) function getRandom(min, max) { return Math.random() * (max - min) + min; } // Toggles on or off DEBUG_MODE function setDebug(val) { DEBUG_MODE = val; localStorage.setItem('DEBUG_MODE', val); localStorage.setItem('showTutorial', 'false'); window.location.reload(); }