每日算法 — 使用java实现Tron光循环:空间占据博弈与A*区域评估策略

一、游戏简介与算法核心

Tron(光循环)是一款经典的街机对战游戏。两名玩家各自控制一辆”光轮”在矩形网格中移动,光轮经过的轨迹会留下不可穿越的光墙。玩家只能通过向上、下、左、右四个方向移动,一旦撞到自己或对手的光墙、或者 grid 边界,即判负。游戏的核心算法挑战在于:在有限的局部信息下,做出能最大化自身生存空间的决策。

本文将用 Java 实现一个完整的 Tron 对战引擎,重点讲解以下算法模块:
– A* 寻路:计算到达某个目标点的最短安全路径
– 空间占据评估:基于 Flood Fill 计算每个玩家可到达的格子数
– 区域划分策略:通过 Voronoi 图思想划分势力范围,评估局势优劣

二、核心数据结构

import java.util.*;

/**
 * Tron 光循环游戏核心引擎
 * 包含棋盘状态、玩家位置、光墙记录与决策算法
 */
public class TronEngine {
    // 棋盘尺寸
    public static final int WIDTH = 16;
    public static final int HEIGHT = 16;

    // 四个移动方向:上、右、下、左
    public static final int[][] DIRS = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    public static final String[] DIR_NAMES = {"UP", "RIGHT", "DOWN", "LEFT"};

    // 棋盘状态:0=空地,1=玩家A的光墙,2=玩家B的光墙
    private int[][] board;

    // 两名玩家的当前位置 [row, col]
    private int[] playerA;
    private int[] playerB;

    // 当前回合编号
    private int turn;

    public TronEngine() {
        board = new int[HEIGHT][WIDTH];
        // 初始位置:对角线放置
        playerA = new int[]{HEIGHT / 2, WIDTH / 4};
        playerB = new int[]{HEIGHT / 2, 3 * WIDTH / 4};
        board[playerA[0]][playerA[1]] = 1;
        board[playerB[0]][playerB[1]] = 2;
        turn = 0;
    }

    /**
     * 检查某个位置是否在棋盘内且为空地
     */
    public boolean isValid(int r, int c) {
        return r >= 0 && r < HEIGHT && c >= 0 && c < WIDTH && board[r][c] == 0;
    }

    /**
     * 尝试移动指定玩家,返回是否成功
     * @param playerId 1=玩家A, 2=玩家B
     * @param dirIndex 方向索引 0-3
     */
    public boolean movePlayer(int playerId, int dirIndex) {
        int[] player = (playerId == 1) ? playerA : playerB;
        int nr = player[0] + DIRS[dirIndex][0];
        int nc = player[1] + DIRS[dirIndex][1];
        if (!isValid(nr, nc)) {
            return false; // 撞墙或越界,移动失败
        }
        // 更新位置并留下光墙
        player[0] = nr;
        player[1] = nc;
        board[nr][nc] = playerId;
        turn++;
        return true;
    }

    // Getters
    public int[][] getBoard() { return board; }
    public int[] getPlayerA() { return playerA; }
    public int[] getPlayerB() { return playerB; }
    public int getTurn() { return turn; }
}

三、A* 寻路与安全路径评估

A 算法用于评估当前玩家到达目标区域的最短安全路径。在 Tron 中,我们常用 A 来估算”如果 straight-line 前进,能活多久”。

    /**
     * A* 寻路:计算从起点到终点的最短路径长度
     * 使用曼哈顿距离作为启发函数(网格中无对角移动,曼哈顿距离可采纳)
     * @return 最短路径长度,不可达返回 -1
     */
    public int aStarDistance(int[] start, int[] target) {
        // 优先队列:按 f = g + h 排序
        PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
        boolean[][] visited = new boolean[HEIGHT][WIDTH];

        pq.offer(new Node(start[0], start[1], 0,
                manhattan(start[0], start[1], target[0], target[1]), null));

        while (!pq.isEmpty()) {
            Node cur = pq.poll();
            if (visited[cur.r][cur.c]) continue;
            visited[cur.r][cur.c] = true;

            // 到达目标
            if (cur.r == target[0] && cur.c == target[1]) {
                return cur.g;
            }

            // 扩展四个方向
            for (int[] d : DIRS) {
                int nr = cur.r + d[0];
                int nc = cur.c + d[1];
                if (isValid(nr, nc) && !visited[nr][nc]) {
                    int g = cur.g + 1;
                    int h = manhattan(nr, nc, target[0], target[1]);
                    pq.offer(new Node(nr, nc, g, g + h, cur));
                }
            }
        }
        return -1; // 不可达
    }

    private int manhattan(int r1, int c1, int r2, int c2) {
        return Math.abs(r1 - r2) + Math.abs(c1 - c2);
    }

    /**
     * A* 节点内部类
     */
    private static class Node {
        int r, c, g, f;
        Node parent;
        Node(int r, int c, int g, int f, Node parent) {
            this.r = r; this.c = c; this.g = g; this.f = f; this.parent = parent;
        }
    }

