// Battleship — pure game logic (no Phaser, no rendering). // // Two players, each with a fleet of 5 ships on a 10×10 grid. Hidden // information: a player only learns enemy ship positions by firing at them. // Salvo mode: on your turn you fire one shot per ship still afloat. export const SIZE = 10; // Standard fleet, largest → smallest. export const SHIPS = [ { name: 'Carrier', len: 5 }, { name: 'Battleship', len: 4 }, { name: 'Cruiser', len: 3 }, { name: 'Submarine', len: 3 }, { name: 'Destroyer', len: 2 }, ]; export function other(player) { return player === 'player1' ? 'player2' : 'player1'; } function emptyGrid() { return Array.from({ length: SIZE }, () => Array(SIZE).fill(null)); } // A ship occupies `len` contiguous cells from (r,c), horizontal or vertical. function shipCells(len, r, c, horizontal) { const cells = []; for (let i = 0; i < len; i++) { cells.push(horizontal ? { r, c: c + i } : { r: r + i, c }); } return cells; } // True if a ship of `len` placed at (r,c) fits in bounds and overlaps nothing // already in `fleet`. Ships are allowed to touch (standard rules). export function canPlace(fleet, len, r, c, horizontal) { const cells = shipCells(len, r, c, horizontal); for (const cell of cells) { if (cell.r < 0 || cell.r >= SIZE || cell.c < 0 || cell.c >= SIZE) return false; } const occupied = new Set(); for (const ship of fleet) for (const cell of ship.cells) occupied.add(`${cell.r},${cell.c}`); for (const cell of cells) if (occupied.has(`${cell.r},${cell.c}`)) return false; return true; } // Build a ship object ready to drop into a fleet. export function makeShip(spec, r, c, horizontal) { const cells = shipCells(spec.len, r, c, horizontal); return { name: spec.name, len: spec.len, horizontal, cells, hits: Array(spec.len).fill(false), sunk: false, }; } // A valid random 5-ship layout. Used by the Randomize button and the AI. export function placeShipsRandom() { const fleet = []; for (const spec of SHIPS) { // Try random placements until one is legal (always terminates on 10×10). for (let guard = 0; guard < 1000; guard++) { const horizontal = Math.random() < 0.5; const r = Math.floor(Math.random() * (horizontal ? SIZE : SIZE - spec.len + 1)); const c = Math.floor(Math.random() * (horizontal ? SIZE - spec.len + 1 : SIZE)); if (canPlace(fleet, spec.len, r, c, horizontal)) { fleet.push(makeShip(spec, r, c, horizontal)); break; } } } return fleet; } export function createInitialState() { return { phase: 'placement', // 'placement' | 'battle' | 'game_over' turn: 'player1', fleets: { player1: [], player2: [] }, shots: { player1: emptyGrid(), player2: emptyGrid() }, // firer's view of enemy winner: null, }; } export function cloneState(state) { const cloneFleet = (fleet) => fleet.map((s) => ({ name: s.name, len: s.len, horizontal: s.horizontal, cells: s.cells.map((cell) => ({ r: cell.r, c: cell.c })), hits: s.hits.slice(), sunk: s.sunk, })); const cloneGrid = (g) => g.map((row) => row.slice()); return { phase: state.phase, turn: state.turn, fleets: { player1: cloneFleet(state.fleets.player1), player2: cloneFleet(state.fleets.player2) }, shots: { player1: cloneGrid(state.shots.player1), player2: cloneGrid(state.shots.player2) }, winner: state.winner, }; } // Has `player` already fired at (r,c)? export function shotAt(state, player, r, c) { return state.shots[player][r][c] !== null; } // Find the ship in `fleet` occupying (r,c), or null. function shipAt(fleet, r, c) { for (const ship of fleet) { for (let i = 0; i < ship.cells.length; i++) { if (ship.cells[i].r === r && ship.cells[i].c === c) return { ship, idx: i }; } } return null; } export function aliveCount(fleet) { return fleet.reduce((n, s) => n + (s.sunk ? 0 : 1), 0); } // Number of shots `player` gets this turn = their own surviving ships. export function salvoCount(state, player) { return aliveCount(state.fleets[player]); } // Apply a single shot. Mutates `state` (callers pass a clone). Returns the // outcome plus the ship that was hit/sunk (for reveal animations). export function fireShot(state, byPlayer, r, c) { const target = other(byPlayer); const hit = shipAt(state.fleets[target], r, c); if (!hit) { state.shots[byPlayer][r][c] = 'miss'; return { result: 'miss', ship: null }; } state.shots[byPlayer][r][c] = 'hit'; hit.ship.hits[hit.idx] = true; if (hit.ship.hits.every(Boolean)) { hit.ship.sunk = true; return { result: 'sunk', ship: hit.ship }; } return { result: 'hit', ship: hit.ship }; } // Fire a whole salvo of cells, resolve game over / turn handoff. // Returns { state, results: [{ r, c, result, ship }] }. export function applySalvo(state, byPlayer, cells) { const next = cloneState(state); const results = []; for (const { r, c } of cells) { if (shotAt(next, byPlayer, r, c)) continue; // guard against duplicates const outcome = fireShot(next, byPlayer, r, c); results.push({ r, c, ...outcome }); } if (isGameOver(next)) { next.phase = 'game_over'; next.winner = getWinner(next); } else { next.turn = other(byPlayer); } return { state: next, results }; } export function isGameOver(state) { return aliveCount(state.fleets.player1) === 0 || aliveCount(state.fleets.player2) === 0; } export function getWinner(state) { if (aliveCount(state.fleets.player2) === 0) return 'player1'; if (aliveCount(state.fleets.player1) === 0) return 'player2'; return null; }