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

游戏介绍

贪吃蛇是一款经典的益智游戏,玩家控制一条蛇在网格中移动,通过吃食物来增长身体。本文将用Java实现一个带AI的贪吃蛇,核心是让蛇自动寻找食物并避免撞到自己或墙壁。

核心算法

A星寻路算法

A星(A*)算法是启发式搜索算法,通过评估函数 f(n) = g(n) + h(n) 来找到最短路径:
– g(n):从起点到当前节点的实际代价
– h(n):从当前节点到目标节点的估计代价(启发函数,这里用曼哈顿距离)

贪心策略

蛇的AI决策流程:
1. 使用A*计算到食物的最短路径
2. 模拟走该路径后,检查蛇尾是否仍有可达空间
3. 如果安全则执行,否则转向最长路径策略,沿墙壁游走保证生存空间

Java实现

import java.util.*;

public class SnakeAI {
    // 游戏配置
    static final int WIDTH = 20;
    static final int HEIGHT = 20;
    static final int[][] DIRS = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; // 上、下、左、右
    static final String[] DIR_NAMES = {"UP", "DOWN", "LEFT", "RIGHT"};

    // 蛇身和食物
    Deque<int[]> snake = new ArrayDeque<>();
    boolean[][] occupied = new boolean[HEIGHT][WIDTH];
    int[] food;
    Random rand = new Random();

    public SnakeAI() {
        // 初始化蛇在中心位置
        int cx = WIDTH / 2, cy = HEIGHT / 2;
        snake.addFirst(new int[]{cx, cy});
        occupied[cy][cx] = true;
        spawnFood();
    }

    // 生成食物
    void spawnFood() {
        List<int[]> empty = new ArrayList<>();
        for (int y = 0; y < HEIGHT; y++) {
            for (int x = 0; x < WIDTH; x++) {
                if (!occupied[y][x]) empty.add(new int[]{x, y});
            }
        }
        if (empty.isEmpty()) {
            food = null;
            return;
        }
        food = empty.get(rand.nextInt(empty.size()));
    }

    // A*寻路:返回从蛇头到食物的方向序列
    List<int[]> aStar(int[] start, int[] goal) {
        PriorityQueue<Node> open = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
        Map<String, Node> openMap = new HashMap<>();
        Map<String, Node> closed = new HashMap<>();

        Node startNode = new Node(start[0], start[1], 0, heuristic(start, goal), null);
        open.add(startNode);
        openMap.put(key(start), startNode);

        while (!open.isEmpty()) {
            Node cur = open.poll();
            openMap.remove(key(cur));
            closed.put(key(cur), cur);

            if (cur.x == goal[0] && cur.y == goal[1]) {
                return reconstructPath(cur);
            }

            for (int[] d : DIRS) {
                int nx = cur.x + d[0], ny = cur.y + d[1];
                if (!inBounds(nx, ny) || occupied[ny][nx]) continue;
                if (closed.containsKey(key(nx, ny))) continue;

                int ng = cur.g + 1;
                String nk = key(nx, ny);
                Node neighbor = openMap.get(nk);
                if (neighbor == null || ng < neighbor.g) {
                    Node newNode = new Node(nx, ny, ng, heuristic(new int[]{nx, ny}, goal), cur);
                    if (neighbor != null) open.remove(neighbor);
                    open.add(newNode);
                    openMap.put(nk, newNode);
                }
            }
        }
        return null; // 无路径
    }

    // 启发函数:曼哈顿距离
    int heuristic(int[] a, int[] b) {
        return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
    }

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

    // 检查坐标是否合法
    boolean inBounds(int x, int y) {
        return x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT;
    }

    String key(int[] p) { return p[0] + "," + p[1]; }
    String key(int x, int y) { return x + "," + y; }

