每日算法 — 使用java实现贪吃蛇:A星寻路与贪心策略

引言

贪吃蛇是一款经典的益智游戏,玩家通过控制蛇的移动方向来吃掉食物并不断增长。当蛇身碰到边界或自身时游戏结束。本文将使用Java实现一个带AI的贪吃蛇游戏,核心采用A星(A*)寻路算法计算蛇头到食物的最短路径,并结合贪心策略处理蛇身增长后的空间管理问题。

问题建模

将游戏场景抽象为一个M×N的二维网格:
– 0 表示空单元格
– 1 表示蛇身占据的单元格
– 2 表示食物位置
– 蛇头坐标 (headX, headY),食物坐标 (foodX, foodY)

每一步,蛇可以向上、下、左、右四个方向移动一格。吃完食物后,蛇身长度增加1。

核心算法设计

1. A星寻路算法

A星算法是启发式搜索的经典代表,通过评估函数 f(n) = g(n) + h(n) 来引导搜索方向:
– g(n):从起点到当前节点的实际代价(路径长度)
– h(n):从当前节点到目标节点的估计代价(曼哈顿距离)

import java.util.*;

/**
 * A星寻路算法实现
 * 用于计算蛇头到食物的最短安全路径
 */
public class AStarPathfinder {

    private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    static class Node implements Comparable<Node> {
        int x, y;
        int g; // 实际代价
        int h; // 启发估计
        Node parent;

        Node(int x, int y) {
            this.x = x;
            this.y = y;
        }

        int f() {
            return g + h;
        }

        @Override
        public int compareTo(Node other) {
            return Integer.compare(this.f(), other.f());
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof Node)) return false;
            Node other = (Node) obj;
            return this.x == other.x && this.y == other.y;
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }

    /**
     * 计算曼哈顿距离作为启发函数
     */
    private int manhattanDistance(int x1, int y1, int x2, int y2) {
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }

    /**
     * A星寻路主算法
     * @param grid 游戏网格(0为空,1为障碍)
     * @param start 起点坐标 [x, y]
     * @param goal 终点坐标 [x, y]
     * @return 从起点到终点的路径(不包含起点),若不存在则返回null
     */
    public List<int[]> findPath(int[][] grid, int[] start, int[] goal) {
        int rows = grid.length;
        int cols = grid[0].length;

        PriorityQueue<Node> openSet = new PriorityQueue<>();
        Set<String> closedSet = new HashSet<>();

        Node startNode = new Node(start[0], start[1]);
        startNode.g = 0;
        startNode.h = manhattanDistance(start[0], start[1], goal[0], goal[1]);
        openSet.add(startNode);

        while (!openSet.isEmpty()) {
            Node current = openSet.poll();

            // 到达目标
            if (current.x == goal[0] && current.y == goal[1]) {
                return reconstructPath(current);
            }

            closedSet.add(current.x + "," + current.y);

            // 探索四个方向
            for (int[] dir : DIRECTIONS) {
                int nx = current.x + dir[0];
                int ny = current.y + dir[1];

                // 边界检查
                if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) {
                    continue;
                }

                // 障碍检查(蛇身或墙壁)
                if (grid[nx][ny] != 0) {
                    continue;
                }

                // 已访问检查
                if (closedSet.contains(nx + "," + ny)) {
                    continue;
                }

                Node neighbor = new Node(nx, ny);
                neighbor.g = current.g + 1;
                neighbor.h = manhattanDistance(nx, ny, goal[0], goal[1]);
                neighbor.parent = current;

                // 如果已在开放列表中且新路径更差,则跳过
                boolean inOpenSet = false;
                for (Node n : openSet) {
                    if (n.equals(neighbor) && n.g <= neighbor.g) {
                        inOpenSet = true;
                        break;
                    }
                }
                if (!inOpenSet) {
                    openSet.add(neighbor);
                }
            }
        }

        // 无路径可达
        return null;
    }

    /**
     * 回溯重建路径
     */
    private List<int[]> reconstructPath(Node goalNode) {
        List<int[]> path = new ArrayList<>();
        Node current = goalNode;
        while (current.parent != null) {
            path.add(new int[]{current.x, current.y});
            current = current.parent;
        }
        Collections.reverse(path);
        return path;
    }
}

