中国象棋游戏开发教程
中国象棋比五子棋更适合作为“规则引擎”练习:棋盘不大,只有 10 x 9,但每种棋子的走法都不同,还要处理马腿、象眼、炮架、九宫、过河兵、将帅照面、不能送将等规则。
这次在游戏模块新增了一个本地中国象棋页面:/game/puzzle/chinese-chess。它支持双人对战和人机对战,AI 使用一层候选走法生成加启发式评分,适合作为网页棋类算法入门版本。
更新日期:2026-07-07。
一、核心数据结构
棋盘用二维数组表示:
type XiangqiSide = 'red' | 'black'
type PieceType =
| 'general'
| 'advisor'
| 'elephant'
| 'horse'
| 'chariot'
| 'cannon'
| 'soldier'
type XiangqiPiece = {
side: XiangqiSide
type: PieceType
}
type Board = Array<Array<XiangqiPiece | null>>
这里没有把棋子编码成字符串,是为了让规则函数更清楚。比如判断某个棋子是不是红方的马,只需要看:
piece.side === 'red' && piece.type === 'horse'
一次移动记录为:
type XiangqiMove = {
from: Point
to: Point
piece: XiangqiPiece
captured: XiangqiPiece | null
}
captured 会保留被吃掉的棋子,用来显示棋谱、判断是否吃到将帅,也方便后续做悔棋和复盘。
二、棋盘初始化
中国象棋棋盘有 10 行 9 路:
const rowCount = 10
const colCount = 9
黑方在上,红方在下。初始化时先放后排:
const backRank = [
'chariot',
'horse',
'elephant',
'advisor',
'general',
'advisor',
'elephant',
'horse',
'chariot',
]
再补炮和兵卒:
board[2][1] = createPiece('black', 'cannon')
board[2][7] = createPiece('black', 'cannon')
board[7][1] = createPiece('red', 'cannon')
board[7][7] = createPiece('red', 'cannon')
for (const c of [0, 2, 4, 6, 8]) {
board[3][c] = createPiece('black', 'soldier')
board[6][c] = createPiece('red', 'soldier')
}
这种初始化方式的好处是直观,和真实棋盘位置一一对应。
三、单个棋子的走法校验
第一层算法只判断“这枚棋子从 A 到 B 是否符合自身走法”,不关心是否会让己方被将。
车
车走直线,路径不能有棋子:
from.r === to.r || from.c === to.c
然后用 countPiecesBetween 统计中间棋子数:
countPiecesBetween(board, from, to) === 0
马
马走日字,但要判断马腿:
const leg =
absR === 2
? { r: from.r + Math.sign(dr), c: from.c }
: { r: from.r, c: from.c + Math.sign(dc) }
如果马腿位置有棋子,就不能走。
炮
炮最特别,分为不吃子和吃子两种:
const screenCount = countPiecesBetween(board, from, to)
return target ? screenCount === 1 : screenCount === 0
没有目标棋子时,炮像车一样直走;吃子时,中间必须刚好隔一个炮架。
相和士
相走田字,不能过河,还要判断象眼:
absR === 2 && absC === 2 && staysHome && !board[eye.r][eye.c]
士只能在九宫内斜走一步:
isInPalace(to, side) && absR === 1 && absC === 1
将帅
将帅只能在九宫内上下左右一步,但还有一个特殊规则:如果同一路上中间没有棋子,将帅可以直接照面。
target?.type === 'general' &&
from.c === to.c &&
countPiecesBetween(board, from, to) === 0
这条规则也会用于判断“将帅照面”是否合法。
兵卒
兵卒只能向前,过河后可以左右走,不能后退:
const movesForward = dc === 0 && dr === forward
const movesSideways = isAcrossRiver(from, side) && dr === 0 && absC === 1
四、合法走法过滤:不能送将
只校验棋子走法还不够。比如红方把中间棋子挪开,让黑将和红帅直接照面,这步棋在真实规则里是不允许的。
所以完整合法走法要分三步:
- 检查棋子自己的走法。
- 复制棋盘并应用这一步。
- 判断新局面里己方是否被将。
代码结构是:
function isLegalMove(board, from, to, side) {
const move = buildMove(board, from, to)
if (!move || !isLegalPieceMove(board, from, to, move.piece)) {
return false
}
const next = applyMove(board, move)
if (generalsFace(next)) {
return false
}
return !isGeneralInCheck(next, side)
}
这里的关键不是某一个棋子的规则,而是“试走一步,再验证王是否安全”。这是棋类程序里非常通用的模式,国际象棋、将棋、中国象棋都可以用。
五、将军检测
判断某一方是否被将,可以反过来想:
找到这一方的将帅,然后看对方任意棋子能不能按自身走法攻击到它。
伪代码如下:
function isGeneralInCheck(board, side) {
const general = findGeneral(board, side)
for (const enemyPiece of enemyPieces) {
if (isLegalPieceMove(board, enemyPiece.from, general, enemyPiece)) {
return true
}
}
return false
}
注意这里用的是 isLegalPieceMove,不是 isLegalMove。因为检测攻击范围时,只关心对方棋子能不能攻击到将帅,不需要再递归判断对方走完以后自己是不是安全。
六、生成全部合法走法
AI 和将死判断都需要生成某一方所有合法走法。最简单可靠的方式是暴力枚举:
for (const from of allBoardPoints) {
if (board[from.r][from.c]?.side !== side) continue
for (const to of allBoardPoints) {
if (isLegalMove(board, from, to, side)) {
moves.push(buildMove(board, from, to))
}
}
}
中国象棋只有 90 个交叉点,这种枚举方式在网页小游戏里完全够用。它的优势是实现简单、边界清晰,也方便后续接入更强的搜索算法。
七、胜负判断
当前实现有两种结束方式:
- 吃到对方将帅,直接胜利。
- 对方没有任何合法走法,并且正在被将,判定将死。
逻辑是:
const nextLegalMoves = generateLegalMoves(nextBoard, nextTurn)
if (nextLegalMoves.length === 0) {
if (isGeneralInCheck(nextBoard, nextTurn)) {
winner = currentSide
} else {
status = 'draw'
}
}
这一步会同时覆盖“被将死”和“无合法走法和棋”。
八、简单 AI 设计
这个版本的 AI 没有做深层搜索,而是使用一层候选走法评分:
生成黑方所有合法走法
-> 对每一步试走
-> 计算吃子价值
-> 判断是否将军或将死
-> 判断对方下一手最大反击
-> 加一点位置分
选择分数最高的一步
评估函数的核心权重:
const pieceValue = {
general: 10000,
chariot: 900,
cannon: 450,
horse: 420,
elephant: 220,
advisor: 210,
soldier: 120,
}
吃到高价值棋子会加分:
if (move.captured) {
score += pieceValue[move.captured.type] * 10
}
如果能将军,会额外加分:
if (isGeneralInCheck(next, opponent)) {
score += 520
}
如果对方下一手能吃到我方大子,则扣分:
const opponentBestCapture = opponentMoves.reduce(
(best, candidate) =>
Math.max(best, candidate.captured ? pieceValue[candidate.captured.type] : 0),
0,
)
score -= opponentBestCapture * 1.4
这不是最强 AI,但已经具备基础棋感:会吃子、会将军、会避免明显送大子,也会让兵卒过河往前压。
九、为什么不一开始就上极大极小搜索
中国象棋的分支数比较高,直接做深层搜索会遇到性能和剪枝问题。更稳的路线是:
- 先把走法生成写正确。
- 再写一层评分 AI。
- 然后引入
minimax或negamax。 - 最后加
alpha-beta剪枝、置换表和开局库。
本次页面把第 1 步和第 2 步做好,后续可以在 chooseAiMove 上继续升级。
十、页面交互
React 层只做三件事:
- 记录棋盘、回合、模式、历史和状态。
- 点击棋子时调用
isLegalMove判断。 - 人机模式下轮到黑方时调用
chooseAiMove。
人机触发逻辑很轻:
if (mode === 'ai' && turn === 'black') {
const aiMove = chooseAiMove(board, 'black')
playMove(aiMove, 'ai')
}
棋谱导出则把每一步的起点、终点、棋子和吃子信息保存成 JSON,方便以后扩展复盘模式。
总结
中国象棋网页游戏的难点不在棋盘 UI,而在规则引擎:
- 每个棋子先独立校验自身走法。
- 走法通过后必须试走并检查不能送将。
- 将军检测通过“对方是否能攻击到将帅”实现。
- AI 先从合法走法生成开始,再做评估函数。
- 未来增强 AI 时,继续在合法走法和评估函数之上叠搜索即可。
这套结构清晰、可维护,也很适合作为后续做更强棋类 AI 的基础。
