Claude3.5 实现完整单机五子棋

五子棋是一个比较简单的棋类游戏,规则比较简单,如果用 Claude3.5 来实现,可能一句话就能实现一个简单版本的五子棋了。不过仔细研究了五子棋的禁手规则后,发现还是有点复杂的。五子棋的规则包含了各种禁手规则,比如三三禁手、四四禁手、长连禁手等。

如果用 Claude3.5 来实现禁手,会有难度吗?对于复杂的规则,当使用 Claude 辅助开发的时候,如何才能保证实现的正确性?本文记录用 Claude 实现单机五子棋的完整过程,一起来体会和 AI 结对编程的乐趣。

单机版五子棋,支持各种禁手规则

可以在这里体验:在线五子棋 游戏,目前只有单机版,后续会支持联网对战和 人、AI 对战。代码全部开源,在 Github 上。

快速实现 UI

对于我这前端新手菜鸟,如果没有 AI,让我实现五子棋,可能需要花一天时间来实现棋盘的展示和棋子布局。然后有了 Claude3.5,这类事情就简单了很多,直接简单提示下,就能拿到一个基本可用的五子棋棋盘了。

这里经过了两三轮提示,主要是补充实现方法以及屏幕适配。核心包括:

帮我画一个五子棋棋盘,要求每行每列都是放 15 个棋子,用react 和tailwind css实现,然后做好屏幕适配。

效果如下:

AI 实现的五子棋棋盘

不过如果仔细观察,这里的棋子在方格里面,其实正规棋盘,棋子不是放到方格里面,而是放到交叉线地方的。想实现也很简单,再描述清楚,然后 Claude 就能快速实现。另外在棋盘边界线周围还需要留一点空白,这样边上的棋子也有足够的位置。

整体体验下来,对于这种 UI 的需求,只要描述清楚需求,Claude 实现的质量还是很不错的

禁手规则拆分

五子棋的禁手规则比较复杂,直接让 AI 实现完整的禁手规则,有不少错误。其实也尝试让 AI 来解释五子棋禁手,不过 AI 解释有点不太清晰。于是先找了一个比较权威的对于禁手的解释,然后让 AI 实现。

实现这种比较复杂的规则,肯定要拆分子问题了。为了实现三三禁手检测,这里先单独实现一个连续活三的检测。对于一个落点,检查前、后两个点是否是同一方,之后检测两端是否是空白的。比如落点 X,连续活三有下面几种情况:

_00X_
_X00_
_0X0_

这里 _ 代表空格,0 代表先手棋子,X 代表先手将要下的位置,任意 1 种都是活三。这里因为模式比较固定,就想着直接让 Claude3.5 根据模式来匹配。AI 给出的代码还是可以的,没有什么错误。跳跃活三稍微复杂些,不过可以用同样的思路解决,只是模式稍微多一些,Claude3.5 也解决的不错。整体代码如下:

export function checkContinuousOpenThree(board, row, col, dx, dy, player) {  
  const patterns = [
    {indices: [-2, -1, 0, 1, 2], values: ["", player, player, player, ""]},  // _XOX_
    {indices: [-1, 0, 1, 2, 3], values: ["", player, player, player, ""]},   // _OXX_
    {indices: [-3, -2, -1, 0, 1], values: ["", player, player, player, ""]}  // _XXO_
  ];

  let matchCount = 0;
  let matchedPositions = null;

  for (let pattern of patterns) {
    let match = true;
    let positions = [];
    for (let i = 0; i < pattern.indices.length; i++) {
      const x = row + pattern.indices[i] * dx;
      const y = col + pattern.indices[i] * dy;

      if (!isValidPosition(x, y) || board[x][y] !== pattern.values[i]) {
        match = false;
        break;
      }
      if (pattern.values[i] === player) {
        positions.push([x, y]);
      }
    }
    // ....
  }

  return matchCount === 1 ? { isOpen: true, positions: matchedPositions } : { isOpen: false };
}

四四禁手的难点

比较难的是四四禁手实现。四四禁手是说:先手一子落下同时形成两个或两个以上的四,只要是两个“四”即为禁手,无论是活四、冲四都算。活四是说两端都可形成连五的四,冲四是说仅一端能形成连五的四。

这里开始想着拆分活四和冲四的检测,首先让 AI 实现活四的检测,这里 Claude 没有用规则匹配,直接在每个方向上扫描相连的棋子数量,然后检测,整体比较简单。

比较难的是冲四检测,这里直接让 Claude 实现,给的方法总有各种错误。于是想着用模式匹配的方法,不过粗略看了下,满足的模式有点多,每一个都列出来有点笨笨的感觉。中间想了几个方法,比如

扫描 row,col 周边的点,保证有 3 个同样的 player 以及一个空格。
如果空格在最边的一端位置,还要保证另一端有阻挡(对手棋子或者边界)
下面 1 代表边界或者对手棋子,_ 代表空格,X代表检测冲四的棋子位置。0 代表先手棋子。

比如下面都是冲四:
10_X00
0_X000

