每日算法 — 使用java实现贪吃蛇:迭代深化A*搜索与安全路径规划

引言

贪吃蛇(Snake)是电子游戏史上最经典的益智游戏之一。玩家控制一条不断移动的蛇,通过吃食物来增长身体,同时避免撞墙或撞到自己的身体。随着蛇身越来越长,寻找安全路径的难度呈指数级增长。本文将使用Java实现一个基于迭代深化A(IDA)搜索算法的贪吃蛇AI,在有限的状态空间内高效规划安全路径,确保蛇能持续存活并吃到食物。

算法核心思想

迭代深化A(IDA)

IDA是A搜索算法的内存优化版本。它通过迭代加深的方式,逐步增加搜索的阈值(f值上限),在每次迭代中执行深度优先搜索,只扩展f值不超过当前阈值的节点。当找到目标时立即返回,避免了A*需要维护大规模优先队列的内存开销。

f(n) = g(n) + h(n),其中:
– g(n):从起点到当前节点的实际代价(已走步数)
– h(n):启发式函数估计的剩余代价

安全路径规划策略

贪吃蛇AI需要解决两个核心问题:
1. 寻找食物路径:使用IDA搜索从蛇头到食物的最短路径
2.
尾部避让*:吃完食物后,蛇身会变长,必须确保新位置不会堵住后续退路

启发式函数采用曼哈顿距离,同时增加尾部空间评估,确保路径终点附近有足够的”逃生空间”。

完整Java实现

import java.util.*;

/**
 * 贪吃蛇游戏与IDA* AI求解器
 * 使用迭代深化A*搜索算法规划安全路径
 */
public class SnakeIDAStar {

    // 游戏区域大小
    private static final int WIDTH = 20;
    private static final int HEIGHT = 20;

    // 方向向量:上、右、下、左
    private static final int[][] DIRS = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    private static final char[] DIR_NAMES = {'U', 'R', 'D', 'L'};

    // 游戏状态
    private List<int[]> snake; // 蛇身坐标,头部在索引0
    private int[] food;        // 食物坐标
    private boolean[][] board; // true表示被蛇身占据

    public SnakeIDAStar() {
        this.snake = new ArrayList<>();
        this.board = new boolean[HEIGHT][WIDTH];
    }

    /**
     * 初始化游戏状态
     */
    public void initGame() {
        snake.clear();
        for (boolean[] row : board) Arrays.fill(row, false);

        // 蛇初始位置在中心
        int startX = HEIGHT / 2;
        int startY = WIDTH / 2;
        snake.add(new int[]{startX, startY});
        board[startX][startY] = true;

        // 生成食物
        generateFood();
    }

    /**
     * 随机生成食物,确保不在蛇身上
     */
    private void generateFood() {
        Random rand = new Random();
        do {
            food = new int[]{rand.nextInt(HEIGHT), rand.nextInt(WIDTH)};
        } while (board[food[0]][food[1]]);
    }

    /**
     * 曼哈顿距离启发式函数
     */
    private int heuristic(int x, int y) {
        return Math.abs(x - food[0]) + Math.abs(y - food[1]);
    }

    /**
     * 检查坐标是否在边界内且不被占据
     * @param tailIgnored 是否忽略蛇尾(蛇会移动,尾部将空出)
     */
    private boolean isValid(int x, int y, boolean tailIgnored) {
        if (x < 0 || x >= HEIGHT || y < 0 || y >= WIDTH) return false;
        if (!board[x][y]) return true;
        if (tailIgnored) {
            int[] tail = snake.get(snake.size() - 1);
            return x == tail[0] && y == tail[1];
        }
        return false;
    }

    // IDA*搜索全局状态
    private int threshold;
    private int minExceed;
    private List<Integer> path;
    private List<Integer> bestPath;

    /**
     * 使用IDA*搜索从蛇头到食物的路径
     * @return 方向序列,若找不到则返回null
     */
    public List<Integer> findPathToFood() {
        int[] head = snake.get(0);
        threshold = heuristic(head[0], head[1]);

        // 模拟蛇移动后的占据状态(预测蛇尾会空出)
        while (true) {
            path = new ArrayList<>();
            minExceed = Integer.MAX_VALUE;

            // 执行深度受限搜索
            boolean found = idaSearch(head[0], head[1], 0, -1);
            if (found) {
                bestPath = new ArrayList<>(path);
                return bestPath;
            }

            // 无解或阈值溢出
            if (minExceed == Integer.MAX_VALUE) return null;
            threshold = minExceed;
        }
    }

