做这个项目的念头,最早不是来自什么高深的理论。当时我把 Java 基础语法、集合、多线程都过了一遍,市面上常见的图书管理、学生选课这类练习已经写烦了,总感觉缺一个能把自己掌握的零碎知识串起来的东西。后来看到有人用 Python 写国际象棋 AI,我就想:中国象棋为什么不能用 Java 搞一个?于是就有了这个带 AI 功能的中国象棋项目——支持人机对战、人人对弈、机机对弈三种模式,核心是一套基于 Minimax 搜索和 Alpha-Beta 剪枝的走棋引擎。这篇文章不讲花架子,就把我怎么用纯 Java 把这个项目从命令行版一步步做成带界面的完整程序,踩过哪些坑、算法细节怎么调、面试怎么讲,全部分享出来。
如果你正处于 Java 基础学完、想找一个有深度但又不会大到没法掌控的练手项目,或者准备在简历上写一个有实际算法含量的项目,这个象棋项目的完整思路值得你看完。它没有用到什么冷门框架,核心就是 Java 标准库,从数据结构设计到递归搜索、多线程调度、Swing 界面,一路全走了一遍。
1. 为什么我要拿 Java 写一个会下棋的象棋程序
1.1 象棋 AI 本质上是一个“小号搜索引擎”
很多人一听 AI 就觉得很玄,但传统棋类 AI 的核心其实只有一个问题:在有限的搜索空间里,找到尽可能好的那一步棋。这个逻辑和搜索引擎很像——你把所有可能的后续局面展开,用一套评估标准给每个局面打分,然后挑分数最高的一条路走。
把人机对战拆开来看,真正复杂的东西就三块:
- 数据结构:棋盘怎么存、棋子怎么表示、走法怎么生成。
- 搜索算法:在当前局面下,向前模拟若干步,评估哪个走法更好。
- 评估函数:棋盘上一个局面的好坏,怎样用一个数值描述。
整明白了这三块,中国象棋、国际象棋、五子棋全部是同一套方法论。只是中国象棋的规则比五子棋复杂不少,又没有国际象棋那么多现成资料,所以对 Java 学习者的数据结构功底和代码组织能力要求更高。这恰恰是这个项目的价值所在。
1.2 三种对战模式背后的统一架构
标题里写了三种功能:人机对战、人人对弈、机机对弈。一开始我以为是三个独立模块,后来做着做着发现,它们本质上可以被同一个“对弈循环”统一起来。
对弈循环就四步:
- 判断当前轮到哪一方走棋。
- 根据该方类型决定走法来源:人类点击棋盘,AI 调用搜索引擎。
- 执行走法,检查将军、被将死、困毙等终局条件。
- 切换走棋方,回到第 1 步。
人机模式 = 红方 HUMAN + 黑方 AI;人人模式 = 红方 HUMAN + 黑方 HUMAN;机机模式 = 红方 AI + 黑方 AI。差别只有一个玩家类型的枚举值。把架构写到这个层面,代码的扩展性一下就上来了。
1.3 这个项目能锻炼哪些 Java 能力
到项目收尾的时候,我回头盘点了一下,发现自己其实把 Java 开发里最常考的一批知识点全用上了:
- 面向对象设计:棋子、棋盘、着法、玩家、AI 引擎这些类怎么划分。
- 常用集合:ArrayList 存走法列表、HashMap 做局面缓存、Stack 做悔棋记录。
- 递归与回溯:搜索算法的核心就是递归,每一步尝试走棋后要能“悔棋”回退现场。
- 多线程:AI 思考必须放到后台线程,否则界面会卡死。
- Lambda 与 Stream:走法排序、集合遍历时用起来很顺手。
- Swing 事件驱动:鼠标监听、重绘机制。
这些都是 Java 面试八股文里反复出现的东西。区别在于,你背了十遍 ArrayList 和 HashMap,不如在一个真实项目里亲手用一遍。你在简历上写“熟悉集合框架”,和写“在象棋项目中用 HashMap 做重复局面缓存”,说服力完全不是一个级别。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 棋盘和棋子的建模:先把数据结构做扎实
2.1 二维数组和坐标体系的选择
棋盘建模我用的是最简单直接的方案:int[][] board = new int[10][9]。
中国象棋是 9 列 10 行,红方在下方,黑方在上方。数组的行索引对应棋盘的纵向,列索引对应横向。board 里的每个元素用一个整数表示棋子类型:
- 0 表示空位
- 正数表示红方棋子
- 负数表示黑方棋子
具体的编码方案我是这样定的:1 帅,2 仕,3 相,4 马,5 车,6 炮,7 兵。黑方就是 -1 到 -7。
java复制public class ChessBoard {
public static final int EMPTY = 0;
public static final int RED_KING = 1;
public static final int RED_ADVISOR = 2;
public static final int RED_BISHOP = 3;
public static final int RED_KNIGHT = 4;
public static final int RED_ROOK = 5;
public static final int RED_CANNON = 6;
public static final int RED_PAWN = 7;
// 黑方用 -1 到 -7 表示
private int[][] board = new int[10][9];
public boolean isRed(int piece) {
return piece > 0;
}
public boolean isBlack(int piece) {
return piece < 0;
}
public boolean isSameSide(int piece1, int piece2) {
return piece1 * piece2 > 0;
}
}
用正负号区分红黑有个额外的好处:判断敌我只需要看乘积正负。取棋子所属方、判断是否同一边,代码都极其简洁。
2.2 为什么不用对象来表示每个棋子
最开始我设计过每个棋子都是 ChessPiece 对象,里面存类型、位置、颜色。但写了一阵子发现这个方案在“走子-悔棋-搜索回退”的场景下非常别扭:你走一步棋以后,棋子的位置要变、可能吃掉对方一个棋子、被吃掉的棋子还要留着以备悔棋恢复。
用二维数组加整数表示之后,走一步棋只需要三条语句:
java复制board[toX][toY] = board[fromX][fromY]; // 目标位置放上移动的棋子(覆盖掉被吃的子)
board[fromX][fromY] = 0; // 原位置清空
悔棋就反过来。搜索算法递归时,每次尝试走棋后也要悔棋,这个开销非常关键——数组访问比创建和销毁对象快得多,搜到第四层时这个性能差距是肉眼可见的。
2.3 着法生成:规则细节全在这里
着法生成是整个引擎里最容易写错的部分。中国象棋的规则细节特别多,马有马脚,象有象眼,将帅不能照面,炮吃子要隔一个炮架,兵过河才能横走。我建议把每种棋子的走法拆成独立方法,不要写成一坨巨大的 if-else。
以马为例,它走的是“日”字,八个方向跳一步,但只要马腿被别住就废了:
java复制public void generateKnightMoves(int x, int y, List<int[]> moves) {
int[][] directions = {{1, 2}, {2, 1}, {2, -1}, {1, -2},
{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}};
int[][] legOffsets = {{0, 1}, {1, 0}, {1, 0}, {0, -1},
{0, -1}, {-1, 0}, {-1, 0}, {0, 1}};
for (int i = 0; i < directions.length; i++) {
int nx = x + directions[i][0];
int ny = y + directions[i][1];
int legX = x + legOffsets[i][0];
int legY = y + legOffsets[i][1];
if (inBoard(nx, ny) && board[legX][legY] == EMPTY // 马腿不能有子
&& !isSameSide(board[x][y], board[nx][ny])) { // 目标位置不能是自己人
moves.add(new int[]{nx, ny});
}
}
}
这里有一个设计窍门:方向数组和腿偏移数组必须一一对应。我建议把这两种偏移放到同一个私有内部类里封装,写成数组反而容易下标对不上。我当时就因为这个下标错位,AI 的马走了两步全在跳“假日字”,排查了大半天。
车的走法是最经典的直线扫描,沿四个方向往外走,遇到空位就继续、遇到敌方棋子就吃掉并停止、遇到己方棋子就停止。炮是最特殊的:不吃子时和车一样走直线,吃子时必须隔且只能隔一个棋子(炮架),而且炮架必须是在自己和目标之间。
2.4 将军、被将死、困毙的判断逻辑
着法生成完成以后,还要处理终局判断。这部分的逻辑要非常小心:
- 是否被将军:找到己方将帅的位置,看对方所有棋子中是否有任何一个能一步吃到它。
- 是否被将死:当前方被将军,并且所有合法的应对走法都不能解除将军。
- 是否困毙:当前方没被将军,但没有任何合法走法可走。
一个常见的坑是:程序生成了走法以后,直接执行,然后判断对方能不能吃帅。这有问题——你走了一步之后,自己依然处于被将军状态,或者把帅走去了对方车口上。正确的做法是走完一步棋后,检查己方将帅是否暴露在对方火力下,如果是,这个走法就是非法的,必须丢弃。
java复制public boolean isMoveLegal(int fromX, int fromY, int toX, int toY) {
// 1. 先保存现场
int captured = board[toX][toY];
int moving = board[fromX][fromY];
// 2. 模拟走子
board[toX][toY] = moving;
board[fromX][fromY] = EMPTY;
// 3. 判断走后己方是否被将军
boolean legal = !isInCheck(moving > 0 ? RED : BLACK);
// 4. 恢复现场
board[fromX][fromY] = moving;
board[toX][toY] = captured;
return legal;
}
先模拟、再检查、最后恢复。这个模式也是后面 AI 搜索时反复用的核心套路,所以写成一个通用方法,后面能少写很多重复代码。
3. AI 引擎设计:让电脑会思考
3.1 评估函数:棋子在棋盘上的真实价值
搜索算法负责往前推演,但推演到最后一层时,程序必须知道“这个局面是红方好还是黑方好”。这就是评估函数干的事。
最简单的评估就是算棋子价值总和:车 1000,马 400,炮 450,士相 200,兵 100,帅 10000。但只用基础值是远远不够的,我实测过,这种 AI 走出来的棋非常“愣”,车马炮经常走出白送的位置,因为所有同类棋子在哪个位置价值都一样。
所以我在基础值上加了位置价值表。比如马在棋盘中央要强于在边角,炮在河口有威慑力,兵过河以后价值应该翻倍。做法就是给每种棋子准备一张 10x9 的表,不同位置加不同的调整分。
以“兵”为例,过河兵和未过河兵的价值差很多。我给兵的加分逻辑是:
java复制public int evaluatePawn(int x, int y) {
int score = 100;
if (isRed(x, y)) {
if (y < 5) score += 120; // 过河加分
if (y <= 2) score += 60; // 深入对方腹地再加分
} else {
if (y >= 5) score += 120;
if (y >= 7) score += 60;
}
return score;
}
完整位置表的调参,是我整个项目里花时间最多的事之一。我的做法是不停地让 AI 和自己下、让 AI 和 AI 下,记录哪一方赢得多,再反过来微调分数。后面说机机对战的调参部分,我还会展开讲。
3.2 搜索树:从两步到四步,AI 的“棋力”飞跃
有了评估函数之后,AI 就可以“想两步”了:我走一步,然后模拟对方走一步,再对局面打分。如果能只想到这一步,AI 的水平非常弱,基本只会吃看得见的子。
更合理的做法是做一个深度为 4 的递归搜索:红方走、黑方走、红方走、黑方走。到第四层再评估,红方选择自己能拿到的最高分路径,黑方则相反,会选择让红方分数最低的路径。这就是 Minimax 算法。
java复制public int minimax(int depth, int side, int alpha, int beta) {
if (depth == 0) {
return evaluate();
}
List<Move> moves = generateAllMoves(side);
if (moves.isEmpty()) {
// 被将死或困毙
return side == RED ? -MATE_SCORE : MATE_SCORE;
}
if (side == RED) { // 红方想最大化
int best = Integer.MIN_VALUE;
for (Move move : moves) {
makeMove(move);
best = Math.max(best, minimax(depth - 1, BLACK, alpha, beta));
unmakeMove(move);
alpha = Math.max(alpha, best);
if (beta <= alpha) break; // 剪枝
}
return best;
} else { // 黑方想最小化
int best = Integer.MAX_VALUE;
for (Move move : moves) {
makeMove(move);
best = Math.min(best, minimax(depth - 1, RED, alpha, beta));
unmakeMove(move);
beta = Math.min(beta, best);
if (beta <= alpha) break;
}
return best;
}
}
把深度从 2 调整为 4 以后,AI 水平是肉眼可见的质变。深度 2 时它完全不懂得弃子引离、捉双这些战术;深度 4 时它已经知道下一步不能无脑吃,因为吃了可能会被反吃回去。
3.3 Alpha-Beta 剪枝:同样深度,搜索量少了一个数量级
纯 Minimax 的问题是搜索量爆炸。中国象棋平均每一步合法走法大约有三四十种,深度 4 就是 40 的 4 次方,约 256 万个节点。每一层还要生成着法、模拟走子,Java 跑起来能明显卡顿。
Alpha-Beta 剪枝的意思是:红方已经找到了一个分数为 500 的走法,现在看另一个分支,黑方走完一步后红方只能拿到 300,那么这个分支就完全不用往下搜索了。因为黑方足够聪明,肯定不会把你导向那个更好的 300,红方前面那个 500 已经足够优秀。
加上剪枝以后,搜索节点数能降到原来的十分之一甚至更低。我测试时打印过搜索节点数,深度 4 带剪枝以后通常在 20 万到 40 万之间,Java 在几百毫秒内就能跑完一轮。这个体量对于电脑下棋完全够用。
3.4 着法排序:剪枝效率的隐形开关
Alpha-Beta 剪枝的效率严重依赖搜索顺序。如果每次都是先搜好棋,剪枝会非常频繁;如果先搜差棋,剪枝就少,搜索量接近纯 Minimax,慢到没法用。
我用的排序策略很简单粗暴,但效果很好:
- 吃子走法排前面,被吃棋子价值高的排更前面。
- 历史得分高的走法排前面——一个走法如果之前在搜索中让局面变好,下次优先看它。
- 其余走法按生成顺序排列。
吃子排在前面这个策略尤其关键。因为大多数局面里,吃掉对方大子的走法往往是比较强的,先搜到好走法就能更早触发剪枝。实测下来,同一个深度,排序前后的耗时差距能达到三到五倍。
不要小看这个细节。同样深度 4,排序好和排序差的 AI,体验是一个快准狠,一个又慢又钝。
3.5 迭代加深:让电脑“先快想一步,再慢慢想四步”
迭代加深的思路更符合人的思考习惯:先算深度 1,马上得到一步棋;如果时间还有富余,再算深度 2;再算深度 3……直到超出设定时间限制,就返回上一次完整搜索的结果。
这样做有两个好处。一是用户体验好,电脑每一步能在一个可控时间内(比如 1 秒内)给出回应,不会突然卡住几秒不动。二是上一层搜索得到的最佳走法可以拿来排序下一层,搜索效率会进一步提升。
我的实现是在主循环里不断调用带指定深度的 minimax,每次记录最佳走法,一旦超出期限就退出循环,用上一次的 bestMove 作为最终结果。
4. 三种对战模式的统一架构
4.1 玩家类型枚举:一个字段控制全部模式
对弈循环我前面提过,玩家类型是核心变量。我定义了一个枚举:
java复制public enum PlayerType {
HUMAN, // 人类玩家
AI // 电脑玩家
}
然后游戏主类里存两个字段:redPlayer 和 blackPlayer,分别代表红方和黑方的类型。对弈循环启动后,根据当前轮到哪方,看对应的 PlayerType 是 HUMAN 还是 AI,决定走法来源。
三种模式切换,其实就是给这两个字段赋不同的值:
java复制// 人机对战:红方人类,黑方 AI
GameMode.HUMAN_VS_AI;
// 人人对弈:双方人类
GameMode.HUMAN_VS_HUMAN;
// 机机对弈:双方 AI
GameMode.AI_VS_AI;
这样设计不仅自然,而且为未来的网络对战留了口子——只要再增加一个 REMOTE 枚举值,把走法来源换成网络消息就行。后面如果要做联机功能,架构不需要动。
4.2 线程池调度:AI 思考不能卡住 UI
做 Swing 界面时第一个大坑就是:AI 思考放在事件分发线程里,一搜索,界面整个冻住,转圈都不转,像死机了一样。直到搜索完才恢复。
正确做法是把 AI 搜索放到后台线程里跑。Java 里最方便的是 SwingWorker,它可以在后台执行耗时任务,并在任务完成后回到事件线程更新界面。
java复制SwingWorker<Move, Void> aiWorker = new SwingWorker<>() {
@Override
protected Move doInBackground() {
return engine.findBestMove(board, currentSide, 4);
}
@Override
protected void done() {
try {
Move bestMove = get();
game.playMove(bestMove);
} catch (Exception e) {
e.printStackTrace();
}
}
};
aiWorker.execute();
机机对战模式下,整个对局就是一个循环:红方 AI 在后台搜索,返回走法后执行,然后立刻调度黑方 AI 开始搜索。中间可以加一个几百毫秒的延迟,模拟“双方在思考”,不然两个 AI 秒下完一盘棋,观战感全无。
4.3 机机对战:不只是“看着玩”,更是调参利器
机机对战一开始只是我的一个实验功能,做着做着发现它太有用了。每一次修改评估函数或者搜索策略,我就能让两个 AI 自己下 20 盘,统计胜负比例,判断改动到底有没有效果。
为了统计方便,我加了命令行无界面模式。这种模式下不创建任何 Swing 组件,双方 PlayerType 都是 AI,每一步走完往控制台输出一个棋谱,最后统计结果。跑 20 盘棋大概也就是十几秒的事,比人肉对局验证高效太多。
这个功能让我意识到:做 AI 项目,能自动对抗是刚需。你不可能靠肉眼在一盘棋里看出某个参数调整的效果,得靠多盘统计。
5. 界面交互:Swing 其实完全够用
5.1 绘制棋盘与棋子
Swing 画棋盘用 JPanel 的 paintComponent 重写即可。网格线、九宫斜线、河界文字都可以用 Graphics2D 画出来。棋子我用了实心圆加文字:
java复制@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 画网格线
for (int i = 0; i < 10; i++) {
g.drawLine(MARGIN, MARGIN + i * CELL_SIZE,
MARGIN + 8 * CELL_SIZE, MARGIN + i * CELL_SIZE);
}
// 画棋子
for (int x = 0; x < 9; x++) {
for (int y = 0; y < 10; y++) {
drawPiece(g, board[y][x], x, y);
}
}
}
棋子文字用中文显示,红方用红色,黑方用黑色。棋盘上方的状态栏显示当前轮到谁走、是否处于将军状态、对战模式名称。
5.2 走法合法性提示与选中状态
人类玩家操作时,点击一个己方棋子,程序先判断它是否有合法走法;如果有,就高亮该棋子并显示出所有可走的位置(用圆圈标记)。再点击一个可走位置就执行走子。
这里有个交互细节值得注意:当你选中一个棋子,然后点击的目标位置不合法时,应该取消当前选中状态还是切换选择?我的处理是:如果点击的是己方另一个棋子,就切换选中;如果点击的是空位或敌方棋子但位置不合法,就取消选中。这种交互逻辑贴近常见的象棋软件,用户上手零成本。
5.3 悔棋与棋谱记录
悔棋功能我刚开始做的时候以为很简单,结果被坑了一把。如果只是记录每一步的移动起点和终点,是不够的,因为中间可能发生了吃子、兵到终点升变(中国象棋没有升变但有可能被吃掉后要恢复)、上一步是什么玩家等信息。我最后用了一个数据结构:
java复制public class MoveRecord {
int fromX, fromY;
int toX, toY;
int capturedPiece; // 被吃掉的棋子类型
int movedPiece; // 移动的棋子类型
PlayerType player; // 哪一方走的
}
悔棋时从栈顶弹出 MoveRecord,把 movedPiece 挪回原地,capturedPiece 恢复,玩家位置切换回上一步。有了这个记录,做棋谱列表、导出 PGN 格式、重放功能都顺理成章。
6. 实测调参:我在让 AI 变强的过程中踩过的坑
6.1 为什么 AI 有时走出“送吃”的臭棋
第一次把完整 AI 跑起来后,我下了一盘,结果发现 AI 在深度 4 的情况下还是会走出车被炮打这种低级送吃。我很困惑,因为如果搜索深度足够,它明明应该能看到下一层被吃回来。
排查之后发现问题出在搜索的着法生成上。我在终局判断时只判断了“将死”和“困毙”,没有在搜索的中间层处理“将军”状态。将军情况下的合法走法必须全部覆盖,否则 AI 只考虑吃子方案,没有把“被将军时必须应将”这个条件完全贯彻。
修复方式是:在搜索的每层递归里,生成本方所有合法走法后,先全部过滤一遍,保证不会被将军。这样 AI 才能保证每一步都“有棋可下”,也才能真正避免送吃。修复后,深度 4 的 AI 基本不会主动送大子。
6.2 无限循环与重复局面
另一个让我头疼很久的问题是 AI 会在优势局面下疯狂走马,来回踱步,就是不对对方造成任何实际威胁。原因是搜索深度有限,它只知道“走马不吃亏”,但评估分数没有对“重复局面”做惩罚,它就永远在重复那几步。
我的解决方案有两层:
第一层,加入重复局面检测。用 HashMap 记录每个局面出现的次数,如果同一个局面出现了三次,评估时会打上一个明显低分,逼迫 AI 换个走法。
第二层,加入简单的不耐烦策略。如果 AI 连续很多步都在同一个区域内走来走去,降低这类走法的排序优先级。
这个处理和不和你对抗没关系,纯粹是让 AI 看上去更像一个“有目的性”的棋手,而不是一个只会刷评估分数的循环机器。
6.3 深度和时间的取舍
AI 搜索深度不是越大越好。我的电脑上深度 4 时,一步棋大概需要 300 到 800 毫秒;深度 5 时就要 2 到 4 秒。如果电脑配置一般,深度 5 会明显卡。但深度 5 的棋力提升是比较明显的,能看出更好的防守和利用位置优势。
我做了一个调节滑块,让用户选择“快速棋”(深度 3)、“标准”(深度 4)、“困难”(深度 5),甚至“大师”(深度 6,但要等很久)。这样一来,不同配置、不同水平的用户都能找到合适自己的模式。这个参数化设计也让我在测试时非常灵活,不用每次改代码。
6.4 调调试 AI 的“上帝视角”技巧
调 AI 最有用的工具不是调试器,而是日志。我给搜索引擎写了一个选项,开启后每次搜索都会打印出前几步的所有走法、评估分数、搜索节点数、剪枝次数。这样我能非常直观地看到:AI 认为哪一步最好,为什么它放弃了某一步,是不是评估函数有 bug,或者剪枝剪掉了一个不该剪的分支。
尤其是遇到“AI 为什么走这一步”的疑问时,把日志一拉,看它前三步候选走法的分数排序,基本上立刻就能定位问题。这个习惯后来一直保留在项目里,写任何算法类代码我都先打日志再优化。
6.5 机机对战的调参实验
调出“机机对弈”模式后,我做了大量参数对比实验:
- 评估函数里马的价值从 400 调到 450,红黑方胜率的变化。
- 兵过河加分从 120 调到 200,AI 是否更积极过河。
- 搜索深度从 4 调到 5,胜率是否大幅提升。
结论是:评估函数微调对棋力的影响,很多时候不如搜索深度提升明显。深度 5 打深度 4,胜率能到七成以上。而评估函数的小改动胜负大概五五开。这也说明棋力提升的下限其实在搜索,上限才是评估。
这个结论对我后续做面试讲解特别有用,面试官问我“AI 棋力怎么提升”时,我就有了真实的数据支撑,而不是假装什么都会。
7. 面试价值和后续扩展
7.1 在简历上怎么讲、面试怎么答
这个项目写在简历上,核心描述可以这样写:
“基于纯 Java 实现的中国象棋游戏,支持人机对战、人人对弈、机机对弈三种模式。自研 AI 引擎采用 Minimax 搜索算法,结合 Alpha-Beta 剪枝优化,通过位置价值表、着法排序和迭代加深提升搜索效率和棋力。使用 Swing 构建界面,通过 SwingWorker 将搜索任务异步化,避免界面卡顿,并实现了悔棋、棋谱记录等功能。”
面试官大概率会围绕几个问题深挖:
- Minimax 是怎么实现的?回答时讲清楚最大化方和最小化方的交替逻辑。
- Alpha-Beta 剪枝的触发条件是什么?能减少多少计算量?
- 评估函数怎么设计的?为什么这样给棋子打分?
- SwingWorker 解决了什么问题?能不能对比不用它的情况?
- 三种模式是怎么用统一架构实现的?
这些问题只要你真的写完了项目,几乎都能对答如流。比背八股文扎实多了。
7.2 还能怎么扩展:从传统 AI 到大模型辅助
这个项目做完之后,扩展空间也很大。我列几个我认真考虑过的方向:
- 网络对战:引入 Socket 或 WebSocket,把 PlayerType 加一个 REMOTE,就能实现联机。
- 棋谱导入导出:中国象棋有标准的 XQF 等格式,做出来就是一个完整的象棋工具。
- 残局库:预设一些经典残局,AI 在残局阶段直接查表走棋。
- 对接大模型:结合 Spring AI 这类库,让大模型在开局阶段提供策略分析,或者对棋局进行实时解说。
最后这一点尤其值得提:很多人以为“带 AI”就一定要接大模型,但传统棋类项目里,搜索算法和评估函数永远是棋力的根基。大模型更适合做辅助性的分析和自然语言交互,比如解说棋局、解释为什么走这步、分析优劣。把两者结合,才是完整的“带 AI 功能”。
我自己做完这一版之后,最深的体会是:从纯命令行版到有界面、有 AI、有三种模式,每一步看起来都不大,但合在一起就是一个能拿得出手的真实项目。如果你也想练手,建议先从命令行版开始,规则和 AI 引擎跑通了,再上状态机和界面。界面是锦上添花,骨架永远是数据结构加搜索算法。这个顺序反过来,很容易被一堆绘制代码拖住,最后象棋逻辑反而写得很糙。
