炸弹人(Bomberman)是诞生于1983年的经典街机游戏,玩家操控角色在网格迷宫中放置炸弹,炸毁墙壁和敌人。其核心算法挑战在于:如何高效模拟炸弹的连锁爆炸传播、如何让敌人智能追击玩家、以及如何在复杂局势下做出最优的炸弹放置决策。本文将用Java完整实现炸弹人的核心算法引擎,深入讲解BFS爆炸范围计算、A*寻路驱动的敌人AI,以及基于安全距离评估的策略决策系统。
一、问题建模与地图表示
炸弹人的游戏世界可抽象为一个二维网格,每个格子具有不同的属性。我们用枚举类型定义格子状态:
/**
* 地图格子类型
*/
enum CellType {
EMPTY, // 空地,可行走
WALL, // 固定墙壁,不可破坏
BRICK, // 砖块,可被炸弹炸毁
BOMB, // 已放置的炸弹
EXPLOSION // 爆炸中的火焰
}
/**
* 地图单元格
*/
class Cell {
CellType type;
int bombPower; // 若type=BOMB,记录炸弹威力
int bombTimer; // 若type=BOMB,记录剩余引爆时间(帧数)
int explosionTimer; // 若type=EXPLOSION,记录火焰持续时间
Cell(CellType type) {
this.type = type;
}
}
游戏地图采用二维数组存储,配合玩家位置和敌人列表即可完整描述游戏状态:
class GameMap {
private final Cell[][] grid;
private final int rows;
private final int cols;
// 四个方向:上、下、左、右
static final int[][] DIRECTIONS = {{-1,0},{1,0},{0,-1},{0,1}};
GameMap(int rows, int cols) {
this.rows = rows;
this.cols = cols;
this.grid = new Cell[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 边界固定为墙壁
if (i == 0 || i == rows-1 || j == 0 || j == cols-1) {
grid[i][j] = new Cell(CellType.WALL);
} else {
// 随机生成砖块(约30%概率),确保可通行
grid[i][j] = new Cell(Math.random() < 0.3 ? CellType.BRICK : CellType.EMPTY);
}
}
}
// 确保玩家和敌人出生点为空
grid[1][1].type = CellType.EMPTY;
grid[rows-2][cols-2].type = CellType.EMPTY;
}
boolean inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
boolean isWalkable(int r, int c) {
return inBounds(r, c) && (grid[r][c].type == CellType.EMPTY || grid[r][c].type == CellType.EXPLOSION);
}
Cell get(int r, int c) { return grid[r][c]; }
int getRows() { return rows; }
int getCols() { return cols; }
}
二、BFS炸弹爆炸传播算法
炸弹引爆后,火焰沿四个方向直线蔓延,直到遇到固定墙壁为止,砖块会被炸毁并阻挡火焰继续传播。爆炸可能引燃其他炸弹,形成连锁反应。使用BFS可以高效计算所有受影响的格子:
import java.util.*;
/**
* 炸弹爆炸传播计算器
* 使用BFS处理连锁反应,确保所有被引燃的炸弹都被正确计算
*/
class ExplosionEngine {
private final GameMap map;
ExplosionEngine(GameMap map) {
this.map = map;
}
/**
* 计算并执行爆炸
* @param bombR 炸弹行坐标
* @param bombC 炸弹列坐标
* @return 被爆炸影响的坐标列表(包括炸弹本身位置)
*/
List<int[]> computeExplosion(int bombR, int bombC) {
List<int[]> affected = new ArrayList<>();
Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[map.getRows()][map.getCols()];
// 从初始炸弹开始BFS
queue.offer(new int[]{bombR, bombC});
visited[bombR][bombC] = true;
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int r = cur[0], c = cur[1];
Cell cell = map.get(r, c);
// 获取炸弹威力,若当前格子不是炸弹(被引燃的情况),使用默认威力3
int power = (cell.type == CellType.BOMB) ? cell.bombPower : 3;
// 当前格子被爆炸影响
affected.add(new int[]{r, c});
// 向四个方向传播
for (int[] dir : GameMap.DIRECTIONS) {
for (int step = 1; step <= power; step++) {
int nr = r + dir[0] * step;
int nc = c + dir[1] * step;
if (!map.inBounds(nr, nc)) break;
Cell target = map.get(nr, nc);
// 固定墙壁阻挡火焰
if (target.type == CellType.WALL) break;
// 记录受影响位置
affected.add(new int[]{nr, nc});
// 砖块被炸毁后阻挡后续传播
if (target.type == CellType.BRICK) {
break;
}
// 遇到其他炸弹,加入队列处理连锁反应
if (target.type == CellType.BOMB && !visited[nr][nc]) {
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc});
break; // 炸弹位置阻挡火焰继续沿此方向传播
}
}
}
}
return affected;
}
/**
* 执行一帧的爆炸计时更新
* 返回本帧内新引爆的炸弹位置列表
*/
List<int[]> updateBombs() {
List<int[]> exploded = new ArrayList<>();
for (int i = 0; i < map.getRows(); i++) {
for (int j = 0; j < map.getCols(); j++) {
Cell cell = map.get(i, j);
if (cell.type == CellType.BOMB) {
cell.bombTimer--;
if (cell.bombTimer <= 0) {
exploded.add(new int[]{i, j});
}
} else if (cell.type == CellType.EXPLOSION) {
cell.explosionTimer--;
if (cell.explosionTimer <= 0) {
cell.type = CellType.EMPTY;
}
}
}
}
// 处理爆炸
for (int[] pos : exploded) {
List<int[]> affected = computeExplosion(pos[0], pos[1]);
for (int[] a : affected) {
Cell cell = map.get(a[0], a[1]);
if (cell.type == CellType.BRICK) {
cell.type = CellType.EMPTY; // 炸毁砖块
} else if (cell.type == CellType.BOMB) {
// 连锁爆炸已在computeExplosion中通过BFS处理
cell.type = CellType.EXPLOSION;
cell.explosionTimer = 3;
} else if (cell.type != CellType.WALL) {
cell.type = CellType.EXPLOSION;
cell.explosionTimer = 3;
}
}
}
return exploded;
}
}
三、A*寻路驱动的敌人AI
敌人需要能够智能追击玩家,同时避开即将爆炸的区域。A*算法配合动态障碍物更新是实现这一行为的核心:
/**
* A*寻路节点
*/
class Node implements Comparable<Node> {
int r, c; // 坐标
int g; // 从起点到当前节点的实际代价
int h; // 启发式估计值(曼哈顿距离)
Node parent; // 父节点,用于回溯路径
Node(int r, int c, int g, int h, Node parent) {
this.r = r; this.c = c;
this.g = g; this.h = h;
this.parent = parent;
}
int f() { return g + h; }
@Override
public int compareTo(Node o) {
return Integer.compare(this.f(), o.f());
}
}
/**
* 敌人AI控制器
* 基于A*寻路,结合安全区域判断实现智能追击
*/
class EnemyAI {
private final GameMap map;
private int posR, posC; // 敌人当前位置
EnemyAI(GameMap map, int startR, int startC) {
this.map = map;
this.posR = startR;
this.posC = startC;
}
/**
* A*寻路:从当前位置到目标位置的最短路径
* @param targetR 目标行
* @param targetC 目标列
* @return 下一步应移动的坐标 [r, c],若无法到达返回null
*/
int[] findPath(int targetR, int targetC) {
PriorityQueue<Node> open = new PriorityQueue<>();
boolean[][] closed = new boolean[map.getRows()][map.getCols()];
open.offer(new Node(posR, posC, 0, heuristic(posR, posC, targetR, targetC), null));
while (!open.isEmpty()) {
Node cur = open.poll();
if (cur.r == targetR && cur.c == targetC) {
// 回溯找到第一步
Node step = cur;
while (step.parent != null && step.parent.parent != null) {
step = step.parent;
}
return new int[]{step.r, step.c};
}
if (closed[cur.r][cur.c]) continue;
closed[cur.r][cur.c] = true;
for (int[] dir : GameMap.DIRECTIONS) {
int nr = cur.r + dir[0];
int nc = cur.c + dir[1];
if (!map.isWalkable(nr, nc) || closed[nr][nc]) continue;
// 避开爆炸区域(增加额外代价)
int extraCost = (map.get(nr, nc).type == CellType.EXPLOSION) ? 100 : 0;
int ng = cur.g + 1 + extraCost;
int nh = heuristic(nr, nc, targetR, targetC);
open.offer(new Node(nr, nc, ng, nh, cur));
}
}
return null; // 无可行路径
}
/**
* 曼哈顿距离启发式
*/
private int heuristic(int r1, int c1, int r2, int c2) {
return Math.abs(r1 - r2) + Math.abs(c1 - c2);
}
/**
* 敌人决策:优先追击玩家,若玩家附近危险则随机游走
* @param playerR 玩家行
* @param playerC 玩家列
* @return 下一步移动方向 [dr, dc]
*/
int[] decideMove(int playerR, int playerC) {
// 首先尝试直接追击
int[] next = findPath(playerR, playerC);
if (next != null) {
return new int[]{next[0] - posR, next[1] - posC};
}
// 若无法直接到达,随机选择安全方向
List<int[]> safeDirs = new ArrayList<>();
for (int[] dir : GameMap.DIRECTIONS) {
int nr = posR + dir[0];
int nc = posC + dir[1];
if (map.isWalkable(nr, nc) && map.get(nr, nc).type != CellType.EXPLOSION) {
safeDirs.add(dir);
}
}
if (!safeDirs.isEmpty()) {
return safeDirs.get((int)(Math.random() * safeDirs.size()));
}
return new int[]{0, 0}; // 原地不动
}
void move(int dr, int dc) {
int nr = posR + dr;
int nc = posC + dc;
if (map.isWalkable(nr, nc)) {
posR = nr;
posC = nc;
}
}
int getR() { return posR; }
int getC() { return posC; }
}
四、玩家策略决策:安全区域评估与炸弹放置
玩家AI需要决定何时放置炸弹、往哪个方向移动。核心策略是:在保证自身安全的前提下,最大化炸毁砖块和消灭敌人的机会。我们通过BFS计算每个格子的”安全时间”——即该格子何时会被爆炸波及:
/**
* 安全评估引擎
* 计算地图每个格子的安全状态,辅助AI决策
*/
class SafetyEvaluator {
private final GameMap map;
SafetyEvaluator(GameMap map) {
this.map = map;
}
/**
* 计算安全距离图:每个格子的最近爆炸时间(帧数)
* 返回二维数组,值越大越安全(Integer.MAX_VALUE表示绝对安全)
*/
int[][] computeSafetyMap() {
int rows = map.getRows();
int cols = map.getCols();
int[][] safety = new int[rows][cols];
for (int i = 0; i < rows; i++) {
Arrays.fill(safety[i], Integer.MAX_VALUE);
}
// 遍历所有即将爆炸的炸弹
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
Cell cell = map.get(i, j);
if (cell.type == CellType.BOMB) {
int timeToExplode = cell.bombTimer;
// 该炸弹所在位置
safety[i][j] = Math.min(safety[i][j], timeToExplode);
// 沿四个方向计算波及范围
for (int[] dir : GameMap.DIRECTIONS) {
for (int step = 1; step <= cell.bombPower; step++) {
int ni = i + dir[0] * step;
int nj = j + dir[1] * step;
if (!map.inBounds(ni, nj)) break;
Cell target = map.get(ni, nj);
if (target.type == CellType.WALL) break;
safety[ni][nj] = Math.min(safety[ni][nj], timeToExplode);
if (target.type == CellType.BRICK) break;
}
}
}
}
}
return safety;
}
/**
* 判断从指定位置能否在爆炸前到达安全区
* @param startR 起始行
* @param startC 起始列
* @param safetyMap 安全距离图
* @return 若存在逃生路径返回true
*/
boolean hasEscapeRoute(int startR, int startC, int[][] safetyMap) {
Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[map.getRows()][map.getCols()];
queue.offer(new int[]{startR, startC, 0}); // {r, c, elapsedTime}
visited[startR][startC] = true;
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int r = cur[0], c = cur[1], t = cur[2];
// 找到安全区:当前格子不会被爆炸波及,或爆炸时间足够我们离开
if (safetyMap[r][c] == Integer.MAX_VALUE || safetyMap[r][c] > t + 2) {
return true;
}
for (int[] dir : GameMap.DIRECTIONS) {
int nr = r + dir[0];
int nc = c + dir[1];
int nt = t + 1;
if (!map.isWalkable(nr, nc) || visited[nr][nc]) continue;
// 确保移动到这个格子时它还安全
if (safetyMap[nr][nc] > nt) {
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc, nt});
}
}
}
return false;
}
/**
* 评估在指定位置放置炸弹的价值
* 综合考虑:可炸砖块数、可消灭敌人数、自身安全性
*/
double evaluateBombPlacement(int r, int c, int power, List<EnemyAI> enemies, int playerR, int playerC) {
// 模拟放置炸弹后的安全评估
int[][] simulatedSafety = simulateExplosion(r, c, power);
// 检查自身是否能逃生
if (!hasEscapeRoute(playerR, playerC, simulatedSafety)) {
return Double.NEGATIVE_INFINITY; // 会炸到自己,绝对禁止
}
double score = 0;
// 1. 可炸毁的砖块数(每个+10分)
for (int i = 0; i < map.getRows(); i++) {
for (int j = 0; j < map.getCols(); j++) {
if (simulatedSafety[i][j] < Integer.MAX_VALUE && map.get(i, j).type == CellType.BRICK) {
score += 10;
}
}
}
// 2. 可消灭的敌人(每个+50分)
for (EnemyAI enemy : enemies) {
if (simulatedSafety[enemy.getR()][enemy.getC()] < Integer.MAX_VALUE) {
score += 50;
}
}
// 3. 距离敌人的远近(越近越鼓励放置)
for (EnemyAI enemy : enemies) {
int dist = Math.abs(r - enemy.getR()) + Math.abs(c - enemy.getC());
score += Math.max(0, 20 - dist);
}
return score;
}
/**
* 模拟在(r,c)放置威力为power的炸弹后的安全图
*/
private int[][] simulateExplosion(int r, int c, int power) {
int rows = map.getRows();
int cols = map.getCols();
int[][] sim = new int[rows][cols];
for (int i = 0; i < rows; i++) Arrays.fill(sim[i], Integer.MAX_VALUE);
// 假设炸弹3帧后爆炸
sim[r][c] = 3;
for (int[] dir : GameMap.DIRECTIONS) {
for (int step = 1; step <= power; step++) {
int ni = r + dir[0] * step;
int nj = c + dir[1] * step;
if (!map.inBounds(ni, nj)) break;
if (map.get(ni, nj).type == CellType.WALL) break;
sim[ni][nj] = 3;
if (map.get(ni, nj).type == CellType.BRICK) break;
}
}
return sim;
}
}
五、游戏主循环与完整运行示例
将以上模块整合,我们得到完整的炸弹人游戏引擎:
public class BombermanGame {
private final GameMap map;
private final ExplosionEngine explosionEngine;
private final SafetyEvaluator safetyEvaluator;
private final List<EnemyAI> enemies;
private int playerR, playerC;
private int playerBombPower = 2;
private boolean gameOver = false;
public BombermanGame(int rows, int cols) {
this.map = new GameMap(rows, cols);
this.explosionEngine = new ExplosionEngine(map);
this.safetyEvaluator = new SafetyEvaluator(map);
this.enemies = new ArrayList<>();
this.playerR = 1;
this.playerC = 1;
// 生成敌人
enemies.add(new EnemyAI(map, rows-2, cols-2));
enemies.add(new EnemyAI(map, rows-2, 2));
}
/**
* 玩家移动
*/
void movePlayer(int dr, int dc) {
int nr = playerR + dr;
int nc = playerC + dc;
if (map.isWalkable(nr, nc)) {
playerR = nr;
playerC = nc;
}
}
/**
* 玩家放置炸弹
*/
void placeBomb() {
Cell cell = map.get(playerR, playerC);
if (cell.type == CellType.EMPTY) {
cell.type = CellType.BOMB;
cell.bombPower = playerBombPower;
cell.bombTimer = 60; // 60帧后爆炸(约3秒,假设20FPS)
}
}
/**
* 玩家AI自动决策:移动或放炸弹
*/
void playerAutoAct() {
int[][] safety = safetyEvaluator.computeSafetyMap();
// 当前位置危险,优先逃跑
if (safety[playerR][playerC] < Integer.MAX_VALUE && safety[playerR][playerC] < 5) {
int[] escapeDir = findEscapeDirection(safety);
if (escapeDir != null) {
movePlayer(escapeDir[0], escapeDir[1]);
return;
}
}
// 评估当前位置放炸弹的价值
double score = safetyEvaluator.evaluateBombPlacement(
playerR, playerC, playerBombPower, enemies, playerR, playerC);
if (score > 20) {
placeBomb();
// 放完炸弹立即寻找逃生方向
int[][] newSafety = safetyEvaluator.computeSafetyMap();
int[] escapeDir = findEscapeDirection(newSafety);
if (escapeDir != null) {
movePlayer(escapeDir[0], escapeDir[1]);
}
} else {
// 向最近的敌人移动
if (!enemies.isEmpty()) {
EnemyAI target = enemies.get(0);
int minDist = Math.abs(playerR - target.getR()) + Math.abs(playerC - target.getC());
for (EnemyAI e : enemies) {
int d = Math.abs(playerR - e.getR()) + Math.abs(playerC - e.getC());
if (d < minDist) {
minDist = d;
target = e;
}
}
int dr = Integer.compare(target.getR(), playerR);
int dc = Integer.compare(target.getC(), playerC);
if (dr != 0 && map.isWalkable(playerR + dr, playerC)) {
movePlayer(dr, 0);
} else if (dc != 0 && map.isWalkable(playerR, playerC + dc)) {
movePlayer(0, dc);
}
}
}
}
/**
* 寻找最安全的逃跑方向
*/
private int[] findEscapeDirection(int[][] safety) {
int bestScore = -1;
int[] bestDir = null;
for (int[] dir : GameMap.DIRECTIONS) {
int nr = playerR + dir[0];
int nc = playerC + dir[1];
if (!map.isWalkable(nr, nc)) continue;
int score = safety[nr][nc];
if (score == Integer.MAX_VALUE) score = 1000;
if (score > bestScore) {
bestScore = score;
bestDir = dir;
}
}
return bestDir;
}
/**
* 更新一帧
*/
void update() {
// 更新炸弹与爆炸
explosionEngine.updateBombs();
// 敌人行动
for (EnemyAI enemy : enemies) {
int[] move = enemy.decideMove(playerR, playerC);
enemy.move(move[0], move[1]);
}
// 玩家AI自动行动
playerAutoAct();
// 碰撞检测
checkCollisions();
}
private void checkCollisions() {
// 玩家被爆炸击中或碰到敌人
if (map.get(playerR, playerC).type == CellType.EXPLOSION) {
gameOver = true;
return;
}
for (EnemyAI enemy : enemies) {
if (enemy.getR() == playerR && enemy.getC() == playerC) {
gameOver = true;
return;
}
if (map.get(enemy.getR(), enemy.getC()).type == CellType.EXPLOSION) {
// 敌人被炸死
}
}
}
void printMap() {
for (int i = 0; i < map.getRows(); i++) {
for (int j = 0; j < map.getCols(); j++) {
if (i == playerR && j == playerC) {
System.out.print("P ");
} else {
boolean isEnemy = false;
for (EnemyAI e : enemies) {
if (e.getR() == i && e.getC() == j) {
isEnemy = true;
break;
}
}
if (isEnemy) {
System.out.print("E ");
} else {
switch (map.get(i, j).type) {
case WALL: System.out.print("# "); break;
case BRICK: System.out.print("B "); break;
case BOMB: System.out.print("O "); break;
case EXPLOSION: System.out.print("* "); break;
default: System.out.print(". ");
}
}
}
}
System.out.println();
}
System.out.println();
}
public static void main(String[] args) throws InterruptedException {
BombermanGame game = new BombermanGame(11, 15);
System.out.println("=== 炸弹人游戏启动 ===");
game.printMap();
// 模拟运行30帧
for (int frame = 0; frame < 30 && !game.gameOver; frame++) {
game.update();
System.out.println("Frame " + (frame + 1) + ":");
game.printMap();
Thread.sleep(100);
}
System.out.println(game.gameOver ? "游戏结束!" : "模拟结束");
}
}
六、复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| BFS爆炸传播 | O(b·p) | O(b) | b为炸弹数,p为单炸弹平均波及格子数 |
| A*寻路 | O(E + V log V) | O(V) | V为网格节点数,E为可通行边数 |
| 安全图计算 | O(b·p + V) | O(V) | 遍历所有炸弹和网格 |
| 逃生路径判断 | O(V) | O(V) | BFS搜索安全区域 |
| 炸弹价值评估 | O(p + E) | O(V) | 模拟爆炸+统计敌人位置 |
七、总结与延伸
本文通过Java实现了炸弹人的核心算法引擎,涵盖了三个关键技术点:
- BFS连锁爆炸传播:通过队列处理炸弹之间的引燃关系,确保连锁反应被正确计算,避免简单递归导致的栈溢出或重复计算。
- A*智能寻路:敌人AI利用A*算法实时追踪玩家位置,并通过动态调整代价函数避开爆炸危险区域,实现有”危机感”的追击行为。
- 安全评估与策略决策:玩家AI通过预计算安全距离图评估局势,只在确保有逃生路径时才放置炸弹,实现攻防兼备的决策逻辑。
延伸方向包括:引入多智能体协同追击策略、使用蒙特卡洛搜索(MCTS)优化炸弹放置的长期收益、以及通过状态压缩动态规划求解特定地图的最优通关路径。