四、空间占据评估:Flood Fill 与 Voronoi 区域

在 Tron 中,最核心的评估指标是”我能到达的格子数 vs 对手能到达的格子数”。我们使用 BFS/Flood Fill 从双方当前位置同时扩散,计算各自的 Voronoi 区域。

    /**
     * 计算双方的空间占据评分(Voronoi-like 区域划分)
     * @return 正数表示玩家A优势,负数表示玩家B优势
     */
    public int voronoiEvaluation() {
        int[][] distA = floodFillDistance(playerA);
        int[][] distB = floodFillDistance(playerB);

        int scoreA = 0, scoreB = 0;
        for (int r = 0; r < HEIGHT; r++) {
            for (int c = 0; c < WIDTH; c++) {
                if (board[r][c] != 0) continue; // 已被占据
                int da = distA[r][c];
                int db = distB[r][c];
                if (da == -1 && db == -1) continue; // 都不可达
                if (da == -1) scoreB++;
                else if (db == -1) scoreA++;
                else if (da < db) scoreA++;
                else if (db < da) scoreB++;
                // da == db 时中立,不计分
            }
        }
        return scoreA - scoreB;
    }

    /**
     * 多源 BFS:计算从指定起点到所有可达空地的最短距离
     */
    private int[][] floodFillDistance(int[] start) {
        int[][] dist = new int[HEIGHT][WIDTH];
        for (int[] row : dist) Arrays.fill(row, -1);
        ArrayDeque<int[]> q = new ArrayDeque<>();
        q.offer(start);
        dist[start[0]][start[1]] = 0;

        while (!q.isEmpty()) {
            int[] cur = q.poll();
            for (int[] d : DIRS) {
                int nr = cur[0] + d[0];
                int nc = cur[1] + d[1];
                if (isValid(nr, nc) && dist[nr][nc] == -1) {
                    dist[nr][nc] = dist[cur[0]][cur[1]] + 1;
                    q.offer(new int[]{nr, nc});
                }
            }
        }
        return dist;
    }

五、博弈决策:贪心 + Minimax 混合策略

Tron 的决策空间巨大(4^T),我们采用以下分层策略:
1. 启发式评估:优先选择能扩大 Voronoi 区域的走法
2. 终点模拟:对每种走法模拟若干步,用 Minimax 评估终局
3. ** Articulation Point 检测**:避免走入”死胡同”(割点)

    /**
     * 为指定玩家选择最优移动方向
     * @param playerId 1=玩家A, 2=玩家B
     * @return 最优方向索引 0-3
     */
    public int bestMove(int playerId) {
        int[] me = (playerId == 1) ? playerA : playerB;
        int bestDir = -1;
        int bestScore = Integer.MIN_VALUE;

        for (int dir = 0; dir < 4; dir++) {
            int nr = me[0] + DIRS[dir][0];
            int nc = me[1] + DIRS[dir][1];
            if (!isValid(nr, nc)) continue;

            // 模拟走这一步
            int oldR = me[0], oldC = me[1];
            me[0] = nr; me[1] = nc;
            int oldBoard = board[nr][nc];
            board[nr][nc] = playerId;

            // 综合评分
            int score = evaluateMove(playerId);

            // 回滚
            me[0] = oldR; me[1] = oldC;
            board[nr][nc] = oldBoard;

            if (score > bestScore) {
                bestScore = score;
                bestDir = dir;
            }
        }
        return bestDir == -1 ? 0 : bestDir; // 默认向上
    }

    /**
     * 综合评估函数:Voronoi 差值 + 可行动空间 + 割点惩罚
     */
    private int evaluateMove(int playerId) {
        int base = voronoiEvaluation();
        // 如果评估玩家B,需要取反
        if (playerId == 2) base = -base;

        int[] me = (playerId == 1) ? playerA : playerB;
        int[][] myDist = floodFillDistance(me);
        int reachable = 0;
        for (int[] row : myDist) {
            for (int d : row) if (d >= 0) reachable++;
        }

        // 割点惩罚:如果当前位置是 Articulation Point,大幅减少分数
        int articulationPenalty = isArticulation(me[0], me[1]) ? -50 : 0;

        return base * 10 + reachable + articulationPenalty;
    }

    /**
     * 简化版割点检测:检查移除当前格后是否分裂可达区域
     */
    private boolean isArticulation(int r, int c) {
        // 临时标记为障碍物
        int tmp = board[r][c];
        board[r][c] = 3; // 3=临时障碍

        // 从邻居开始 BFS,看能否连通所有邻居
        List<int[]> neighbors = new ArrayList<>();
        for (int[] d : DIRS) {
            int nr = r + d[0], nc = c + d[1];
            if (isValid(nr, nc)) neighbors.add(new int[]{nr, nc});
        }

        boolean isArt = false;
        if (neighbors.size() >= 2) {
            int[][] dist = new int[HEIGHT][WIDTH];
            for (int[] row : dist) Arrays.fill(row, -1);
            ArrayDeque<int[]> q = new ArrayDeque<>();
            q.offer(neighbors.get(0));
            dist[neighbors.get(0)[0]][neighbors.get(0)[1]] = 0;
            while (!q.isEmpty()) {
                int[] cur = q.poll();
                for (int[] d : DIRS) {
                    int nr = cur[0] + d[0], nc = cur[1] + d[1];
                    if (isValid(nr, nc) && dist[nr][nc] == -1) {
                        dist[nr][nc] = dist[cur[0]][cur[1]] + 1;
                        q.offer(new int[]{nr, nc});
                    }
                }
            }
            // 如果有邻居不可达,则是割点
            for (int i = 1; i < neighbors.size(); i++) {
                if (dist[neighbors.get(i)[0]][neighbors.get(i)[1]] == -1) {
                    isArt = true;
                    break;
                }
            }
        }

        board[r][c] = tmp; // 恢复
        return isArt;
    }

