Skip to content

实战案例:记忆翻牌游戏开发实录

难度:⭐⭐ 进阶 | 工具:Codex / Claude Code | 耗时:约 25 分钟 | 技术栈:HTML + CSS + JS

项目背景

记忆翻牌游戏是一个逻辑性较强的项目——涉及状态管理(翻牌、匹配、锁定)、动画(3D 翻转)、游戏规则(计时、计步、通关判定)。非常适合用来练习如何向 AI 描述复杂的交互逻辑,也很适合结合 理解 AI 的输出:代码审查与调试 一起练。

第一阶段:需求规划

MVP:4×4 网格、翻牌匹配、通关提示 第二版:计时、计步、最佳记录 第三版:难度选择、音效、庆祝动画

第二阶段:MVP 版本

提示词 #1:核心游戏逻辑

text
帮我创建一个记忆翻牌(Memory Card Game)网页游戏。

这是 MVP 版本,专注于核心游戏逻辑:

【游戏规则——务必严格实现】
1. 4×4 网格,共 16 张卡片(8 对不同的 emoji 图案)
2. 游戏开始时,所有卡片背面朝上
3. 玩家每次可以翻开 1 张卡片(点击翻开)
4. 翻开第 2 张后:
   - 如果两张图案相同 → 匹配成功,两张卡片保持正面朝上,不可再点击
   - 如果两张图案不同 → 匹配失败,等待 0.8 秒后两张都翻回背面
5. 匹配判定期间(等待 0.8 秒期间),不能翻开第 3 张牌(要锁定点击)
6. 已经匹配成功的卡片不能再次点击
7. 当所有 8 对都匹配完成 → 显示"恭喜通关!"的提示
8. 有一个"重新开始"按钮,点击后重新洗牌、重置游戏

【洗牌逻辑】
使用 Fisher-Yates 洗牌算法打乱卡片数组。
emoji 图案使用:🍎🍊🍋🍇🍓🍑🍒🥝

【卡片翻转动画】
- 使用 CSS 3D transform 实现翻牌效果
- 每张卡片由两层组成:正面(显示 emoji)和背面(显示 ?)
- 翻转动画:rotateY(180deg),duration 0.5s,ease-in-out
- 需要设置 perspective 在父容器上
- 卡片需要 transform-style: preserve-3d
- 正面和背面分别用 backface-visibility: hidden

【界面设计】
- 页面居中
- 顶部标题 "记忆翻牌" + 重新开始按钮
- 4×4 网格,间距 10px
- 每张卡片 80×80px(手机端自适应缩小)
- 卡片背面:蓝紫渐变 #667eea→#764ba2,圆角 12px,中间显示白色 ? 号
- 卡片正面:白色背景,圆角 12px,显示大号 emoji
- 整体深色背景 #0f172a

【技术要求】
- 纯 HTML + CSS + JavaScript,单文件
- 响应式:手机端卡片尺寸自适应

注意:游戏逻辑的正确性是第一优先级。请确保状态管理没有 bug(尤其是锁定机制和匹配判定)。

请生成完整代码。

重点测试

这个项目的测试比前面几个更重要,因为游戏逻辑 bug 体验很差

测试场景预期实际状态
翻开第 1 张显示正面
翻开第 2 张(匹配)两张保持正面
翻开第 2 张(不匹配)0.8s 后翻回
在等待翻回期间快速点第 3 张不应该翻开⚠️ 偶尔能翻需修复
点击已匹配的卡片不应该有反应
点击当前已翻开的卡片不应该触发匹配⚠️ 会触发需修复
16 张全部匹配显示通关
重新开始洗牌重置

提示词 #2:修复游戏逻辑 bug

text
游戏逻辑有两个 bug 需要修复:

Bug 1:匹配判定期间的锁定不够严格
- 问题:在两张不匹配的牌等待翻回的 0.8 秒期间,快速连续点击有时能翻开第 3 张牌
- 原因分析:锁定变量(isLocked 或类似的)可能在某些时机没有正确设置
- 修复要求:
  - 定义一个 isLocked 变量
  - 在翻开第 2 张牌后立即设置 isLocked = true
  - 在所有 click 事件处理器最开始检查 if (isLocked) return
  - 匹配成功或翻回结束后才设置 isLocked = false

Bug 2:点击同一张已翻开的卡片触发了匹配
- 问题:翻开一张牌后,再次点击同一张牌,被当作翻开了第2张,触发匹配判定(自己和自己匹配了)
- 修复要求:
  - 在 click 事件中检查:如果这张卡已经翻开(正面朝上),直接 return
  - 可以通过检查卡片的 CSS class 或者一个 isFlipped 属性来判断

