先交代一下背景。我最近想练练手,做一个Android小游戏。选来选去挑了五子棋,规则简单、边界清晰,但又足够撑起一个完整App的骨架——棋盘、交互、算法、状态管理全都能覆盖。正好我一直在用open claw这个AI编程工具,就想着干脆让AI全程辅助写这个项目,看它到底能帮到哪一步。这篇就把整个流程、核心代码逻辑、以及踩过的坑都记下来。
先说结论:open claw这类工具,配合一个需求边界明确的小游戏项目,是真的能出活的。但前提是你自己得懂逻辑、会提需求、能看懂代码。它是个效率放大器,不是替代品。下面进入正题。
1. 动手之前先想清楚:五子棋App的核心功能边界
很多人一上来就催AI写代码,结果写了半天发现根本不是自己想要的。我的习惯是,先用几分钟把项目拆清楚。五子棋看着简单,但你要是直接给open claw一句"帮我写个五子棋App",它大概率会给你一个用TextView铺棋盘的玩具,或者一个搞了网络对战但根本跑不通的半成品。
1.1 简易版不等于功能残缺,界定了四个核心模块
我给自己定的目标是:一个能在Android真机上运行的单机五子棋App,支持双人对战和人机对战两种模式。按这个目标,我拆出了四个模块:
- 棋盘数据模型:15x15的二维数组,负责记录每个位置的状态(空、黑子、白子)。
- 胜负判定算法:每落一子检查从该点出发的四个方向,看是否有五子连珠。
- AI落子逻辑:简易版不追求多智能,用评分函数遍历空位打分,取最高分落子。
- UI层:用自定义View + Canvas绘制棋盘和棋子,处理触摸事件。
这个拆分很关键。open claw在写代码时,上下文窗口有限,你一次性把所有需求倒给它,它很容易顾此失彼。把大问题拆成小模块,一个一个喂给它,质量的稳定性会高很多。
1.2 技术选型:为什么用原生View而不是Flutter或Unity
我的出发点很简单——这个项目是为了理解人机交互和博弈算法,不是研究跨平台框架。用原生Android + Java(后来为了省事,关键类也用了Kotlin),好处有三:
- 零依赖,不需要额外搭环境。
- Canvas绘制逻辑直白,方便看代码、调坐标。
- 触摸事件、生命周期管理这些Android基本功,正好借这个项目练一练。
同时,我让open claw生成的项目结构,也刻意保持了单Activity + 自定义View的极简结构。没有用复杂架构,因为简易版项目不需要过度设计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零部署open claw:环境准备与初始化配置
2.1 open claw是什么
如果你还没用过,简单说,open claw是一个跑在终端里的开源AI编程助手。你把需求用自然语言描述给它,它能基于你的代码仓库上下文,直接帮你写代码、改文件、执行命令。跟那些聊天框里给代码片段的工具有本质区别——它是直接操作你本地文件的。
我这边的使用流程是:在项目根目录启动open claw,它会读取当前目录的代码结构,然后我通过对话交代任务,它生成或修改文件,我再手动跑测试验证结果。
2.2 安装过程与容易卡住的三个点
安装本身不复杂,但有几个点值得注意。我在Linux环境下操作,如果你的系统不同,细节略有差异。
注意:open claw的运行需要Node.js环境,建议使用Node 18或更高版本。另外它需要调用大模型API,所以你要准备好一个可用的API Key,并在初始化时完成配置。
安装核心步骤如下:
bash复制# 1. 全局安装open claw
npm install -g open-claw
# 2. 进入你的项目目录
cd five-in-a-row
# 3. 初始化open claw(会引导配置模型和API Key)
open-claw init
初始化过程中,它一般会让你做几件事:选模型服务商、填API Key、设置工作目录。这个环境搭好之后,我实际踩过几个小坑:
- 第一个坑,初始化时目标目录不能为空。就是说你要先建好项目文件夹,里面随便放个README.md都行,否则init会提示目录不存在或不可写。
- 第二个坑,模型选择不是越贵越好。我试过用大杯模型跑小任务,响应时间多了好几倍,生成质量在高频小步迭代中差异不大。后来稳定用中端模型,性价比最高。
- 第三个坑,权限问题。open claw需要读写项目目录的权限,如果你发现它"没反应",先检查是不是文件权限导致的静默失败。
这些配置都搞定后,就可以正式开始对话了。
3. 过一遍完整实现:棋盘、胜负判定、AI算法加UI,一个都不少
这部分是整个项目的主菜。我不是直接甩代码,而是把每一个模块的设计意图讲清楚——这不光对你看懂有好处,对你怎么给open claw提需求也特别关键。
3.1 棋盘数据模型:15x15数组与坐标体系约定
标准的五子棋棋盘是15路。我让open claw先写了一个Board类,核心是一个二维数组:
java复制public class Board {
public static final int SIZE = 15;
public static final int EMPTY = 0;
public static final int BLACK = 1;
public static final int WHITE = 2;
private int[][] grid = new int[SIZE][SIZE];
public boolean placeStone(int row, int col, int player) {
if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) {
return false;
}
if (grid[row][col] != EMPTY) {
return false;
}
grid[row][col] = player;
return true;
}
public int getStone(int row, int col) {
return grid[row][col];
}
public void clear() {
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
grid[i][j] = EMPTY;
}
}
}
}
这里有个容易忽略的细节:坐标体系从0开始还是从1开始。UI层触摸事件拿到的是像素坐标,转成行列号时如果差一个1,棋子就全偏了。我用的约定是:内部逻辑全部使用0~14的行列号,UI绘制时再加偏移。这个约定在open claw生成的代码里如果没统一,就会出现棋子对不齐的情况。
3.2 胜负判定算法:四个方向从零构建逻辑
胜负判定,最直接的想法是每次落子后遍历整个棋盘检查有没有五连。但更高效的做法是只检查新落子点周边的四个方向:水平、垂直、主对角线、副对角线。我从这四个方向分别数连续同色棋子的数量,数到大于等于5就判胜。
java复制public class WinChecker {
private static final int[][] DIRECTIONS = {
{0, 1}, // 水平
{1, 0}, // 垂直
{1, 1}, // 主对角线
{1, -1} // 副对角线
};
public boolean checkWin(Board board, int row, int col, int player) {
for (int[] dir : DIRECTIONS) {
int count = 1;
// 正方向数
count += countInDirection(board, row, col, dir[0], dir[1], player);
// 反方向数
count += countInDirection(board, row, col, -dir[0], -dir[1], player);
if (count >= 5) {
return true;
}
}
return false;
}
private int countInDirection(Board board, int row, int col, int dr, int dc, int player) {
int count = 0;
int r = row + dr;
int c = col + dc;
while (r >= 0 && r < Board.SIZE && c >= 0 && c < Board.SIZE
&& board.getStone(r, c) == player) {
count++;
r += dr;
c += dc;
}
return count;
}
}
这代码看起来很简单,但AI第一次生成时大概率会有两个问题:一是只用单方向数,忘了反方向;二是数组越界保护写得不到位。open claw生成的版本里我第一次跑就发现,落子在边界时大概率会崩溃,调试时一眼就定位到了——它最外层的while循环条件里忘了加边界判断。
3.3 简易AI:评分函数与进攻防守权重
简易版的AI,我采用的是评分函数法。思路是对棋盘上每个空位打分,分数等于"如果我方落在这个位置的价值 + 防守权重 x 对方落在这个位置的价值"。每个空位的价值,通过考察以该点为中心的四条线上,形成的连子模式来估算。
具体来说,对每个空位,我向八个方向延伸,统计出如果在该位置放上某方棋子,能形成多少个连续的棋子,再根据连续棋子的数量分配分数:
| 连子数 | 分值 | 含义 |
|---|---|---|
| 1 | 10 | 单子,基本没威胁 |
| 2 | 100 | 活二,有发展潜力 |
| 3 | 1000 | 活三,对方必须防了 |
| 4 | 10000 | 冲四或活四,接近必胜 |
| 5 | 100000 | 直接赢 |
当然,真实实现会把死四、活三、眠三区分开,分数会更细致。简易版我就先统一按连子数给分。
java复制public class SimpleAI {
private static final int WIN_SCORE = 100000;
private static final int DEFEND_WEIGHT = 90; // 防守权重稍低于进攻
public int[] getBestMove(Board board, int aiPlayer) {
int opponent = (aiPlayer == Board.BLACK) ? Board.WHITE : Board.BLACK;
int maxScore = -1;
int bestRow = -1;
int bestCol = -1;
for (int r = 0; r < Board.SIZE; r++) {
for (int c = 0; c < Board.SIZE; c++) {
if (board.getStone(r, c) != Board.EMPTY) {
continue;
}
int attackScore = evaluatePoint(board, r, c, aiPlayer);
int defendScore = evaluatePoint(board, r, c, opponent);
int totalScore = attackScore + (defendScore * DEFEND_WEIGHT / 100);
if (totalScore > maxScore) {
maxScore = totalScore;
bestRow = r;
bestCol = c;
}
}
}
return new int[] { bestRow, bestCol };
}
private int evaluatePoint(Board board, int row, int col, int player) {
// 遍历四个方向,统计连子模式,返回该点分值
// 简化实现:对四个方向分别调用countInDirection,再加权求和
int score = 0;
int[][] dirs = { {0,1}, {1,0}, {1,1}, {1,-1} };
for (int[] dir : dirs) {
int count = 1;
count += countInLine(board, row, col, dir[0], dir[1], player);
count += countInLine(board, row, col, -dir[0], -dir[1], player);
score += scoreForCount(count);
}
return score;
}
private int scoreForCount(int count) {
if (count >= 5) return WIN_SCORE;
switch (count) {
case 4: return 10000;
case 3: return 1000;
case 2: return 100;
default: return 10;
}
}
private int countInLine(Board board, int row, int col, int dr, int dc, int player) {
// 与WinChecker类似,统计单方向连续同色棋子数
// 实现省略,与WinChecker同理
}
}
为什么防守权重要低于进攻? 这是个很微妙的点。如果防守权重设成100%,AI会变得过度保守,经常放弃自己的进攻机会去堵对手,导致局面陷入僵持。设成90%左右,AI会优先把握自己的必胜手,但同时也对对手的威胁保持警觉。这个值不是拍脑袋定的,网上很多开源五子棋项目的经验值都在85%~95%之间,我直接抄了这个区间。
3.4 UI层实现了:自定义View与Canvas绘制
UI层是open claw帮写代码时最容易跑偏的地方。我让它先画一个静态棋盘,再把触摸事件接上。最终的关键代码,是一个自定义View:
java复制public class BoardView extends View {
private Board board;
private Paint paint;
private float cellSize;
private float offset;
public BoardView(Context context) {
super(context);
paint = new Paint();
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBoardLines(canvas);
drawStones(canvas);
}
private void drawBoardLines(Canvas canvas) {
paint.setColor(Color.BLACK);
paint.setStrokeWidth(2);
// 计算格子大小和偏移
int size = Math.min(getWidth(), getHeight());
cellSize = (float) (size - 40) / (Board.SIZE - 1);
offset = (size - cellSize * (Board.SIZE - 1)) / 2;
for (int i = 0; i < Board.SIZE; i++) {
float y = offset + i * cellSize;
canvas.drawLine(offset, y, offset + cellSize * (Board.SIZE - 1), y, paint);
float x = offset + i * cellSize;
canvas.drawLine(x, offset, x, offset + cellSize * (Board.SIZE - 1), paint);
}
}
private void drawStones(Canvas canvas) {
for (int r = 0; r < Board.SIZE; r++) {
for (int c = 0; c < Board.SIZE; c++) {
int stone = board.getStone(r, c);
if (stone == Board.EMPTY) {
continue;
}
float cx = offset + c * cellSize;
float cy = offset + r * cellSize;
paint.setColor(stone == Board.BLACK ? Color.BLACK : Color.WHITE);
canvas.drawCircle(cx, cy, cellSize * 0.42f, paint);
// 加个细边框,避免白棋在浅色背景下看不清
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(cx, cy, cellSize * 0.42f, paint);
paint.setStyle(Paint.Style.FILL);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
float x = event.getX();
float y = event.getY();
int col = Math.round((x - offset) / cellSize);
int row = Math.round((y - offset) / cellSize);
if (listener != null) {
listener.onCellClicked(row, col);
}
}
return true;
}
}
坐标换算这个点,是最容易出bug的地方。 触点的像素坐标,先减去棋盘左上角的偏移量,再除以格子大小,四舍五入得到行列号。这里如果忘了先减offset,点击棋盘左边区域时会得到负数行列,然后AI在数组里一查就越界。我在调试时遇到了,后面那个坑会细说。
4. 从命令行到安卓工程:open claw生成项目文件的完整链路
4.1 分步对话:我向open claw交代了哪些核心指令
open claw不像对话框式的AI,可以在多轮对话中持续操作文件。我的方式是先让它搭骨架,再逐层填入逻辑。下面是几轮关键指令的思路,你可以直接参考:
- 第一轮,搭骨架:
请在当前目录初始化一个Android项目,包名为com.example.fiveinarow,语言用Java。先不要写具体逻辑,只创建标准的Android项目结构,包括MainActivity、AndroidManifest.xml和gradle文件。
- 第二轮,加核心类:
创建Board类,实现15x15棋盘数据模型,包含落子、查询、清空方法。坐标从0开始,越界或已有棋子时返回false。
- 第三轮,加AI:
创建SimpleAI类,实现基于评分函数的AI落子算法。防守权重设为进攻的90%。返回最佳落子的行列号。
- 第四轮,加View:
创建BoardView类继承自View,用Canvas绘制15x15棋盘和所有棋子。实现onTouchEvent,将触摸事件转换为行列号,并通过回调接口通知MainActivity。
- 第五轮,串联:
在MainActivity中,将BoardView、Board、WinChecker和SimpleAI整合起来。实现游戏状态管理:轮到谁落子、是否结束、重新开始。另外加一个人机对战的简单开始按钮。
每一轮之间,我会在本地跑一次编译,确保当前阶段的代码没问题再进入下一轮。这种小步迭代的方式,比一口气生成全部代码要稳得多。
4.2 为什么同样的需求,AI生成的东西差别很大
用了几次open claw后,我发现需求描述的颗粒度直接决定输出质量。你只说"画个棋盘",它可能给你画在Canvas上,也可能给你画在XML布局里。你明确说"用Canvas绘制,自定义View",它就知道底线在哪。
另外,上下文信息特别重要。第二轮的Board类我明确了坐标从0开始、越界返回false,这样后续AI生成WinChecker和SimpleAI时,就会自然沿用这个约定,不会出现坐标换算各写各的的情况。
4.3 Gradle与依赖管理:零依赖方案的坚持
简易版项目,我刻意让open claw不引入任何第三方库。Gradle配置里只有最基本的Android依赖。有人可能会说,用RecyclerView或者数据绑定库不好吗?但我的判断是,五子棋这种频繁重绘的小游戏,自定义View + invalidate() 就是最简单直接的方式。每落一子就调用invalidate()重绘整个棋盘,性能完全够用。
额外的库意味着额外的构建时间、额外的API学习成本,对一个小游戏来说是负资产。
5. 实测与翻车现场:AI生成的代码跑起来会踩哪些坑
这部分是文章的主体。我不光写每一步在做什么,还把每一步踩到的雷、以及怎么排查的都记录下来。open claw生成的代码不是不能用,而是需要你带着验证的心态去看。
5.1 运行时崩溃:数组越界
第一个崩溃出现在点击棋盘边界区域时。现象是点击后App闪退,Logcat里清清楚楚地报着:
code复制java.lang.ArrayIndexOutOfBoundsException: length=15; index=15
at com.example.fiveinarow.Board.getStone(Board.java:...)
排查过程是这样的:我先定位异常发生在getStone,说明行列号传进来了一个等于15的值。再看BoardView中的坐标换算,发现原来open claw生成的换算代码用的是 (int)((x - offset) / cellSize),而不是 Math.round(...)。这个差别很微妙。当点击的位置正好在最后一格的右半部分时,浮点除法的结果可能大于14.5,取整后变成15,越界。
修复方式很简单,把行列号做一次边界钳制:
java复制col = Math.max(0, Math.min(Board.SIZE - 1, col));
row = Math.max(0, Math.min(Board.SIZE - 1, row));
这个坑让我总结出一条经验:AI生成的坐标换算代码,边界条件往往写得不够严谨,你在验证时要把边界情况单独列出来测。左边界、右边界、上边界、下边界、四个角,各点一遍。
5.2 逻辑错误:只判了一个方向
第二个问题隐蔽得多。跟AI对局时,明明横着已经四子连珠了,却一直不判我赢。我翻game状态判断代码,才发现WinChecker里虽然DIRECTIONS数组定义了四个方向,但countInDirection的循环只有正方向,没有反向累计。
也就是说,落子在连子中间时能正确检测,落子在连子端点时就漏了。我立即在本地加了一个测试用例,手动构造棋盘数据:
code复制- 在(7,7)、(7,8)、(7,9)、(7,10)放四颗黑子
- 调用checkWin(board, 7, 11, BLACK) // 应返回true
结果返回false,问题复现。修复就是补上反向遍历,也就是我前面代码里写的那个版本。这个经历告诉我,AI生成算法类代码时,对方向、边界这类对称性的处理常常会偷懒,逻辑验证比形式验证更重要。
5.3 行为怪异:AI总是防守不进攻
第三类问题属于"代码能跑,但行为不对劲"。棋局中AI经常守着对手的棋子,却忽略了自己已经形成的活三活四。我看SimpleAI的评估函数,发现open claw生成的逻辑里,进攻分数算出来总是0。
原因出在评估函数里——它统计连子时,把当前待落子点也被算作了"已经放上棋子"。但因为该位置在棋盘上还是EMPTY,countInLine里调用getStone(b, r, c, player)取到的是EMPTY,跟player不相等,所以从第一步起就数不到任何连续棋子,所有方向的count都是1,进攻分数自然几乎为零。
这个修复比较绕,最直观的做法是在评估时,先临时把当前位置设为当前玩家,完成打分后再恢复为EMPTY:
java复制private int evaluatePoint(Board board, int row, int col, int player) {
board.placeStone(row, col, player);
int score = calculateScore(board, row, col, player);
board.setStone(row, col, Board.EMPTY); // 需要额外的set方法
return score;
}
如果Board类里没有setStone,让AI补一个即可。这种“临时置位-评估-恢复”的技巧,是我在实际编码中常用的模式,AI第一次往往是想不到的。
5.4 性能问题:看似没卡,但低端机上描画不流畅
本来15x15的棋盘,重绘不会卡。但我在低端测试机上跑时,发现落子时偶尔有肉眼可见的延迟。分析后发现,是onDraw里的双层for循环每次都会完整遍历棋盘,同时Paint对象在循环里频繁调用setColor和setStyle,导致GPU状态切换比较频繁。
优化方法有三个方向,我选了前两个一起做:
- 把Paint的样式和颜色变化在循环之外尽量归类,减少状态切换。比如先画所有黑子,再画所有白子。
- 只在棋子状态变化时调用invalidate(),不在Activity的onResume等生命周期方法里无脑重绘。
- 极端情况下可以缓存静态棋盘背景为Bitmap,每次重绘只画变化的棋子。这个简易版里我没做,但如果你要处理更大的棋盘时可以考虑。
优化后实测,低端机上的跟手度明显改善。这个坑说明,AI生成的代码常常在逻辑上正确,但在图形渲染性能上缺少工程直觉,这需要你自己补足。
6. 项目最终形态与使用体验:AI协助开发的一点心得
这个简易版五子棋最终跑通了双人对战和人机对战。双人模式下,两个玩家轮流点击棋盘落子;人机模式下,玩家执黑先行,AI执白,每轮玩家落子后AI自动计算并落子。同时加了检测到五连后弹出胜利提示,以及一个"重新开始"按钮。
整个开发过程,open claw帮我省掉了大量模板代码和重复性的机械工作,但它生成的代码必须在动手测试前经过仔细审查。尤其是坐标边界、方向遍历、状态复位这类普适逻辑,AI的错误率比我预期的高不少。
如果你也想用类似的AI工具做小项目,我有几条很实际的心得:
- 提示词要把"边界条件"写清楚。比如坐标从0开始、越界返回false、胜负同时判断正反方向。这些约束在需求阶段提出来,比生成代码后再去改要省力得多。
- 每轮只让AI改一个模块。不要让它"同时优化所有代码",它无法保证修改的局部性,很容易把本来能跑的东西改坏。
- 让它写单测。你可以要求它为Board和WinChecker生成简单的JUnit测试,这个能力AI相对靠谱,比手工测试效率高不少。
说回五子棋本身,这个项目的代码量其实不多,核心也就四五个类。但正因为简单,你才能把注意力集中在"怎么驾驭AI帮你干活"这件事上。当你习惯了先拆解、再喂需求、最后验证的闭环,再回去写复杂的项目,也会顺手很多。
我目前正在琢磨的是,把AI的评估函数升级成考虑活三、眠三、冲四等模式的版本。这个升级已经超出了"简易版"的范畴,但正好可以当作下一个练手项目。到时候再把经验拿出来分享。
