working version

This commit is contained in:
ak 2023-08-02 18:18:27 -07:00
parent 7c8e1ad7d0
commit 5b5fbc5775
36 changed files with 17670 additions and 1 deletions

11
.babelrc Normal file
View file

@ -0,0 +1,11 @@
{
"plugins": ["@babel/syntax-dynamic-import"],
"presets": [
[
"@babel/preset-env",
{
"modules": "auto"
}
]
]
}

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
.eslint*
.prettier*

15
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View file

@ -1,3 +1,3 @@
# js-battleship # js-battleship
Battleship implemented in ES6 via TDD Battleship implemented in ES6

3
babel.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};

BIN
dist/195d86f550b47476dbb8.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

BIN
dist/4a2cd718d7031b732e76.ttf vendored Normal file

Binary file not shown.

BIN
dist/bb975c966c37455a1bc3.woff2 vendored Normal file

Binary file not shown.

38
dist/index.html vendored Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="Description" content="Battleship"/>
<title>Battleship</title>
<script defer src="main.js"></script></head>
<body class="d-flex flex-column text-center text-white">
<div class="cover-container d-flex p-3 mx-auto flex-column">
<header class="mb-3">
<h3 class="float-md-start mb-0">Battleship</h3>
<nav class="nav nav-masthead justify-content-center float-md-end gap-2">
<button type="button" class="btn newbtn">
New
</button>
</nav>
</header>
<div class="rounded-3 px-5 pb-5 pt-1">
<div class="d-flex gap-5">
<div class="d-flex justify-content-center py-1 align-items-center">
<div class="text-success">Player</div>
</div>
<div class="d-flex justify-content-center py-1 align-items-center">
<div class="text-danger">Enemy</div>
</div>
</div>
<div class="d-flex gap-5" id="boards">
<div class="text-black" id="playerboard">
</div>
<div class="text-black" id="enemyboard">
</div>
</div>
</div>
</div>
</body>
</html>

1157
dist/main.js vendored Normal file

File diff suppressed because one or more lines are too long

1
jest-setup.js Normal file
View file

@ -0,0 +1 @@
import '@testing-library/jest-dom'

1
jest.config.js Normal file
View file

@ -0,0 +1 @@
setupFilesAfterEnv: ['<rootDir>/jest-setup.js']

15360
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

48
package.json Normal file
View file

@ -0,0 +1,48 @@
{
"name": "js-battleship",
"version": "1.0.0",
"description": "Battleship implemented in ES6 via TDD",
"main": "index.js",
"scripts": {
"start": "webpack serve",
"test": "jest",
"watch": "jest --watch src/js/*.test.js",
"build": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@testing-library/jest-dom": "^5.17.0",
"@webpack-cli/generators": "^3.0.7",
"autoprefixer": "^10.4.14",
"babel-jest": "^29.6.1",
"babel-loader": "^9.1.2",
"css-loader": "^6.8.1",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"html-webpack-plugin": "^5.5.3",
"jest": "^29.6.1",
"jest-environment-jsdom": "^29.6.2",
"mini-css-extract-plugin": "^2.7.6",
"postcss": "^8.4.27",
"postcss-loader": "^7.3.3",
"prettier": "^3.0.0",
"sass": "^1.64.1",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.1"
}
}

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
// Add you postcss configuration here
// Learn more about it at https://github.com/webpack-contrib/postcss-loader#config-files
plugins: [["autoprefixer"]],
};

BIN
src/images/battleship1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

BIN
src/images/battleship2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

BIN
src/images/battleship3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

BIN
src/images/battleship4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

BIN
src/images/battleship5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

BIN
src/images/battleship6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

BIN
src/images/battleship7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

