UNPKG

battleship-ai

Version:

This repository contains a Battleship AI implementation built in TypeScript. The AI focuses on grid-based probability calculations, strategic ship placement, and targeted attack mechanisms to effectively play the game. This README explains the AI’s logic,

338 lines (334 loc) 10.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { BattleShipAI: () => BattleShipAI, CellType: () => CellType, Grid: () => Grid }); module.exports = __toCommonJS(src_exports); // src/types/index.ts var CellType = /* @__PURE__ */ ((CellType2) => { CellType2[CellType2["Empty"] = 0] = "Empty"; CellType2[CellType2["Miss"] = 2] = "Miss"; CellType2[CellType2["Hit"] = 3] = "Hit"; return CellType2; })(CellType || {}); // src/grid.ts var Grid = class { size; cells; constructor(size) { this.size = size; this.cells = []; this.init(); } init() { for (let x = 0; x < this.size; x++) { const row = []; this.cells[x] = row; for (let y = 0; y < this.size; y++) { row.push(0 /* Empty */); } } } updateCell(x, y, type) { this.cells[x][y] = type; } isMiss(x, y) { return this.cells[x][y] === 2 /* Miss */; } isDamagedShip(x, y) { return this.cells[x][y] === 3 /* Hit */; } }; // src/index.ts var BattleShipAI = class _BattleShipAI { probGrid; virtualGrid; ships; constructor(gameSize, ships) { this.virtualGrid = new Grid(gameSize); this.ships = ships; this.probGrid = []; this.initProbs(gameSize); } static PROB_WEIGHT = 5e3; // Arbitrarily big number static OPEN_LOW_MIN = 10; static OPEN_LOW_MAX = 20; static OPEN_MED_MIN = 15; static OPEN_MED_MAX = 25; static OPEN_HIGH_MIN = 20; static OPEN_HIGH_MAX = 30; static OPENINGS = [ { x: 7, y: 3, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 6, y: 2, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 3, y: 7, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 2, y: 6, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 6, y: 6, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 3, y: 3, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 5, y: 5, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 4, y: 4, weight: getRandom(this.OPEN_LOW_MIN, this.OPEN_LOW_MAX) }, { x: 0, y: 8, weight: getRandom(this.OPEN_MED_MIN, this.OPEN_MED_MAX) }, { x: 1, y: 9, weight: getRandom(this.OPEN_HIGH_MIN, this.OPEN_HIGH_MAX) }, { x: 8, y: 0, weight: getRandom(this.OPEN_MED_MIN, this.OPEN_MED_MAX) }, { x: 9, y: 1, weight: getRandom(this.OPEN_HIGH_MIN, this.OPEN_HIGH_MAX) }, { x: 9, y: 9, weight: getRandom(this.OPEN_HIGH_MIN, this.OPEN_HIGH_MAX) }, { x: 0, y: 0, weight: getRandom(this.OPEN_HIGH_MIN, this.OPEN_HIGH_MAX) } ]; // Initializes the probability grid initProbs(size) { this.probGrid = Array.from( { length: size }, () => Array.from({ length: size }, () => 0) ); } setProbs(value) { for (let x = 0; x < this.virtualGrid.size; x++) { for (let y = 0; y < this.virtualGrid.size; y++) { this.probGrid[x][y] = value; } } } updateGrid(moves) { this.resetGrid(); moves.forEach((move) => { this.virtualGrid.updateCell( move.x, move.y, move.outcome === "hit" ? 3 /* Hit */ : 2 /* Miss */ ); }); } resetGrid() { for (let x = 0; x < this.virtualGrid.size; x++) { for (let y = 0; y < this.virtualGrid.size; y++) { this.virtualGrid.updateCell(x, y, 0 /* Empty */); } } } // Resets the probability grid resetProbs() { this.initProbs(this.virtualGrid.size); } isValidCell(x, y) { return x >= 0 && y >= 0 && x < this.virtualGrid.size && y < this.virtualGrid.size; } canPlaceShip(x, y, length, direction) { for (let i = 0; i < length; i++) { const nx = x + (direction === "horizontal" ? i : 0); const ny = y + (direction === "vertical" ? i : 0); if (!this.isValidCell(nx, ny) || this.virtualGrid.cells[nx][ny] === 2 /* Miss */) { return false; } } return true; } // Updates the probability grid based on ship positions and previous outcomes updateProbs(previousAttacks) { this.updateGrid(previousAttacks); this.resetProbs(); for (const cell of _BattleShipAI.OPENINGS) { if (cell.x < this.virtualGrid.size && cell.y < this.virtualGrid.size) { if (this.probGrid[cell.x][cell.y] !== 0) { this.probGrid[cell.x][cell.y] += cell.weight; } } } for (let x = 0; x < this.virtualGrid.size; x++) { for (let y = 0; y < this.virtualGrid.size; y++) { if (this.virtualGrid.cells[x][y] === 3 /* Hit */) { this.evaluateAdjacentTiles(x, y); } else if (this.virtualGrid.cells[x][y] === 0 /* Empty */) { this.evaluateCellForShips(x, y); } } } for (const attack of previousAttacks) { this.probGrid[attack.x][attack.y] = 0; } const hitTiles = previousAttacks.filter((a) => a.outcome === "hit").length; if (hitTiles === this.ships.reduce((a, b) => a + b, 0)) { this.setProbs(0); } } evaluateAdjacentTiles(hitX, hitY) { const directions = [ { dx: 1, dy: 0 }, // Horizontal right { dx: -1, dy: 0 }, // Horizontal left { dx: 0, dy: 1 }, // Vertical down { dx: 0, dy: -1 } // Vertical up ]; for (const { dx, dy } of directions) { let nx = hitX + dx; let ny = hitY + dy; while (this.isValidCell(nx, ny) && this.virtualGrid.cells[nx][ny] === 0 /* Empty */) { this.probGrid[nx][ny] += _BattleShipAI.PROB_WEIGHT; nx += dx; ny += dy; } } } numHitCellsCovered(coords) { return coords.reduce((count, coord) => { return this.virtualGrid.cells[coord.x][coord.y] === 3 /* Hit */ ? count + 1 : count; }, 0); } getShipCoordinates(x, y, length, direction) { const coords = []; for (let i = 0; i < length; i++) { if (direction === "horizontal") { coords.push({ x: x + i, y }); } else { coords.push({ x, y: y + i }); } } return coords; } evaluateCellForShips(x, y) { for (const ship of this.ships) { const directions = ["horizontal", "vertical"]; for (const direction of directions) { if (this.canPlaceShip(x, y, ship, direction)) { const coords = this.getShipCoordinates(x, y, ship, direction); const hitCount = this.numHitCellsCovered(coords); const baseProbability = 1; if (hitCount > 0) { for (const coord of coords) { this.probGrid[coord.x][coord.y] += baseProbability + hitCount * _BattleShipAI.PROB_WEIGHT; } } else { for (const coord of coords) { this.probGrid[coord.x][coord.y] += baseProbability; } } } } } } // Get the coordinates of the highest probability cell getHighestProbabilityTarget(previousMoves) { this.updateProbs(previousMoves); let maxProb = -1; const maxProbs = []; for (let x = 0; x < this.probGrid.length; x++) { for (let y = 0; y < this.probGrid[x].length; y++) { if (this.probGrid[x][y] > maxProb) { maxProb = this.probGrid[x][y]; maxProbs.length = 0; maxProbs.push({ x, y }); } else if (this.probGrid[x][y] === maxProb) { maxProbs.push({ x, y }); } } } if (maxProb === 0) { return { x: -1, y: -1 }; } return maxProbs[Math.floor(Math.random() * maxProbs.length)]; } getHeatmap() { return this.probGrid; } getRandomShipPlacements() { const ships = []; const inner = Array(this.virtualGrid.size).fill("empty"); const board = Array.from( { length: this.virtualGrid.size }, () => [...inner] ); const isValidPlacement = (x, y, size, isHorizontal) => { if (isHorizontal) { if (x + size > this.virtualGrid.size) return false; for (let i = 0; i < size; i++) { if (board[y][x + i] !== "empty") return false; } } else { if (y + size > this.virtualGrid.size) return false; for (let i = 0; i < size; i++) { if (board[y + i][x] !== "empty") return false; } } return true; }; const findValidPositions = (size) => { const validPositions = []; for (let x = 0; x < this.virtualGrid.size; x++) { for (let y = 0; y < this.virtualGrid.size; y++) { if (isValidPlacement(x, y, size, true)) { validPositions.push({ x, y, isHorizontal: true }); } if (isValidPlacement(x, y, size, false)) { validPositions.push({ x, y, isHorizontal: false }); } } } return validPositions; }; const placeShip = (size) => { const ship = { size, positions: [] }; const validPositions = findValidPositions(size); if (validPositions.length === 0) { throw new Error(`No valid placements for ship of size ${String(size)}`); } const randomPosition = validPositions[Math.floor(Math.random() * validPositions.length)]; const { x, y, isHorizontal } = randomPosition; for (let i = 0; i < size; i++) { if (isHorizontal) { board[y][x + i] = "ship"; ship.positions.push({ x: x + i, y }); } else { board[y + i][x] = "ship"; ship.positions.push({ x, y: y + i }); } } return ship; }; for (const ship of this.ships) { ships.push(placeShip(ship)); } return ships; } calculateOutcome(x, y, ships) { for (const ship of ships) { for (const position of ship.positions) { if (position.x === x && position.y === y) { return "hit"; } } } return "miss"; } }; function getRandom(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { BattleShipAI, CellType, Grid }); //# sourceMappingURL=index.js.map