battleships-engine
Version:
TypeScript engine for the classic game Battleship
253 lines (226 loc) • 8.17 kB
text/typescript
import GameBoard from "@/gameboard";
import Ship from "@/ship";
import type { ShipType, TCoords } from "@/types/type";
import { shipsLength } from "@/consts";
import { isGameboardValid } from "@/utils";
const ships = [
new Ship({
type: "cruiser",
coords: { x: 2, y: 1 },
direction: "hor",
}),
new Ship({
type: "battleship",
coords: { x: 9, y: 1 },
direction: "vert",
}),
new Ship({
type: "submarine",
coords: { x: 4, y: 9 },
direction: "hor",
}),
];
const receiveAttackSetup = (
ships: Ship[],
extraCoords?: TCoords | TCoords[],
spyOnShipHit?: ShipType[],
) => {
const gameboard = new GameBoard(ships);
const receiveAttack = jest.spyOn(gameboard, "receiveAttack");
let spies = undefined;
if (spyOnShipHit) {
spies = spyOnShipHit.map((type) =>
jest.spyOn(gameboard.ships.get(type)!, "hit"),
);
}
for (const ship of ships) {
for (const coord of ship) gameboard.receiveAttack(coord);
}
if (extraCoords) {
if (Array.isArray(extraCoords)) {
for (const coord of extraCoords) {
gameboard.receiveAttack(coord);
}
} else gameboard.receiveAttack(extraCoords);
}
if (spies) return { spies, gameboard, receiveAttack };
return { gameboard, receiveAttack };
};
describe("GameBoard", () => {
it("should place a ship", () => {
const gameboard = new GameBoard();
gameboard.placeShip({
type: "cruiser",
coords: { x: 1, y: 4 },
direction: "hor",
});
expect(gameboard.ships.get("cruiser")).toBeInstanceOf(Ship);
expect(gameboard.ships.get("cruiser")).toHaveProperty("coords");
expect(gameboard.ships.get("cruiser")).toHaveProperty("direction");
expect(gameboard.ships.get("cruiser")?.coords.x).toBe(1);
expect(gameboard.ships.get("cruiser")?.coords.y).toBe(4);
expect(gameboard.ships.get("cruiser")?.direction).toBe("hor");
});
it("should throw the ship overlap error", () => {
const gameboard = new GameBoard();
gameboard.placeShip({
type: "cruiser",
coords: { x: 1, y: 4 },
direction: "hor",
});
try {
gameboard.placeShip({
type: "cruiser",
coords: { x: 2, y: 4 },
direction: "vert",
});
expect(1).toBe(2);
} catch (e) {
expect(e instanceof Error ? e.message : "Bad").toBe(
"Ship placement error: The ship overlaps with another ship.",
);
}
});
describe("utils", () => {
it("should fill taken cells with ship coordinates", () => {
const gameboard = new GameBoard();
const type: ShipType = "aircraft_carrier";
const size = shipsLength[type];
gameboard.takenCells = new Map();
expect(gameboard.takenCells.size).toBe(0);
const ship = new Ship({
type,
coords: { x: 1, y: 4 },
direction: "hor",
});
(gameboard as any).fillTakenCellsWithShip(ship, "aircraft_carrier");
for (const coord of ship) {
expect(gameboard.takenCells.has(coord.toString())).toBe(true);
}
expect(gameboard.takenCells.size).toBe(size);
});
it("should inspect", () => {
const gameboard = new GameBoard([
new Ship({
type: "cruiser",
direction: "hor",
coords: { x: 1, y: 1 },
}),
new Ship({
type: "battleship",
direction: "vert",
coords: { x: 1, y: 4 },
}),
]);
const matchCb = jest.fn();
const missCb = jest.fn();
(gameboard as any).inspectCoordsInShips({
coords: { x: 1, y: 1 },
matchCb,
missCb,
});
(gameboard as any).inspectCoordsInShips({
coords: { x: 1, y: 5 },
matchCb,
missCb,
});
(gameboard as any).inspectCoordsInShips({
coords: { x: 9, y: 9 },
matchCb,
missCb,
});
expect(missCb).toHaveBeenCalledTimes(1);
expect(matchCb).toHaveBeenCalledTimes(2);
});
});
describe("receiveAttack", () => {
let hookRes: ReturnType<typeof receiveAttackSetup> | undefined =
undefined;
beforeAll(() => {
hookRes = receiveAttackSetup(ships, { x: 4, y: 10 }, ["cruiser"]);
});
it("should call the hit function after the successful receiveAttack call", () => {
const { gameboard } = hookRes!;
const ship = gameboard!.ships.get("cruiser")!;
expect(ship.hit).toHaveBeenCalledTimes(ship.length);
expect(gameboard!.receiveAttack).toHaveBeenCalledTimes(10);
});
it("should sunk the ship", () => {
const { gameboard } = hookRes!;
expect(gameboard!.ships.get("cruiser")?.isSunk()).toBe(true);
expect(gameboard!.ships.get("cruiser")?.beenHitTimes).toBe(2);
});
it("should add the coordinates to the 'hitCells' array", () => {
const { gameboard } = hookRes!;
expect(gameboard!.hitCells.get("(3,1)")).toBe(true);
});
it("should add the coordinates to the 'missed' array", () => {
const { gameboard } = hookRes!;
expect(gameboard!.missed.get("(4,10)")).toBe(true);
});
});
describe("defeat check", () => {
it("should report whether or not all of the ships have been sunk.", () => {
const gameboard = new GameBoard();
const ships: { type: ShipType; coords: TCoords }[] = [
{
type: "cruiser",
coords: { x: 1, y: 1 },
},
{
type: "battleship",
coords: { x: 1, y: 4 },
},
];
ships.forEach(({ type, coords }) => {
gameboard.placeShip({
type,
coords,
direction: "hor",
});
for (let i = 0; i < shipsLength[type]; i++) {
gameboard.receiveAttack({ x: coords.x + i, y: coords.y });
}
});
expect(gameboard.hasLost()).toBe(true);
});
});
it("should randomly place ships", () => {
const gameboard = new GameBoard();
const oldShips = gameboard.ships;
gameboard.randomlyPlaceShips();
const newShips = gameboard.ships;
expect(isGameboardValid(gameboard)).toBe(true);
expect(oldShips).not.toMatchObject(newShips);
});
it("should remove a ship", () => {
const gameboard = new GameBoard();
const [ship] = ships;
gameboard.placeShip(ship!);
gameboard.removeShip(ship!);
expect(gameboard.ships.has("cruiser")).toBe(false);
for (const coords of ship!) {
expect(gameboard.takenCells.has(coords.toString())).toBe(false);
}
});
it("should move a ship", () => {
const gameboard = new GameBoard();
const [firstShip, secondShip] = ships;
gameboard.placeShip(firstShip!);
gameboard.placeShip(secondShip!);
try {
gameboard.moveShip(firstShip!, {
coords: { x: 9, y: 1 },
direction: "vert",
});
expect(1).toBe(2);
} catch (e) {
expect(1).toBe(1);
}
gameboard.moveShip(firstShip!, {
coords: { x: 3, y: 3 },
direction: "vert",
});
expect(gameboard.ships.get("cruiser")!.coords).toEqual({ x: 3, y: 3 });
});
});