battleship/参考项目/battleboat/battleboat.js
2022-09-14 17:34:39 +08:00

1372 lines
45 KiB
JavaScript

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