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
JavaScript
"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