working version
11
.babelrc
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"plugins": ["@babel/syntax-dynamic-import"],
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"modules": "auto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
.eslint*
|
||||||
|
.prettier*
|
||||||
15
.vscode/launch.json
vendored
Normal 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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# js-battleship
|
# js-battleship
|
||||||
|
|
||||||
Battleship implemented in ES6 via TDD
|
Battleship implemented in ES6
|
||||||
3
babel.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
|
||||||
|
};
|
||||||
BIN
dist/195d86f550b47476dbb8.jpg
vendored
Normal file
|
After Width: | Height: | Size: 630 KiB |
BIN
dist/4a2cd718d7031b732e76.ttf
vendored
Normal file
BIN
dist/bb975c966c37455a1bc3.woff2
vendored
Normal file
38
dist/index.html
vendored
Normal 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
1
jest-setup.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
1
jest.config.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/jest-setup.js']
|
||||||
15360
package-lock.json
generated
Normal file
48
package.json
Normal 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
|
|
@ -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
|
After Width: | Height: | Size: 496 KiB |
BIN
src/images/battleship2.jpg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
BIN
src/images/battleship3.jpg
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
src/images/battleship4.jpg
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
src/images/battleship5.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
src/images/battleship6.jpg
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
src/images/battleship7.jpg
Normal file
|
After Width: | Height: | Size: 630 KiB |
38
src/index.html
Normal 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
|
|
@ -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 player’s 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
};
|
||||||