fertig-classic-games/public/src/games/battleship/BattleshipLogic.js

176 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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