battleships-engine
Version:
TypeScript engine for the classic game Battleship
339 lines (332 loc) • 9.93 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Coords: () => Coords,
GameBoard: () => GameBoard,
Ship: () => Ship
});
module.exports = __toCommonJS(src_exports);
// src/consts.ts
var shipsLength = {
aircraft_carrier: 5,
battleship: 4,
destroyer: 3,
submarine: 3,
cruiser: 2
};
var directionTypes = ["vert", "hor"];
var coordsType = ["x", "y"];
var numberRegExp = /\d+/;
// src/coords.ts
var import_random = __toESM(require("lodash/random"), 1);
var Coords = class {
x = (0, import_random.default)(1, 10);
y = (0, import_random.default)(1, 10);
constructor(coords) {
if (coords) {
const { x, y } = coords;
if (x > 10) {
throw new Error("X should be less than or equal to 10");
} else {
this.x = x;
}
if (y > 10) {
throw new Error("Y should be less than or equal to 10");
} else {
this.y = y;
}
if (x < 1) {
throw new Error("X should be greater than 0");
} else {
this.x = x;
}
if (y < 1) {
throw new Error("Y should be greater than 0");
} else {
this.y = y;
}
}
}
toString() {
return `(${this.x},${this.y})`;
}
*[Symbol.iterator]() {
for (const coordType of coordsType) {
yield {
type: coordType,
number: this[coordType]
};
}
}
};
// src/ship.ts
var Ship = class {
length = 0;
type;
beenHitTimes = 0;
coords;
direction = "hor";
constructor({
coords,
type,
direction
}) {
if (!Object.keys(shipsLength).includes(type))
throw new Error("Invalid ship type");
this.type = type;
this.length = shipsLength[type];
if (this.length < 2)
throw new Error("Length should more than or equal to 2");
if (!directionTypes.includes(direction))
throw new Error("Invalid direction type");
this.coords = new Coords(coords);
this.direction = direction;
}
hit() {
if (this.beenHitTimes >= this.length) {
throw new Error("This ship has already sunk.");
}
this.beenHitTimes++;
}
isSunk() {
return this.beenHitTimes === this.length;
}
*[Symbol.iterator]() {
for (let i = 0; i < this.length; i++) {
let obj = { x: this.coords.x, y: this.coords.y + i };
if (this.direction === "hor")
obj = {
x: this.coords.x + i,
y: this.coords.y
};
yield { toString: new Coords(obj).toString, ...obj };
}
}
};
// src/utils.ts
var import_random2 = __toESM(require("lodash/random"), 1);
var generateRandomCoords = () => new Coords({ x: (0, import_random2.default)(1, 10), y: (0, import_random2.default)(1, 10) });
var generateRandomDir = () => directionTypes[(0, import_random2.default)(1)];
var generateRandomShip = ({
gameboard,
shipType
}) => {
let coords = generateRandomCoords();
const direction = generateRandomDir();
if (direction === "hor" && coords.x + shipsLength[shipType] > 10) {
const { x, y } = coords;
coords.x = x - 1;
} else if (direction === "vert" && coords.y + shipsLength[shipType] > 10) {
const { x, y } = coords;
coords.y = y - 1;
}
try {
gameboard.placeShip({
coords,
direction,
type: shipType
});
} catch (e) {
generateRandomShip({ gameboard, shipType });
}
};
var generateGameBoardCells = () => {
const map = /* @__PURE__ */ new Map();
coordsType.forEach((coordsType2) => {
for (let i = 1; i <= 10; i++) {
for (let j = 1; j <= 10; j++) {
map.set(`(${i},${j})`, false);
}
}
});
return map;
};
var convertStringToCoords = (str) => {
const [x, y] = str.split(",").map((word) => {
if (word) {
const matches = word.match(numberRegExp);
if (!matches) return;
return Number(matches[0]);
}
});
if (!x || !y) throw new Error("Invalid string provided");
return { x, y };
};
// src/gameboard.ts
var import_random3 = __toESM(require("lodash/random"), 1);
var GameBoard = class {
ships = /* @__PURE__ */ new Map();
takenCells = /* @__PURE__ */ new Map();
missed = generateGameBoardCells();
hitCells = generateGameBoardCells();
constructor(ships) {
if (ships) {
ships.forEach((ship) => this.ships.set(ship.type, ship));
this.ships.forEach(
(...params) => this.fillTakenCellsWithShip(...params)
);
}
}
fillTakenCellsWithShip(ship, shipType, _map) {
for (const coord of ship)
this.takenCells.set(coord.toString(), shipType);
}
inspectCoordsInShips({
coords: paramCoords,
missCb,
matchCb
}) {
if (this.ships.size > 0) {
const coords = new Coords(paramCoords);
const shipType = this.takenCells.get(coords.toString());
if (shipType) {
const ship = this.ships.get(shipType);
if (!ship) throw new Error(`${shipType} does not exist`);
matchCb(ship);
} else missCb();
} else missCb();
}
placeShip(params) {
this.inspectCoordsInShips({
coords: params.coords,
missCb: () => {
const newShip = new Ship(params);
for (const coords of newShip) {
if (this.takenCells.has(coords.toString())) {
throw new Error(
"Ship placement error: The ship overlaps with another ship."
);
}
}
this.ships.set(params.type, newShip);
this.fillTakenCellsWithShip(newShip, params.type);
},
matchCb: () => {
throw new Error(
"Ship placement error: The ship overlaps with another ship."
);
}
});
}
removeShip(ship) {
this.ships.delete(ship.type);
for (const coords of ship) {
this.takenCells.delete(coords.toString());
}
}
moveShip(startingShip, newShipInfo) {
this.removeShip(startingShip);
this.placeShip({ type: startingShip.type, ...newShipInfo });
}
receiveAttack(coords) {
const coordsClass = new Coords(coords);
if (this.missed.get(coordsClass.toString()) === true)
throw new Error(
`The coordinate (X: ${coords.x}, Y: ${coords.y}) has already been targeted and missed.`
);
const fromTaken = this.takenCells.get(coordsClass.toString());
if (fromTaken) {
const ship = this.ships.get(fromTaken);
if (!ship) throw new Error(`${fromTaken} does not exist`);
else {
ship.hit();
this.hitCells.set(coordsClass.toString(), true);
}
} else this.missed.set(coordsClass.toString(), true);
}
hasLost() {
const currShips = Array.from(this.ships.keys());
if (currShips.length > 0) {
return !currShips.map((ship) => this.ships.get(ship)?.isSunk()).includes(false);
} else return false;
}
randomlyPlaceShip({
type,
direction = generateRandomDir()
}) {
if (this.takenCells.size > 0) {
const allCells = generateGameBoardCells();
const emptyCells = [];
for (const [cell] of allCells) {
if (!this.takenCells.has(cell)) emptyCells.push(cell);
}
const possibleStarts = emptyCells.filter((str) => {
const { x, y } = convertStringToCoords(str);
const newShip = new Ship({
coords: { x, y },
direction,
type
});
let isValid = true;
if (direction === "hor") isValid = x + shipsLength[type] <= 10;
else isValid = y + shipsLength[type] <= 10;
if (isValid) {
for (const coord of newShip) {
isValid = !this.takenCells.has(coord.toString());
if (!isValid) break;
}
} else {
return false;
}
return isValid;
});
if (possibleStarts.length === 0) {
this.randomlyPlaceShip({
type,
direction: directionTypes.find((dir) => dir !== direction)
});
} else {
const randomStart = possibleStarts[(0, import_random3.default)(possibleStarts.length - 1)];
if (!randomStart) throw new Error("No available space");
this.placeShip({
type,
coords: convertStringToCoords(randomStart),
direction
});
}
} else {
generateRandomShip({ gameboard: this, shipType: type });
}
}
randomlyPlaceShips() {
this.ships = /* @__PURE__ */ new Map();
this.takenCells = /* @__PURE__ */ new Map();
Object.keys(shipsLength).forEach(
(type) => this.randomlyPlaceShip({ type })
);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Coords,
GameBoard,
Ship
});