AI编程实战:用Claude Code复刻经典扫雷游戏

AI编程实战:用Claude Code复刻经典扫雷游戏

1. 项目概述与核心价值

最近在AI编程圈子里,Claude Code的热度持续攀升,很多开发者都在讨论用它来复刻经典项目。我花了几天时间,用Claude Code完整复刻了Windows经典的扫雷游戏,不仅实现了核心的布雷、排雷逻辑,连右键标记、双击展开、计时器这些原汁原味的细节都一一还原了。整个过程下来,我最大的感受是:Claude Code已经从一个“高级代码补全工具”,进化成了一个真正能理解复杂项目上下文、并主动规划执行的“AI编程伙伴”。这个扫雷项目,就是一个绝佳的案例,它能让你直观地感受到,在AI的辅助下,一个人如何高效、流畅地完成一个从前端UI到后端逻辑的完整小游戏开发。

这个项目非常适合想入门Claude Code,或者想体验AI如何辅助完成一个具体、完整功能的开发者。无论你是前端新手,想学习Canvas绘图和事件处理;还是后端开发者,想看看AI如何设计游戏状态机;亦或是单纯对“用AI写代码”感到好奇,这个案例都能给你带来不少启发。你会发现,Claude Code不仅能帮你写代码片段,更能理解“做一个扫雷游戏”这个宏观目标,并拆解成“绘制网格”、“随机布雷”、“处理点击事件”、“计算周围雷数”等一系列子任务,一步步引导你完成。接下来,我就把这趟“AI辅助复刻扫雷”之旅的完整过程、核心思路、踩过的坑以及独家心得,毫无保留地分享给你。

2. 项目整体设计与技术选型

2.1 为什么选择HTML5 Canvas + 纯JavaScript

在启动项目前,我面临第一个选择:用什么技术栈来实现?Claude Code支持多种语言和框架,但为了最纯粹地复现经典扫雷的体验,并最大化Claude Code在代码生成和逻辑推理上的优势,我选择了最经典的组合:HTML5 Canvas 和 原生 JavaScript。

这么选的核心考量有三点:

  1. 复杂度适中,适合AI理解与生成:扫雷的核心算法(如布雷算法、数字计算、空白区域展开)逻辑清晰但有一定复杂度,正好在Claude Code擅长处理的范围内。使用原生JS,避免了React/Vue等框架的模板语法和状态管理概念可能带来的额外解释成本,让AI能更专注于游戏逻辑本身。
  2. 可视化效果直接,便于调试:Canvas提供了像素级的绘图控制,能够精确还原扫雷棋盘上每一个格子的状态(未点开、已点开、标记为旗子、标记为问号)。在开发过程中,我可以随时让Claude Code生成一段绘制特定状态的代码,并立刻在浏览器中看到效果,形成快速反馈闭环。
  3. 零依赖,项目最简化:我们的目标是验证Claude Code的完整项目构建能力,而不是搭建一个生产级应用。纯JS+Canvas的方案没有任何外部依赖,项目结构极其简单(一个HTML,一个JS,一个CSS足矣),这能让Claude Code更专注于核心代码的生成,我也能更清晰地观察AI是如何思考和组装这些代码模块的。

当然,Claude Code也完全可以处理React、Vue甚至更复杂的全栈项目。但对于这个“100个真实案例”系列的第一个项目,从简单、经典、可视化强的开始,是理解其工作模式的最佳切入点。

2.2 核心功能模块拆解

在动手写第一行代码之前,我和Claude Code进行了一次“需求澄清会话”。我没有直接说“写一个扫雷游戏”,而是引导它和我一起拆解功能模块。这是用好Claude Code的关键:将模糊的需求转化为清晰、可执行的任务列表

