每日算法 — 使用java实现五子棋:威胁空间搜索与VCF必胜路径检测

引言

五子棋(Gomoku)是一种两人对弈的纯策略型棋类游戏。在标准规则下,先形成五子连珠的一方获胜。除了基础的Alpha-Beta剪枝与估值函数之外,威胁空间搜索(Threat Space Search, TSS)是一种专门针对五子棋的高效搜索算法,它通过识别棋盘上的”威胁点”来大幅剪枝,尤其擅长寻找必胜路径。本文将用Java实现基于威胁空间搜索的五子棋AI,重点讲解VCF(Victory by Continuous Four,连续冲四胜)必胜路径的检测算法。

威胁空间搜索核心思想

威胁空间搜索不同于传统的Minimax全宽度搜索,它只关注能够产生致命威胁的着法。在五子棋中,威胁分为不同等级:

  • 活四:四个棋子连成一线且两端开放,对手无法防守,直接获胜。
  • 冲四:四个棋子连成一线且只有一端开放,需要对手立即防守。
  • 活三:三个棋子连成一线且两端开放,下一步可以形成活四或冲四。
  • 跳活三:中间有空格的活三,同样具有极大威胁。

威胁空间搜索的核心策略是:只生成能够产生威胁的着法,并递归检测对手是否能够化解这些威胁。如果存在一系列威胁使得对手始终处于被动防守状态,最终形成无法防守的活四,则找到了必胜路径。

VCF必胜路径检测

VCF是指通过连续制造冲四(Four)来迫使对手防守,最终形成活四获胜的策略。VCF检测算法的基本流程如下:

  1. 识别当前棋盘上所有能够形成冲四的点(即”四”点)。
  2. 对于每一个冲四点,模拟落子后检查是否已经形成五连(直接获胜)。
  3. 如果没有直接获胜,检查对手是否必须防守(即对手的冲四/活三情况)。
  4. 在对手防守后,递归检查是否还能继续制造新的冲四。
  5. 如果在某条分支上能够连续制造冲四直至获胜,则找到了VCF必胜路径。

Java实现

棋盘表示与基础工具类

import java.util.*;

/**
 * 五子棋棋盘表示与威胁检测
 */
public class GomokuBoard {
    public static final int EMPTY = 0;
    public static final int BLACK = 1;
    public static final int WHITE = 2;
    public static final int BOARD_SIZE = 15;

    // 四个方向:横、竖、左上-右下、左下-右上
    private static final int[][] DIRECTIONS = {
        {0, 1}, {1, 0}, {1, 1}, {1, -1}
    };

    private int[][] board;

    public GomokuBoard() {
        this.board = new int[BOARD_SIZE][BOARD_SIZE];
    }

    public GomokuBoard(int[][] board) {
        this.board = new int[BOARD_SIZE][BOARD_SIZE];
        for (int i = 0; i < BOARD_SIZE; i++) {
            System.arraycopy(board[i], 0, this.board[i], 0, BOARD_SIZE);
        }
    }

    public int get(int row, int col) {
        return board[row][col];
    }

    public void set(int row, int col, int player) {
        board[row][col] = player;
    }

    public boolean isValid(int row, int col) {
        return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
    }

    public boolean isEmpty(int row, int col) {
        return isValid(row, col) && board[row][col] == EMPTY;
    }

    /**
     * 检查某位置落子后是否形成五连
     */
    public boolean checkWin(int row, int col, int player) {
        for (int[] dir : DIRECTIONS) {
            int count = 1;
            // 正向搜索
            for (int step = 1; step < 5; step++) {
                int r = row + dir[0] * step;
                int c = col + dir[1] * step;
                if (isValid(r, c) && board[r][c] == player) {
                    count++;
                } else {
                    break;
                }
            }
            // 反向搜索
            for (int step = 1; step < 5; step++) {
                int r = row - dir[0] * step;
                int c = col - dir[1] * step;
                if (isValid(r, c) && board[r][c] == player) {
                    count++;
                } else {
                    break;
                }
            }
            if (count >= 5) return true;
        }
        return false;
    }

