从规则、数据结构、核心算法、AI 到界面交互,逐层拆解 FiveInRow 五子棋的实现思路:落子校验、四向胜负、Renju 禁手、棋形评分 AI 与 Compose 点击落子流程。
本文从「规则 → 数据结构 → 核心算法 → AI → 界面交互」逐层拆解 FiveInRow 的实现思路。即使你不熟悉 Android 或 C,也能看懂五子棋程序是如何「下棋、判胜负、禁手、让 AI 落子」的。
五子棋的规则人人会,但写成程序,至少要稳定回答下面几类问题:
| 问题 | 典型场景 |
|---|---|
| 这步能下吗? | 点已有子的交叉点、越界、Renju 禁手 |
| 谁赢了? | 刚下的那步是否形成五连;Renju 下长连不算赢 |
| 怎么悔棋? | 撤销最近一步或人机各一步 |
| 电脑下哪? | 评估每个空位的攻防价值,选最优 |
| 界面怎么响应? | 手指点屏幕 → 换算成 (row, col) → 调引擎 |
FiveInRow 把这些逻辑集中在 C 引擎(engine/src/main/cpp/),Kotlin 只负责界面、存档和流程编排。好处是:规则只写一遍,换 UI 或换平台不必重写棋规。
整体数据流:
flowchart LR
A[手指点击棋盘] --> B[BoardCanvas 换算行列]
B --> C[GameViewModel]
C --> D[NativeBoardEngine]
D --> E[JNI]
E --> F[C 引擎 fir_place_stone 等]
F --> G[返回结果 / 棋盘快照]
G --> C
C --> H[Compose 重绘]
引擎核心结构定义在 engine_internal.h:
// Function/Android/engine/src/main/cpp/src/engine_internal.h
typedef struct {
int size;
int rule_mode;
int *board;
FiveInRowMove history[FIVEINROW_MAX_MOVES];
int history_count;
int current_player;
} FiveInRowEngine;
可以这样理解:
board:一维数组,长度 size × size,按行优先存每个交叉点
0 = 空,1 = 黑,2 = 白history:落子顺序表,悔棋时按栈顶往回清current_player:下一手该谁下(黑先)rule_mode:0 标准五子棋,1 Renju(黑棋禁手)例如 15×15 棋盘上,坐标 (row=7, col=8) 在数组中的下标是:
index = row * size + col = 7 * 15 + 8 = 113
重置棋盘时,用 memset 全部置空,并把 current_player 设回黑棋:
// Function/Android/engine/src/main/cpp/src/board.c
void fir_board_reset(FiveInRowEngine *e, int size) {
if (e == NULL || size <= 0 || size > FIVEINROW_MAX_BOARD_SIZE) {
return;
}
e->size = size;
memset(e->board, 0, (size_t)(size * size) * sizeof(int));
e->history_count = 0;
e->current_player = FIVEINROW_STONE_BLACK;
}
对外 API 是 fiveinrow_place_stone,内部由 fir_place_stone 实现。落子前按顺序检查:
越界? → 已有子? → 步数已满? → Renju 禁手? → 写入棋盘 → 记入 history → 换手
核心代码:
// Function/Android/engine/src/main/cpp/src/rules.c
int fir_place_stone(FiveInRowEngine *e, int row, int col) {
FiveInRowMove move;
if (e == NULL) {
return FIVEINROW_PLACE_OUT_OF_BOUNDS;
}
if (!fir_in_bounds(e, row, col)) {
return FIVEINROW_PLACE_OUT_OF_BOUNDS;
}
if (e->board[row * e->size + col] != FIVEINROW_STONE_EMPTY) {
return FIVEINROW_PLACE_OCCUPIED;
}
if (e->history_count >= FIVEINROW_MAX_MOVES) {
return FIVEINROW_PLACE_GAME_OVER;
}
if (fir_renju_is_forbidden(e, row, col)) {
return FIVEINROW_PLACE_FORBIDDEN;
}
e->board[row * e->size + col] = e->current_player;
move.row = row;
move.col = col;
move.player = e->current_player;
e->history[e->history_count++] = move;
e->current_player = fir_opponent(e->current_player);
return FIVEINROW_PLACE_OK;
}
返回值约定(见 types.h):
| 值 | 含义 | UI 层表现 |
|---|---|---|
| 0 | 成功 | 刷新棋盘,检查胜负 |
| 1 | 已有棋子 | Toast「此处已有棋子」 |
| 2 | 越界 | 不应出现(UI 已 clamp) |
| 3 | 对局结束 | 不应再落子 |
| 4 | Renju 禁手 | Toast「禁手,不能在此落子」 |
设计要点:合法性在引擎里一次性判完,ViewModel 只根据返回码决定提示语,不在 Kotlin 里重复写规则。
五子棋的赢法只有一种:在横、竖、两条斜线任一方向上连续五颗同色子。
实现上不需要遍历全盘——只需在刚落子的那一点,向四个方向数连子长度。
水平 → (0, 1)
垂直 ↓ (1, 0)
主斜 ↘ (1, 1)
副斜 ↙ (1, -1)
fir_count_dir 从 (row, col) 沿 (dr, dc) 方向数同色子个数(不含自身)。
对刚下的点 (row, col),每个方向:
total = 1 + 正向连子数 + 反向连子数
若 total >= 5 → 可能获胜
Renju 特殊规则:黑棋长连(超过 5 连)不算赢,代码里会 continue 跳过该方向:
// Function/Android/engine/src/main/cpp/src/rules.c
for (d = 0; d < 4; d++) {
int dr = dirs[d][0];
int dc = dirs[d][1];
int total = 1 + fir_count_dir(e, row, col, dr, dc, p)
+ fir_count_dir(e, row, col, -dr, -dc, p);
if (total >= 5) {
if (e->rule_mode == FIVEINROW_RULE_RENJU && p == FIVEINROW_STONE_BLACK && total > 5) {
continue;
}
若判定获胜,还会回填 五连坐标 out_cells[5][2],供 UI 高亮获胜棋子。
棋盘没有空位且无人五连 → fir_is_board_full 返回真,ViewModel 进入和棋状态。
标准五子棋黑白规则对称;Renju(连珠) 为平衡先手,规定 仅黑棋 在形成五连前不得下禁手。FiveInRow 实现了三类:
| 禁手 | 含义 |
|---|---|
| 长连 | 同一方向连续超过 5 颗黑子 |
| 三三 | 一步棋同时形成两个「活三」 |
| 四四 | 一步棋同时形成两个「四」 |
禁手不能只看当前盘面——要看 「如果黑棋落在这里,会形成什么」。
因此 fir_renju_simulate_black 会:
(row, col) 放一颗黑子// Function/Android/engine/src/main/cpp/src/renju.c
static int fir_renju_simulate_black(FiveInRowEngine *e, int row, int col) {
int forbidden;
e->board[row * e->size + col] = FIVEINROW_STONE_BLACK;
forbidden = fir_renju_check_placed(e, row, col);
e->board[row * e->size + col] = FIVEINROW_STONE_EMPTY;
return forbidden;
}
只有 当前行棋方是黑棋 且 规则为 Renju 时才检查:
// Function/Android/engine/src/main/cpp/src/renju.c
int fir_renju_is_forbidden(FiveInRowEngine *e, int row, int col) {
if (e == NULL || e->rule_mode != FIVEINROW_RULE_RENJU) {
return 0;
}
if (e->current_player != FIVEINROW_STONE_BLACK) {
return 0;
}
...
return fir_renju_simulate_black(e, row, col);
}
对每个方向,以落子点为中心取 9 个连续交叉点 组成线段 line[9],在窗口内识别棋形:
. . . . ● . . . .
0 1 2 3 4 5 6 7 8 ← index 4 是落子点
●●● 且两端至少一侧为空(如 _●●●_)若 open_threes >= 2 或 fours >= 2,则判为禁手。
人机模式下,若 AI 执黑且为 Renju,fir_pick_ai_move 会跳过禁手点;提示功能 fir_pick_hint 同样过滤,避免推荐非法着法。
FiveInRow 的 AI 不是神经网络,而是经典的 启发式搜索:对每个空位模拟落子,按棋形打分,取得分最高者。
fir_score_line 在某方向统计连子长度和 开口数(一端或两端是否为空):
| 棋形 | 大致分值 |
|---|---|
| 五连 | 100000 |
| 活四(4 连且至少 1 口) | 10000 |
| 活三(3 连且 2 口) | 1000 |
| 活二(2 连且 2 口) | 100 |
| 其他 | 连子数 |
// Function/Android/engine/src/main/cpp/src/ai.c
if (count >= 5) {
return 100000;
}
if (count == 4 && open >= 1) {
return 10000;
}
if (count == 3 && open >= 2) {
return 1000;
}
if (count == 2 && open >= 2) {
return 100;
}
return count;
四个方向分数相加,得到该点的 进攻分。
对空位 (r, c):
attack = 假设 AI 下在这里的得分
defend = 假设对手下在这里的得分(挡棋)
score = attack × 1.1 + defend - 距棋盘中心曼哈顿距离 × 0.5
attack × 1.1:略偏进攻,主动成五defend:对手在此下会很强 → AI 应优先占住| 难度 | 行为 |
|---|---|
| 入门 | 在最优解上加随机扰动;约 35% 概率随机选合法空位 |
| 普通 | 纯按 score 选最大 |
| 困难 | 额外加权 attack + defend,更重视双杀与必防点 |
// Function/Android/engine/src/main/cpp/src/ai.c
if (level == FIVEINROW_AI_EASY) {
score += fir_rand01() * 80.0;
} else if (level == FIVEINROW_AI_HARD) {
score += attack * 0.5 + defend * 0.5;
}
提示(fir_pick_hint)与 AI 共用 fir_evaluate_move,但只算当前玩家进攻分,不混入防守与随机。
每步落子写入 history[]。悔棋时 fir_undo_steps 从栈顶弹出,清空对应格子,并把 current_player 恢复为那步的落子方:
// Function/Android/engine/src/main/cpp/src/history.c
int fir_undo_steps(FiveInRowEngine *e, int steps) {
int i;
if (e == NULL || steps <= 0 || e->history_count < steps) {
return 0;
}
for (i = 0; i < steps; i++) {
FiveInRowMove last;
...
e->board[last.row * e->size + last.col] = FIVEINROW_STONE_EMPTY;
e->history_count--;
e->current_player = last.player;
}
return 1;
}
ViewModel 层约定:
BoardCanvas 用 Compose Canvas 画线画子。棋盘为正方形,n×n 交叉点均匀分布:
step = 画布宽度 / (n - 1)
col = round( touchX / step )
row = round( touchY / step )
// Function/Android/app/src/main/kotlin/com/sdtech/fiveinrow/ui/game/BoardCanvas.kt
detectTapGestures { offset ->
val n = gridSize.coerceAtLeast(2)
val stepPx = this.size.width / (n - 1)
val col = ((offset.x + stepPx / 2) / stepPx).toInt().coerceIn(0, gridSize - 1)
val row = ((offset.y + stepPx / 2) / stepPx).toInt().coerceIn(0, gridSize - 1)
onCellTap(row, col)
加 stepPx / 2 是为了让点击落在交叉点附近时归到正确的格点。
用户点击后,GameViewModel.onCellTap:
engine.placeStone(row, col)afterMove:判胜负 → 刷新 UI → 存档 → 调度 AI// Function/Android/app/src/main/kotlin/com/sdtech/fiveinrow/ui/game/GameViewModel.kt
private fun afterMove(row: Int, col: Int) {
val settings = internal.value.settings
val win = engine.checkWinAt(row, col)
if (win != null) {
finishGame(win.player, "五连达成", true, win.cells.toSet())
...
return
}
if (engine.isBoardFull()) {
finishGame(null, "棋盘已满", true)
...
return
}
refresh(modeFlow.value, settings, row to col)
persistIfPlaying()
maybeScheduleAiMove(settings)
}
当轮到 AI 时,scheduleAiMove 在协程里:
engine.pickAiMove 取点 → placeStone → 再走一遍 afterMove这样 胜负判定、音效、存档 对人机共用同一套路径,不会出现「人下完一套逻辑、AI 下另一套」的分叉。
进行中的对局通过 GameRepository 写入 DataStore,保存:
history 步序续局时 restoreGame 先 reset,再按 history 逐步 placeStone 回放,引擎状态与离开前一致。
| 模块 | 文件 | 职责 |
|---|---|---|
| 类型与错误码 | types.h |
棋子、规则、AI 难度、返回值 |
| 棋盘 | board.c |
重置、满盘检测 |
| 规则 | rules.c |
落子、四向胜负 |
| Renju | renju.c |
三三 / 四四 / 长连禁手 |
| AI | ai.c |
评分、选点、提示 |
| 历史 | history.c |
悔棋 |
| 演示 | demo.c |
加载示例中盘 |
| JNI | engine_jni.cpp |
C ↔ Kotlin |
| 域模型 | NativeBoardEngine.kt |
封装引擎为 GameEngine 接口 |
| 编排 | GameViewModel.kt |
状态机、AI 调度、存档 |
| 绘制 | BoardCanvas.kt |
棋盘渲染与点击 |
规则下沉引擎
胜负、禁手、AI 都在 C 层;UI 只展示结果,避免双份逻辑不同步。
刚落子点判胜负
不必全盘扫描,四方向数连子即可,复杂度 O(1) 每步。
Renju 用「模拟落子」
禁手是「下在这里会怎样」,临时改盘再恢复,比维护复杂棋形库更直观。
AI 用棋形分足够休闲
入门加随机、困难加强攻防权重,三档难度无需 Minimax 也能玩。
history 栈统一悔棋与存档
一步一记录,悔棋 pop、续局 replay,数据结构简单可靠。
ViewModel 管流程,不管棋规
点击 → 引擎 → 判终局 → 存档 → AI,一条链清晰可测。
想对照源码阅读,建议顺序:
Function/Android/engine/src/main/cpp/include/fiveinrow/types.h — 常量Function/Android/engine/src/main/cpp/src/rules.c — 落子与胜负Function/Android/engine/src/main/cpp/src/renju.c — 禁手Function/Android/engine/src/main/cpp/src/ai.c — AIFunction/Android/app/src/main/kotlin/com/sdtech/fiveinrow/ui/game/GameViewModel.kt — 产品流程API 索引见 Function/Android/docs/API.md。
若需了解工程目录与分层设计,请参阅 《工程架构介绍》。