battleships-engine
Version:
TypeScript engine for the classic game Battleship
300 lines (295 loc) • 8.09 kB
JavaScript
// 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
import random from "lodash/random";
var Coords = class {
x = random(1, 10);
y = random(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
import random2 from "lodash/random";
var generateRandomCoords = () => new Coords({ x: random2(1, 10), y: random2(1, 10) });
var generateRandomDir = () => directionTypes[random2(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
import random3 from "lodash/random";
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[random3(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 })
);
}
};
export {
Coords,
GameBoard,
Ship
};