38
src/index.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="Description" content="Battleship"/>
<title>Battleship</title>
</head>
<body class="d-flex flex-column text-center text-white">
<div class="cover-container d-flex p-3 mx-auto flex-column">
<header class="mb-3">
<h3 class="float-md-start mb-0">Battleship</h3>
<nav class="nav nav-masthead justify-content-center float-md-end gap-2">
<button type="button" class="btn newbtn">
New
</button>
</nav>
</header>
<div class="rounded-3 px-5 pb-5 pt-1">
<div class="d-flex gap-5">
<div class="d-flex justify-content-center py-1 align-items-center">
<div class="text-success">Player</div>
</div>
<div class="d-flex justify-content-center py-1 align-items-center">
<div class="text-danger">Enemy</div>
</div>
</div>
<div class="d-flex gap-5" id="boards">
<div class="text-black" id="playerboard">
</div>
<div class="text-black" id="enemyboard">
</div>
</div>
</div>
</div>
</body>
</html>

56
src/js/Game.js Normal file
View file

@ -0,0 +1,56 @@
import { Player } from "./Player.js";
import { UI } from "./UI.js";
// The game loop should set up a new game by creating Players and Gameboards
export const Game = () => {
const bot = Player("AI");
const user = Player("Steve");
// populate each Gameboard
// bot goes first
let dir, y, x;
function roll() {
// math.ceil() because the function needs to hit 4 and 9 inclusively more often than not
dir = Math.ceil(Math.random() * 4);
y = Math.ceil(Math.random() * 9);
x = Math.ceil(Math.random() * 9);
}
// for all ship sizes
for (let i = 5; i > 0; i--) {
let length;
i == 1 ? (length = 3) : (length = i);
// perpetual loop
while (true) {
// if valid, place ship and stop loop
try {
bot.board.placeShip(y, x, length, dir);
break;
} catch {
// otherwise re-roll variables and try again
roll();
continue;
}
}
}
(async function start() {
const DOM = UI(user, bot);
await DOM.initialized;
// Create conditions so that the game ends once one players ships have all been sunk.
let aiTurn = false;
while (bot.board.allSunk() === false && user.board.allSunk() === false) {
if (aiTurn === true) {
aiTurn = false;
DOM.aiTurnPopup();
await DOM.aiTurnOkayed();
bot.move(user);
DOM.displayBoard(user.board.board);
} else {
aiTurn = true;
DOM.playerTurnPopup();
let target = await DOM.playerTurnFinished();
user.move(bot, target[0], target[1]);
DOM.displayBoard(bot.board.board);
}
}
if (bot.board.allSunk()) DOM.displayWinner(user);
else if (user.board.allSunk()) DOM.displayWinner(bot);
})();
};

100
src/js/Gameboard.js Normal file
View file

@ -0,0 +1,100 @@
import { Ship } from "./Ship.js";
import { square } from "./square.js";
// game board factory
export const Gameboard = () => {
const ships = [],
misses = [];
// initializes board
const init = (size) => {
const yarr = [];
for (let y = 0; y <= size - 1; y++) {
const xarr = [];
for (let x = 0; x <= size - 1; x++) {
xarr.push(square(y, x));
}
yarr.push(xarr);
}
return yarr.flat();
};
const board = init(10);
// finds specified square
const find = (y, x) => {
for (let i = 0; i < board.length; i++) {
if (board[i].y === y && board[i].x === x) {
return board[i];
}
}
return null;
};
// places ships at specific coordinates
const placeShip = (y, x, length, direction) => {
if ((!x && x != 0) || typeof x != "number")
throw new Error("x-coordinate invalid!");
if ((!y && y != 0) || typeof y != "number")
throw new Error("y-coordinate invalid!");
if (!length || typeof length != "number" || length === 0)
throw new Error("Ship length invalid!");
if (!direction) throw new Error("Ship direction not provided!");
// check for valid squares first
const valid = [];
for (let i = 0; i < length; i++) {
// ship can go one of eight directions
let found;
if (direction === "N" || direction === 1) found = find(y + i, x);
else if (direction === "E" || direction === 2) found = find(y, x + i);
else if (direction === "S" || direction === 3) found = find(y - i, x);
else if (direction === "W" || direction === 4) found = find(y, x - i);
// if direction not one of the above, throw error
else if (direction) throw new Error("Ship direction invalid!");
// if square is not found on the board, throw out of bounds error
if (!found) throw new Error("Ship position out of bounds!");
// by default an empty square has null for a ship. if it exists, a ship is already occupying this square
if (found.ship)
throw new Error("Ship position conflicts with another ship!");
// otherwise push to valid squares array
valid.push(found);
}
// assuming error-free execution,
// make new ship of length and add to global ships variable
const ship = Ship(length);
ships.push(ship);
// each valid square's ship property is set to current ship
valid.forEach((sq) => (sq.ship = ship));
};
// receive attack and process misses or hits
const receiveAttack = (y, x) => {
if ((!x && x != 0) || typeof x != "number")
throw new Error("x-coordinate invalid!");
if ((!y && y != 0) || typeof y != "number")
throw new Error("y-coordinate invalid!");
const found = find(y, x);
if (!found) throw new Error("Attack coordinates out of bounds!");
if (!found.ship) {
found.shot = true;
misses.push(found);
} else {
found.ship.hit();
found.shot = true;
}
};
// check whether all ships are sunk
const allSunk = () => {
let sunk = 0;
for (let i = 0; i < ships.length; i++) {
if (ships[i].isSunk()) sunk++;
}
if (sunk === ships.length) {
return true;
}
return false;
};
return {
board,
placeShip,
receiveAttack,
misses,
allSunk,
find,
ships,
};
};