    /**
     * 获取指定方向上的棋子序列,用于威胁识别
     * 返回 [连续棋子数, 左端空格数, 右端空格数, 是否被阻挡]
     */
    public LineInfo getLineInfo(int row, int col, int[] dir, int player) {
        // 从(row, col)出发,沿dir方向获取连续信息
        int count = 0;
        int leftSpace = 0;
        int rightSpace = 0;
        boolean leftBlocked = false;
        boolean rightBlocked = false;

        // 先检查(row, col)本身
        if (isValid(row, col) && board[row][col] == player) {
            count++;
        }

        // 正向
        for (int step = 1; step < 6; step++) {
            int r = row + dir[0] * step;
            int c = col + dir[1] * step;
            if (!isValid(r, c)) {
                rightBlocked = true;
                break;
            }
            if (board[r][c] == player) {
                if (rightSpace == 0) count++;
            } else if (board[r][c] == EMPTY) {
                rightSpace++;
                if (rightSpace > 1) break; // 最多统计一个空格
            } else {
                rightBlocked = true;
                break;
            }
        }

        // 反向
        for (int step = 1; step < 6; step++) {
            int r = row - dir[0] * step;
            int c = col - dir[1] * step;
            if (!isValid(r, c)) {
                leftBlocked = true;
                break;
            }
            if (board[r][c] == player) {
                if (leftSpace == 0) count++;
            } else if (board[r][c] == EMPTY) {
                leftSpace++;
                if (leftSpace > 1) break;
            } else {
                leftBlocked = true;
                break;
            }
        }

        return new LineInfo(count, leftSpace, rightSpace, leftBlocked, rightBlocked);
    }

    /**
     * 获取棋盘上所有空位
     */
    public List<int[]> getEmptyCells() {
        List<int[]> cells = new ArrayList<>();
        for (int i = 0; i < BOARD_SIZE; i++) {
            for (int j = 0; j < BOARD_SIZE; j++) {
                if (board[i][j] == EMPTY) {
                    cells.add(new int[]{i, j});
                }
            }
        }
        return cells;
    }

    public GomokuBoard copy() {
        return new GomokuBoard(this.board);
    }
}

/**
 * 线路信息辅助类
 */
class LineInfo {
    int count;      // 连续棋子数
    int leftSpace;  // 左端空格数
    int rightSpace; // 右端空格数
    boolean leftBlocked;
    boolean rightBlocked;

    LineInfo(int count, int leftSpace, int rightSpace, boolean leftBlocked, boolean rightBlocked) {
        this.count = count;
        this.leftSpace = leftSpace;
        this.rightSpace = rightSpace;
        this.leftBlocked = leftBlocked;
        this.rightBlocked = rightBlocked;
    }

    /**
     * 判断是否为活四(四子连珠,两端开放)
     */
    boolean isOpenFour() {
        return count == 4 && leftSpace >= 1 && rightSpace >= 1 && !leftBlocked && !rightBlocked;
    }

    /**
     * 判断是否为冲四(四子连珠,仅一端开放)
     */
    boolean isFour() {
        return count == 4 && ((leftSpace >= 1 && rightBlocked) || (rightSpace >= 1 && leftBlocked)
            || (leftSpace >= 1 && !leftBlocked && rightSpace == 0 && !rightBlocked)
            || (rightSpace >= 1 && !rightBlocked && leftSpace == 0 && !leftBlocked));
    }

    /**
     * 判断是否为活三(三子连珠,两端各至少一个空格)
     */
    boolean isOpenThree() {
        return count == 3 && leftSpace >= 1 && rightSpace >= 1 && !leftBlocked && !rightBlocked;
    }

    /**
     * 判断是否为跳活三(中间有空格的活三)
     */
    boolean isSplitThree() {
        return count == 3 && (leftSpace + rightSpace >= 2) && !leftBlocked && !rightBlocked;
    }
}

威胁识别与VCF搜索

/**
 * 威胁空间搜索与VCF必胜检测
 */
public class ThreatSpaceSearch {

    private static final int MAX_DEPTH = 20; // VCF搜索最大深度

    /**
     * 威胁类型枚举
     */
    enum ThreatType {
        WIN,        // 直接获胜(五连)
        FOUR,       // 冲四(必须防守)
        OPEN_FOUR,  // 活四(无法防守)
        OPEN_THREE  // 活三(潜在威胁)
    }

    /**
     * 威胁节点
     */
    static class Threat {
        int row, col;
        ThreatType type;
        List<int[]> defenses; // 对手的防守点

        Threat(int row, int col, ThreatType type, List<int[]> defenses) {
            this.row = row;
            this.col = col;
            this.type = type;
            this.defenses = defenses;
        }
    }

