引言
中国象棋是流传千年的双人博弈游戏,其状态空间复杂度约为10^48,远超国际象棋和围棋。要实现一个高水平的象棋AI,核心在于高效的搜索算法和精准的局势评估。本文将使用Java实现象棋AI引擎,重点讲解Alpha-Beta剪枝算法、置换表(Transposition Table)缓存优化以及历史启发式(History Heuristic)着法排序策略。
象棋状态表示
public class ChineseChessBoard {
// 棋盘大小:10行 x 9列
public static final int ROWS = 10;
public static final int COLS = 9;
// 棋子编码(正数为红方,负数为黑方)
public static final int EMPTY = 0;
public static final int RED_KING = 1, RED_ADVISOR = 2, RED_ELEPHANT = 3;
public static final int RED_HORSE = 4, RED_ROOK = 5, RED_CANNON = 6, RED_PAWN = 7;
public static final int BLACK_KING = -1, BLACK_ADVISOR = -2, BLACK_ELEPHANT = -3;
public static final int BLACK_HORSE = -4, BLACK_ROOK = -5, BLACK_CANNON = -6, BLACK_PAWN = -7;
private final int[][] board = new int[ROWS][COLS];
// Zobrist哈希值(用于置换表)
private long zobristKey;
private static final long[][][] ZOBRIST_TABLE = new long[ROWS][COLS][15];
static {
Random rand = new Random(0x5F3759DF); // 固定种子保证可复现
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
for (int p = 0; p < 15; p++) {
ZOBRIST_TABLE[r][c][p] = rand.nextLong();
}
}
}
}
public void setPiece(int row, int col, int piece) {
int oldPiece = board[row][col];
if (oldPiece != EMPTY) {
zobristKey ^= ZOBRIST_TABLE[row][col][oldPiece + 7];
}
if (piece != EMPTY) {
zobristKey ^= ZOBRIST_TABLE[row][col][piece + 7];
}
board[row][col] = piece;
}
public long getZobristKey() {
return zobristKey;
}
}
局势评估函数
public class Evaluation {
// 基础棋子分值
private static final int[] PIECE_VALUE = {0, 10000, 250, 250, 400, 900, 450, 100,
0, -10000, -250, -250, -400, -900, -450, -100};
// 位置加成表(以红方视角,黑方需要镜像)
private static final int[][] PAWN_POSITION = {
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 10, 10, 10, 0, 0, 0},
{20, 20, 20, 30, 30, 30, 20, 20, 20},
{30, 30, 30, 40, 40, 40, 30, 30, 30},
{40, 40, 40, 50, 50, 50, 40, 40, 40},
{50, 50, 50, 60, 60, 60, 50, 50, 50}
};
/**
* 评估当前局面分数(红方为正,黑方为负)
*/
public int evaluate(ChineseChessBoard board) {
int score = 0;
for (int r = 0; r < ChineseChessBoard.ROWS; r++) {
for (int c = 0; c < ChineseChessBoard.COLS; c++) {
int piece = board.getPiece(r, c);
if (piece != ChineseChessBoard.EMPTY) {
score += PIECE_VALUE[piece + 7];
// 位置加成
if (Math.abs(piece) == ChineseChessBoard.RED_PAWN) {
int pr = piece > 0 ? r : (9 - r);
score += (piece > 0 ? 1 : -1) * PAWN_POSITION[pr][c];
}
}
}
}
// 灵活性评估:计算各方有效着法数
score += mobilityBonus(board, true) - mobilityBonus(board, false);
return score;
}
private int mobilityBonus(ChineseChessBoard board, boolean isRed) {
// 简化实现:统计可移动位置数量
return generateAllMoves(board, isRed).size() * 5;
}
public List<Move> generateAllMoves(ChineseChessBoard board, boolean isRed) {
List<Move> moves = new ArrayList<>();
for (int r = 0; r < ChineseChessBoard.ROWS; r++) {
for (int c = 0; c < ChineseChessBoard.COLS; c++) {
int piece = board.getPiece(r, c);
if ((isRed && piece > 0) || (!isRed && piece < 0)) {
moves.addAll(generatePieceMoves(board, r, c, piece));
}
}
}
return moves;
}
private List<Move> generatePieceMoves(ChineseChessBoard board, int r, int c, int piece) {
List<Move> moves = new ArrayList<>();
// 根据棋子类型生成合法着法(简化示意)
int[][] directions = getDirections(piece);
for (int[] d : directions) {
int nr = r + d[0], nc = c + d[1];
if (isValidMove(board, r, c, nr, nc, piece)) {
moves.add(new Move(r, c, nr, nc));
}
}
return moves;
}
private int[][] getDirections(int piece) {
// 简化方向定义
return switch (Math.abs(piece)) {
case 5 -> new int[][]{{-1,0},{1,0},{0,-1},{0,1}}; // 车
case 4 -> new int[][]{{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}}; // 马
default -> new int[][]{{-1,0},{1,0},{0,-1},{0,1}};
};
}
private boolean isValidMove(ChineseChessBoard board, int fr, int fc, int tr, int tc, int piece) {
// 边界检查
if (tr < 0 || tr >= ChineseChessBoard.ROWS || tc < 0 || tc >= ChineseChessBoard.COLS) return false;
// 不能吃己方棋子
int target = board.getPiece(tr, tc);
if (target != ChineseChessBoard.EMPTY && (piece * target > 0)) return false;
return true;
}
}
class Move {
int fromRow, fromCol, toRow, toCol;
Move(int fr, int fc, int tr, int tc) {
this.fromRow = fr; this.fromCol = fc;
this.toRow = tr; this.toCol = tc;
}
}
Alpha-Beta剪枝与置换表
public class ChessEngine {
private static final int MAX_DEPTH = 6;
private static final int INFINITY = 1000000;
// 置换表
private final Map<Long, TranspositionEntry> transpositionTable = new HashMap<>();
// 历史启发表
private final int[][][][] historyTable = new int[10][9][10][9];
private int nodesSearched = 0;
private int cutoffs = 0;
static class TranspositionEntry {
int depth, score, flag; // flag: 0=exact, 1=lower, 2=upper
Move bestMove;
TranspositionEntry(int d, int s, int f, Move m) {
depth = d; score = s; flag = f; bestMove = m;
}
}
/**
* Alpha-Beta搜索主入口
*/
public Move searchBestMove(ChineseChessBoard board, boolean isRed) {
nodesSearched = 0;
cutoffs = 0;
transpositionTable.clear();
Move bestMove = null;
int bestScore = -INFINITY;
List<Move> moves = new Evaluation().generateAllMoves(board, isRed);
sortMoves(moves, board);
for (Move move : moves) {
board.makeMove(move);
int score = -alphaBeta(board, MAX_DEPTH - 1, -INFINITY, -bestScore, !isRed);
board.undoMove(move);
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
System.out.printf("搜索节点: %d, 剪枝次数: %d, 最佳分数: %d%n",
nodesSearched, cutoffs, bestScore);
return bestMove;
}
/**
* 带置换表的Alpha-Beta剪枝
*/
private int alphaBeta(ChineseChessBoard board, int depth, int alpha, int beta, boolean isRed) {
nodesSearched++;
// 置换表查找
long key = board.getZobristKey();
TranspositionEntry entry = transpositionTable.get(key);
if (entry != null && entry.depth >= depth) {
if (entry.flag == 0) return entry.score;
if (entry.flag == 1 && entry.score >= beta) return entry.score;
if (entry.flag == 2 && entry.score <= alpha) return entry.score;
}
if (depth == 0) {
return new Evaluation().evaluate(board);
}
List<Move> moves = new Evaluation().generateAllMoves(board, isRed);
if (moves.isEmpty()) {
return isRed ? -INFINITY + (MAX_DEPTH - depth) : INFINITY - (MAX_DEPTH - depth);
}
// 历史启发排序:优先搜索历史得分高的着法
moves.sort((a, b) -> historyTable[b.fromRow][b.fromCol][b.toRow][b.toCol]
- historyTable[a.fromRow][a.fromCol][a.toRow][a.toCol]);
Move bestMove = null;
int originalAlpha = alpha;
for (Move move : moves) {
board.makeMove(move);
int score = -alphaBeta(board, depth - 1, -beta, -alpha, !isRed);
board.undoMove(move);
if (score >= beta) {
// Beta剪枝
cutoffs++;
historyTable[move.fromRow][move.fromCol][move.toRow][move.toCol] += depth * depth;
transpositionTable.put(key, new TranspositionEntry(depth, beta, 1, move));
return beta;
}
if (score > alpha) {
alpha = score;
bestMove = move;
}
}
// 存入置换表
int flag = (alpha > originalAlpha) ? 0 : 2;
transpositionTable.put(key, new TranspositionEntry(depth, alpha, flag, bestMove));
return alpha;
}
private void sortMoves(List<Move> moves, ChineseChessBoard board) {
// 初始排序:优先考虑吃子着法
moves.sort((a, b) -> {
int va = Math.abs(board.getPiece(a.toRow, a.toCol));
int vb = Math.abs(board.getPiece(b.toRow, b.toCol));
return vb - va;
});
}
}
走法执行与撤销
// 在ChineseChessBoard类中补充
class MoveRecord {
int fromRow, fromCol, toRow, toCol, fromPiece, toPiece;
}
// makeMove和undoMove方法
public MoveRecord makeMove(Move move) {
MoveRecord record = new MoveRecord();
record.fromRow = move.fromRow; record.fromCol = move.fromCol;
record.toRow = move.toRow; record.toCol = move.toCol;
record.fromPiece = getPiece(move.fromRow, move.fromCol);
record.toPiece = getPiece(move.toRow, move.toCol);
setPiece(move.toRow, move.toCol, record.fromPiece);
setPiece(move.fromRow, move.fromCol, EMPTY);
return record;
}
public void undoMove(MoveRecord record) {
setPiece(record.fromRow, record.fromCol, record.fromPiece);
setPiece(record.toRow, record.toCol, record.toPiece);
}
复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 着法生成 | O(N) per piece | O(1) |
| Alpha-Beta搜索(无剪枝) | O(b^d) | O(d) |
| Alpha-Beta搜索(理想剪枝) | O(b^(d/2)) | O(d) |
| 置换表查询 | O(1) amortized | O(T) |
其中b为平均分支因子(约40-50),d为搜索深度,T为置换表容量。
总结
本文实现了中国象棋AI的核心搜索框架,通过Alpha-Beta剪枝将搜索空间从指数级降为接近平方根级别,置换表避免了重复局面的重复计算,历史启发式则进一步优化了着法排序效率。在实际对局中,配合更深的搜索深度和更精细的评估函数,可以构建出具有相当水平的象棋AI程序。