100
src/js/Gameboard.test.js Normal file
View file

@ -0,0 +1,100 @@
// game board factory
import { Gameboard } from "./Gameboard.js";
test("Gameboard returns .board object", () => {
expect(Gameboard().board).toEqual(expect.any(Object));
});
test(".board object has 10x10 entries for board grid", () => {
expect(Gameboard().board.length).toEqual(100);
});
test("Gameboard returns .placeShip() function", () => {
expect(Gameboard().placeShip).toEqual(expect.any(Function));
});
test(".placeShip() function throws error if any argument is not provided", () => {
expect(() => Gameboard().placeShip(null, 0, 4, "N")).toThrow();
expect(() => Gameboard().placeShip(0, null, 4, "N")).toThrow();
expect(() => Gameboard().placeShip(0, 0, null, "N")).toThrow();
expect(() => Gameboard().placeShip(0, 0, 4, null)).toThrow();
});
test(".placeShip() function throws error if ship length is 0", () => {
expect(() => Gameboard().placeShip(0, 0, 0, "N")).toThrow();
});
// syntax is compass syntax - 'N, NE, etc.'
test(".placeShip() function throws error if direction argument is invalid", () => {
expect(() => Gameboard().placeShip(0, 0, 4, "bogusDirection")).toThrow(
"Ship direction invalid!",
);
});
test(".placeShip() function throws error if ship starting position is out-of-bounds", () => {
expect(() => Gameboard().placeShip(1, 10, 4, "N")).toThrow(
"Ship position out of bounds!",
);
});
test(".placeShip() function throws error if ship starting position is already occupied by another ship", () => {
const current = Gameboard();
current.placeShip(0, 0, 4, "N");
expect(() => current.placeShip(4, 0, 3, "S")).toThrow(
"Ship position conflicts with another ship!",
);
});
test(".placeShip() function marks relevant squares as being occupied by the ship", () => {
const current = Gameboard();
current.placeShip(0, 0, 4, "N");
const testSquare = current.board[0];
expect(testSquare.ship.length).toBe(4);
});
test("Gameboard returns .receiveAttack() function", () => {
expect(Gameboard().receiveAttack).toEqual(expect.any(Function));
});
test(".receiveAttack() function throws error if arguments are not provided or invalid", () => {
expect(() => Gameboard().receiveAttack(null, 8)).toThrow();
expect(() => Gameboard().receiveAttack(8, null)).toThrow();
});
test(".receiveAttack() function throws error if coordinates are out of bounds", () => {
expect(() => Gameboard().receiveAttack(11, 11)).toThrow();
});
test(".receiveAttack() function registers missed hits as misses", () => {
const current = Gameboard();
current.placeShip(0, 0, 4, "N");
current.receiveAttack(1, 5);
expect(current.misses.length).toBeGreaterThan(0);
});
test(".receiveAttack() function registers on-target hits and sinks ships", () => {
const current = Gameboard();
current.placeShip(0, 0, 4, "E");
current.receiveAttack(0, 0);
current.receiveAttack(0, 1);
current.receiveAttack(0, 2);
current.receiveAttack(0, 3);
const testSquare = current.board[0];
expect(testSquare.ship.isSunk()).toBe(true);
});
test("Gameboard can report whether or not all ships have been sunk", () => {
const current = Gameboard();
current.placeShip(0, 0, 4, "E");
current.placeShip(1, 0, 4, "E");
expect(current.allSunk()).toBe(false);
current.receiveAttack(0, 0);
current.receiveAttack(0, 1);
current.receiveAttack(0, 2);
current.receiveAttack(0, 3);
expect(current.allSunk()).toBe(false);
current.receiveAttack(1, 0);
current.receiveAttack(1, 1);
current.receiveAttack(1, 2);
current.receiveAttack(1, 3);
expect(current.allSunk()).toBe(true);
});
test("Gameboard can return square at specified x and y coordinates", () => {
const current = Gameboard();
current.placeShip(0, 0, 2, "N");
current.receiveAttack(0, 0);
expect(current.find(0, 0).x).toBe(0);
expect(current.find(0, 0).y).toBe(0);
});
test("Gameboard returns .ships array", () => {
const current = Gameboard();
current.placeShip(0, 0, 2, "N");
expect(current.ships).toEqual(expect.any(Array));
expect(current.ships[0].length).toBe(2);
});

