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