    /**
     * 查找当前棋盘上指定玩家的所有威胁
     */
    public static List<Threat> findThreats(GomokuBoard board, int player) {
        List<Threat> threats = new ArrayList<>();
        int opponent = (player == GomokuBoard.BLACK) ? GomokuBoard.WHITE : GomokuBoard.BLACK;

        for (int i = 0; i < GomokuBoard.BOARD_SIZE; i++) {
            for (int j = 0; j < GomokuBoard.BOARD_SIZE; j++) {
                if (board.get(i, j) != GomokuBoard.EMPTY) continue;

                // 模拟落子
                board.set(i, j, player);

                // 检查是否直接获胜
                if (board.checkWin(i, j, player)) {
                    List<int[]> defenses = new ArrayList<>();
                    threats.add(new Threat(i, j, ThreatType.WIN, defenses));
                    board.set(i, j, GomokuBoard.EMPTY);
                    continue;
                }

                // 检查四个方向上的威胁
                List<int[]> allDefenses = new ArrayList<>();
                boolean isOpenFour = false;
                boolean isFour = false;
                boolean isOpenThree = false;

                for (int[] dir : new int[][]{{0,1}, {1,0}, {1,1}, {1,-1}}) {
                    LineInfo info = board.getLineInfo(i, j, dir, player);

                    if (info.isOpenFour()) {
                        isOpenFour = true;
                    } else if (info.isFour()) {
                        isFour = true;
                        // 冲四的防守点是活四的另一端
                        if (info.leftSpace >= 1 && !info.leftBlocked) {
                            // 需要找到具体的防守坐标
                            int dr = i, dc = j;
                            for (int step = 1; step <= 5; step++) {
                                int nr = i - dir[0] * step;
                                int nc = j - dir[1] * step;
                                if (board.isEmpty(nr, nc)) {
                                    allDefenses.add(new int[]{nr, nc});
                                    break;
                                }
                            }
                        }
                        if (info.rightSpace >= 1 && !info.rightBlocked) {
                            for (int step = 1; step <= 5; step++) {
                                int nr = i + dir[0] * step;
                                int nc = j + dir[1] * step;
                                if (board.isEmpty(nr, nc)) {
                                    allDefenses.add(new int[]{nr, nc});
                                    break;
                                }
                            }
                        }
                    } else if (info.isOpenThree() || info.isSplitThree()) {
                        isOpenThree = true;
                    }
                }

                board.set(i, j, GomokuBoard.EMPTY);

                // 分类威胁
                if (isOpenFour) {
                    threats.add(new Threat(i, j, ThreatType.OPEN_FOUR, new ArrayList<>()));
                } else if (isFour) {
                    // 去重防守点
                    List<int[]> uniqueDefenses = new ArrayList<>();
                    for (int[] d : allDefenses) {
                        boolean exists = false;
                        for (int[] u : uniqueDefenses) {
                            if (u[0] == d[0] && u[1] == d[1]) {
                                exists = true;
                                break;
                            }
                        }
                        if (!exists) uniqueDefenses.add(d);
                    }
                    threats.add(new Threat(i, j, ThreatType.FOUR, uniqueDefenses));
                } else if (isOpenThree) {
                    threats.add(new Threat(i, j, ThreatType.OPEN_THREE, new ArrayList<>()));
                }
            }
        }

        return threats;
    }

    /**
     * VCF搜索:寻找连续冲四必胜路径
     * @return 如果找到必胜路径,返回获胜的落子序列;否则返回null
     */
    public static List<int[]> searchVCF(GomokuBoard board, int player, int depth) {
        if (depth > MAX_DEPTH) return null;

        List<Threat> threats = findThreats(board, player);

        // 优先处理直接获胜
        for (Threat t : threats) {
            if (t.type == ThreatType.WIN) {
                List<int[]> result = new ArrayList<>();
                result.add(new int[]{t.row, t.col});
                return result;
            }
        }

        // 处理活四(无法防守,直接获胜)
        for (Threat t : threats) {
            if (t.type == ThreatType.OPEN_FOUR) {
                List<int[]> result = new ArrayList<>();
                result.add(new int[]{t.row, t.col});
                return result;
            }
        }

        // 尝试冲四序列
        for (Threat t : threats) {
            if (t.type != ThreatType.FOUR) continue;

            // 模拟落子(制造冲四)
            board.set(t.row, t.col, player);

            if (t.defenses.isEmpty()) {
                // 没有防守点,直接获胜
                List<int[]> result = new ArrayList<>();
                result.add(new int[]{t.row, t.col});
                board.set(t.row, t.col, GomokuBoard.EMPTY);
                return result;
            }

            // 对手必须防守每一个防守点
            boolean allWin = true;
            List<int[]> bestSequence = null;

            for (int[] defense : t.defenses) {
                if (!board.isEmpty(defense[0], defense[1])) {
                    allWin = false;
                    break;
                }

                // 对手防守
                int opponent = (player == GomokuBoard.BLACK) ? GomokuBoard.WHITE : GomokuBoard.BLACK;
                board.set(defense[0], defense[1], opponent);

                // 递归搜索
                List<int[]> subSequence = searchVCF(board, player, depth + 1);

                board.set(defense[0], defense[1], GomokuBoard.EMPTY);

                if (subSequence == null) {
                    allWin = false;
                    break;
                }

                if (bestSequence == null || subSequence.size() > bestSequence.size()) {
                    bestSequence = subSequence;
                }
            }

            board.set(t.row, t.col, GomokuBoard.EMPTY);

            if (allWin && bestSequence != null) {
                List<int[]> result = new ArrayList<>();
                result.add(new int[]{t.row, t.col});
                result.addAll(bestSequence);
                return result;
            }
        }

        return null;
    }