40
src/js/Player.js Normal file
View file

@ -0,0 +1,40 @@
import { Gameboard } from "./Gameboard.js";
// Players can take turns playing the game by attacking the enemy Gameboard
export const Player = (name = "AI") => {
// initializes Player's gameboard
const board = Gameboard();
// for AI only, initialize moves array
const moves = [];
// move function takes an enemy and coordinates as args
const move = (enemy, y = null, x = null) => {
// if Player is AI, do a random move
if (name === "AI") {
(function roll() {
y = Math.round(Math.random() * 9);
x = Math.round(Math.random() * 9);
})();
function unique() {
let bool = true;
for (let i = 0; i < moves.length; i++) {
if (moves[i][0] === y && moves[i][1] === x) {
bool = false;
}
}
return bool;
}
while (!unique) roll();
// push random move to moves array and FIRE!
moves.push(new Array(y, x));
enemy.board.receiveAttack(y, x);
return;
}
// otherwise get coords from user - these will be passed in from UI
enemy.board.receiveAttack(y, x);
};
return {
board,
move,
name,
};
};

22
src/js/Player.test.js Normal file
View file

@ -0,0 +1,22 @@
import { Player } from "./Player.js";
import { Gameboard } from "./Gameboard.js";
// Players can take turns playing the game by attacking the enemy Gameboard
test("Player returns object", () => {
expect(Player("Steve")).toEqual(expect.any(Object));
});
test("Player returns Gameboard object", () => {
expect(JSON.stringify(Player("Steve").board)).toEqual(
JSON.stringify(Gameboard()),
);
});
test("Player returns move function", () => {
expect(Player("Steve").move).toEqual(expect.any(Function));
});
test("AI Player move affects opponent Gameboard", () => {
const bot = Player("AI");
const user = Player("Steve");
bot.move(user);
// expect the modified Gameboard to not equal a blank Gameboard
expect(JSON.stringify(user.board)).not.toEqual(JSON.stringify(Gameboard()));
});

23
src/js/Ship.js Normal file
View file

@ -0,0 +1,23 @@
// ship factory function - takes length as an argument
export const Ship = (length) => {
//let variable;
let hits = 0;
const getHits = () => {
return hits;
};
// increase by one when hit
function hit() {
hits++;
}
// if hits are at or more than length, ship is sunk
const isSunk = () => {
if (hits >= length) return true;
else return false;
};
return {
length,
hit,
getHits,
isSunk,
};
};

19
src/js/Ship.test.js Normal file
View file

