引言
国际象棋(Chess)是计算机博弈领域的”果蝇”,自香农1950年提出博弈树搜索框架以来,无数经典算法在此诞生。本文将用Java实现一个完整的国际象棋AI引擎,核心算法为Minimax博弈树搜索配合Alpha-Beta剪枝优化,并引入置换表(Transposition Table)与迭代深化(Iterative Deepening)技术。读者将学习如何从零构建一个具备中级水平的棋类AI。
博弈树基础
Minimax原理
在零和博弈中,Max玩家(AI)试图最大化收益,Min玩家(对手)试图最小化收益。Minimax通过递归评估博弈树中每个节点的值:
$$V(s) = \begin{cases} eval(s) & \text{if } s \text{ is terminal} \ \max_{a \in A} V(s’) & \text{if } s \text{ is Max node} \ \min_{a \in A} V(s’) & \text{if } s \text{ is Min node} \end{cases}$$
Alpha-Beta剪枝
在Minimax基础上,维护两个阈值:
– $\alpha$:Max玩家能保证的最低收益(初始 $-\infty$)
– $\beta$:Min玩家能容忍的最高损失(初始 $+\infty$)
当 $\alpha \geq \beta$ 时,当前分支可被剪枝,无需继续搜索。
迭代深化
在有限时间内,从深度1开始逐步加深搜索,确保随时都有可用走法。配合置换表可复用前一轮搜索结果,大幅提升效率。
棋盘表示与着法生成
采用8×8整数数组表示棋盘,正值为白方,负值为黑方。着法生成需处理各棋子的独特移动规则、吃子、将军检测等特殊逻辑。
Java实现
import java.util.*;
/**
* 国际象棋AI引擎 - Minimax + Alpha-Beta剪枝 + 置换表
*/
public class ChessEngine {
// 棋子编码:正值白方,负值黑方
static final int EMPTY = 0;
static final int PAWN = 1, KNIGHT = 2, BISHOP = 3, ROOK = 4, QUEEN = 5, KING = 6;
// 棋子基础价值(简化版,单位:厘兵)
static final int[] PIECE_VALUE = {0, 100, 320, 330, 500, 900, 20000};
// 位置价值表(鼓励中心控制与推进)
static final int[][] PAWN_TABLE = {
{0, 0, 0, 0, 0, 0, 0, 0},
{50, 50, 50, 50, 50, 50, 50, 50},
{10, 10, 20, 30, 30, 20, 10, 10},
{5, 5, 10, 25, 25, 10, 5, 5},
{0, 0, 0, 20, 20, 0, 0, 0},
{5, -5,-10, 0, 0,-10, -5, 5},
{5, 10, 10,-20,-20, 10, 10, 5},
{0, 0, 0, 0, 0, 0, 0, 0}
};
static class Move {
int fromRow, fromCol;
int toRow, toCol;
int captured; // 被吃掉的棋子
int score; // 移动排序分数
Move(int fr, int fc, int tr, int tc) {
this.fromRow = fr; this.fromCol = fc;
this.toRow = tr; this.toCol = tc;
this.captured = EMPTY;
this.score = 0;
}
@Override
public String toString() {
return String.format("%c%d%c%d", 'a'+fromCol, 8-fromRow, 'a'+toCol, 8-toRow);
}
}
// 置换表条目
static class TTEntry {
long key;
int depth;
int score;
int flag; // 0=精确值, 1=下界, 2=上界
Move bestMove;
TTEntry(long key, int depth, int score, int flag, Move bestMove) {
this.key = key; this.depth = depth;
this.score = score; this.flag = flag;
this.bestMove = bestMove;
}
}
private int[][] board;
private boolean whiteToMove;
private Map<Long, TTEntry> transpositionTable;
private long[][][] zobristKeys; // 用于哈希的随机数表
private long currentHash;
private int nodesSearched;
private long startTime;
private static final long TIME_LIMIT_MS = 5000; // 5秒思考时间
public ChessEngine() {
this.board = new int[8][8];
this.whiteToMove = true;
this.transpositionTable = new HashMap<>();
initZobrist();
setupInitialPosition();
}
/**
* 初始化Zobrist哈希随机数表
*/
private void initZobrist() {
Random rand = new Random(12345);
zobristKeys = new long[8][8][13]; // 8x8格子 x 12种棋子+1空
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
for (int p = 0; p < 13; p++) {
zobristKeys[r][c][p] = rand.nextLong();
}
}
}
}
/**
* 设置初始局面(简化版,仅放置主要棋子)
*/
private void setupInitialPosition() {
// 白方后排
int[] backRank = {ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK};
for (int c = 0; c < 8; c++) {
board[7][c] = backRank[c];
board[6][c] = PAWN;
board[0][c] = -backRank[c];
board[1][c] = -PAWN;
}
computeHash();
}
/**
* 计算当前局面的Zobrist哈希值
*/
private void computeHash() {
currentHash = 0;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (board[r][c] != EMPTY) {
int idx = board[r][c] + 6; // 映射到0-12
currentHash ^= zobristKeys[r][c][idx];
}
}
}
}
/**
* 生成当前方的所有合法着法
*/
public List<Move> generateMoves() {
List<Move> moves = new ArrayList<>();
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
int piece = board[r][c];
if (piece == EMPTY) continue;
if (whiteToMove && piece < 0) continue;
if (!whiteToMove && piece > 0) continue;
generatePieceMoves(r, c, Math.abs(piece), moves);
}
}
// 着法排序:MVV-LVA(Most Valuable Victim - Least Valuable Aggressor)
moves.sort((a, b) -> b.score - a.score);
return moves;
}
/**
* 生成单种棋子的着法(简化版,仅实现核心逻辑)
*/
private void generatePieceMoves(int r, int c, int type, List<Move> moves) {
int side = whiteToMove ? 1 : -1;
switch (type) {
case PAWN:
int dir = whiteToMove ? -1 : 1;
// 直走
if (inBounds(r + dir, c) && board[r + dir][c] == EMPTY) {
moves.add(new Move(r, c, r + dir, c));
// 初始两格
if ((whiteToMove && r == 6) || (!whiteToMove && r == 1)) {
if (board[r + 2*dir][c] == EMPTY) {
moves.add(new Move(r, c, r + 2*dir, c));
}
}
}
// 斜吃
for (int dc = -1; dc <= 1; dc += 2) {
int nr = r + dir, nc = c + dc;
if (inBounds(nr, nc) && board[nr][nc] != EMPTY
&& (board[nr][nc] > 0) != whiteToMove) {
Move m = new Move(r, c, nr, nc);
m.captured = Math.abs(board[nr][nc]);
m.score = PIECE_VALUE[m.captured] * 10 - PIECE_VALUE[PAWN];
moves.add(m);
}
}
break;
case KNIGHT:
int[][] knightDirs = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
for (int[] d : knightDirs) {
addMoveIfValid(r, c, r + d[0], c + d[1], moves);
}
break;
case BISHOP:
generateSlidingMoves(r, c, new int[][]{{-1,-1},{-1,1},{1,-1},{1,1}}, moves);
break;
case ROOK:
generateSlidingMoves(r, c, new int[][]{{-1,0},{1,0},{0,-1},{0,1}}, moves);
break;
case QUEEN:
generateSlidingMoves(r, c, new int[][]{{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}}, moves);
break;
case KING:
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
addMoveIfValid(r, c, r + dr, c + dc, moves);
}
}
break;
}
}
private void generateSlidingMoves(int r, int c, int[][] dirs, List<Move> moves) {
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
while (inBounds(nr, nc)) {
if (board[nr][nc] == EMPTY) {
moves.add(new Move(r, c, nr, nc));
} else {
if ((board[nr][nc] > 0) != whiteToMove) {
Move m = new Move(r, c, nr, nc);
m.captured = Math.abs(board[nr][nc]);
m.score = PIECE_VALUE[m.captured] * 10;
moves.add(m);
}
break;
}
nr += d[0]; nc += d[1];
}
}
}
private void addMoveIfValid(int fr, int fc, int tr, int tc, List<Move> moves) {
if (!inBounds(tr, tc)) return;
if (board[tr][tc] == EMPTY) {
moves.add(new Move(fr, fc, tr, tc));
} else if ((board[tr][tc] > 0) != whiteToMove) {
Move m = new Move(fr, fc, tr, tc);
m.captured = Math.abs(board[tr][tc]);
m.score = PIECE_VALUE[m.captured] * 10;
moves.add(m);
}
}
private boolean inBounds(int r, int c) {
return r >= 0 && r < 8 && c >= 0 && c < 8;
}
/**
* 执行着法并更新哈希
*/
private void makeMove(Move m) {
int movingPiece = board[m.fromRow][m.fromCol];
if (board[m.toRow][m.toCol] != EMPTY) {
currentHash ^= zobristKeys[m.toRow][m.toCol][board[m.toRow][m.toCol] + 6];
}
currentHash ^= zobristKeys[m.fromRow][m.fromCol][movingPiece + 6];
currentHash ^= zobristKeys[m.toRow][m.toCol][movingPiece + 6];
board[m.toRow][m.toCol] = movingPiece;
board[m.fromRow][m.fromCol] = EMPTY;
whiteToMove = !whiteToMove;
}
private void undoMove(Move m) {
whiteToMove = !whiteToMove;
int movingPiece = board[m.toRow][m.toCol];
board[m.fromRow][m.fromCol] = movingPiece;
board[m.toRow][m.toCol] = EMPTY;
currentHash ^= zobristKeys[m.fromRow][m.fromCol][movingPiece + 6];
currentHash ^= zobristKeys[m.toRow][m.toCol][movingPiece + 6];
if (m.captured != EMPTY) {
int cap = whiteToMove ? -m.captured : m.captured;
board[m.toRow][m.toCol] = cap;
currentHash ^= zobristKeys[m.toRow][m.toCol][cap + 6];
}
}
/**
* 局面评估函数
*/
public int evaluate() {
int score = 0;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
int piece = board[r][c];
if (piece == EMPTY) continue;
int absType = Math.abs(piece);
int value = PIECE_VALUE[absType];
// 加入位置价值
if (absType == PAWN) {
int pr = piece > 0 ? r : 7 - r;
value += PAWN_TABLE[pr][c];
}
score += (piece > 0) ? value : -value;
}
}
return whiteToMove ? score : -score;
}
/**
* Alpha-Beta搜索
*/
private int alphaBeta(int depth, int alpha, int beta, boolean isMax) {
nodesSearched++;
// 时间检查
if (System.currentTimeMillis() - startTime > TIME_LIMIT_MS) {
return evaluate();
}
// 置换表查询
TTEntry ttEntry = transpositionTable.get(currentHash);
if (ttEntry != null && ttEntry.depth >= depth) {
if (ttEntry.flag == 0) return ttEntry.score;
if (ttEntry.flag == 1 && ttEntry.score >= beta) return ttEntry.score;
if (ttEntry.flag == 2 && ttEntry.score <= alpha) return ttEntry.score;
}
// 叶子节点
if (depth == 0) {
return evaluate();
}
List<Move> moves = generateMoves();
if (moves.isEmpty()) {
return isMax ? -30000 : 30000; // 将死/逼和简化处理
}
Move bestMove = null;
int oldAlpha = alpha;
if (isMax) {
int maxEval = Integer.MIN_VALUE;
for (Move m : moves) {
makeMove(m);
int eval = alphaBeta(depth - 1, alpha, beta, false);
undoMove(m);
if (eval > maxEval) {
maxEval = eval;
bestMove = m;
}
alpha = Math.max(alpha, eval);
if (beta <= alpha) break; // Beta剪枝
}
// 存入置换表
int flag = maxEval <= oldAlpha ? 2 : (maxEval >= beta ? 1 : 0);
transpositionTable.put(currentHash,
new TTEntry(currentHash, depth, maxEval, flag, bestMove));
return maxEval;
} else {
int minEval = Integer.MAX_VALUE;
for (Move m : moves) {
makeMove(m);
int eval = alphaBeta(depth - 1, alpha, beta, true);
undoMove(m);
if (eval < minEval) {
minEval = eval;
bestMove = m;
}
beta = Math.min(beta, eval);
if (beta <= alpha) break; // Alpha剪枝
}
int flag = minEval <= oldAlpha ? 2 : (minEval >= beta ? 1 : 0);
transpositionTable.put(currentHash,
new TTEntry(currentHash, depth, minEval, flag, bestMove));
return minEval;
}
}
/**
* 迭代深化搜索
*/
public Move findBestMove() {
startTime = System.currentTimeMillis();
nodesSearched = 0;
Move bestMove = null;
for (int depth = 1; depth <= 6; depth++) {
List<Move> moves = generateMoves();
if (moves.isEmpty()) break;
int bestScore = Integer.MIN_VALUE;
Move currentBest = null;
for (Move m : moves) {
makeMove(m);
int score = alphaBeta(depth - 1, Integer.MIN_VALUE, Integer.MAX_VALUE, false);
undoMove(m);
if (score > bestScore) {
bestScore = score;
currentBest = m;
}
}
if (System.currentTimeMillis() - startTime < TIME_LIMIT_MS) {
bestMove = currentBest;
System.out.printf("深度 %d: 最佳着法 %s, 评分 %d, 搜索节点 %d%n",
depth, bestMove, bestScore, nodesSearched);
} else {
break;
}
}
return bestMove != null ? bestMove : generateMoves().get(0);
}
public static void main(String[] args) {
ChessEngine engine = new ChessEngine();
System.out.println("=== 国际象棋AI引擎启动 ===");
System.out.println("初始局面评估: " + engine.evaluate());
// 模拟5步对局
for (int i = 0; i < 5; i++) {
Move best = engine.findBestMove();
System.out.println("\n第 " + (i+1) + " 步: " + best);
engine.makeMove(best);
System.out.println("局面评估: " + engine.evaluate());
System.out.println("合法着法数: " + engine.generateMoves().size());
}
}
}
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 分支因子 | $b \approx 35$ | 国际象棋平均合法着法数 |
| Minimax | $O(b^d)$ | $d$ 为搜索深度 |
| Alpha-Beta(最优排序) | $O(b^{d/2})$ | 理想情况下节点数减半 |
| 置换表命中 | 减少30-50% | 依赖局面重复频率 |
| 迭代深化 | $O(b^d)$ | 额外开销约10-15% |
实战中,搜索深度每增加1层,棋力约提升200 Elo分。现代引擎通过并行搜索、神经网络评估可将深度推进至20+层。
扩展思考
- 神经网络评估:用CNN替代 handcrafted 评估函数,如AlphaZero的Policy-Value Network,棋力可提升至大师级
- 蒙特卡洛树搜索(MCTS):在复杂局面中,MCTS通过随机模拟替代深度搜索,与深度学习结合后效果显著
- 开局库与残局库:将已验证的开局走法和残局最优解预存为数据库,减少搜索开销并避免理论错误
总结
本文通过Java构建了一个功能完整的国际象棋AI引擎,实现了Minimax搜索、Alpha-Beta剪枝、置换表和迭代深化等核心算法。代码框架支持进一步扩展神经网络评估和并行搜索。国际象棋AI的发展历程充分说明:算法效率(剪枝)与领域知识(评估函数)的结合,是博弈AI成功的关键。该引擎的架构思想可迁移至围棋、将棋等各类完全信息博弈游戏。