轻棋局(三):AI 引擎设计与实现
引言
AI 是棋类游戏的灵魂。轻棋局内置了象棋和五子棋两个 AI 引擎,同时支持接入外部引擎(Pikafish、Piskvork、KataGo)。本篇深入讲解 AI 的核心算法、优化技巧和工程实现。
1. 象棋 AI:Negamax + Alpha-Beta 剪枝
核心算法
象棋 AI 使用经典的 Negamax 搜索框架,配合 Alpha-Beta 剪枝:
public class MinimaxAI {
private int negamax(Board board, int depth, int alpha, int beta, PieceColor color) {
// 终止条件
if (depth == 0 || board.isGameOver()) {
return evaluate(board, color);
}
List<Move> moves = board.generateMoves(color);
int bestScore = Integer.MIN_VALUE;
for (Move move : moves) {
board.makeMove(move);
int score = -negamax(board, depth - 1, -beta, -alpha, color.opposite());
board.undoMove(move);
if (score > bestScore) {
bestScore = score;
}
if (score > alpha) {
alpha = score;
}
if (alpha >= beta) {
break; // Beta 剪枝
}
}
return bestScore;
}
}
Negamax 的优势:只需要写一个搜索函数,通过颜色翻转实现双方搜索,代码量减半。
迭代加深
不直接搜索固定深度,而是从深度 1 开始逐步加深:
public Move findBestMove(Board board, PieceColor aiColor) {
Move bestMove = null;
for (int depth = 1; depth <= maxDepth; depth++) {
Move candidate = searchRoot(board, depth, aiColor);
if (candidate != null) {
bestMove = candidate;
}
}
return bestMove;
}
好处:
- 时间可控 — 可以在任意深度中断,返回当前最优解
- 走法排序 — 上一轮的结果可以用于下一轮的走法排序,提高剪枝效率
- 渐进式增强 — 浅层搜索快速给出候选,深层搜索精确验证
评估函数
棋盘评估是 AI 的核心:
private int evaluate(Board board, PieceColor color) {
int score = 0;
// 子力价值
for (int row = 0; row < Board.ROWS; row++) {
for (int col = 0; col < Board.COLS; col++) {
Piece piece = board.getPiece(row, col);
if (piece != null) {
int value = getPieceValue(piece);
if (piece.getColor() == color) {
score += value;
} else {
score -= value;
}
}
}
}
// 位置价值(棋子在不同位置的价值不同)
score += positionalBonus(board, color);
// 机动性(可走步数)
score += mobilityBonus(board, color);
return score;
}
子力价值参考表:
| 棋子 | 基础价值 | 说明 |
|---|---|---|
| 车 | 1000 | 最强攻击子 |
| 马 | 500 | 灵活但受限于蹩马腿 |
| 炮 | 500 | 远程攻击,需要炮架 |
| 士/仕 | 200 | 防守核心 |
| 象/相 | 200 | 防守,不能过河 |
| 兵/卒 | 100-200 | 过河后价值翻倍 |
2. 高级优化技术
置换表(Transposition Table)
用 Zobrist 哈希记录已搜索过的局面,避免重复计算:
private Map<Long, TranspositionEntry> transpositionTable = new HashMap<>();
public int probe(Board board, int depth, int alpha, int beta) {
long hash = board.getZobristHash();
TranspositionEntry entry = transpositionTable.get(hash);
if (entry != null && entry.depth >= depth) {
if (entry.flag == EXACT) {
return entry.score;
} else if (entry.flag == LOWER_BOUND && entry.score > alpha) {
alpha = entry.score;
} else if (entry.flag == UPPER_BOUND && entry.score < beta) {
beta = entry.score;
}
if (alpha >= beta) {
return entry.score;
}
}
return UNKNOWN;
}
Zobrist 哈希的优势:增量更新。走棋时只需 XOR 操作,无需重新计算整个棋盘的哈希。
public class Zobrist {
private static long[][][] keys; // [row][col][pieceType]
public static long compute(Board board) {
long hash = 0;
for (int row = 0; row < Board.ROWS; row++) {
for (int col = 0; col < Board.COLS; col++) {
Piece piece = board.getPiece(row, col);
if (piece != null) {
hash ^= keys[row][col][piece.getType().ordinal()];
}
}
}
return hash;
}
// 增量更新:走棋后 XOR 掉起点和终点的棋子
public static long update(long hash, int fromRow, int fromCol,
int toRow, int toCol, Piece piece) {
hash ^= keys[fromRow][fromCol][piece.getType().ordinal()];
hash ^= keys[toRow][toCol][piece.getType().ordinal()];
return hash;
}
}
历史启发(History Heuristic)
记录导致剪枝的走法,在后续搜索中优先尝试:
private int[][][] historyTable = new int[Board.ROWS][Board.COLS][Board.ROWS * Board.COLS];
public void updateHistory(Move move, int depth) {
historyTable[move.fromRow][move.fromCol][move.toRow * Board.COLS + move.toCol]
+= depth * depth; // 深度越大,优先级越高
}
public int getHistoryScore(Move move) {
return historyTable[move.fromRow][move.fromCol][move.toRow * Board.COLS + move.toCol];
}
杀手着法(Killer Moves)
记录每个搜索深度的两个"杀手着法"(导致剪枝的走法):
private Move[][] killerMoves = new Move[maxDepth][2];
public void recordKiller(Move move, int depth) {
if (killerMoves[depth][0] == null ||
killerMoves[depth][0].equals(move)) {
return;
}
killerMoves[depth][1] = killerMoves[depth][0];
killerMoves[depth][0] = move;
}
走法排序
搜索前对走法排序,提高剪枝效率:
private List<Move> orderMoves(Board board, List<Move> moves, int depth) {
return moves.stream()
.sorted((a, b) -> {
int scoreA = moveScore(a, depth);
int scoreB = moveScore(b, depth);
return scoreB - scoreA; // 降序
})
.collect(Collectors.toList());
}
private int moveScore(Move move, int depth) {
int score = 0;
// 吃子优先(MVV-LVA)
if (move.isCapture()) {
score += 10000 + captureValue(move);
}
// 杀手着法
if (isKiller(move, depth)) {
score += 5000;
}
// 历史启发
score += getHistoryScore(move);
return score;
}
空着剪枝(Null Move Pruning)
如果当前局面即使不走棋(空着)也能获得好分数,则跳过搜索:
if (!inCheck && depth >= 3 && !isNullMove) {
board.makeNullMove();
int score = -negamax(board, depth - 3, -beta, -beta + 1, color.opposite(), true);
board.undoNullMove();
if (score >= beta) {
return beta; // 空着剪枝成功
}
}
静态搜索(Quiescence Search)
在搜索叶子节点时,继续搜索吃子着法,避免「水平线效应」:
private int quiescence(Board board, int alpha, int beta, PieceColor color) {
int standPat = evaluate(board, color);
if (standPat >= beta) return beta;
if (standPat > alpha) alpha = standPat;
List<Move> captures = board.generateCaptures(color);
for (Move move : captures) {
board.makeMove(move);
int score = -quiescence(board, -beta, -alpha, color.opposite());
board.undoMove(move);
if (score >= beta) return beta;
if (score > alpha) alpha = score;
}
return alpha;
}
3. 难度分级
AI 支持四个难度级别,通过搜索深度和时间预算控制:
public enum Difficulty {
EASY(2, 500), // 2层,500ms
MEDIUM(4, 2000), // 4层,2秒
HARD(6, 5000), // 6层,5秒
EXPERT(8, 10000); // 8层,10秒
final int maxDepth;
final long timeBudgetMs;
}
实际搜索深度会根据局面复杂度动态调整:
int effectiveDepth = difficulty.maxDepth;
if (board.isEndgame()) {
effectiveDepth += 2; // 残局阶段加深搜索
}
if (board.isInCheck()) {
effectiveDepth += 1; // 被将军时加深
}
4. 五子棋 AI
五子棋 AI 的设计与象棋有显著不同:
候选点生成
不搜索所有空位,只搜索已有棋子的邻域:
public List<int[]> generateCandidates(GomokuBoard board) {
Set<int[]> candidates = new HashSet<>();
int range = 2; // 搜索范围:已有棋子周围2格
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (board.getStone(row, col) != EMPTY) {
// 收集周围的空位
for (int dr = -range; dr <= range; dr++) {
for (int dc = -range; dc <= range; dc++) {
int r = row + dr, c = col + dc;
if (r >= 0 && r < SIZE && c >= 0 && c < SIZE
&& board.getStone(r, c) == EMPTY) {
candidates.add(new int[]{r, c});
}
}
}
}
}
}
return new ArrayList<>(candidates);
}
战术优先
在搜索前,先做快速战术检查:
// 1. 立即胜利
int[] winMove = findWinningMove(board, aiColor);
if (winMove != null) return winMove;
// 2. 立即防守(对手有四连)
int[] blockMove = findBlockingMove(board, humanColor);
if (blockMove != null) return blockMove;
// 3. 正常搜索
return alphaBetaSearch(board, depth);
禁手规则
黑方有禁手限制,防止先手必胜:
public boolean isForbidden(int row, int col, GomokuStone color) {
if (color != BLACK) return false;
// 长连禁手(超过5子)
if (isOverline(row, col, color)) return true;
// 四四禁手(同时形成两个活四)
if (isDoubleFour(row, col, color)) return true;
// 三三禁手(同时形成两个活三)
if (isDoubleThree(row, col, color)) return true;
return false;
}
5. 引擎抽象设计
接口定义
三种棋类各有独立的引擎接口:
// 象棋引擎接口
public interface XiangqiEngine {
Move findBestMove(Board board, PieceColor aiColor, Difficulty difficulty);
String getEngineId();
String getEngineText();
}
// 五子棋引擎接口
public interface GomokuEngine {
GomokuMove findBestMove(GomokuBoard board, GomokuStone aiColor);
String getEngineId();
String getEngineText();
}
// 围棋引擎接口
public interface GoEngine {
GoEngineMove findBestMove(GoBoard board, GoStone aiColor);
String getEngineId();
String getEngineText();
}
引擎实现层次
XiangqiEngine (接口)
├── BuiltinXiangqiEngine — 内置 Minimax AI
├── ConfigurableXiangqiEngine — 可配置外部引擎
└── PikafishUciEngine — Pikafish UCI 适配器
ConfigurableXiangqiEngine 通过外部进程通信,支持 UCI 协议的引擎:
public class ConfigurableXiangqiEngine implements XiangqiEngine {
private Process engineProcess;
private BufferedWriter stdin;
private BufferedReader stdout;
public Move findBestMove(Board board, PieceColor aiColor, Difficulty difficulty) {
// 1. 发送 UCI 位置信息
stdin.write("position fen " + FenCodec.encode(board));
stdin.write("go movetime " + difficulty.timeBudgetMs);
// 2. 读取引擎返回的最佳着法
String bestMove = readLine(); // "bestmove e2e4"
return parseMove(bestMove);
}
}
围棋外部引擎
围棋 AI 复杂度最高,采用独立进程 + HTTP 通信:
public class ConfigurableGoEngine implements GoEngine {
private final String engineUrl; // XQ_GO_ENGINE_URL
public GoEngineMove findBestMove(GoBoard board, GoStone aiColor) {
// 通过 HTTP 调用外部围棋引擎服务
String response = httpClient.post(engineUrl + "/suggest",
Map.of("board", board.toSgf(), "color", aiColor.name()));
return parseResponse(response);
}
}
6. 残局练习系统
数据格式
残局数据存储在 endgames.json 中:
{
"id": "eg-001",
"name": "马后炮",
"fen": "rnbakab1r/9/1c5c1/p3p1p2/9/6P2/P3P3P/1C4NC1/9/RNBAKAB2 w",
"difficulty": "MEDIUM",
"solution": ["h9g7", "h8f9", "g7e8"],
"description": "经典马后炮杀法"
}
练习流程
1. 用户选择残局 → 创建练习对局
2. Board 加载 FEN → 清空棋盘并摆放残局
3. 用户走棋 → 验证是否符合解法
4. AI 自动应招 → 按预设解法走棋
5. 完成 → 记录到 learn_progress 表
7. 性能基准
在普通 PC 上的测试结果:
| 搜索深度 | 考虑节点数 | 耗时 | 棋力评估 |
|---|---|---|---|
| 2 层 | ~1,000 | <50ms | 初学者 |
| 4 层 | ~50,000 | ~500ms | 中级 |
| 6 层 | ~500,000 | ~3s | 高级 |
| 8 层 | ~5,000,000 | ~10s | 业余高手 |
加上置换表和历史启发后,搜索效率提升约 3-5 倍。
小结
象棋 AI 的核心优化路径:
- Alpha-Beta 剪枝 — 基础优化,剪掉无需搜索的分支
- 迭代加深 — 时间可控,走法排序更高效
- 置换表 — 记忆化搜索,避免重复计算
- 走法排序 — 吃子优先、杀手着法、历史启发
- 空着剪枝 — 快速判断是否值得搜索
- 静态搜索 — 解决水平线效应
这些技术组合使用,可以让一个简单的 Minimax 搜索在普通硬件上达到相当强的棋力。
上一篇:(二)后端核心架构
下一篇:(四)前端 SPA 实战
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)