@ -0,0 +1,19 @@
// ship factory function
// ships to include length, number of times hit, and whether or not they have been sunk
import { Ship } from "./Ship.js";
test("Ship is created when passed length argument", () => {
expect(Ship(4)).toEqual(expect.any(Object));
});
test("Ship outputs length", () => {
expect(Ship(4).length).toBe(4);
});
test("Ship is sunk if full length hit", () => {
const destroyer = Ship(2);
destroyer.hit();
destroyer.hit();
expect(destroyer.isSunk()).toBe(true);
});
test("Ship outputs hits", () => {
expect(Ship(4).getHits()).toBe(0);
});

362
src/js/UI.js Normal file
View file

@ -0,0 +1,362 @@
// UI displays Gameboards
import { start } from "@popperjs/core";
import { square } from "./square";
export const UI = (player, enemy) => {
// variables to smooth operations
let length, placing, turnOkayed, playerInputTime, playerMoved, playerMove;
let unplaced = true;
const currentShip = {};
// highlights nav button based on mouse hover
const navButtonInit = () => {
// gets buttons
const navButtons = [document.querySelector(".newbtn")];
// button hovered over
function activate() {
this.className = this.className + " active";
}
// mouse moved off button
function deactivate() {
this.className = this.className.replace(" active", "");
}
// assign for all buttons
navButtons.forEach((button) => {
button.onmouseenter = activate;
button.onmouseleave = deactivate;
});
// initialize 'New' button
document.querySelector(".newbtn").onclick = function () {
location.reload();
};
};
// displays board
function displayBoard(board) {
// clear board when displaying (refresh function)
board === player.board.board
? (document.querySelector("#playerboard").innerHTML = "")
: (document.querySelector("#enemyboard").innerHTML = "");
// for board length
for (let i = 0; i < board.length; i++) {
// make square with coordinates
let square = document.createElement("div");
square.className =
"griditem d-flex justify-content-center align-items-center";
if (board === enemy.board.board) {
// enemy board
square.id = `e${board[i].y}${board[i].x}`;
if (board[i].shot) {
// if square has been shot
let indicator;
if (board[i].ship) {
// if it's a hit
indicator = document.createElement("i");
indicator.className = "fa-solid fa-bullseye fa-2x";
} else {
// if it's a miss
indicator = document.createElement("i");
indicator.className = "fa-solid fa-xmark fa-2x";
}
square.appendChild(indicator);
} else square.onclick = () => clicked(square, enemy.board);
document.querySelector("#enemyboard").appendChild(square);
} else {
// if board is player board
square.id = `p${board[i].y}${board[i].x}`;
let indicator;
if (board[i].shot) {
// if square has been shot
if (board[i].ship) {
// if it's a hit
indicator = document.createElement("i");
indicator.className = "fa-solid fa-fire fa-2x";
} else {
// if it's a miss
indicator = document.createElement("i");
indicator.className = "fa-solid fa-xmark fa-2x";
}
} else if (board[i].ship) {
// else if square is unshot but has a player ship
indicator = document.createElement("i");
// if any ship is unplaced, that is if player is still placing ships, do fade pulse animation otherwise solid
unplaced
? (indicator.className = "fa-solid fa-anchor fa-2x fa-beat")
: (indicator.className = "fa-solid fa-anchor fa-2x");
}
if (indicator)
square.appendChild(
indicator,
); // if any of the previous conditions initialized indicator, append
else square.onclick = () => clicked(square, player.board);
document.querySelector("#playerboard").appendChild(square);
}
}
}
// creates basic window and returns main div to append to, as well as title div and custom exit callback
const makeWindow = () => {
// create custom close function callback variable
let callback;
// make divs
const darken = document.createElement("div");
darken.className = "darken";
document.querySelector("body").appendChild(darken);
const card = document.createElement("div");
card.className = "card";
darken.appendChild(card);
// main div to append to
const main = document.createElement("div");
main.className = "card-body";
card.appendChild(main);
// title div
const title = document.createElement("h6");
title.className = "card-text my-3";
main.appendChild(title);
// okay button/close button
const okayButton = document.createElement("div");
okayButton.className = "btn btn-success";
okayButton.textContent = "Ok";
main.appendChild(okayButton);
okayButton.onclick = close;
// detects Enter keystroke
document.addEventListener("keypress", function enterHandler(e) {
if (e.code === "Enter") {
if (document.querySelector("body").lastChild.className === "darken") {
e.preventDefault();
document.removeEventListener("keypress", enterHandler);
okayButton.click();
}
}
});
function customClose(fn) {
callback = fn;
}
function close() {
document
.querySelector("body")
.removeChild(document.querySelector(".darken"));
if (callback) callback();
}
return {
main,
title,
customClose,
};
};
// gets player name
function getPlayerName() {
// makes window
let enchilada = makeWindow();
enchilada.title.textContent = "Welcome! Please enter your name:";
// gets input
enchilada.main.insertBefore(
document.createElement("br"),
enchilada.title.nextSibling,
);
const nameInput = document.createElement("input");
nameInput.className = "mb-3";
nameInput.setAttribute("type", "text");
enchilada.main.insertBefore(nameInput, enchilada.title.nextSibling);
nameInput.focus();
// sets name and passes off to placeShip(5) -- carrier
function finish() {
player.name = nameInput.value;
const playerNameUI = document.querySelector(".text-success");
playerNameUI.textContent = player.name;
while (playerNameUI.offsetWidth > 500) {
const currWeight = window
.getComputedStyle(playerNameUI)
.fontSize.replace("px", "");
if (currWeight === 1) {
break;
}
const newWeight = currWeight - 1 + "px";
playerNameUI.style.fontSize = newWeight;
}
length = 5;
displayBoard(player.board.board);
startPlacement();
}
enchilada.customClose(finish);
}
// displays ship placement tooltip window and begins ship placement
function startPlacement() {
let enchilada = makeWindow();
// change based on length
switch (length) {
case 5:
currentShip.name = "Carrier";
currentShip.size = 5;
const greeter = document.createElement("h6"); // add a greeter since carrier is always placed first
greeter.className = "card-text my-3";
greeter.innerHTML = `Welcome, ${
document.querySelector(".text-success").textContent
}! Please begin by placing your ships.`;
enchilada.main.insertBefore(greeter, enchilada.title);
unplaced = true; // set unplaced to true until all ships placed
break;
case 4:
currentShip.name = "Battleship";
currentShip.size = 4;
break;
case 3:
currentShip.name = "Cruiser";
currentShip.size = 3;
break;
case 2:
currentShip.name = "Submarine";
currentShip.size = 3;
break;
case 1:
currentShip.name = "Destroyer";
currentShip.size = 2;
}
enchilada.title.innerHTML = `Please choose starting square for a ${currentShip.name} sized ${currentShip.size} squares.`;
function finish() {
// sets placing to true
placing = true;
}
enchilada.customClose(finish);
}
// ends placement by hooking into Game() functionality
function endPlacement(square) {
let enchilada = makeWindow();
enchilada.title.textContent = `Next, please choose a direction for your ${currentShip.name} sized ${currentShip.size} squares.`;
const directions = document.createElement("select");
directions.className = "form-select";
const def = document.createElement("option");
def.selected = true;
def.textContent = "Direction";
directions.appendChild(def);
const dirs = ["North", "East", "South", "West"];
for (let i = 0; i < dirs.length; i++) {
const dir = document.createElement("option");
dir.setAttribute("value", i + 1);
dir.textContent = dirs[i];
directions.appendChild(dir);
}
enchilada.main.insertBefore(
document.createElement("br"),
enchilada.title.nextSibling,
);
enchilada.main.insertBefore(directions, enchilada.title.nextSibling);
function finish() {
if (directions.selectedIndex === 0) {
const errWindow = makeWindow();
errWindow.title.textContent = "Select direction!";
errWindow.customClose(function () {
endPlacement(square);
});
return;
}
const dirName = dirs[directions.selectedIndex - 1]; // -1 for default 'Direction' option in the dropdown
const dir = dirName.replace(/[^A-Z]/g, "");
try {
const y = parseInt(square.id[1]);
const x = parseInt(square.id[2]);
// add ship to board
player.board.placeShip(y, x, currentShip.size, dir);
// custom congratulatory popup window
const shipPlaced = makeWindow();
shipPlaced.title.textContent = "Ship successfully placed!";
shipPlaced.customClose(function () {
// if there are still ships to place, place new ships otherwise mark unplaced as false and notify game that we are turnOkayedy to go
length--;
if (length > 0) startPlacement();
else {
displayBoard(enemy.board.board);
unplaced = false;
}
// display updated board
displayBoard(player.board.board);
});
} catch (error) {
const errWindow = makeWindow();
errWindow.title.textContent = error;
errWindow.customClose(function () {
startPlacement();
});
}
}
enchilada.customClose(finish);
}
// function for registering clicks on squares
function clicked(sq, gameboard) {
if (gameboard === player.board && length && placing)
endPlacement(
sq,
); // playerboard only active if placing ships and there are ships to be placed
else if (gameboard === enemy.board && playerInputTime === true) {
// enemyboard only active if player's input time
playerMove = [parseInt(sq.id[1]), parseInt(sq.id[2])]; // mark square
playerMoved = true; // toggle boolean
}
}
// ai turn notification window - ai then makes move on player's board
function aiTurnPopup() {
let enchilada = makeWindow();
enchilada.title.textContent = "Enemy's turn";
enchilada.customClose(function () {
turnOkayed = true;
});
}
// player move notification window
function playerTurnPopup() {
let enchilada = makeWindow();
enchilada.title.textContent = "Your turn";
playerMove = null;
playerInputTime = true;
}
// initializes UI
navButtonInit();
getPlayerName();
// promise to wait until everything is initialized
let initialized = new Promise((resolve, reject) => {
(function waitForPlacement() {
if (unplaced === false) return resolve(true);
setTimeout(waitForPlacement, 1000);
})();
});
// promise to wait until player ok's aiTurn prompt
let aiTurnOkayed = async () => {
let promise = new Promise((resolve, reject) => {
(function waitForAcknowledgement() {
if (turnOkayed === true) {
turnOkayed = false;
return resolve(Math.random());
}
setTimeout(waitForAcknowledgement, 100);
})();
});
return await promise;
};
// promise to wait until player chooses square
let playerTurnFinished = async () => {
let promise = new Promise((resolve, reject) => {
(function waitForMove() {
if (playerMoved === true) {
playerMoved = false;
return resolve(playerMove);
}
setTimeout(waitForMove, 100);
})();
});
return await promise;
};
// display winner
function displayWinner(who) {
const enchilada = makeWindow();
enchilada.title.textContent = `${who.name} won!`;
enchilada.customClose(function () {
location.reload();
});
}
return {
displayBoard,
initialized,
aiTurnPopup,
aiTurnOkayed,
playerTurnPopup,
playerTurnFinished,
displayWinner,
};
};