    // 模拟移动后的可达区域(BFS flood fill)
    int floodFillCount(int[] head, boolean[][] occ) {
        boolean[][] vis = new boolean[HEIGHT][WIDTH];
        Queue<int[]> q = new LinkedList<>();
        q.add(head);
        vis[head[1]][head[0]] = true;
        int count = 0;
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            count++;
            for (int[] d : DIRS) {
                int nx = cur[0] + d[0], ny = cur[1] + d[1];
                if (inBounds(nx, ny) && !occ[ny][nx] && !vis[ny][nx]) {
                    vis[ny][nx] = true;
                    q.add(new int[]{nx, ny});
                }
            }
        }
        return count;
    }

    // 蛇的AI决策
    String decideMove() {
        if (food == null) return null;
        int[] head = snake.peekFirst();
        List<int[]> path = aStar(head, food);

        if (path != null && path.size() > 1) {
            // 模拟走该路径到食物后的状态
            int[] next = path.get(1);
            boolean[][] simOcc = copyOccupied();
            simOcc[next[1]][next[0]] = true;
            // 吃食物后蛇增长,不释放尾部;否则释放尾部
            if (!(next[0] == food[0] && next[1] == food[1])) {
                int[] tail = snake.peekLast();
                simOcc[tail[1]][tail[0]] = false;
            }
            int reachable = floodFillCount(next, simOcc);
            // 如果可达区域足够大(至少剩余空格的一半),则走最短路径
            if (reachable >= (WIDTH * HEIGHT - snake.size()) / 2) {
                return dirName(head, next);
            }
        }

        //  fallback:选择最远的邻居,沿墙壁走(最长路径策略)
        int bestDir = -1, maxDist = -1;
        for (int i = 0; i < DIRS.length; i++) {
            int nx = head[0] + DIRS[i][0], ny = head[1] + DIRS[i][1];
            if (!inBounds(nx, ny) || occupied[ny][nx]) continue;
            int dist = heuristic(new int[]{nx, ny}, food);
            if (dist > maxDist) {
                maxDist = dist;
                bestDir = i;
            }
        }
        return bestDir >= 0 ? DIR_NAMES[bestDir] : null;
    }

    String dirName(int[] from, int[] to) {
        int dx = to[0] - from[0], dy = to[1] - from[1];
        for (int i = 0; i < DIRS.length; i++) {
            if (DIRS[i][0] == dx && DIRS[i][1] == dy) return DIR_NAMES[i];
        }
        return "UP";
    }

    boolean[][] copyOccupied() {
        boolean[][] c = new boolean[HEIGHT][WIDTH];
        for (int y = 0; y < HEIGHT; y++) {
            System.arraycopy(occupied[y], 0, c[y], 0, WIDTH);
        }
        return c;
    }

    // 执行移动
    boolean move(String dir) {
        int[] head = snake.peekFirst();
        int idx = Arrays.asList(DIR_NAMES).indexOf(dir);
        int nx = head[0] + DIRS[idx][0], ny = head[1] + DIRS[idx][1];
        if (!inBounds(nx, ny) || occupied[ny][nx]) return false;

        snake.addFirst(new int[]{nx, ny});
        occupied[ny][nx] = true;

        if (food != null && nx == food[0] && ny == food[1]) {
            spawnFood(); // 吃到食物,不删除尾部
        } else {
            int[] tail = snake.pollLast();
            occupied[tail[1]][tail[0]] = false;
        }
        return true;
    }

    // 节点类
    static class Node {
        int x, y, g, f;
        Node parent;
        Node(int x, int y, int g, int h, Node parent) {
            this.x = x; this.y = y; this.g = g;
            this.f = g + h;
            this.parent = parent;
        }
    }

    public static void main(String[] args) {
        SnakeAI ai = new SnakeAI();
        int steps = 0;
        while (ai.food != null && steps < 10000) {
            String move = ai.decideMove();
            if (move == null) {
                System.out.println("无路可走,步数: " + steps + ",蛇长: " + ai.snake.size());
                break;
            }
            if (!ai.move(move)) {
                System.out.println("碰撞,步数: " + steps + ",蛇长: " + ai.snake.size());
                break;
            }
            steps++;
        }
        System.out.println("完成!总步数: " + steps + ",最终蛇长: " + ai.snake.size());
    }
}

复杂度分析

  • A*寻路时间复杂度:$O(b^d)$,$b$为分支因子,$d$为解的深度。在最优启发函数下接近 $O(d)$
  • 空间复杂度:$O(WIDTH \times HEIGHT)$,用于存储open/closed集合
  • Flood Fill检测:每次决策额外执行一次BFS,时间 $O(WH)$,确保蛇不会把自己困住

总结

本文通过A*算法让贪吃蛇智能寻找食物,配合贪心策略和Flood Fill可达性检测,有效避免了蛇陷入死胡同。读者可以在此基础上扩展更高级的策略,如哈密尔顿回路实现无限通关。