六、主程序与演示

    /**
     * 主程序:运行一场完整的 Tron 对战
     */
    public static void main(String[] args) {
        TronEngine engine = new TronEngine();
        Random rand = new Random(42);

        System.out.println("=== Tron 光循环对战开始 ===");
        engine.printBoard();

        int currentPlayer = 1;
        while (true) {
            int dir;
            if (currentPlayer == 1) {
                dir = engine.bestMove(1); // AI 决策
            } else {
                dir = engine.bestMove(2); // AI 决策
            }

            boolean success = engine.movePlayer(currentPlayer, dir);
            if (!success) {
                System.out.println("玩家 " + currentPlayer + " 撞墙!游戏结束!");
                int winner = (currentPlayer == 1) ? 2 : 1;
                System.out.println("获胜者:玩家 " + winner);
                break;
            }

            System.out.println("
玩家 " + currentPlayer + " 移动: " + DIR_NAMES[dir]);
            engine.printBoard();

            currentPlayer = (currentPlayer == 1) ? 2 : 1;

            // 安全上限
            if (engine.getTurn() > 200) {
                System.out.println("达到回合上限,平局!");
                break;
            }
        }
    }

    /**
     * 打印当前棋盘状态
     */
    public void printBoard() {
        for (int r = 0; r < HEIGHT; r++) {
            for (int c = 0; c < WIDTH; c++) {
                if (r == playerA[0] && c == playerA[1]) System.out.print("A ");
                else if (r == playerB[0] && c == playerB[1]) System.out.print("B ");
                else if (board[r][c] == 1) System.out.print("a ");
                else if (board[r][c] == 2) System.out.print("b ");
                else System.out.print(". ");
            }
            System.out.println();
        }
    }

七、复杂度分析

模块 时间复杂度 空间复杂度
A* 寻路 O(WIDTH × HEIGHT) O(WIDTH × HEIGHT)
Flood Fill (Voronoi) O(WIDTH × HEIGHT) O(WIDTH × HEIGHT)
割点检测 O(WIDTH × HEIGHT) O(WIDTH × HEIGHT)
每步决策 O(4 × WIDTH × HEIGHT) O(WIDTH × HEIGHT)

在 16×16 的小棋盘上,每步决策可在毫秒级完成。对于更大棋盘,可通过增量式 Voronoi 更新和区域缓存进一步优化。

八、扩展思路

  1. Minimax + Alpha-Beta:对双方各模拟 3-4 步,构建博弈树剪枝搜索
  2. 神经网络评估:用 CNN 评估棋盘局面,替代手工 Voronoi 评分
  3. 多层防御策略:早期扩张、中期围堵、晚期收缩的安全策略切换

Tron 光循环将几何空间占据与博弈论完美结合,是理解”领土控制”类 AI 的绝佳入门案例。