4
src/js/main.js Normal file
View file

@ -0,0 +1,4 @@
import "../scss/styles.scss";
import * as bootstrap from "bootstrap";
import { Game } from "./Game.js";
Game();

11
src/js/square.js Normal file
View file

@ -0,0 +1,11 @@
// simple square, shows occupying ship (if any) and whether its been shot at
export const square = (y, x) => {
let shot = false;
let ship = null;
return {
y,
x,
ship,
shot,
};
};

20
src/js/square.test.js Normal file
View file

@ -0,0 +1,20 @@
// simple square with a visited boolean
import { square } from "./square.js";
import { Ship } from "./Ship.js";
const ship = Ship(4);
const sq = square(1, 2);
test("Square object outputs y-coordinate", () => {
expect(sq.y).toBe(1);
});
test("Square object outputs x-coordinate", () => {
expect(sq.x).toBe(2);
});
test("Square object outputs occupying Ship", () => {
sq.ship = ship;
expect(sq.ship).toEqual(ship);
});
test("Square object outputs whether it has been shot", () => {
sq.shot = true;
expect(sq.shot).toBe(true);
});

145
src/scss/styles.scss Normal file
View file

@ -0,0 +1,145 @@
// Import all of Bootstrap's CSS
@import "bootstrap/scss/bootstrap";
// Import Font Awesome icons
@import "@fortawesome/fontawesome-free/css/fontawesome.min.css";
@import "@fortawesome/fontawesome-free/css/solid.min.css";
header, header > nav > .btn, header > nav > div > .btn {
text-shadow: 1px 1px 1px black !important;
}
body {
background-image: url("../images/battleship7.jpg");
background-size: 100vw 100vh;
}
.cover-container > .rounded-3 {
background-color: rgba($color: $light, $alpha: 0.5);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
border: 1px solid rgba(0, 0, 0, 0.5);
}
@mixin unclicked {
color: #fff;
background-color: rgba($color: #000000, $alpha: 0.0) !important;
border: 1px solid rgba(0, 0, 0, 0);
}
.newbtn {
@include unclicked();
}
.newbtn.active {
background-color: $success !important;
border: 1px solid rgba($color: $dark, $alpha: 0.5);
}
.nav>.btn-group>.dropdown-toggle {
@include unclicked();
}
.nav>.btn-group>.dropdown-toggle.active, .nav>.btn-group>.dropdown-toggle.show {
background-color: $primary !important;
border: 1px solid rgba($color: $dark, $alpha: 0.5);
}
.griditem {
border: 1px solid $dark;
transform: rotate(180deg);
}
#playerboard, #enemyboard {
height: 500px;
width: 500px;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
text-shadow: none;
transform: rotate(180deg);
direction: rtl;
}
.griditem {
}
.fa-solid {
direction: ltr;
}
.fa-bullseye::before {
color: #e01b24;
}
.fa-fire::before {
color: #ff4d00;
}
.fa-anchor::before {
color: $primary;
text-shadow: 1px 1px 1px $info;
}
.d-flex > .flex-fill.rounded-3 {
background: rgba($color: $dark, $alpha: 0.25);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(6x);
-webkit-backdrop-filter: blur(6x);
border: 1px solid rgba(0, 0, 0, 0.5);
}
.darken {
background: rgba($color: $dark, $alpha: 0.75);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(0, 0, 0, 0.5);
z-index: 1;
height: 100%;
width: 100%;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
.cover-container {
z-index: 0;
}
.fa-circle-xmark {
justify-content: end;
cursor: pointer;
align-items: center;
border: 1px black;
}
.fa-circle-xmark::before {
color: #e01b24;
}
.text-success {
font-size: 1.5rem;
font-size: 2vw;
}
.d-flex.gap-5 > .d-flex.justify-content-center.py-1 {
flex: 0 50%;
}
.d-flex.justify-content-center > div {
font-size: 1.5rem;
text-shadow: 1px 1px 0px black !important;
}
.dropdown-menu.show {
background: rgba($color: $light, $alpha: 0.7);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(0, 0, 0, 0.5);
}
.dropdown-menu > li {
text-shadow: none;
}

87
webpack.config.js Normal file
View file

@ -0,0 +1,87 @@
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isProduction = process.env.NODE_ENV == 'production';
const HtmlWebpackPlugin = require('html-webpack-plugin');
const autoprefixer = require('autoprefixer')
const stylesHandler = isProduction ? MiniCssExtractPlugin.loader : 'style-loader';
const config = {
entry: './src/js/main.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
devServer: {
static: path.resolve(__dirname, 'dist'),
port: 5000,
hot: true
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' })
],
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
{
// Adds CSS to the DOM by injecting a `<style>` tag
loader: 'style-loader'
},
{
// Interprets `@import` and `url()` like `import/require()` and will resolve them
loader: 'css-loader'
},
{
// Loader for webpack to process CSS with PostCSS
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
autoprefixer
]
}
}
},
{
// Loads a SASS/SCSS file and compiles it to CSS
loader: 'sass-loader'
}
]
},
{
test: /\.css$/i,
use: [stylesHandler, 'css-loader', 'postcss-loader'],
},
{
test: /\.(js|jsx)$/i,
loader: 'babel-loader',
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: 'asset',
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
};
module.exports = () => {
if (isProduction) {
config.mode = 'production';
config.plugins.push(new MiniCssExtractPlugin());
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {
config.mode = 'development';
}
return config;
};