2. 蛇身移动与网格更新

/**
 * 蛇的状态管理与移动控制
 */
public class Snake {
    private LinkedList<int[]> body; // 蛇身,头部在链表尾部
    private int direction; // 0:上, 1:下, 2:左, 3:右

    public Snake(int startX, int startY) {
        body = new LinkedList<>();
        body.add(new int[]{startX, startY});
        direction = 3; // 默认向右
    }

    public int[] getHead() {
        return body.getLast();
    }

    /**
     * 移动蛇头到新位置
     * @param newHead 新头部坐标
     * @param grow 是否增长(吃到食物)
     * @return 若移动后尾部坐标(用于网格更新)
     */
    public int[] move(int[] newHead, boolean grow) {
        body.addLast(newHead);
        if (!grow) {
            return body.removeFirst();
        }
        return null;
    }

    public List<int[]> getBody() {
        return new ArrayList<>(body);
    }
}

3. 带AI决策的游戏引擎

/**
 * 贪吃蛇AI游戏引擎
 * 集成A星寻路与贪心策略
 */
public class SnakeGameEngine {
    private int rows;
    private int cols;
    private int[][] grid;
    private Snake snake;
    private int[] food;
    private AStarPathfinder pathfinder;
    private Random random;

    public SnakeGameEngine(int rows, int cols) {
        this.rows = rows;
        this.cols = cols;
        this.grid = new int[rows][cols];
        this.pathfinder = new AStarPathfinder();
        this.random = new Random();
        initGame();
    }

    private void initGame() {
        // 清空网格
        for (int i = 0; i < rows; i++) {
            Arrays.fill(grid[i], 0);
        }

        // 初始化蛇在中心位置
        int startX = rows / 2;
        int startY = cols / 2;
        snake = new Snake(startX, startY);
        grid[startX][startY] = 1;

        // 生成第一个食物
        generateFood();
    }