我们共同梳理出的核心模块如下:

  1. 游戏棋盘(Grid):一个行 x 列的矩阵,每个单元格(Cell)是一个对象,包含其状态(是否是雷、周围雷数、是否被打开、是否被标记)。
  2. 初始化与布雷算法:游戏开始时,随机在棋盘上布置指定数量的地雷。这里的关键是确保随机性,且同一个位置不会重复布雷。
  3. 渲染引擎:负责将棋盘的数据状态(Cell对象数组)绘制到Canvas上。包括绘制格子边框、数字、地雷、旗帜、问号等不同状态。
  4. 事件处理系统
    • 左键点击:揭开格子。如果是雷,游戏结束;如果是数字,显示数字;如果是空白(周围无雷),则自动展开一片空白区域。
    • 右键点击:循环标记格子(无标记 -> 旗子 -> 问号 -> 无标记)。这是还原经典体验的重点。
    • 双击(左右键同时按或快速双击):当格子已显示数字,且周围标记的旗子数量等于该数字时,快速揭开周围未打开的格子。这是高手提速的关键操作。
  5. 游戏状态管理:管理游戏是“进行中”、“胜利”还是“失败”。同时需要实现计时器和剩余雷数计数器。
  6. 重置与难度选择:提供重新开始游戏的功能,以及可选的不同难度(如初级9x9-10雷,中级16x16-40雷,高级16x30-99雷)。

把这些模块明确后,我对Claude Code说:“好的,我们现在有了清晰的任务列表。让我们从创建项目基础结构和游戏棋盘的数据模型开始。” 这种结构化的沟通方式,能让Claude Code的输出非常聚焦和高效。

注意:在与Claude Code协作时,切忌一次性抛出过于庞大和模糊的指令。像“做一个扫雷游戏”这样的指令,AI可能会生成一个庞大但结构混乱的代码块。更好的方式是扮演“技术负责人”的角色,先进行顶层设计拆解,然后像分配任务一样,让AI逐个实现模块。你可以说“接下来,我们实现布雷算法”,或者“现在来处理左键点击事件”。

3. 核心实现细节与Claude Code协作实录

3.1 构建数据模型与初始化棋盘

首先,我创建了项目的基础文件:index.html,style.css,game.js。然后,在game.js中,我开始与Claude Code协作。

我的提示词是:“我们需要一个扫雷游戏的数据模型。请定义一个Game类,它应该包含行数(rows)、列数(cols)、雷数(mines)、一个二维数组grid来表示棋盘,以及游戏状态(playing,gameOver,gameWon)。再定义一个Cell类,代表每个格子,包含属性:isMine(是否是雷),revealed(是否被揭开),flagged(是否被标记为旗),question(是否被标记为问号),neighborMines(周围雷数)。请先给出这两个类的代码框架。”

Claude Code迅速生成了结构清晰的代码:

