战舰游戏(Battleship)是一款经典的双人策略棋类游戏,双方在各自的10×10海域上秘密部署不同长度的战舰,然后轮流射击对方的网格以击沉全部敌舰。本文将用Java实现一个带AI的战舰游戏,核心在于讲解概率密度推理算法——AI如何根据历史命中信息,计算每个未探索格子上存在敌舰的概率,从而选择最优射击点。
一、游戏规则与问题建模
标准战舰游戏的海域为10×10网格,包含以下舰队:
– 航空母舰(Carrier):5格
– 战列舰(Battleship):4格
– 巡洋舰(Cruiser):3格
– 潜艇(Submarine):3格
– 驱逐舰(Destroyer):2格
每艘船可以水平或垂直放置,船与船之间不能重叠。玩家轮流声明一个坐标(如”A5″),对方告知”命中”(Hit)或”未命中”(Miss)。当一艘船的所有格子都被命中时,对方需告知”击沉”(Sunk)。先击沉对方全部舰船的一方获胜。
二、AI核心策略:概率密度搜索
最简单的AI采用随机射击,但效率极低。更聪明的AI采用概率密度分布策略:对于每一个尚未被射击的格子,计算在所有可能的敌舰布局中,该格子被某艘未击沉的舰船覆盖的总次数。覆盖次数越高的格子,存在敌舰的概率越大。
2.1 算法流程
- 状态维护:记录己方舰船布局、对方射击历史;记录对对方的射击结果(未探索/未命中/命中/击沉)。
- 阶段区分:
- 狩猎模式(Hunt):当没有未击沉的命中格子时,AI采用概率密度搜索。
- 目标模式(Target):当存在已命中但未击沉的格子时,AI优先射击该命中区域的相邻格子。
- 概率计算:对于狩猎模式,遍历所有未击沉舰船的所有可能放置方式,统计每个空格被覆盖的次数。
- 目标模式启发式:当存在”热点”(已命中但未击沉)时,使用BFS扩展相邻格子,优先沿命中方向延伸。
2.2 概率密度计算
假设剩余未击沉舰船集合为 remainingShips。对于每个格子 (r, c),其概率分数计算如下:
score[r][c] = sum over all ships s in remainingShips:
count of valid placements of s that cover (r, c)
这里的”valid placement”需要满足:
– 不超出10×10边界
– 不覆盖任何已知”未命中”的格子
– 不覆盖任何已”击沉”舰船的格子
– 必须覆盖所有已”命中”但未”击沉”的格子(在目标模式下)
这种蒙特卡洛枚举虽然计算量较大,但10×10网格的搜索空间完全在可接受范围内。
三、Java完整实现
import java.util.*;
/**
* 战舰游戏AI实现 — 概率密度分布与最优搜索策略
*/
public class BattleshipAI {
private static final int SIZE = 10;
// 舰船类型:[名称, 长度]
private static final String[][] SHIPS = {
{"Carrier", "5"},
{"Battleship", "4"},
{"Cruiser", "3"},
{"Submarine", "3"},
{"Destroyer", "2"}
};
// 格子状态枚举
enum CellState {
UNKNOWN, // 未探索
MISS, // 未命中
HIT, // 命中但未击沉
SUNK // 已击沉的舰船格子
}
private CellState[][] enemyBoard = new CellState[SIZE][SIZE];
// 记录哪些舰船已被击沉
private boolean[] shipSunk = new boolean[SHIPS.length];
// 当前剩余舰船的索引列表
private List<Integer> remainingShips = new ArrayList<>();
public BattleshipAI() {
for (int i = 0; i < SIZE; i++) {
Arrays.fill(enemyBoard[i], CellState.UNKNOWN);
}
for (int i = 0; i < SHIPS.length; i++) {
remainingShips.add(i);
}
}
/**
* 接收射击结果并更新状态
*/
public void reportShot(int row, int col, boolean hit, boolean sunk, int shipIndex) {
enemyBoard[row][col] = hit ? CellState.HIT : CellState.MISS;
if (sunk && shipIndex >= 0) {
shipSunk[shipIndex] = true;
remainingShips.remove(Integer.valueOf(shipIndex));
// 将所有属于该船的HIT标记为SUNK(简化处理,实际需根据连续HIT推断)
markSunkShip(row, col, Integer.parseInt(SHIPS[shipIndex][1]));
}
}
/**
* 标记已击沉舰船的所有格子为SUNK
*/
private void markSunkShip(int lastRow, int lastCol, int shipLen) {
// 查找与(lastRow,lastCol)相连的所有HIT,标记为SUNK
List<int[]> hits = findConnectedHits(lastRow, lastCol);
if (hits.size() == shipLen) {
for (int[] h : hits) {
enemyBoard[h[0]][h[1]] = CellState.SUNK;
}
}
}
private List<int[]> findConnectedHits(int r, int c) {
List<int[]> result = new ArrayList<>();
boolean[][] visited = new boolean[SIZE][SIZE];
Queue<int[]> q = new LinkedList<>();
q.offer(new int[]{r, c});
visited[r][c] = true;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
while (!q.isEmpty()) {
int[] cur = q.poll();
result.add(cur);
for (int[] d : dirs) {
int nr = cur[0] + d[0], nc = cur[1] + d[1];
if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE
&& !visited[nr][nc] && enemyBoard[nr][nc] == CellState.HIT) {
visited[nr][nc] = true;
q.offer(new int[]{nr, nc});
}
}
}
return result;
}
/**
* 获取AI推荐的下一个射击坐标
*/
public int[] getNextShot() {
List<int[]> unsunkHits = getUnsunkHits();
if (!unsunkHits.isEmpty()) {
return targetMode(unsunkHits);
}
return huntMode();
}
/**
* 获取所有已命中但未击沉的格子
*/
private List<int[]> getUnsunkHits() {
List<int[]> hits = new ArrayList<>();
for (int r = 0; r < SIZE; r++) {
for (int c = 0; c < SIZE; c++) {
if (enemyBoard[r][c] == CellState.HIT) {
hits.add(new int[]{r, c});
}
}
}
return hits;
}
/**
* 目标模式:围绕已命中格子进行扩展射击
*/
private int[] targetMode(List<int[]> unsunkHits) {
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
int bestScore = -1;
int[] best = null;
for (int[] hit : unsunkHits) {
for (int[] d : dirs) {
int nr = hit[0] + d[0], nc = hit[1] + d[1];
if (isValidUnknown(nr, nc)) {
int score = targetScore(nr, nc, unsunkHits);
if (score > bestScore) {
bestScore = score;
best = new int[]{nr, nc};
}
}
}
}
return best != null ? best : huntMode();
}
/**
* 目标模式下格子的评分:优先沿命中方向延伸
*/
private int targetScore(int r, int c, List<int[]> unsunkHits) {
int score = 10;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE) {
if (enemyBoard[nr][nc] == CellState.HIT) score += 50;
if (enemyBoard[nr][nc] == CellState.UNKNOWN) score += 1;
}
}
return score;
}
/**
* 狩猎模式:概率密度搜索
*/
private int[] huntMode() {
int[][] scoreBoard = new int[SIZE][SIZE];
for (int shipIdx : remainingShips) {
int shipLen = Integer.parseInt(SHIPS[shipIdx][1]);
// 水平放置
for (int r = 0; r < SIZE; r++) {
for (int c = 0; c <= SIZE - shipLen; c++) {
if (canPlaceShip(r, c, shipLen, true)) {
for (int k = 0; k < shipLen; k++) {
scoreBoard[r][c + k]++;
}
}
}
}
// 垂直放置
for (int r = 0; r <= SIZE - shipLen; r++) {
for (int c = 0; c < SIZE; c++) {
if (canPlaceShip(r, c, shipLen, false)) {
for (int k = 0; k < shipLen; k++) {
scoreBoard[r + k][c]++;
}
}
}
}
}
// 选择概率最高的格子
int maxScore = -1;
int[] best = null;
for (int r = 0; r < SIZE; r++) {
for (int c = 0; c < SIZE; c++) {
if (enemyBoard[r][c] == CellState.UNKNOWN && scoreBoard[r][c] > maxScore) {
maxScore = scoreBoard[r][c];
best = new int[]{r, c};
}
}
}
return best;
}
/**
* 检查舰船是否可以放置在指定位置
*/
private boolean canPlaceShip(int row, int col, int len, boolean horizontal) {
for (int k = 0; k < len; k++) {
int r = horizontal ? row : row + k;
int c = horizontal ? col + k : col;
if (!isValidUnknown(r, c)) return false;
}
return true;
}
private boolean isValidUnknown(int r, int c) {
return r >= 0 && r < SIZE && c >= 0 && c < SIZE
&& enemyBoard[r][c] == CellState.UNKNOWN;
}
// ====== 主程序:演示AI与随机玩家的对战 ======
public static void main(String[] args) {
// 创建两个AI实例,一个使用概率密度策略,一个使用随机策略
BattleshipGame game1 = new BattleshipGame();
BattleshipGame game2 = new BattleshipGame();
game1.randomPlaceShips();
game2.randomPlaceShips();
SmartAI ai1 = new SmartAI();
RandomAI ai2 = new RandomAI();
int turns1 = 0, turns2 = 0;
boolean turn1 = true;
while (!game1.allSunk() && !game2.allSunk()) {
if (turn1) {
int[] shot = ai1.getNextShot(game2);
boolean[] result = game2.shoot(shot[0], shot[1]);
ai1.reportResult(shot[0], shot[1], result[0], result[1], (int)result[2]);
turns1++;
} else {
int[] shot = ai2.getNextShot(game1);
boolean[] result = game1.shoot(shot[0], shot[1]);
ai2.reportResult(shot[0], shot[1], result[0], result[1], (int)result[2]);
turns2++;
}
turn1 = !turn1;
}
System.out.println("SmartAI turns: " + turns1);
System.out.println("RandomAI turns: " + turns2);
}
}
/**
* 简化版游戏引擎
*/
class BattleshipGame {
private static final int SIZE = 10;
private int[][] board = new int[SIZE][SIZE]; // 0=empty, 1..n=ship index+1
private boolean[][] hit = new boolean[SIZE][SIZE];
private int[] shipHealth = {5, 4, 3, 3, 2};
private boolean[] shipAlive = {true, true, true, true, true};
private Random rand = new Random();
void randomPlaceShips() {
int[][] ships = {{5}, {4}, {3}, {3}, {2}};
for (int i = 0; i < ships.length; i++) {
int len = ships[i][0];
boolean placed = false;
while (!placed) {
boolean horiz = rand.nextBoolean();
int r = rand.nextInt(horiz ? SIZE : SIZE - len + 1);
int c = rand.nextInt(horiz ? SIZE - len + 1 : SIZE);
if (canPlace(r, c, len, horiz)) {
for (int k = 0; k < len; k++) {
board[horiz ? r : r + k][horiz ? c + k : c] = i + 1;
}
placed = true;
}
}
}
}
boolean canPlace(int r, int c, int len, boolean horiz) {
for (int k = 0; k < len; k++) {
int rr = horiz ? r : r + k;
int cc = horiz ? c + k : c;
if (board[rr][cc] != 0) return false;
}
return true;
}
boolean[] shoot(int r, int c) {
hit[r][c] = true;
int ship = board[r][c];
boolean isHit = ship > 0;
boolean isSunk = false;
if (isHit) {
shipHealth[ship - 1]--;
if (shipHealth[ship - 1] == 0) {
shipAlive[ship - 1] = false;
isSunk = true;
}
}
return new boolean[]{isHit, isSunk, ship > 0 ? ship - 1 : -1};
}
boolean allSunk() {
for (boolean a : shipAlive) if (a) return false;
return true;
}
boolean[][] getHitBoard() { return hit; }
int[][] getBoard() { return board; }
}
/**
* 智能AI封装
*/
class SmartAI extends BattleshipAI {
public int[] getNextShot(BattleshipGame game) {
return getNextShot();
}
public void reportResult(int r, int c, boolean hit, boolean sunk, int shipIdx) {
reportShot(r, c, hit, sunk, shipIdx);
}
}
/**
* 随机AI对比
*/
class RandomAI {
private boolean[][] shot = new boolean[10][10];
private Random rand = new Random();
int[] getNextShot(BattleshipGame game) {
while (true) {
int r = rand.nextInt(10), c = rand.nextInt(10);
if (!shot[r][c]) {
shot[r][c] = true;
return new int[]{r, c};
}
}
}
void reportResult(int r, int c, boolean hit, boolean sunk, int shipIdx) {}
}
四、算法复杂度分析
- 概率密度计算:对于每艘长度为
L的剩余舰船,在10×10网格上有约2 × 10 × (10-L+1)种放置方式。5艘船总计约几百种组合,计算量极小。 - 目标模式:BFS扩展已命中区域,最多访问
O(SIZE²)个格子。 - 整体时间复杂度:每轮决策
O(SIZE² × totalShipCells),对于10×10网格可视为常数时间。 - 空间复杂度:
O(SIZE²)存储游戏状态。
五、策略优化方向
- 奇偶棋盘优化:由于最小舰船长度为2,狩猎模式下可以仅射击棋盘黑白相间的格子,效率提升约一倍。
- 联合概率计算:当前实现独立计算每艘船的概率,更精确的模型应考虑多船之间的互斥约束。
- 深度目标追踪:当命中形成线段时,AI应优先沿该线段两端延伸,而非向垂直方向扩展。
通过概率密度分布策略,AI的平均射击次数可控制在40~50轮以内,远优于纯随机策略的约96轮,展现了信息推理在博弈中的强大威力。