///
/**
* Battleship Shared Game Logic
*
* Pure game logic functions used by both pass-and-play and multiplayer
* battleship patterns.
*/
import type { Coordinate, Ship, ShipType, SquareState } from "./types.tsx";
import { BOARD_SIZE, PLAYER_COLORS, SHIP_SIZES } from "./constants.tsx";
// ============ GRID HELPERS ============
export function createEmptyGrid(): SquareState[][] {
return Array.from(
{ length: BOARD_SIZE },
() => Array.from({ length: BOARD_SIZE }, () => "empty" as SquareState),
);
}
// ============ SHIP COORDINATE HELPERS ============
export function getShipCoordinates(ship: Ship): Coordinate[] {
const size = SHIP_SIZES[ship.type];
const coords: Coordinate[] = [];
for (let i = 0; i < size; i++) {
if (ship.orientation === "horizontal") {
coords.push({ row: ship.start.row, col: ship.start.col + i });
} else {
coords.push({ row: ship.start.row + i, col: ship.start.col });
}
}
return coords;
}
export function findShipAt(ships: Ship[], coord: Coordinate): Ship | null {
for (const ship of ships) {
const shipCoords = getShipCoordinates(ship);
if (shipCoords.some((c) => c.row === coord.row && c.col === coord.col)) {
return ship;
}
}
return null;
}
export function buildShipPositions(ships: Ship[]): Record {
const positions: Record = {};
for (const ship of ships) {
const coords = getShipCoordinates(ship);
for (const c of coords) {
positions[`${c.row},${c.col}`] = ship.type;
}
}
return positions;
}
// ============ GAME STATE HELPERS ============
export function isShipSunk(ship: Ship, shots: SquareState[][]): boolean {
const coords = getShipCoordinates(ship);
return coords.every((c) => shots[c.row]?.[c.col] === "hit");
}
export function areAllShipsSunk(
ships: Ship[],
shots: SquareState[][],
): boolean {
return ships.every((ship) => isShipSunk(ship, shots));
}
// ============ SHIP PLACEMENT ============
function canPlaceShip(ship: Ship, occupiedPositions: Set): boolean {
const coords = getShipCoordinates(ship);
for (const c of coords) {
if (c.row < 0 || c.row >= BOARD_SIZE || c.col < 0 || c.col >= BOARD_SIZE) {
return false;
}
if (occupiedPositions.has(`${c.row},${c.col}`)) {
return false;
}
}
return true;
}
export function generateRandomShips(): Ship[] {
const ships: Ship[] = [];
const occupiedPositions = new Set();
const shipTypes: ShipType[] = [
"carrier",
"battleship",
"cruiser",
"submarine",
"destroyer",
];
for (const type of shipTypes) {
let placed = false;
let attempts = 0;
const maxAttempts = 100;
while (!placed && attempts < maxAttempts) {
attempts++;
const orientation: "horizontal" | "vertical" = Math.random() < 0.5
? "horizontal"
: "vertical";
const size = SHIP_SIZES[type];
const maxRow = orientation === "vertical"
? BOARD_SIZE - size
: BOARD_SIZE - 1;
const maxCol = orientation === "horizontal"
? BOARD_SIZE - size
: BOARD_SIZE - 1;
const row = Math.floor(Math.random() * (maxRow + 1));
const col = Math.floor(Math.random() * (maxCol + 1));
const ship: Ship = { type, start: { row, col }, orientation };
if (canPlaceShip(ship, occupiedPositions)) {
ships.push(ship);
const coords = getShipCoordinates(ship);
for (const c of coords) {
occupiedPositions.add(`${c.row},${c.col}`);
}
placed = true;
}
}
if (!placed) {
// Fallback: use deterministic placement by scanning the board
console.warn(`Failed to place ${type} randomly, using fallback`);
const size = SHIP_SIZES[type];
// Try horizontal placement first, then vertical
for (const orientation of ["horizontal", "vertical"] as const) {
if (placed) break;
const maxRow = orientation === "vertical"
? BOARD_SIZE - size
: BOARD_SIZE - 1;
const maxCol = orientation === "horizontal"
? BOARD_SIZE - size
: BOARD_SIZE - 1;
for (let row = 0; row <= maxRow && !placed; row++) {
for (let col = 0; col <= maxCol && !placed; col++) {
const ship: Ship = { type, start: { row, col }, orientation };
if (canPlaceShip(ship, occupiedPositions)) {
ships.push(ship);
const coords = getShipCoordinates(ship);
for (const c of coords) {
occupiedPositions.add(`${c.row},${c.col}`);
}
placed = true;
}
}
}
}
if (!placed) {
console.error(
`Could not place ${type} even with fallback - board may be too crowded`,
);
}
}
}
return ships;
}
// ============ UI HELPERS ============
export function getRandomColor(index: number): string {
return PLAYER_COLORS[index % PLAYER_COLORS.length];
}
export function getInitials(name: string): string {
if (!name || typeof name !== "string") return "?";
const trimmed = name.trim();
if (!trimmed) return "?";
return trimmed
.split(/\s+/)
.map((word) => word[0])
.join("")
.toUpperCase()
.slice(0, 2);
}