海战棋(Battleship)是一款经典的双人对战策略游戏。玩家各自在隐蔽的棋盘上部署战舰,然后轮流射击对方棋盘上的坐标,目标是率先击沉对方所有战舰。对于AI玩家而言,如何在信息不完全的情况下做出最优射击决策,是一个极具挑战的算法问题。本文将用Java实现一个带AI的海战棋系统,核心算法包括概率密度分布计算、贪心最优射击策略,以及命中后的定向搜索追击。
一、游戏规则与数据建模
海战棋通常在10×10的棋盘上进行。标准舰队包含5艘战舰,分别为:航空母舰(5格)、战列舰(4格)、巡洋舰(3格)、潜艇(3格)和驱逐舰(2格)。战舰只能水平或垂直放置,且不能重叠。
1.1 核心数据结构
我们用枚举类型表示棋盘格子的状态,用二维数组存储整个棋盘:
/**
* 棋盘格子状态枚举
*/
enum CellState {
EMPTY, // 空白水域
SHIP, // 有战舰(对敌方隐藏)
MISS, // 射击未命中
HIT, // 射击命中
SUNK // 战舰被击沉
}
/**
* 战舰类型定义
*/
enum ShipType {
CARRIER(5, "航空母舰"),
BATTLESHIP(4, "战列舰"),
CRUISER(3, "巡洋舰"),
SUBMARINE(3, "潜艇"),
DESTROYER(2, "驱逐舰");
final int length;
final String name;
ShipType(int length, String name) {
this.length = length;
this.name = name;
}
}
/**
* 战舰实体类,记录位置和状态
*/
class Ship {
final ShipType type;
final int startRow, startCol;
final boolean horizontal; // true为水平,false为垂直
final boolean[] hits; // 记录各部位是否被击中
Ship(ShipType type, int startRow, int startCol, boolean horizontal) {
this.type = type;
this.startRow = startRow;
this.startCol = startCol;
this.horizontal = horizontal;
this.hits = new boolean[type.length];
}
/**
* 判断坐标是否属于本战舰
*/
boolean contains(int row, int col) {
if (horizontal) {
return row == startRow && col >= startCol && col < startCol + type.length;
} else {
return col == startCol && row >= startRow && row < startRow + type.length;
}
}
/**
* 标记命中,返回是否击沉
*/
boolean hit(int row, int col) {
int index = horizontal ? (col - startCol) : (row - startRow);
hits[index] = true;
return isSunk();
}
/**
* 检查战舰是否已完全击沉
*/
boolean isSunk() {
for (boolean h : hits) {
if (!h) return false;
}
return true;
}
}
1.2 棋盘类设计
/**
* 海战棋棋盘,管理格子状态和战舰部署
*/
class Board {
static final int SIZE = 10;
private final CellState[][] grid;
private final List<Ship> ships;
Board() {
grid = new CellState[SIZE][SIZE];
for (int i = 0; i < SIZE; i++) {
Arrays.fill(grid[i], CellState.EMPTY);
}
ships = new ArrayList<>();
}
/**
* 尝试在指定位置放置战舰,检查边界和碰撞
*/
boolean placeShip(ShipType type, int row, int col, boolean horizontal) {
// 边界检查
if (horizontal) {
if (col + type.length > SIZE) return false;
} else {
if (row + type.length > SIZE) return false;
}
// 碰撞检测(包括相邻格子的缓冲检查,确保战舰不接触)
for (int i = 0; i < type.length; i++) {
int r = horizontal ? row : row + i;
int c = horizontal ? col + i : col;
if (!canPlaceAt(r, c)) return false;
}
// 实际放置
Ship ship = new Ship(type, row, col, horizontal);
ships.add(ship);
for (int i = 0; i < type.length; i++) {
int r = horizontal ? row : row + i;
int c = horizontal ? col + i : col;
grid[r][c] = CellState.SHIP;
}
return true;
}
/**
* 检查某个格子及其八邻域是否可以放置新战舰
*/
private boolean canPlaceAt(int row, int col) {
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
int nr = row + dr, nc = col + dc;
if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE) {
if (grid[nr][nc] == CellState.SHIP) return false;
}
}
}
return true;
}
/**
* 对指定坐标进行射击
*/
ShotResult shoot(int row, int col) {
if (grid[row][col] == CellState.SHIP) {
grid[row][col] = CellState.HIT;
for (Ship ship : ships) {
if (ship.contains(row, col)) {
boolean sunk = ship.hit(row, col);
if (sunk) {
markSunk(ship);
return new ShotResult(true, true, ship.type);
}
return new ShotResult(true, false, null);
}
}
}
grid[row][col] = CellState.MISS;
return new ShotResult(false, false, null);
}
/**
* 战舰被击沉后,将其所有格子标记为SUNK
*/
private void markSunk(Ship ship) {
for (int i = 0; i < ship.type.length; i++) {
int r = ship.horizontal ? ship.startRow : ship.startRow + i;
int c = ship.horizontal ? ship.startCol + i : ship.startCol;
grid[r][c] = CellState.SUNK;
}
}
boolean allShipsSunk() {
return ships.stream().allMatch(Ship::isSunk);
}
CellState getCell(int row, int col) {
return grid[row][col];
}
List<Ship> getShips() {
return ships;
}
}
/**
* 射击结果记录
*/
class ShotResult {
final boolean hit;
final boolean sunk;
final ShipType sunkShip;
ShotResult(boolean hit, boolean sunk, ShipType sunkShip) {
this.hit = hit;
this.sunk = sunk;
this.sunkShip = sunkShip;
}
}
二、AI核心算法:概率密度推理
当AI没有任何命中信息时,最佳策略是计算每个格子被战舰覆盖的概率。概率越高的格子,射击价值越大。
2.1 概率密度计算原理
核心思想:枚举所有可能的战舰放置方式,统计每个格子被多少种放置方式覆盖。覆盖次数越多,该格子有战舰的概率就越高。
具体步骤:
1. 对于每一种尚未被击沉的战舰类型,枚举它在当前棋盘上的所有合法放置位置
2. 每种放置位置必须满足:不覆盖已知的MISS格子,不覆盖已被击沉战舰的格子
3. 如果某格已经是HIT(命中但未沉),则只统计覆盖该格的放置方式(用于定向搜索阶段)
4. 累加每个格子的覆盖次数,得到概率密度图
/**
* 概率密度计算器
*/
class ProbabilityDensityCalculator {
private final Board enemyBoard; // AI视角的对手棋盘(仅知道MISS和HIT)
private final int[][] density;
private final Set<ShipType> remainingShips;
ProbabilityDensityCalculator(Board enemyBoard, Set<ShipType> remainingShips) {
this.enemyBoard = enemyBoard;
this.remainingShips = remainingShips;
this.density = new int[Board.SIZE][Board.SIZE];
}
/**
* 计算当前棋盘的概率密度图
*/
int[][] calculate() {
// 清空密度图
for (int[] row : density) {
Arrays.fill(row, 0);
}
// 对每种剩余的战舰,枚举所有合法放置
for (ShipType ship : remainingShips) {
enumeratePlacements(ship);
}
return density;
}
/**
* 枚举指定战舰的所有合法放置并累加密度
*/
private void enumeratePlacements(ShipType ship) {
// 水平放置
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col <= Board.SIZE - ship.length; col++) {
if (isValidPlacement(row, col, ship.length, true)) {
addDensity(row, col, ship.length, true);
}
}
}
// 垂直放置
for (int row = 0; row <= Board.SIZE - ship.length; row++) {
for (int col = 0; col < Board.SIZE; col++) {
if (isValidPlacement(row, col, ship.length, false)) {
addDensity(row, col, ship.length, false);
}
}
}
}
/**
* 检查某放置是否合法
* 合法条件:不覆盖MISS、不覆盖SUNK、覆盖所有已知的HIT(定向搜索时)
*/
private boolean isValidPlacement(int row, int col, int length, boolean horizontal) {
for (int i = 0; i < length; i++) {
int r = horizontal ? row : row + i;
int c = horizontal ? col + i : col;
CellState state = enemyBoard.getCell(r, c);
// 不能覆盖已知的MISS和SUNK
if (state == CellState.MISS || state == CellState.SUNK) {
return false;
}
}
return true;
}
/**
* 将某放置覆盖的格子密度加1
*/
private void addDensity(int row, int col, int length, boolean horizontal) {
for (int i = 0; i < length; i++) {
int r = horizontal ? row : row + i;
int c = horizontal ? col + i : col;
density[r][c]++;
}
}
}
2.2 带约束的定向搜索概率
当AI命中某格但战舰尚未击沉时,需要进入”定向搜索”模式。此时概率计算应增加约束:只统计覆盖已知HIT格子的放置方式。这样AI会优先射击与已知HIT在同一直线上的格子。
/**
* 定向搜索模式:只统计覆盖所有已知HIT的放置方式
*/
int[][] calculateWithHits(List<int[]> knownHits) {
for (int[] row : density) {
Arrays.fill(row, 0);
}
for (ShipType ship : remainingShips) {
enumerateConstrainedPlacements(ship, knownHits);
}
return density;
}
private void enumerateConstrainedPlacements(ShipType ship, List<int[]> knownHits) {
// 水平放置
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col <= Board.SIZE - ship.length; col++) {
if (isValidPlacement(row, col, ship.length, true)
&& coversHits(row, col, ship.length, true, knownHits)) {
addDensity(row, col, ship.length, true);
}
}
}
// 垂直放置
for (int row = 0; row <= Board.SIZE - ship.length; row++) {
for (int col = 0; col < Board.SIZE; col++) {
if (isValidPlacement(row, col, ship.length, false)
&& coversHits(row, col, ship.length, false, knownHits)) {
addDensity(row, col, ship.length, false);
}
}
}
}
/**
* 检查放置是否覆盖所有已知的HIT坐标
*/
private boolean coversHits(int row, int col, int length, boolean horizontal, List<int[]> knownHits) {
for (int[] hit : knownHits) {
boolean covered = false;
for (int i = 0; i < length; i++) {
int r = horizontal ? row : row + i;
int c = horizontal ? col + i : col;
if (r == hit[0] && c == hit[1]) {
covered = true;
break;
}
}
if (!covered) return false;
}
return true;
}
三、AI射击决策引擎
AI的决策分为两个阶段:探索阶段和定向追击阶段。
3.1 决策状态机
/**
* AI射击决策引擎
*/
class AIPlayer {
private final Board enemyBoard; // AI视角的对手棋盘
private final Set<ShipType> remainingShips; // 对手尚未沉没的战舰类型
private final List<int[]> pendingHits; // 已命中但未沉的坐标
private boolean huntingMode; // 是否处于定向追击模式
AIPlayer() {
this.enemyBoard = new Board();
this.remainingShips = new LinkedHashSet<>(Arrays.asList(ShipType.values()));
this.pendingHits = new ArrayList<>();
this.huntingMode = false;
}
/**
* 根据射击结果更新AI状态
*/
void reportShotResult(int row, int col, ShotResult result) {
if (result.hit) {
pendingHits.add(new int[]{row, col});
huntingMode = true;
if (result.sunk) {
// 战舰被击沉,从待处理列表中移除属于该舰的命中
remainingShips.remove(result.sunkShip);
removeSunkHits(result.sunkShip);
if (pendingHits.isEmpty()) {
huntingMode = false;
}
}
}
}
/**
* 从pendingHits中移除已被击沉战舰的坐标
*/
private void removeSunkHits(ShipType sunkShip) {
// 简化为清空所有pendingHits(假设一次只击沉一艘)
// 更精确的做法是根据战舰长度和位置匹配
pendingHits.clear();
}
/**
* 选择下一个射击目标
*/
int[] selectTarget() {
ProbabilityDensityCalculator calc = new ProbabilityDensityCalculator(enemyBoard, remainingShips);
int[][] density;
if (huntingMode && !pendingHits.isEmpty()) {
// 定向追击:计算覆盖所有已知HIT的概率分布
density = calc.calculateWithHits(pendingHits);
} else {
// 探索阶段:计算全局概率分布
density = calc.calculate();
}
// 贪心策略:选择概率密度最高的未射击格子
int maxDensity = -1;
int bestRow = -1, bestCol = -1;
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col < Board.SIZE; col++) {
CellState state = enemyBoard.getCell(row, col);
if (state == CellState.EMPTY || state == CellState.SHIP) {
if (density[row][col] > maxDensity) {
maxDensity = density[row][col];
bestRow = row;
bestCol = col;
}
}
}
}
return new int[]{bestRow, bestCol};
}
Board getEnemyBoard() {
return enemyBoard;
}
}
3.2 探索阶段的优化:棋盘着色策略
在没有任何命中信息的探索阶段,可以采用棋盘着色(Checkerboard)策略进一步优化。由于战舰最小长度为2,相邻的格子不可能同时属于同一艘战舰的最小覆盖。因此,优先射击黑白相间的格子可以提高早期发现战舰的效率。
/**
* 带棋盘着色优化的目标选择
*/
int[] selectTargetOptimized() {
ProbabilityDensityCalculator calc = new ProbabilityDensityCalculator(enemyBoard, remainingShips);
int[][] density;
if (huntingMode && !pendingHits.isEmpty()) {
density = calc.calculateWithHits(pendingHits);
} else {
density = calc.calculate();
}
int maxDensity = -1;
int bestRow = -1, bestCol = -1;
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col < Board.SIZE; col++) {
CellState state = enemyBoard.getCell(row, col);
if (state != CellState.EMPTY && state != CellState.SHIP) continue;
int d = density[row][col];
// 探索阶段应用棋盘着色加权
if (!huntingMode) {
if ((row + col) % 2 == 0) {
d = d * 3 / 2; // 优先射击某种颜色的格子
}
}
if (d > maxDensity) {
maxDensity = d;
bestRow = row;
bestCol = col;
}
}
}
return new int[]{bestRow, bestCol};
}
四、完整可运行代码
以下是将所有模块整合后的完整海战棋游戏,包含战舰随机部署、AI对战和胜负判定:
import java.util.*;
/**
* 海战棋主程序:概率密度AI对战演示
*/
public class BattleshipGame {
public static void main(String[] args) {
// 创建玩家棋盘和AI棋盘
Board playerBoard = createRandomBoard();
Board aiBoard = createRandomBoard();
AIPlayer ai = new AIPlayer();
Random random = new Random();
System.out.println("=== 海战棋AI对战开始 ===");
int round = 0;
while (!playerBoard.allShipsSunk() && !aiBoard.allShipsSunk()) {
round++;
System.out.println("\n--- 第 " + round + " 回合 ---");
// AI射击
int[] target = ai.selectTargetOptimized();
int row = target[0], col = target[1];
ShotResult result = aiBoard.shoot(row, col);
ai.reportShotResult(row, col, result);
System.out.printf("AI射击: (%d, %c) -> ", row, (char)('A' + col));
if (result.sunk) {
System.out.println("命中并击沉 " + result.sunkShip.name + "!");
} else if (result.hit) {
System.out.println("命中!");
} else {
System.out.println("未命中");
}
// 玩家随机射击(简化演示)
int pr, pc;
do {
pr = random.nextInt(Board.SIZE);
pc = random.nextInt(Board.SIZE);
} while (playerBoard.getCell(pr, pc) == CellState.MISS
|| playerBoard.getCell(pr, pc) == CellState.HIT
|| playerBoard.getCell(pr, pc) == CellState.SUNK);
ShotResult playerResult = playerBoard.shoot(pr, pc);
System.out.printf("玩家射击: (%d, %c) -> ", pr, (char)('A' + pc));
if (playerResult.sunk) {
System.out.println("命中并击沉 " + playerResult.sunkShip.name + "!");
} else if (playerResult.hit) {
System.out.println("命中!");
} else {
System.out.println("未命中");
}
}
System.out.println("\n=== 游戏结束 ===");
if (aiBoard.allShipsSunk()) {
System.out.println("AI获胜!共用了 " + round + " 回合");
} else {
System.out.println("玩家获胜!");
}
}
/**
* 随机生成一个合法部署的棋盘
*/
static Board createRandomBoard() {
Board board = new Board();
Random rand = new Random();
List<ShipType> types = Arrays.asList(ShipType.values());
for (ShipType type : types) {
boolean placed = false;
int attempts = 0;
while (!placed && attempts < 1000) {
boolean horizontal = rand.nextBoolean();
int row = rand.nextInt(Board.SIZE);
int col = rand.nextInt(Board.SIZE);
placed = board.placeShip(type, row, col, horizontal);
attempts++;
}
if (!placed) {
throw new RuntimeException("无法放置战舰: " + type.name);
}
}
return board;
}
}
// 将前文定义的所有枚举和类放入同一文件中即可编译运行
// (CellState, ShipType, Ship, Board, ShotResult,
// ProbabilityDensityCalculator, AIPlayer 等)
五、算法复杂度分析
| 阶段 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 战舰部署 | O(1) | O(1) | 固定5艘战舰,最多尝试1000次随机放置 |
| 全局概率密度 | O(k × n³) | O(n²) | k为剩余战舰种类数,n=10为棋盘边长 |
| 定向搜索概率 | O(k × n³) | O(n²) | 增加HIT约束后有效放置减少,实际更快 |
| 贪心目标选择 | O(n²) | O(1) | 遍历棋盘选择密度最大值 |
| 单局总回合 | 通常40-60回合 | — | AI平均50回合内可完成游戏 |
在10×10的标准棋盘上,概率密度计算的时间开销完全可以接受。对于更大的棋盘,可以通过剪枝和缓存进一步优化:只重新计算受上次射击影响的局部区域,而非整盘概率图。
六、算法扩展与优化方向
- 蒙特卡洛模拟:不单纯依赖概率密度,而是用蒙特卡洛方法模拟大量随机部署,统计每个格子的命中频率,在复杂约束下更准确。
- 动态战舰长度推断:当某艘战舰被击沉后,利用剩余战舰的长度信息更新概率模型,排除不可能的放置。
- 猎杀模式优化:命中后不仅考虑覆盖所有HIT的放置,还引入战舰长度约束,排除长度不足或方向矛盾的放置方式。
- 对手建模:在对战中学习对手的部署偏好(如是否倾向边缘放置),调整先验概率分布。
七、总结
海战棋AI的核心在于将不完全信息博弈转化为概率推理问题。通过概率密度分布计算,AI能够在没有任何命中信息时做出最优探索决策;命中后则通过定向搜索快速锁定并击沉目标战舰。贪心策略配合棋盘着色优化,使AI在早期探索阶段也能保持高效。这种将概率统计与贪心决策相结合的思路,不仅适用于海战棋,也是许多不完全信息博弈问题的通用解法。