虽然上面实现算法有问题,也不够清晰,但 claude3.5 还是尬吹一波,然后就去实现了。现阶段 AI 这点还是挺恶心的,有时候你提出了错误的思路,Claude 也会尽量去拟合,而不能指出你的错误

后面重新思考了下,这里冲四和活四的判断规则,其实核心点在于再增加一个棋子就可以实现五连。区别在于活四可以有两个落子点,冲四只能有一个落子点。于是让 AI 重修修改这里的逻辑,不单独判断冲四,直接增加一个计算活四或者冲四连接的方法。具体步骤:

  1. 对于某个先手落子,算出再下一个棋子会五连的所有情况。
  2. 对于每一种五连,记录用到的当前 4 个棋子的位置,以及需要再下棋子的位置。
  3. 然后按照用到的 4 连棋子的位置进行聚合,计算出每种情况下,需要下的棋子的位置,如果对于某四个棋子,只有一个位置可以行成五连,就是冲四。有两个位置可以形成五连,就是活四。
  4. 函数返回两部分,一个是冲四的棋子位置,一个是活四的棋子位置

上面算法其实手写的话,还是挺费劲的,特别是我甚至都不太熟悉 JavaScript 语言。但是我描述给 Claude3.5,它竟然真的懂了,一遍就给出了正确的代码,核心部分如下,完整的在 github 可以看到:

export function checkFourInRow(board, row, col, player) {
  const potentialFives = [];
  for (const [dx, dy] of directions) {
    for (let start = -4; start <= 0; start++) {
      //...
      for (let i = 0; i < 5; i++) {
        const newRow = row + (start + i) * dx;
        const newCol = col + (start + i) * dy;

        if (!isValidPosition(newRow, newCol)) break;
        positions.push([newRow, newCol]);
        if (board[newRow][newCol] === player) {
          playerCount++;
        } else if (board[newRow][newCol] === '') {
          emptyCount++;
          emptyPositions.push([newRow, newCol]);
        } else {
          break;
        }
      }

      if (playerCount === 4 && emptyCount === 1) {
        potentialFives.push({
          fourPositions: positions.filter(([r, c]) => board[r][c] === player),
          emptyPosition: emptyPositions[0]
        });
      }
    }
  }
  //.... 
}

这就很强了,超预期的强悍。

测试用例

不过上面的实现正确吗?如果只是单纯从代码上看,感觉是没啥问题的。但是谁来保证呢?这时候就需要写测试用例了。

编写测试用例是个很废时间和体力的活,没太多技术含量。这时候就体验 AI 的作用了,告诉他想给某个函数写测试用例,一下子一堆用例就来了。比如对于 checkFourInRow 函数,AI 很快就给出了各种用例。不过这里为了让用例对人友好,这里故意把整个棋盘写出来,方便一眼看清。比如下面的双活三棋盘:

board: [
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "B", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "B", "X", "B", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "B", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
    ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]
    ],

AI 能快速给出各种测试用例,不过还是免不了人工干预。毕竟有些边界条件和专门的测试用例,靠 AI 来生成还是有点难。好在这里只用改下棋盘中棋子位置,人工来做也很方便了。

测试运行结果:

五子棋禁手测试代码

有测试用例做保证,这里禁手的实现基本没啥问题了。不过如果仔细看专业的禁手规则,发现还有太专业的非禁手情况,等后面有时间精力了再来看看怎么实现吧。

AI 可以写大项目吗?

加禁手的单机五子棋,其实算是复杂了,Claude 实现的还算可以。其实更大的项目,比如这整个站点,全都是在 Claude3.5 和 cursor 的辅助下快速完成的。

最开始想着只是体验下 Claude3.5 的能力,写几个简单的页面就行了。慢慢写着写着,这个演示站点的的页面越来越多,功能也越来越丰富。最近 cursor 编辑器又集成了 Claude3.5,写复杂项目的体验也越来越好。

对于不太需要复杂算法实现的功能,基本上描述清楚需求,Claude3.5 都能很好的实现。有时候实现的会比预期好,毕竟 Claude 大模型其实有很多知识。结合 Cursor 的文件引用,AI 对于整个项目的理解也越来越好,改动一个地方后,AI 甚至会提醒修改其他相关的文件,这点还是很不错的。

体验下来比较欠缺的地方有几个:

  1. 复杂的逻辑,要描述清楚,claude 才能实现;如果开发者自己都没想明白怎么实现,AI 大概率也是没法实现;
  2. AI 堆代码的时候,有时候会给出一些冗余代码,比如同样的函数在项目多个文件到处都有。需要开发者自己不断去重构一些实现,保证代码整体的简洁与可维护性
  3. 当你提了一些错误的思路,Claude 会尽量去拟合,而不能指出你的错误。有时候错的很离谱,我不小心打开了一个其他页面的代码,然后在 cursor 中,把这个文件当做五子棋的功能来修改,结果 AI 真的就在这里乱改了!

回答问题的答案,AI 完全可以来写大项目,只是需要人不断去提示,去纠错。人在这个过程,更像是监工,去提需求,检查质量,去重构,保证项目质量