    /**
     * IDA*深度优先搜索
     * @param x, y 当前坐标
     * @param g 已花费代价
     * @param fromDir 来时的方向(避免立即回头)
     */
    private boolean idaSearch(int x, int y, int g, int fromDir) {
        int f = g + heuristic(x, y);
        if (f > threshold) {
            minExceed = Math.min(minExceed, f);
            return false;
        }

        // 到达食物
        if (x == food[0] && y == food[1]) {
            return true;
        }

        // 评估可移动方向,按启发式排序优化搜索顺序
        List<int[]> candidates = new ArrayList<>();
        for (int d = 0; d < 4; d++) {
            if (d == (fromDir ^ 2)) continue; // 不回头
            int nx = x + DIRS[d][0];
            int ny = y + DIRS[d][1];
            // 搜索时假设蛇尾会移动(尾部空出),因为吃食物前蛇身不变
            if (isValid(nx, ny, true)) {
                candidates.add(new int[]{d, heuristic(nx, ny), nx, ny});
            }
        }

        // 按启发式值升序排列,优先搜索更优方向
        candidates.sort(Comparator.comparingInt(a -> a[1]));

        for (int[] cand : candidates) {
            int d = cand[0], nx = cand[2], ny = cand[3];

            // 临时标记当前位置为已占据
            boolean wasFree = !board[nx][ny];
            board[nx][ny] = true;
            path.add(d);

            if (idaSearch(nx, ny, g + 1, d)) return true;

            path.remove(path.size() - 1);
            if (wasFree) board[nx][ny] = false;
        }

        return false;
    }

    /**
     * 验证吃完食物后是否有安全路径通向尾部
     * 这是确保蛇不会把自己困死的关键检查
     */
    public boolean hasSafePathAfterEat() {
        if (bestPath == null || bestPath.isEmpty()) return false;

        // 模拟蛇吃完食物后的状态
        int[] head = snake.get(0);
        int firstDir = bestPath.get(0);
        int nx = head[0] + DIRS[firstDir][0];
        int ny = head[1] + DIRS[firstDir][1];

        // 模拟移动一步后的棋盘
        boolean[][] simBoard = new boolean[HEIGHT][WIDTH];
        for (int i = 0; i < HEIGHT; i++) {
            simBoard[i] = board[i].clone();
        }

        // 新头部
        simBoard[nx][ny] = true;
        // 若没吃到食物,尾部移除;吃到则保留
        boolean willEat = (nx == food[0] && ny == food[1]);
        if (!willEat && snake.size() > 1) {
            int[] tail = snake.get(snake.size() - 1);
            simBoard[tail[0]][tail[1]] = false;
        }

        // 使用BFS检查从新的头部能否到达尾部
        return canReachTail(simBoard, nx, ny);
    }

    /**
     * BFS检查从指定位置能否到达蛇尾区域
     */
    private boolean canReachTail(boolean[][] simBoard, int sx, int sy) {
        int[] tail = snake.get(snake.size() - 1);
        boolean[][] vis = new boolean[HEIGHT][WIDTH];
        Queue<int[]> q = new LinkedList<>();
        q.offer(new int[]{sx, sy});
        vis[sx][sy] = true;

        while (!q.isEmpty()) {
            int[] cur = q.poll();
            if (cur[0] == tail[0] && cur[1] == tail[1]) return true;

            for (int[] dir : DIRS) {
                int nx = cur[0] + dir[0];
                int ny = cur[1] + dir[1];
                if (nx >= 0 && nx < HEIGHT && ny >= 0 && ny < WIDTH 
                        && !simBoard[nx][ny] && !vis[nx][ny]) {
                    vis[nx][ny] = true;
                    q.offer(new int[]{nx, ny});
                }
            }
        }
        return false;
    }

    /**
     * 执行AI移动一步
     * @return 是否成功移动
     */
    public boolean aiMove() {
        List<Integer> path = findPathToFood();
        if (path == null || path.isEmpty()) {
            // 找不到去食物的路径,尝试随机安全移动
            return randomSafeMove();
        }

        // 检查吃完食物后是否安全
        if (!hasSafePathAfterEat() && snake.size() > 5) {
            // 不安全时尝试跟随尾巴策略
            return followTailStrategy();
        }

        int dir = path.get(0);
        return move(dir);
    }

