/// /** * 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); }