每日算法 — 使用java实现战舰游戏:概率密度分布与最优搜索策略

战舰游戏(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 算法流程

  1. 状态维护:记录己方舰船布局、对方射击历史;记录对对方的射击结果(未探索/未命中/命中/击沉)。
  2. 阶段区分:
  3. 狩猎模式(Hunt):当没有未击沉的命中格子时,AI采用概率密度搜索。
  4. 目标模式(Target):当存在已命中但未击沉的格子时,AI优先射击该命中区域的相邻格子。
  5. 概率计算:对于狩猎模式,遍历所有未击沉舰船的所有可能放置方式,统计每个空格被覆盖的次数。
  6. 目标模式启发式:当存在”热点”(已命中但未击沉)时,使用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²) 存储游戏状态。

五、策略优化方向

  1. 奇偶棋盘优化:由于最小舰船长度为2,狩猎模式下可以仅射击棋盘黑白相间的格子,效率提升约一倍。
  2. 联合概率计算:当前实现独立计算每艘船的概率,更精确的模型应考虑多船之间的互斥约束。
  3. 深度目标追踪:当命中形成线段时,AI应优先沿该线段两端延伸,而非向垂直方向扩展。

通过概率密度分布策略,AI的平均射击次数可控制在40~50轮以内,远优于纯随机策略的约96轮,展现了信息推理在博弈中的强大威力。