    /**
     * 按指定方向移动蛇
     */
    private boolean move(int dir) {
        int[] head = snake.get(0);
        int nx = head[0] + DIRS[dir][0];
        int ny = head[1] + DIRS[dir][1];

        if (!isValid(nx, ny, false)) return false;

        snake.add(0, new int[]{nx, ny});
        board[nx][ny] = true;

        if (nx == food[0] && ny == food[1]) {
            generateFood(); // 吃到食物,不删除尾部,蛇变长
        } else {
            int[] tail = snake.remove(snake.size() - 1);
            board[tail[0]][tail[1]] = false;
        }
        return true;
    }

    /**
     * 跟随尾巴策略:当没有安全食物路径时,尝试移动到离尾巴最近的位置
     */
    private boolean followTailStrategy() {
        int[] head = snake.get(0);
        int[] tail = snake.get(snake.size() - 1);

        int bestDir = -1;
        int bestDist = Integer.MAX_VALUE;

        for (int d = 0; d < 4; d++) {
            int nx = head[0] + DIRS[d][0];
            int ny = head[1] + DIRS[d][1];
            if (isValid(nx, ny, true)) {
                int dist = Math.abs(nx - tail[0]) + Math.abs(ny - tail[1]);
                if (dist < bestDist) {
                    bestDist = dist;
                    bestDir = d;
                }
            }
        }

        if (bestDir >= 0) return move(bestDir);
        return false;
    }

    /**
     * 随机安全移动(最后手段)
     */
    private boolean randomSafeMove() {
        int[] head = snake.get(0);
        List<Integer> safe = new ArrayList<>();
        for (int d = 0; d < 4; d++) {
            int nx = head[0] + DIRS[d][0];
            int ny = head[1] + DIRS[d][1];
            if (isValid(nx, ny, true)) safe.add(d);
        }
        if (safe.isEmpty()) return false;
        return move(safe.get(new Random().nextInt(safe.size())));
    }

    /**
     * 打印当前游戏状态
     */
    public void printBoard() {
        System.out.println("Score: " + (snake.size() - 1));
        for (int i = 0; i < HEIGHT; i++) {
            for (int j = 0; j < WIDTH; j++) {
                if (i == food[0] && j == food[1]) System.out.print("F ");
                else if (i == snake.get(0)[0] && j == snake.get(0)[1]) System.out.print("H ");
                else if (board[i][j]) System.out.print("S ");
                else System.out.print(". ");
            }
            System.out.println();
        }
        System.out.println();
    }

    /**
     * 运行完整游戏
     */
    public void runGame() {
        initGame();
        printBoard();

        int steps = 0;
        while (steps < 1000) {
            if (!aiMove()) {
                System.out.println("Game Over! Final score: " + (snake.size() - 1));
                break;
            }
            steps++;
            if (steps % 50 == 0) printBoard();
        }

        if (steps >= 1000) {
            System.out.println("Reached step limit. Final score: " + (snake.size() - 1));
        }
    }

    public static void main(String[] args) {
        SnakeIDAStar game = new SnakeIDAStar();
        game.runGame();
    }
}

关键算法解析

1. 迭代深化A*搜索

IDA通过逐步增加阈值来控制搜索深度。每次迭代中,所有f值超过阈值的节点被剪枝,同时记录最小的超限值作为下一轮阈值。这种策略兼具了深度优先搜索的低内存开销和A的最优性保证。

2. 尾部避让与BFS验证

贪吃蛇最大的挑战在于吃食物后可能被困住。代码中的hasSafePathAfterEat方法模拟吃食物后的棋盘状态,使用BFS验证新头部能否到达原蛇尾位置。这一检查确保蛇在进食后仍有足够的逃生空间。

3. 跟随尾巴策略

当IDA*找不到安全的食物路径时(通常发生在蛇身很长、空间很窄的情况下),AI会切换到跟随尾巴策略。通过移动到离尾巴曼哈顿距离最近的安全格子,蛇可以有效地”追逐自己的尾巴”,维持一个安全的循环路径。

复杂度分析

指标 复杂度 说明
时间复杂度 O(b^d) b为分支因子(最大4),d为最优解深度
空间复杂度 O(d) 仅需存储当前搜索路径,d为解深度
启发式评估 O(1) 曼哈顿距离常数时间计算

相比标准A需要O(b^d)的优先队列空间,IDA将空间复杂度降低到O(d),非常适合贪吃蛇这种状态空间随蛇身长度动态变化的问题。

总结

本文实现了基于IDA*搜索的贪吃蛇AI,通过迭代深化策略在有限内存内寻找最优路径,结合尾部避让检查和跟随尾巴策略确保长期存活。该算法在20×20棋盘上通常能存活到蛇身占满大部分空间,展示了启发式搜索在实时游戏AI中的强大能力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注