其他功能不变,只修这两个 bug。

💡 关键经验:游戏类项目的 bug 描述一定要包含复现步骤预期 vs 实际行为。AI 修 bug 最怕你只说"有 bug"。

第三阶段:计时计步 + 最佳记录

提示词 #3

text
添加游戏统计功能:

1. 计时器:
   - 从翻开第一张牌开始计时
   - 格式 MM:SS,显示在顶部标题右侧
   - 通关时停止计时
   - 重新开始时归零

2. 步数计数:
   - 每翻开第2张牌算1步(不管匹配不匹配)
   - 显示在计时器旁边:"步数: X"
   - 重新开始时归零

3. 通关弹窗:
   - 所有卡片匹配后,弹出一个居中的模态框
   - 显示:🎉 恭喜通关!
   - 用时:XX:XX
   - 步数:XX 步
   - 最佳记录:XX:XX / XX 步(如果是新记录加 🏆 标记)
   - "再来一局"按钮

4. 最佳记录:
   - 保存到 localStorage
   - 分别记录最佳时间和最少步数
   - 页面底部小字显示当前最佳记录

不要改动已有的游戏逻辑和翻牌动画。

第四阶段:难度选择 + 音效 + 庆祝动画

提示词 #4

text
添加最后一批功能:

1. 难度选择:
   - 在标题下方添加三个难度按钮:简单(3×4=6对)、普通(4×4=8对)、困难(5×4=10对)
   - 当前难度高亮显示
   - 切换难度时重新开始游戏
   - 网格列数随难度变化(简单4列、普通4列、困难5列)
   - 最佳记录按难度分别保存
   - 困难模式需要更多 emoji:追加 🍌🍍🥭🫐

2. 音效(Web Audio API,不用音频文件):
   - 翻牌:短促的 "翻开" 音(高频 600Hz,0.05s)
   - 匹配成功:欢快上升音(400Hz→800Hz,0.2s)
   - 匹配失败:低沉下降音(400Hz→200Hz,0.15s)
   - 通关:简短胜利旋律(连续 5 个升调音符,每个 0.1s)
   - 右上角加一个 🔊/🔇 图标切换静音,默认开启
   - 静音设置保存 localStorage

3. 通关庆祝动画:
   - 通关后所有已匹配的卡片依次做一个弹跳动画(每张延迟0.05s)
   - 页面顶部飘下彩色纸片(用 CSS @keyframes 实现)
   - 生成 30 个小方块,不同颜色,从顶部不同位置飘落
   - 有旋转和左右摇摆的效果
   - 动画持续 3 秒后自动清除

保持所有已有功能不变。

踩坑记录

这轮的主要问题是困难模式的网格布局不对——5列×4行的网格,但 CSS Grid 模板列还是 repeat(4, 1fr)

text
困难模式的网格列数不对。现在还是4列。
请根据当前难度动态设置 grid-template-columns:
- 简单/普通:repeat(4, 1fr)
- 困难:repeat(5, 1fr)

切换难度时同时更新网格 CSS。
卡片尺寸在困难模式下也要适当缩小(70×70px),避免超出屏幕。

开发总结

时间花费

阶段耗时对话轮次
需求规划2 分钟-
MVP + 核心逻辑5 分钟1 轮
修复游戏 bug5 分钟1 轮
计时计步 + 记录5 分钟1 轮
难度+音效+庆祝8 分钟2 轮(1轮生成+1轮修网格)
总计~25 分钟5 轮对话

关键经验

  1. 游戏逻辑要描述得极其精确:每个状态、每个条件、每个时机都要写清楚
  2. 第一版一定要重点测试边界情况:快速点击、重复点击、同时操作
  3. Bug 报告要有复现步骤:不要说"有时候会出bug",要说"快速连续点击3张牌"
  4. 状态变量的管理要明确说清楚:什么时候设 true、什么时候设 false
  5. CSS 3D 翻转有固定套路:perspective + transform-style + backface-visibility,告诉 AI 这几个关键词

踩坑汇总

问题原因解决方式
快速点击可以翻第3张锁定变量设置时机不对明确描述锁定逻辑的开关时机
同一张牌自己匹配自己没检查是否是同一张卡加 isFlipped 状态检查
困难模式网格列数不对CSS 没随难度动态更新明确要求动态设置 grid-template-columns
音效在 iOS Safari 不响需要用户交互才能播放音频第一次点击时初始化 AudioContext

继续阅读

基于新版 vibe-coding 教程全集整理