    /**
     * 综合威胁搜索:结合VCF与估值选择最佳着法
     */
    public static int[] findBestMove(GomokuBoard board, int player) {
        // 首先尝试VCF
        List<int[]> vcfSequence = searchVCF(board, player, 0);
        if (vcfSequence != null && !vcfSequence.isEmpty()) {
            System.out.println("找到VCF必胜路径!");
            return vcfSequence.get(0);
        }

        // 如果没有VCF,尝试阻止对手的VCF
        int opponent = (player == GomokuBoard.BLACK) ? GomokuBoard.WHITE : GomokuBoard.BLACK;
        List<int[]> opponentVCF = searchVCF(board, opponent, 0);
        if (opponentVCF != null && !opponentVCF.isEmpty()) {
            // 对手有VCF,尝试阻止
            System.out.println("检测到对手VCF威胁,进行防守!");
            // 返回一个能够阻止对手VCF的着法
            return findBlockingMove(board, player, opponentVCF);
        }

        // 否则使用启发式评估选择最佳着法
        return heuristicSearch(board, player);
    }

    /**
     * 寻找阻止对手VCF的防守着法
     */
    private static int[] findBlockingMove(GomokuBoard board, int player, List<int[]> opponentVCF) {
        // 简单策略:占据对手VCF路径上的关键点
        if (opponentVCF != null && !opponentVCF.isEmpty()) {
            int[] firstThreat = opponentVCF.get(0);
            return firstThreat;
        }
        return heuristicSearch(board, player);
    }

    /**
     * 启发式搜索:基于威胁数量的简单评估
     */
    private static int[] heuristicSearch(GomokuBoard board, int player) {
        int bestScore = Integer.MIN_VALUE;
        int[] bestMove = null;

        // 只考虑已有棋子附近的空位(减少搜索空间)
        Set<String> candidates = new HashSet<>();
        for (int i = 0; i < GomokuBoard.BOARD_SIZE; i++) {
            for (int j = 0; j < GomokuBoard.BOARD_SIZE; j++) {
                if (board.get(i, j) != GomokuBoard.EMPTY) {
                    for (int di = -2; di <= 2; di++) {
                        for (int dj = -2; dj <= 2; dj++) {
                            int ni = i + di, nj = j + dj;
                            if (board.isEmpty(ni, nj)) {
                                candidates.add(ni + "," + nj);
                            }
                        }
                    }
                }
            }
        }

        // 如果没有候选点(开局),选择中心
        if (candidates.isEmpty()) {
            return new int[]{GomokuBoard.BOARD_SIZE / 2, GomokuBoard.BOARD_SIZE / 2};
        }

        for (String key : candidates) {
            String[] parts = key.split(",");
            int r = Integer.parseInt(parts[0]);
            int c = Integer.parseInt(parts[1]);

            int score = evaluatePoint(board, r, c, player);
            if (score > bestScore) {
                bestScore = score;
                bestMove = new int[]{r, c};
            }
        }

        return bestMove;
    }

    /**
     * 评估某位置的分数
     */
    private static int evaluatePoint(GomokuBoard board, int row, int col, int player) {
        int score = 0;
        board.set(row, col, player);

        List<Threat> myThreats = findThreats(board, player);
        for (Threat t : myThreats) {
            if (t.row == row && t.col == col) {
                switch (t.type) {
                    case WIN: score += 100000; break;
                    case OPEN_FOUR: score += 10000; break;
                    case FOUR: score += 1000; break;
                    case OPEN_THREE: score += 100; break;
                }
            }
        }

        board.set(row, col, GomokuBoard.EMPTY);

        // 同时考虑防守价值
        int opponent = (player == GomokuBoard.BLACK) ? GomokuBoard.WHITE : GomokuBoard.BLACK;
        board.set(row, col, opponent);
        List<Threat> oppThreats = findThreats(board, opponent);
        for (Threat t : oppThreats) {
            if (t.row == row && t.col == col) {
                switch (t.type) {
                    case WIN: score += 50000; break;
                    case OPEN_FOUR: score += 5000; break;
                    case FOUR: score += 500; break;
                    case OPEN_THREE: score += 50; break;
                }
            }
        }
        board.set(row, col, GomokuBoard.EMPTY);

        return score;
    }
}