class Cell { constructor() { this.isMine = false; this.revealed = false; this.flagged = false; this.question = false; this.neighborMines = 0; } } class Game { constructor(rows = 9, cols = 9, mines = 10) { this.rows = rows; this.cols = cols; this.mines = mines; this.grid = []; this.playing = false; this.gameOver = false; this.gameWon = false; this.firstClick = true; // 用于实现首次点击安全 this.timeElapsed = 0; this.timerInterval = null; this.minesLeft = mines; this.initGrid(); } initGrid() { // 初始化一个 rows x cols 的二维数组,每个元素都是新的 Cell 实例 this.grid = Array.from({ length: this.rows }, () => Array.from({ length: this.cols }, () => new Cell()) ); } }

我注意到它自动加入了firstClickminesLeft等属性,这是它基于对扫雷游戏常见实现的“知识”所做的合理补充。接下来是布雷算法。我提出要求:“现在实现一个placeMines方法,它接收一个safeRowsafeCol参数,表示首次点击的坐标。确保地雷随机分布,但避开这个安全坐标及其周围的八个格子(如果存在),并且总数刚好是this.mines。”

Claude Code生成的算法逻辑正确,但初始版本在生成随机坐标时使用了Math.random,我担心在极端情况下(比如格子很少雷很多)可能会陷入循环。我给出了反馈:“这个随机布雷算法在雷密度很高时可能效率低下。可以优化一下吗?比如先创建一个包含所有可能坐标(除了安全区)的数组,然后打乱(shuffle)这个数组,再取前mines个位置。”

Claude Code立刻理解了意图,并给出了优化后的版本:

placeMines(firstClickRow, firstClickCol) { // 1. 构建安全区域坐标集合(首次点击点及其周围8格) const safeCells = new Set(); for (let r = firstClickRow - 1; r <= firstClickRow + 1; r++) { for (let c = firstClickCol - 1; c <= firstClickCol + 1; c++) { if (r >= 0 && r < this.rows && c >= 0 && c < this.cols) { safeCells.add(`${r},${c}`); } } } // 2. 构建所有可布雷的候选坐标数组 const candidateCells = []; for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (!safeCells.has(`${r},${c}`)) { candidateCells.push([r, c]); } } } // 3. 洗牌算法 (Fisher-Yates shuffle) for (let i = candidateCells.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [candidateCells[i], candidateCells[j]] = [candidateCells[j], candidateCells[i]]; } // 4. 放置地雷 for (let i = 0; i < this.mines && i < candidateCells.length; i++) { const [r, c] = candidateCells[i]; this.grid[r][c].isMine = true; } // 5. 计算每个非雷格子周围的雷数 this.calculateNeighbors(); }

这个版本不仅效率更高,逻辑也更清晰。同时,它自动调用了还未实现的calculateNeighbors方法。我接着让它实现这个方法,它完美地写出了遍历每个格子,检查其周围8个邻居并统计雷数的代码。

实操心得:Claude Code在实现经典算法时非常可靠,但有时生成的可能是最直观而非最优的版本。作为开发者,你需要具备基本的算法知识来引导和优化。你可以像代码审查一样向它提问:“这个方法的复杂度是多少?有更优的实现吗?” 它通常能给出改进方案。这种交互模式,很像和一个知识渊博但需要明确需求的初级程序员结对编程。

3.2 Canvas渲染与事件处理的深度集成

数据模型准备好后,接下来是最具挑战性的部分之一:将数据状态渲染到Canvas上,并绑定复杂的事件交互。我创建了一个Renderer类,并让Claude Code帮我实现。

我的提示是:“创建一个Renderer类,它接收Canvas的DOM元素和Game实例。它需要包含以下方法:drawGrid()绘制整个棋盘,drawCell(row, col)绘制单个格子,drawAll()重绘全部。格子的样式参考经典扫雷:未打开是凸起的灰色格子,打开后是凹陷的白色格子,雷是红色背景加黑点,数字1-8用不同颜色,旗子是红色小旗。请先实现drawCell的核心逻辑。”

Claude Code生成的绘图代码非常详细,包括了绘制浮雕效果(用浅色和深色线条模拟凸起/凹陷)、绘制数字和旗帜。但它最初把旗帜画成了一个简单的红色矩形。我立刻指出:“经典的扫雷旗帜是一个有旗杆和三角旗面的图案,能画得更像一点吗?”

它接受了反馈,并给出了一个使用canvaslineTofill绘制简单旗帜图案的版本。虽然比不上像素级复刻,但已经很像了。在这个过程中,我频繁地让它调整颜色、线条粗细,直到视觉效果满意为止。这就是AI辅助开发的高效之处:视觉调整这种琐碎工作,通过自然语言描述就能快速迭代。

事件处理是另一个重头戏。我让Claude Code帮我编写绑定Canvas点击事件的函数,并区分左键、右键和双击。

bindEvents() { this.canvas.addEventListener('click', (e) => this.handleLeftClick(e)); this.canvas.addEventListener('contextmenu', (e) => { e.preventDefault(); // 阻止浏览器默认右键菜单 this.handleRightClick(e); }); // 双击检测稍复杂,需要自己实现逻辑 }

对于左键点击,逻辑是:获取点击的像素坐标,换算成格子坐标(row, col),然后调用游戏的revealCell(row, col)方法。我让Claude Code实现这个revealCell方法,它必须处理多种情况:

  1. 如果游戏已结束或格子已被标记,则忽略。
  2. 如果是第一次点击,需要先调用placeMines布雷(并确保点击位置不是雷)。
  3. 如果点击的是雷,游戏结束,显示所有雷。
  4. 如果点击的格子周围雷数为0(空白),则需要递归或迭代地展开一片相连的空白区域。
  5. 如果点击的格子是数字,直接显示。

Claude Code在实现空白区域展开(Flood Fill)时,给出了递归版本。我提醒它:“递归在棋盘很大时可能导致调用栈溢出。能用迭代(队列)的方式实现吗?” 它很快给出了使用队列的BFS(广度优先搜索)版本,这是更工程化的做法。

右键点击的逻辑相对简单,就是在“无标记 -> 旗子 -> 问号 -> 无标记”之间循环,并更新minesLeft计数器。Claude Code准确地实现了这个状态机。

双击操作的实现最为精妙。我向Claude Code解释逻辑:“当用户在一个已揭开的数字格子上同时按下左右键(或快速双击)时,如果其周围被标记为旗子的格子数量等于该数字,则自动揭开周围所有未被标记且未揭开的格子。如果其中有雷未被正确标记,则游戏失败。” Claude Code完美地理解了这个规则,并写出了相应的检查逻辑和揭开操作。

注意事项:在实现事件处理时,坐标转换是一个易错点。Canvas的e.offsetX, e.offsetY是相对于Canvas元素自身的坐标。Claude Code最初生成的转换代码假设了格子是固定大小(如30px)。我提醒它:“格子大小应该是可配置的,并且要考虑Canvas可能有的边框或边距。” 它随后修改了代码,通过canvas.getBoundingClientRect()来获取更精确的相对位置,使得点击检测更加鲁棒。

4. 游戏状态、计时器与UI集成

4.1 实现游戏逻辑闭环

有了核心玩法,接下来需要让游戏“活”起来,这就需要状态管理。我让Claude Code在Game类中添加以下方法:

  • checkWin():检查胜利条件。当所有非雷格子都被揭开时,游戏胜利。
  • revealAllMines():游戏失败时,显示所有地雷的位置。
  • reset():重置游戏状态到初始值。

checkWin的实现需要遍历所有格子。Claude Code给出的逻辑是:统计已揭开的格子数,如果已揭开格子数 == 总格子数 - 雷数,则胜利。这个逻辑是对的。

计时器的实现也很有趣。我让Claude Code在游戏开始时(第一次有效点击后)启动一个setInterval,每秒更新timeElapsed,并在游戏结束时清除它。同时,需要将计时器和剩余雷数显示在Canvas上方或独立的HTML元素里。我选择了在Canvas上方添加两个<div>来显示这些信息,Claude Code帮我生成了更新这些UI元素的函数。

4.2 难度选择与响应式布局

为了更完整,我增加了难度选择功能。我在HTML中添加了三个按钮:“初级”、“中级”、“高级”。然后让Claude Code编写一个函数,当点击按钮时,用新的行、列、雷数参数重新初始化GameRenderer实例。

这里遇到一个布局问题:Canvas的大小是固定的(比如cols * 30像素)。当切换到大棋盘(如16x30)时,Canvas可能会超出容器宽度。我向Claude Code描述问题:“我们需要让Canvas能够自适应容器大小,或者至少根据棋盘大小动态调整,避免出现滚动条或布局错乱。同时,格子的大小最好能按比例缩放。”

Claude Code提出了两个方案:一是动态计算格子大小,确保整个棋盘能显示在视口内;二是使用CSS的max-width: 100%height: auto来让Canvas缩放。我选择了方案一,因为方案二会导致格子变形。我们合作写了一个resizeCanvas函数,根据棋盘尺寸和最大允许宽度,动态计算出一个合适的格子像素尺寸,然后重新设置Canvas的widthheight属性,并重绘棋盘。

踩坑记录:这里有一个关键点。直接改变Canvas的widthheight属性会清除其上的所有绘制内容。所以必须在resizeCanvas函数的最后调用drawAll()。Claude Code在第一次生成代码时漏掉了这一步,导致切换难度后棋盘变空白。我指出这个bug后,它立刻意识到问题并修正了。这提醒我们,AI生成的代码也需要经过严格的逻辑审查和测试。

5. 调试、优化与Claude Code的排错技巧

5.1 常见问题与交互式调试

在开发过程中,我遇到了几个典型问题,都是通过和Claude Code“对话调试”解决的。

问题一:右键标记后,左键点击仍然生效。

  • 现象:我给一个格子插上旗子后,还能用左键点开它。
  • 排查:我向Claude Code描述了现象,并问:“在revealCell方法中,是不是缺少对flagged状态的检查?” Claude Code检查了代码后回答:“是的,在方法的开头,应该先判断if (cell.flagged || cell.question) { return; },这样就能阻止打开已标记的格子。” 我让它添加了这行检查,问题解决。

问题二:双击展开功能有时会错误地揭开被旗子标记的格子。

  • 现象:在一个数字“2”的格子上双击,周围有两个格子被标记为旗子,但双击后,旁边一个未被标记但也不是雷的格子没有被自动揭开。
  • 排查:我让Claude Code重新审查handleChordClick(双击处理)函数的逻辑。它发现,在检查“周围旗子数是否等于格子数字”时,代码只统计了flaggedtrue的格子,这是正确的。但在后续揭开周围格子的循环中,它写的条件是“如果格子未被揭开(!revealed)且不是雷(!isMine)就揭开”。这漏掉了对“未被标记”的检查。正确的逻辑应该是“如果格子未被揭开(!revealed)且未被标记(!flagged && !question)就揭开”。修正后,功能正常。

问题三:游戏胜利或失败后,计时器没有停止。

  • 现象:踩雷后,计时器还在走。
  • 排查:我让Claude Code搜索所有修改gameOvergameWon状态的地方,看看是否调用了clearInterval(this.timerInterval)。它发现,在revealCell中触发游戏结束和checkWin中触发游戏胜利时,都没有清除计时器。我们在这两处添加了清理代码。

5.2 性能优化与代码重构

在基本功能完成后,我考虑进行一些优化。我问Claude Code:“目前的drawAll方法是遍历所有格子并重绘,在16x30的大棋盘上可能会有效率问题。有没有优化渲染的方法?比如只重绘发生变化的格子?”

Claude Code解释说,对于扫雷这种每个格子独立、状态变化不频繁的游戏,脏矩形渲染(Dirty Rectangle)优化收益可能不大,因为每次点击影响的格子数量有限,但实现复杂度高。它建议了一个更简单的优化:Game类中维护一个needsRedraw数组或集合,记录哪些格子的状态发生了变化,然后在drawAll中只重绘这些格子。我们实现了这个方案,为每个Cell添加了一个dirty标志,在状态改变时标记,在绘制后清除。

此外,我还让Claude Code帮我做了一次代码重构,将一些过长的函数(如revealCell)拆分成更小的、功能单一的函数,如floodFillReveal,revealSingleCell等,提高了代码的可读性。

Claude Code排错心法:当遇到bug时,不要只说“这里出错了”。尽可能详细地向Claude Code描述:1.你做了什么操作(步骤);2.你期望发生什么(预期结果);3.实际发生了什么(实际结果)。最好能提供相关的代码片段。Claude Code就像一个远程的资深调试伙伴,你提供的信息越精确,它给出的诊断和修复方案就越准确。

6. 项目总结与Claude Code实战心得

经过几天的迭代,一个功能完整、体验接近Windows原版的扫雷游戏就完成了。你可以通过左键排雷,右键标记旗子或问号,双击数字快速展开,计时器和雷数计数器正常工作,并且支持三种难度切换。整个项目代码大约500行,结构清晰,完全由Claude Code辅助完成。

回顾整个过程,Claude Code展现出的能力令人印象深刻:

  1. 从设计到实现的完整工作流支持:它不仅能写代码片段,更能理解项目蓝图,并一步步将其实现。从数据建模、算法实现、UI渲染到事件处理,它都能提供有效的代码和建议。
  2. 强大的上下文理解与代码推理:它记得之前定义的类和方法,能在实现新功能时正确地引用和调用它们。在实现“双击展开”时,它能理解这需要用到“周围格子”、“旗子数量”、“数字”等多个已有概念和数据结构。
  3. 自然的“对话式”编程体验:你可以用自然语言描述问题、提出优化想法、甚至进行代码审查(“这里是不是应该加一个空值判断?”)。这种交互模式极大地降低了思维负担,让你更专注于逻辑和架构。

当然,它并非完美无缺。最大的体会是:你必须是一个“有想法”的驾驶员。Claude Code是一个强大的引擎和导航,但方向盘和目的地必须由你来掌控。你需要:

  • 清晰地拆解任务:不要指望一个指令就能得到完美成品。将大任务拆解成小步骤,一步步引导。
  • 具备基本的代码审查能力:AI生成的代码可能存在边界条件遗漏、性能问题或逻辑错误。你需要能看懂代码,并提出具体的修改意见。
  • 善于提问和反馈:当结果不理想时,明确告诉AI哪里不对,你希望它怎么改。多用“为什么”、“如果...会怎样”、“有没有更好的方法”这类问题来引导深度思考。

这个“复刻扫雷”项目,就像一次Claude Code的深度试驾。它证明了在AI的辅助下,独立开发者实现一个完整小项目的效率和体验可以大幅提升。对于想学习新技术的开发者,它像一个随时在线的导师;对于需要快速原型验证的创业者,它像一个不知疲倦的助手。下一步,我打算用Claude Code挑战更复杂的项目,比如一个简单的在线多人游戏服务器,或者一个数据可视化仪表盘。100个真实案例才刚刚开始,这条路,越走越有意思。