做这个项目的念头,最早不是来自什么高深的理论。当时我把 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 为什么不用对象来表示每个棋子
最开