主程序与测试

/**
 * 五子棋威胁空间搜索主程序
 */
public class GomokuTSS {

    public static void main(String[] args) {
        GomokuBoard board = new GomokuBoard();

        // 设置一个测试局面:黑棋有机会通过VCF获胜
        // 创建一个冲四局面
        board.set(7, 7, GomokuBoard.BLACK);
        board.set(7, 8, GomokuBoard.BLACK);
        board.set(7, 9, GomokuBoard.BLACK);
        board.set(7, 10, GomokuBoard.BLACK); // 横向四子

        board.set(8, 7, GomokuBoard.WHITE);
        board.set(6, 7, GomokuBoard.WHITE);

        System.out.println("=== 五子棋威胁空间搜索演示 ===");
        printBoard(board);

        // 测试威胁识别
        System.out.println("\n黑棋的威胁分析:");
        List<ThreatSpaceSearch.Threat> threats = ThreatSpaceSearch.findThreats(board, GomokuBoard.BLACK);
        for (ThreatSpaceSearch.Threat t : threats) {
            if (t.type == ThreatSpaceSearch.ThreatType.WIN || 
                t.type == ThreatSpaceSearch.ThreatType.OPEN_FOUR ||
                t.type == ThreatSpaceSearch.ThreatType.FOUR) {
                System.out.printf("  威胁: (%d, %d) - %s%n", t.row, t.col, t.type);
            }
        }

        // 测试VCF搜索
        System.out.println("\nVCF搜索:");
        List<int[]> vcf = ThreatSpaceSearch.searchVCF(board, GomokuBoard.BLACK, 0);
        if (vcf != null) {
            System.out.println("找到必胜路径!落子序列:");
            for (int[] move : vcf) {
                System.out.printf("  (%d, %d)%n", move[0], move[1]);
            }
        } else {
            System.out.println("当前局面无VCF必胜路径");
        }

        // 测试AI选择最佳着法
        int[] bestMove = ThreatSpaceSearch.findBestMove(board, GomokuBoard.BLACK);
        System.out.printf("\nAI推荐着法: (%d, %d)%n", bestMove[0], bestMove[1]);
    }

    private static void printBoard(GomokuBoard board) {
        System.out.print("   ");
        for (int j = 0; j < GomokuBoard.BOARD_SIZE; j++) {
            System.out.printf("%2d ", j);
        }
        System.out.println();

        for (int i = 0; i < GomokuBoard.BOARD_SIZE; i++) {
            System.out.printf("%2d ", i);
            for (int j = 0; j < GomokuBoard.BOARD_SIZE; j++) {
                char c = '.';
                if (board.get(i, j) == GomokuBoard.BLACK) c = 'X';
                else if (board.get(i, j) == GomokuBoard.WHITE) c = 'O';
                System.out.printf(" %c ", c);
            }
            System.out.println();
        }
    }
}

算法复杂度分析

威胁空间搜索的时间复杂度远低于传统Minimax搜索。在15×15的棋盘上:

  • 全宽度Minimax:每步约225个候选着法,深度为4时搜索空间约为 $225^4 \approx 2.5 \times 10^9$。
  • 威胁空间搜索:每步通常只生成5-20个威胁着法,深度为10时搜索空间约为 $10^{10}$ 但在实际中大量分支会被对手的防守所剪枝。

VCF搜索在实际中通常能在毫秒级完成,因为它只关注致命威胁而非所有可能的着法。

总结

威胁空间搜索是五子棋AI中一种非常高效的 specialized 搜索算法。本文实现了:

  1. 棋盘表示与基础工具:支持15×15棋盘、四方向检测、线路信息提取。
  2. 威胁识别系统:自动识别活四、冲四、活三等威胁类型。
  3. VCF必胜路径检测:通过递归搜索连续冲四序列,检测是否存在必胜路径。
  4. 综合决策引擎:优先VCF攻击,其次防守对手VCF,最后使用启发式评估。

读者通过本文可以深入理解威胁空间搜索的核心思想,并将其应用到其他棋类游戏的AI开发中。