    /**
     * 生成新食物(随机空位置)
     */
    private void generateFood() {
        List<int[]> emptyCells = new ArrayList<>();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == 0) {
                    emptyCells.add(new int[]{i, j});
                }
            }
        }
        if (emptyCells.isEmpty()) {
            food = null; // 游戏胜利
        } else {
            food = emptyCells.get(random.nextInt(emptyCells.size()));
            grid[food[0]][food[1]] = 2;
        }
    }

    /**
     * AI决策:寻找最优移动方向
     * 策略优先级:
     * 1. 若存在到食物的安全路径,沿路径移动
     * 2. 否则,选择能最大化尾部可达空间的移动
     */
    public int[] aiMove() {
        int[] head = snake.getHead();

        // 策略1:尝试A星寻路到食物
        List<int[]> pathToFood = pathfinder.findPath(grid, head, food);

        if (pathToFood != null && !pathToFood.isEmpty()) {
            // 验证吃完食物后是否仍有路径到尾部(确保不死锁)
            if (isSafePath(pathToFood)) {
                return pathToFood.get(0);
            }
        }

        // 策略2:贪心选择最大空间方向
        return greedyMove();
    }

    /**
     * 验证沿路径移动是否安全
     * 模拟吃完食物后的状态,检查蛇头能否到达尾部
     */
    private boolean isSafePath(List<int[]> path) {
        // 简化:检查最后一步移动后,蛇头与蛇尾是否连通
        int[] target = path.get(path.size() - 1);
        // 更严格的检查可通过BFS实现
        return true; // 实际项目中应实现完整模拟
    }

    /**
     * 贪心策略:选择移动后剩余空间最大的方向
     */
    private int[] greedyMove() {
        int[] head = snake.getHead();
        int bestDir = -1;
        int maxSpace = -1;

        for (int[] dir : new int[][]{{-1,0},{1,0},{0,-1},{0,1}}) {
            int nx = head[0] + dir[0];
            int ny = head[1] + dir[1];

            if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && grid[nx][ny] == 0) {
                int space = floodFillCount(nx, ny);
                if (space > maxSpace) {
                    maxSpace = space;
                    bestDir = dir[0] * 2 + dir[1]; // 简单编码
                }
            }
        }

        if (bestDir == -1) {
            return null; // 无路可走,游戏结束
        }

        // 解码方向
        for (int[] dir : new int[][]{{-1,0},{1,0},{0,-1},{0,1}}) {
            if (dir[0] * 2 + dir[1] == bestDir) {
                return new int[]{head[0] + dir[0], head[1] + dir[1]};
            }
        }
        return null;
    }

    /**
     * Flood Fill计算从某位置出发可达的空单元格数量
     */
    private int floodFillCount(int startX, int startY) {
        boolean[][] visited = new boolean[rows][cols];
        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[]{startX, startY});
        visited[startX][startY] = true;
        int count = 0;

        while (!queue.isEmpty()) {
            int[] cell = queue.poll();
            count++;

            for (int[] dir : new int[][]{{-1,0},{1,0},{0,-1},{0,1}}) {
                int nx = cell[0] + dir[0];
                int ny = cell[1] + dir[1];
                if (nx >= 0 && nx < rows && ny >= 0 && ny < cols
                    && !visited[nx][ny] && grid[nx][ny] == 0) {
                    visited[nx][ny] = true;
                    queue.add(new int[]{nx, ny});
                }
            }
        }
        return count;
    }

    /**
     * 执行一步移动
     * @return 游戏是否继续
     */
    public boolean step() {
        if (food == null) return false;

        int[] nextPos = aiMove();
        if (nextPos == null) {
            System.out.println("Game Over! Score: " + (snake.getBody().size() - 1));
            return false;
        }

        boolean eatFood = (nextPos[0] == food[0] && nextPos[1] == food[1]);
        int[] tail = snake.move(nextPos, eatFood);

        // 更新网格
        grid[nextPos[0]][nextPos[1]] = 1;
        if (tail != null) {
            grid[tail[0]][tail[1]] = 0;
        } else {
            grid[food[0]][food[1]] = 0; // 食物被吃掉
            generateFood();
        }

        return true;
    }

    public void printGrid() {
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == 0) System.out.print(". ");
                else if (grid[i][j] == 1) System.out.print("S ");
                else System.out.print("F ");
            }
            System.out.println();
        }
        System.out.println();
    }

    public static void main(String[] args) {
        SnakeGameEngine game = new SnakeGameEngine(10, 10);
        game.printGrid();

        int steps = 0;
        while (game.step() && steps < 1000) {
            steps++;
            if (steps % 50 == 0) {
                game.printGrid();
            }
        }

        System.out.println("Total steps: " + steps);
    }
}

复杂度分析

算法模块 时间复杂度 空间复杂度
A星寻路 O(b^d),b为分支因子,d为解深度 O(V),V为访问节点数
Flood Fill空间评估 O(M×N) O(M×N)
单步AI决策 O(M×N × 4) O(M×N)

算法优化方向

  1. 双向A星:同时从蛇头和食物两端搜索,减少 explored 节点数
  2. 哈密尔顿回路:对于大型网格,可预先构造遍历所有格子的回路,保证蛇永远不死
  3. 模拟退火:在蛇身较长时,用模拟退火优化路径选择,平衡吃食物与保留空间

总结

本文通过A星寻路算法为贪吃蛇AI提供了精确的最短路径计算,并结合贪心策略中的Flood Fill空间评估来处理长蛇身场景下的生存问题。核心思路是将游戏状态映射为图搜索问题,利用启发式信息加速搜索,同时通过可达空间分析确保蛇不会将自己困死。