1. 项目缘起与整体设计思路
国庆七天假期,别人堵在高速上,我把自己关在书房里,用 Codex 辅助写代码,从零搓出了一款完整的《中国象棋》应用,并且顺利通过了 App Store 的审核上架。这件事听起来有点疯狂,但实际做下来,我发现路径比想象中清晰得多。这篇文章不是炫耀贴,而是把我这七天的完整思路、技术选型、踩过的坑、以及最终上架的流程,原原本本地拆开讲一遍。如果你也想利用一个长假做出一个能上架的完整作品,或者你对中国象棋这类棋类应用的实现细节感兴趣,这篇内容应该能给你省下不少试错时间。
先说清楚这个项目到底做了什么。核心功能包括:完整的中国象棋规则引擎(走子合法性校验、将军判定、将死判定、困毙判定)、人机对战 AI(基于极小化极大搜索加 Alpha-Beta 剪枝)、本地双人对战模式、棋谱记录与回放、悔棋与重开、以及适配 iPhone 和 iPad 的响应式棋盘界面。整个项目从零开始,没有使用任何现成的棋类 SDK,所有逻辑都是自己写的。技术栈方面,我选择了 SwiftUI 做界面、Swift 做核心逻辑,AI 部分用纯 Swift 实现,没有引入 C++ 或第三方引擎。之所以这么选,是因为我希望整个项目保持单一语言、单一工程,减少跨语言调用的复杂度,也方便 Codex 在补全代码时保持上下文一致性。
为什么用 Codex 而不是完全手写?坦白说,中国象棋的规则细节非常琐碎,比如“马走日”时的蹩马腿判定、“象走田”时的塞象眼判定、炮的隔子吃子、兵卒过河后的横向移动、将帅不能照面等等。这些规则如果纯手写,光是边界条件就能写一整天。Codex 的优势在于,当你把规则用自然语言描述清楚后,它能快速生成结构化的校验代码,我再逐条 review 和修正。这比从零敲键盘快得多,而且能减少遗漏。但要注意,Codex 生成的代码不能直接信,尤其是棋类规则这种强逻辑场景,必须自己写测试用例逐条验证。
整个项目的架构我分成了四层:最底层是棋盘模型层,负责存储棋盘状态、棋子位置、走子历史;第二层是规则引擎层,负责合法性校验、将军检测、终局判定;第三层是 AI 层,负责搜索和评估;最上层是 UI 层,负责渲染和交互。分层的好处是每一层可以独立测试,AI 层可以脱离 UI 单独跑对局测试,规则引擎可以用单元测试覆盖所有棋子的走法。这个架构不是一开始就想好的,而是我在第一天写代码时发现规则和 UI 混在一起后,果断重构出来的。事实证明这个决定非常正确,后面调试 AI 时,我可以直接让两个 AI 对弈几千局来验证规则引擎的稳定性。
2. 核心细节解析与实操要点
2.1 棋盘表示与数据结构选型
棋盘表示是棋类程序的地基。我试过三种方案:二维数组、一维数组、以及位棋盘。最终选择了 9x10 的二维数组,每个元素是一个可选的棋子结构体。为什么不用位棋盘?位棋盘在国象和将棋里很常见,因为它可以用位运算加速走法生成,但中国象棋的棋盘是 9 列 10 行,且棋子类型和走法规则与国象差异很大,位棋盘的收益在这个规模下并不明显。二维数组的可读性更好,调试时直接打印棋盘就能看懂,Codex 生成代码时也不容易搞错索引。
棋子结构体我定义为包含类型和颜色两个字段。类型用枚举表示:将、士、象、马、车、炮、兵。颜色用枚举表示:红、黑。这里有个细节,中国象棋里红方和黑方的棋子名称不同,比如红方是“帅”“仕”“相”“兵”,黑方是“将”“士”“象”“卒”。我在 UI 层做了名称映射,但逻辑层统一用类型枚举,避免逻辑判断时还要区分颜色和名称的组合。
坐标系统我用的是 (row, col),row 从 0 到 9,col 从 0 到 8。红方在底部,黑方在顶部。这个选择很重要,因为后续所有走法生成、将军检测都依赖坐标方向。比如红方兵向前是 row 减一,黑方卒向前是 row 加一。如果坐标方向搞反了,整个规则引擎都会错乱。我在第一天就定死了这个约定,并且在代码注释里写清楚,后面 Codex 生成代码时也反复强调这个约定,避免它搞混。
2.2 走法生成与合法性校验的实现细节
走法生成是规则引擎的核心。每种棋子的走法我都写了一个独立的函数,输入是当前棋盘和棋子位置,输出是所有可能的落子位置列表。这里的关键是“伪合法走法”和“合法走法”的区分。伪合法走法只考虑棋子本身的移动规则,比如马走日、象走田,但不考虑走完之后己方将帅是否被将军。合法走法是在伪合法走法的基础上,模拟走子后检测己方将帅是否被攻击,如果被攻击则过滤掉。
为什么要分两步?因为如果每次生成走法都做完整的将军检测,性能会很差,尤其是 AI 搜索时需要频繁生成走法。分两步后,AI 搜索时可以用伪合法走法做初步筛选,只在必要时做完整校验。实测下来,这个优化让 AI 的搜索深度提升了至少两层。
具体到每种棋子的走法,有几个容易出错的点。马的蹩马腿判定:马走日字,但如果马腿位置有棋子,则不能往那个方向走。马腿位置是马当前位置向目标方向偏移一格的位置。比如马从 (r,c) 走到 (r-2,c-1),马腿是 (r-1,c)。这个判定必须精确,否则马可以穿过棋子。象的塞象眼判定类似,象走田字,象眼是象当前位置到目标位置的中点。如果象眼有棋子,则不能走。炮的走法最特殊:移动时和车一样,但吃子时必须隔一个棋子。这意味着炮的走法生成要分两种情况:不吃子时,沿直线遇到第一个棋子前的所有空位都是合法落点;吃子时,沿直线遇到第一个棋子后,继续找第二个棋子,如果第二个棋子是敌方,则可以吃。
兵卒的走法也有细节。红方兵过河前只能向前,过河后可以向前或横向移动。黑方卒同理,方向相反。过河的判定是红方兵 row <= 4,黑方卒 row >= 5。这个边界条件我一开始写错了,导致兵在河界上时行为异常,后来用单元测试才发现。
2.3 将军检测与终局判定的实现
将军检测的逻辑是:找到己方将帅的位置,然后检查敌方所有棋子是否能攻击到该位置。这里有个性能陷阱,如果每次都遍历敌方所有棋子生成走法,再检查是否能打到将帅,效率很低。我的优化是反向检测:从将帅位置出发,检查各个方向上是否有敌方棋子能攻击到。比如检查同一列上是否有敌方车或炮,检查马的位置是否有敌方马,等等。这个反向检测只需要检查有限的方向和位置,比正向生成所有走法快得多。
将死判定是:当前玩家所有合法走法都无法解除将军状态。困毙判定是:当前玩家所有合法走法都无法执行,但并未被将军。这两个判定都需要遍历当前玩家的所有合法走法,如果为空,则根据是否被将军来区分将死和困毙。中国象棋里困毙也算输,这点和国际象棋不同,国际象棋里困毙是和棋。这个规则差异我在实现时特意确认过,避免搞错。
还有一个特殊规则:将帅不能照面。即同一列上,如果双方将帅之间没有其他棋子,则走成这种局面的一方判负。这个规则在走法生成时就要处理,即任何导致将帅照面的走法都是非法的。我的实现是在合法性校验的最后一步,检查走子后双方将帅是否在同一列且中间无棋子,如果是则过滤掉该走法。
3. 实操过程与核心环节实现
3.1 第一天到第二天:棋盘模型与规则引擎
第一天上午我花了两个小时搭好 Xcode 工程,选择 SwiftUI 作为 UI 框架,最低支持 iOS 16。然后开始写棋盘模型。棋盘模型的核心是一个 9x10 的二维数组,以及一个走子历史栈。走子历史栈用于悔棋和棋谱记录,每个元素记录走子前后的棋盘状态或者走子信息。我选择记录走子信息而不是完整棋盘快照,因为完整快照占内存,而走子信息可以反向撤销。
走子信息的结构体包含:起始位置、目标位置、被吃掉的棋子(如果有)、以及一些状态标记(比如是否触发将军)。悔棋时,从历史栈弹出最后一条记录,将目标位置的棋子移回起始位置,如果之前有被吃的棋子则恢复。这个逻辑看起来简单,但有一个坑:如果走子导致兵卒过河,悔棋时需要恢复过河前的状态。我的做法是不在棋子结构体里存储“是否过河”的状态,而是每次根据位置动态判断,这样悔棋时不需要额外恢复状态。
规则引擎的单元测试我写了大概 80 个用例,覆盖每种棋子的基本走法、边界情况、以及特殊规则。比如马的蹩马腿,我写了四个方向的测试;炮的隔子吃子,我写了隔一个子、隔两个子、无子可隔等情况;将帅照面,我写了同列无子、同列有子、以及走子后导致照面的情况。这些测试用例是我用 Codex 生成初版后,自己逐条补充和修正的。实测下来,80 个用例能覆盖 95% 以上的规则场景,剩下的靠 AI 对弈来发现。
3.2 第三天到第四天:AI 搜索与评估函数
AI 部分我选择了极小化极大搜索加 Alpha-Beta 剪枝。这是棋类 AI 的经典方案,实现起来不复杂,效果也够用。搜索深度我设为 4 层,加上吃子延伸搜索,实际搜索深度能达到 6 到 8 层。在 iPhone 上,这个深度的搜索时间大约在 0.5 到 2 秒之间,取决于局面复杂度。我试过把深度调到 5 层,搜索时间会增加到 5 秒以上,用户体验不好,所以最终定在 4 层。
评估函数是 AI 强弱的关键。我用的是子力价值加位置价值的方案。子力价值方面,车 900 分、马 400 分、炮 450 分、象 200 分、士 200 分、兵 100 分、将 10000 分。这些分值不是随便定的,而是参考了常见象棋引擎的取值,并根据实测对弈结果做了微调。比如炮的分值我一开始设 400,后来发现炮在中局威力很大,调到 450 后 AI 的进攻性明显增强。
位置价值方面,我给每个棋子的每个可能位置都设了一个分值表。比如马在中心位置加分,在边角减分;兵过河后加分,越靠近对方九宫加分越多;车在开放线上加分。这些分值表是我手动调的,调了大概三个晚上,让 AI 对弈几千局,观察哪些局面下 AI 走法不合理,然后针对性调整。这个过程很枯燥,但效果显著。调整前 AI 经常用马换炮亏子,调整后 AI 会主动保护炮、用马换象等。
Alpha-Beta 剪枝的实现有个细节:走法排序。如果先搜索好的走法,剪枝效率会高很多。我的走法排序策略是:吃子走法优先,吃子价值高的优先,其次是将军走法,最后是普通走法。这个排序让剪枝效率提升了大约 40%,搜索同样深度的时间明显缩短。
3.3 第五天到第六天:UI 界面与交互
UI 部分我用 SwiftUI 实现。棋盘是一个 9x10 的网格,每个格子是一个可点击的区域。棋子用圆形加文字表示,红方用红色,黑方用黑色。选中棋子时高亮显示,并显示所有合法落点。走子时用动画过渡,吃子时被吃的棋子淡出。这些交互细节看起来简单,但实现起来有不少坑。
第一个坑是棋盘尺寸适配。iPhone 和 iPad 的屏幕比例不同,棋盘需要按比例缩放。我的做法是用 GeometryReader 获取可用空间,然后取宽高比的最小值来计算格子大小,保证棋盘完整显示且不变形。第二个坑是点击区域的判定。棋子的点击区域不能只是棋子本身,而应该是整个格子,否则用户点击棋子边缘时容易点不中。我把点击区域设为整个格子,并在视觉上让棋子略小于格子,这样既好点又好看。
第三个坑是动画性能。走子动画如果用 SwiftUI 的 withAnimation,在低端设备上会有卡顿。我的优化是减少动画的复杂度,只对位置变化做动画,不对透明度做动画,并且把动画时长控制在 0.2 秒以内。实测下来,iPhone 12 及以上机型完全流畅,iPhone SE 二代也基本流畅。
棋谱记录与回放功能我用了走子历史栈来实现。回放时,从初始棋盘开始,逐步应用历史栈中的走子信息,每步之间加一个延时。这个功能实现起来不难,但要注意回放时的 UI 状态管理,比如回放过程中要禁用用户交互,回放结束后要恢复。我用了一个状态枚举来管理:正常对弈、AI 思考中、回放中、已结束。每个状态下 UI 的行为不同,这个状态机让代码清晰了很多。
3.4 第七天:上架准备与审核通过
第七天我主要做上架准备。首先是应用图标,我用 Sketch 画了一个简约的象棋棋子图标,红色圆形加“帅”字。然后是截图,我截了六张图:主界面、对弈中、AI 思考中、悔棋确认、棋谱回放、设置界面。截图要符合 App Store 的尺寸要求,iPhone 和 iPad 各一套。接着是应用描述和关键词,描述里写清楚功能特点,关键词选了“中国象棋”“象棋”“棋类”“AI 对战”等。
提交审核前,我特意检查了几个容易被拒的点。第一,应用不能有崩溃,我跑了 100 局 AI 对弈测试,没有崩溃。第二,应用不能有占位内容,所有功能都是完整的。第三,隐私政策,我在应用里加了一个简单的隐私政策页面,说明不收集任何用户数据。第四,年龄分级,我选了 4+,因为象棋是益智游戏,没有暴力内容。
提交后第二天就通过了审核,比我预想的快。这里有个经验:审核速度和你提交的时间有关,月初提交通常比月底快,因为月底积压的申请多。另外,应用描述和截图要清晰说明功能,避免审核员因为看不懂而拒绝。
4. 常见问题与排查技巧实录
4.1 规则引擎常见 Bug 与排查方法
规则引擎最容易出的 Bug 是走法生成遗漏或多余。排查方法很简单:写一个函数,打印出某个局面下某个棋子的所有合法走法,然后手动对照规则检查。我遇到过几个典型 Bug。第一个是马的蹩马腿判定方向搞反了,导致马可以穿过棋子。排查时我打印了马的所有走法,发现有一个方向没有检查马腿,修正后正常。第二个是炮的吃子逻辑,隔一个子后没有继续找第二个子,导致炮可以吃任意距离的棋子。排查时我构造了一个炮和两个敌方棋子的局面,发现炮直接吃了第一个棋子,修正后正常。
第三个是将帅照面判定遗漏。这个 Bug 很隐蔽,因为大多数局面下不会触发。我是用 AI 对弈测试时发现的,两个 AI 对弈了几百局后,出现了一局双方将帅照面但游戏没有结束的情况。排查后发现是走法生成时没有检查照面,修正后正常。这个经验告诉我,规则引擎的测试不能只靠单元测试,还要靠大量对弈来发现边缘情况。
4.2 AI 搜索性能优化与调试
AI 搜索的性能问题通常表现为思考时间过长。我遇到过几次思考时间超过 10 秒的情况,排查后发现是搜索深度太深或者走法排序不合理。解决方法有两个:一是限制搜索深度,根据局面复杂度动态调整;二是优化走法排序,让好的走法先搜索。我还加了一个时间限制,如果搜索时间超过 3 秒,就强制返回当前最优走法,避免用户等待过久。
AI 走法不合理的调试比较麻烦,因为很难直观判断哪一步不好。我的方法是让 AI 和自己对弈,然后回放棋谱,找出 AI 走法明显有问题的局面,分析评估函数在那些局面下的打分,然后调整分值表。这个过程需要耐心,我调了大概三个晚上,AI 的棋力从“乱走”提升到“能赢普通玩家”的水平。
4.3 上架审核常见问题速查
| 问题类型 | 具体表现 | 解决方法 |
|---|---|---|
| 崩溃 | 审核时应用闪退 | 提交前跑大量对弈测试,确保无崩溃 |
| 占位内容 | 某些页面是空的或显示“敬请期待” | 所有功能必须完整,不能有占位 |
| 隐私政策 | 没有隐私政策页面 | 加一个简单的隐私政策页面,说明不收集数据 |
| 截图不符 | 截图尺寸或内容不符合要求 | 按 App Store 要求截取,内容清晰展示功能 |
| 描述不清 | 应用描述模糊,审核员看不懂 | 描述里写清楚功能特点和使用方式 |
| 年龄分级 | 分级选择不当 | 象棋类选 4+ 即可,无暴力内容 |
提示:提交审核前,最好在真机上跑一遍完整流程,包括首次启动、对弈、悔棋、回放、设置等,确保没有遗漏的崩溃点。
4.4 实操心得与避坑技巧
第一个心得:Codex 生成的代码一定要逐行 review,尤其是规则引擎部分。我遇到过 Codex 生成的走法生成代码里,马的走法少了一个方向,导致马不能往某个方向走。这个 Bug 很隐蔽,因为大多数时候马走其他方向没问题,只有特定局面才会触发。所以规则引擎的代码不能偷懒,必须自己理解每一行。
第二个心得:单元测试要覆盖边界情况。比如兵在河界上的走法、炮在底线上的走法、将帅在九宫角落的走法。这些边界情况最容易出 Bug,也最难通过手动测试发现。我写了 80 个单元测试用例,其中 30 个是边界情况,这些用例帮我发现了至少 5 个 Bug。
第三个心得:AI 的评估函数不要一开始就追求完美。先用一个简单的子力价值方案,让 AI 能走棋,然后再逐步调整。我一开始花了两天调评估函数,结果发现规则引擎还有 Bug,导致 AI 的走法被错误规则影响。后来我先确保规则引擎正确,再调 AI,效率高了很多。
第四个心得:上架前一定要在真机上测试。模拟器和真机的性能差异很大,尤其是 AI 搜索部分。我在模拟器上 AI 思考只要 0.5 秒,在真机上要 1.5 秒。如果不做真机测试,用户可能会觉得 AI 太慢。
第五个心得:应用图标和截图很重要。图标要简洁醒目,截图要展示核心功能。我的图标改了三版,第一版太复杂,第二版颜色太暗,第三版才满意。截图我截了六张,覆盖了主要功能,审核员一看就懂。
5. 后续扩展与个人体会
这个项目上架后,我收到了一些用户反馈,主要集中在两个方面:一是希望增加网络对战功能,二是希望 AI 难度可调。网络对战我暂时没做,因为涉及到服务器和匹配逻辑,工作量不小。AI 难度可调倒是很容易实现,只需要调整搜索深度和评估函数的随机性即可。我后来加了一个难度选择:简单模式搜索深度 2 层,普通模式 3 层,困难模式 4 层。这样不同水平的玩家都能找到合适的对手。
还有一个扩展方向是开局库。中国象棋有很多经典开局,比如中炮对屏风马、仙人指路等。如果 AI 在开局阶段能查开局库,而不是纯靠搜索,棋力会提升很多,而且走法更符合人类习惯。开局库的实现不难,就是把经典开局的走法序列存下来,AI 在开局阶段优先查库。我打算下个版本加上这个功能。
最后分享一个小技巧:如果你也想用 Codex 做项目,建议先把核心数据结构和接口定义好,然后再让 Codex 生成具体实现。这样 Codex 生成的代码更符合你的架构,减少后期重构的工作量。我在这个项目里就是这么做的,先定义了棋盘模型和规则引擎的接口,然后让 Codex 填充实现,效率很高,而且代码风格统一。
这个国庆七天,我不仅做出了一个能上架的应用,还重新梳理了中国象棋的规则细节和 AI 搜索的实现原理。如果你也有一个长假,不妨试试用 Codex 做一个完整的项目,过程会比你想的更有收获。