feat: 新增涂鸦PK四课教案(第8-11课)及大纲更新

- 新增 AICODE06-08~11 完整逐字稿教案(每课600+行)
- 涂鸦PK主题:画图工具→基础对战→动画音效→班级锦标赛
- 核心工程思维:需求驱动→测试验证→增量迭代→数据驱动
- 更新 AICODE-06 课程大纲,追加第8-11课内容
- 新增 demo-pk/ 目录(画图工具/对战/动画三个demo)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rocky
2026-04-09 20:28:42 +02:00
parent 63d8edaa18
commit bad433a121
10 changed files with 4785 additions and 244 deletions

View File

@@ -17,6 +17,60 @@
--- ---
---
## 项目实战阶段魔幻俄罗斯方块第6-7课
> 面向已完成前5课的 AICODE-06 学员。以俄罗斯方块为载体系统训练工程师思维Plan Mode 先行、需求审核、自动测试、新窗口原则。
| 课时 | 课程主题 | 学习目标 | 核心概念 | 核心工具 |
|:----:|---------|---------|---------|---------|
| 6 | 魔幻俄罗斯方块(上)— Plan Mode 先行 | • 掌握 Plan Mode 三步流程:整理需求 → 需求审核 → 确认需求<br>• 理解需求质量 = 输出质量需求越详细AI 执行越准确<br>• 建立新窗口原则:审核必须在新窗口进行,避免上下文污染<br>• 能独立完成「需求文档 → 审核 → 生成 → 验收 → 结果溯源」完整闭环 | Plan Mode、需求文档、需求审核、结果溯源、新窗口原则、上下文污染 | Kimi 2.5 |
| 7 | 魔幻俄罗斯方块(下)— 魔改升级 + AI 自动测试 | • 掌握增量需求文档:在已有基础上只写新增功能<br>• 理解自动化测试:让 AI 生成测试脚本替代手动验收<br>• 能读懂测试脚本 ✅❌ 结果并溯源修复<br>• 建立「测试通过才算完成」的质量意识,利用测试脚本安全做第二版、第三版 | 自动化测试、测试覆盖、边界条件、增量需求、新窗口原则 | Kimi 2.5 |
**两课核心工作流:**
```
Plan Mode新窗口A整理需求
需求审核新窗口BAI扮演审核工程师
执行生成新窗口CKimi生成代码
手动验收 → 感受手动测试的局限
AI生成测试脚本新窗口D自动测试
测试全部 ✅ → 有了安全网 → 放心做第二版、第三版
```
---
## 项目实战阶段涂鸦PK第8-11课
> 在工程流程Plan Mode + 测试)已内化的基础上,以「自绘角色对战游戏」为载体,训练数据驱动设计、增量需求迭代、和设计决策表达力。
| 课时 | 课程主题 | 学习目标 | 核心能力 | 核心产出 |
|:----:|---------|---------|---------|---------|
| 8 | 涂鸦PK— 画图工具 + 角色设计 | • 能用需求文档驱动生成自己的HTML5画图工具<br>• 能画出两帧角色Spritesheet帧1待机+帧2攻击<br>• 理解20分属性预算制能根据打法定位分配属性 | 拆解力、审美力 | 自制画图工具 + 角色Spritesheet128×64 PNG+ 角色属性JSON |
| 9 | 涂鸦PK— 基础对战系统 | • 能用需求文档描述战斗规则(公式/先手/特技让AI生成完整对战系统<br>• 理解边界情况的重要性:需求文档必须覆盖所有异常情况<br>• 能用AI在新窗口生成测试脚本验证伤害公式和胜负判定 | 拆解力、韧性力 | 可对战的PK系统有血条/四种行动/AI对手+ 测试脚本验证报告 |
| 10 | 涂鸦PK— 动画 + 音效 + 特技 | • 能用自然语言描述动画「感觉」让AI实现Phaser Tween动画<br>• 理解Web Audio API用代码合成音效零外部素材依赖<br>• 掌握增量需求写法:只写新增部分,不重写已验收功能 | 审美力、提问力 | 有完整动画+音效+特技特效的战斗体验版 |
| 11 | 涂鸦PK— 班级锦标赛 | • 理解数据驱动设计加JSON文件=加角色,不改代码<br>• 能用增量需求实现roles角色系统从文件夹读取所有角色<br>• 能用3分钟路演清晰表达设计决策定位+意图+复盘) | 表达力、共创力 | roles系统 + 班级角色锦标赛 + 设计决策路演 |
**四课核心工作流延伸自第6-7课工程流程**
```
需求驱动窗口A整理 → 窗口B审核 → 窗口C执行
测试验证窗口D生成测试脚本 → 验证核心逻辑)
增量迭代(只写新增需求 → 已验收功能不重写)
数据驱动扩展(加文件=加功能 → 代码与数据分离)
```
---
## 合流说明 ## 合流说明
> **合流时间点待定。** 原计划第5课合流但考虑到 AICODE-03 学生打字和表达能力的成长节奏,合流点可能后延。 > **合流时间点待定。** 原计划第5课合流但考虑到 AICODE-03 学生打字和表达能力的成长节奏,合流点可能后延。

View File

@@ -1,6 +1,6 @@
--- ---
课时: 6 课时: 6
主题: 魔幻俄罗斯方块(上)— 工程师思维启蒙 主题: 魔幻俄罗斯方块(上)— Plan Mode 先行
核心能力: [提问力, 拆解力] 核心能力: [提问力, 拆解力]
核心工具: [Kimi 2.5] 核心工具: [Kimi 2.5]
时长: 90分钟 时长: 90分钟
@@ -11,8 +11,8 @@
### 1. 课程目标 ### 1. 课程目标
**知识目标:** **知识目标:**
- 理解「需求文档」的作用:把脑子里的想法变成 AI 能准确执行的指令 - 理解「Plan Mode计划模式」的概念无论做什么都要先开启计划模式把需求写清楚再执行
- 理解「需求质量 = 输出质量」:AI 做出来的结果不符合预期,根本原因是需求没说清楚 - 理解「需求质量 = 输出质量」:需求写得越详细AI 执行越准确;需求写不清楚,执行过程中就会出问题
- 理解需求是迭代的过程,不是一次写完就能用 - 理解需求是迭代的过程,不是一次写完就能用
**能力目标:** **能力目标:**
@@ -21,7 +21,7 @@
- 能在看到生成结果后,定位是哪条需求没说清楚,修改并重新生成(共创力) - 能在看到生成结果后,定位是哪条需求没说清楚,修改并重新生成(共创力)
**情感目标:** **情感目标:**
- 建立「先想清楚再动手」的工程师直觉,体验「需求清晰 → 结果可预期」的成就感 - 建立「先开 Plan Mode再动手」的习惯——这是课程中所有项目的第一步不可跳过
- 对「自己也能做出一个游戏」产生真实的兴奋感 - 对「自己也能做出一个游戏」产生真实的兴奋感
- 建立「结果不对 = 需求没说清楚」而不是「AI 不行」或「我不行」的归因习惯 - 建立「结果不对 = 需求没说清楚」而不是「AI 不行」或「我不行」的归因习惯
@@ -33,10 +33,11 @@
| 概念 | 学生类比 | 认知层级 | | 概念 | 学生类比 | 认知层级 |
|------|---------|---------| |------|---------|---------|
| Plan Mode计划模式 | 盖房子前先画图纸——没有图纸直接开工,盖到一半发现方向错了,拆掉重来比画图纸贵十倍 | 理解层 |
| 需求文档 | 装修前给师傅的设计图——口头说「弄好看点」和给一张详细图纸,结果完全不同 | 理解层 | | 需求文档 | 装修前给师傅的设计图——口头说「弄好看点」和给一张详细图纸,结果完全不同 | 理解层 |
| 需求压力测试 | 考试前找同学互出题——让别人挑毛病比自己检查更有效 | 应用层 | | 需求压力测试 | 考试前找同学互出题——让别人挑毛病比自己检查更有效 | 应用层 |
| 结果溯源 | 菜做咸了,往回找:是盐放多了,还是酱油放多了?找到根源才能改 | 应用层 | | 结果溯源 | 菜做咸了,往回找:是盐放多了,还是酱油放多了?找到根源才能改 | 应用层 |
| 需求迭代 | 第一版草稿 → 发现问题 → 修改 → 第二版,就像改作文 | 理解层 | | 新窗口原则 | 让同一个同学既写作文又给自己改错,他永远改不出来——必须换一个人来审。审核、测试、评审全部要开新窗口,让「新的 AI」来做才不会被原来的上下文带跑偏 | 应用层 |
**典型误概念表:** **典型误概念表:**
@@ -46,7 +47,9 @@
| M2 | 需求文档写完就是写好了,直接提交 | 写完只是第一步,要经过压力测试才能发现漏洞 | 用「压力测试」提示词,让 AI 提出学生自己没想到的问题 | | M2 | 需求文档写完就是写好了,直接提交 | 写完只是第一步,要经过压力测试才能发现漏洞 | 用「压力测试」提示词,让 AI 提出学生自己没想到的问题 |
| M3 | AI 做出来的结果不对是 AI 的问题 | 结果不对说明需求里有没说清楚的地方 | 「你的需求文档里有没有写这一条?」——让学生自己发现根本没写 | | M3 | AI 做出来的结果不对是 AI 的问题 | 结果不对说明需求里有没说清楚的地方 | 「你的需求文档里有没有写这一条?」——让学生自己发现根本没写 |
| M4 | 需求文档要写得很长很详细 | 精准 > 冗长。需求要「可测试」:能说出什么情况下算做对了 | 「你这条需求,我怎么知道 AI 做没做对?」 | | M4 | 需求文档要写得很长很详细 | 精准 > 冗长。需求要「可测试」:能说出什么情况下算做对了 | 「你这条需求,我怎么知道 AI 做没做对?」 |
| M5 | 先动手做,遇到问题再想 | 先想清楚的时间,会节省后面改来改去的时间 | 对比两种路径的时间消耗:「先做 → 改 → 改 → 改」vs「先想清楚 → 做 → 小改」 | | M5 | 先动手做,遇到问题再想 | 跳过 Plan Mode 直接做,出了问题再改来改去,总时间反而更长 | 对比两种路径:「跳过计划 → 做 → 改 → 改 → 改」vs「Plan Mode → 做 → 小改」 |
| M6 | 需求审核是在挑代码的问题 | 需求审核阶段只审需求本身——哪里没说清楚、哪里有歧义,跟代码无关 | 「AI 现在还没写一行代码,怎么可能审代码问题?」 |
| M7 | 在同一个对话框里写需求、审核、执行、迭代全部搞定 | 这叫「上下文污染」——AI 会被之前的对话带着走,审核时会偏向为自己生成的内容辩护。正确做法:写需求在一个窗口,审核开新窗口,执行再开一个窗口 | 演示:在写了需求的窗口里让 AI 审核自己AI 的回答会非常保守换新窗口审核AI 会找出更多问题 |
--- ---
@@ -75,159 +78,368 @@
### 4. 教学流程 ### 4. 教学流程
---
**第一幕:联系 (Connect) — 10分钟** 🔗 **第一幕:联系 (Connect) — 10分钟** 🔗
*本幕目标:通过「侦探模式」玩游戏,让学生主动发现俄罗斯方块的规则;展示「一句话版」对比引出 Plan Mode 的价值,建立「先写需求再动手」的第一印象*
**【环节】侦探模式导入 (10分钟)** **【环节】侦探模式导入 (10分钟)**
**师:** 今天我们要做一个游戏。但在做之前,先玩两分钟。 **师:** 今天我们要做一个游戏。但在做之前,先玩两分钟。
【投屏展示俄罗斯方块,或让学生在自己电脑上打开】 【投屏展示俄罗斯方块,或让学生在自己电脑上打开】
**师:** 你们的任务不是拿高分,而是当侦探——把这个游戏所有的规则找出来,写在纸上。你能找到多少条就写多少条。两分钟,开始。 **师:** 你们的任务不是拿高分,而是当侦探——把这个游戏所有的规则找出来,写在纸上。你能找到多少条就写多少条。两分钟,开始。
【学生玩游戏同时记录规则,教师走动观察学生找到了哪些规则】
> 教师走动观察:学生在记录哪些规则,是停在「方块会下落」这种表层,还是注意到了「碰到边界会停」「满行才消除」这类细节规则。不要打扰,只观察。
**师:** 好,停。谁来说一条规则? **师:** 好,停。谁来说一条规则?
**生:** 方块会往下落。
**** 好,这是一条。还有呢? **** (预期:方块会往下落)
**生:** 可以左右移动,可以旋转。满一行就消掉。
**师:** 不错。这些都是游戏规则。现在我问一个问题——如果我让 AI 帮我做这个游戏,我直接说「帮我做一个俄罗斯方块」,你们觉得 AI 能做出来吗 **师:** 好,这是一条。写得比较浅,但没错。还有呢
**生:** 能!/ 应该能?
**生:** (预期:可以左右移动,可以旋转)
**师:** 对,左右移动、旋转。还有没有发现更多的?
**生:** (预期:满一行就消掉)
**师:** 消行——这个很关键。消了之后上面的积木会怎样?
**生:** (预期:往下掉 / 往下移一行)
**师:** 对,会往下补。还有吗?积木碰到边界的时候会怎样?
**生:** (预期:停下来,不能再移了)
**师:** 对,碰到边界就停,不能移出去。那碰到底部呢?
**生:** (预期:就固定在那里了)
**师:** 对,固定住,不动了。那什么时候游戏结束?
**生:** (预期:积木堆到顶了就结束)
**师:** 好——你们刚才说出来的这些,就是俄罗斯方块的基本规则。你们注意没有——这些规则,全是游戏的「需求」。每一条都是「这个游戏应该怎么运行」。
【识别层:让学生意识到「游戏规则 = 需求描述」】
**师:** 现在我问一个问题——如果我让 AI 帮我做这个游戏,我直接说「帮我做一个俄罗斯方块」,你们觉得 AI 能做出来吗?
**生:** (预期:能!/ 应该能?)
**师:** 我们来试试。 **师:** 我们来试试。
【投屏展示教师提前生成的「一句话版」俄罗斯方块】 【投屏展示教师提前生成的「一句话版」俄罗斯方块】
**师:** 做出来了。但你们来找找看,这个游戏里,有没有什么地方跟你心里想的「俄罗斯方块」不一样? **师:** 做出来了。但你们来找找看,这个游戏里,有没有什么地方跟你心里想的「俄罗斯方块」不一样?
> 教师给 20 秒让学生仔细看屏幕,甚至可以让一个学生上来玩几秒
【诊断点:学生是否能发现 AI 自作主张的细节——比如得分规则、速度、旋转方向等】【识别层】 【诊断点:学生是否能发现 AI 自作主张的细节——比如得分规则、速度、旋转方向等】【识别层】
**【分支A】若学生发现了不同的地方** **【分支A】若学生发现了不同的地方**
**师:** 你发现了。为什么 AI 做出来跟你想的不一样? **师:** 你发现了。为什么 AI 做出来跟你想的不一样?
**生:** 因为我没有告诉它……
**** 你没说AI 就自己猜了一个。今天我们要学的,就是怎么把「脑子里的游戏」变成 AI 能准确执行的指令。这个东西叫需求文档。 **** (预期:因为我没有告诉它……)
**师:**你没说AI 就自己猜了一个。这就是我们今天要解决的问题。
**师:** 从今天开始,我们做任何项目,第一步都是同一件事——**开启 Plan Mode计划模式**。Plan Mode 只做一件事在动手之前把你的需求写得越详细越好。需求写得越详细AI 执行越准确需求写不清楚AI 就自己猜,猜出来的东西可能跟你想的完全不一样。
【识别层建立「Plan Mode 先行」的第一印象】
**【分支B】若学生觉得「都一样没问题」** **【分支B】若学生觉得「都一样没问题」**
**师:** 那我问你——这个游戏消一行得多少分?消四行一次呢? **师:** 那我问你——这个游戏消一行得多少分?消四行一次呢?
**生:** (去看)……好像是 100 分? **生:** (去看)……好像是 100 分?
**师:** 你想要的是这个分数吗? **师:** 你想要的是这个分数吗?
**生:** 我没想过…… **生:** 我没想过……
**师:** 你没想过AI 就帮你决定了。如果你想要自己的规则,就需要告诉 AI。这就是需求文档的意义。
**师:** 你没想过AI 就帮你决定了。Plan Mode 就是在动手之前,把所有「你没想过」的地方都想清楚,写在文档里,让 AI 按你想的来做。
--- ---
**第二幕:建构 (Construct) — 65分钟** 🛠️ **第二幕:建构 (Construct) — 65分钟** 🛠️
**【分段一:写 Level 0 需求文档】(20分钟)** *本幕目标:完整走完 Plan Mode 三步——整理需求、需求审核、确认需求;再走完「提交生成 → 验收 → 结果溯源」的执行闭环;建立「需求驱动输出」的核心工作习惯*
---
**【分段一:开启 Plan Mode — 第一步:整理需求】(20分钟)**
*本段重点:学习 Plan Mode 三步总览,填写 Level 0 需求文档模板,建立「需求可测试性」的直觉*
**预设误概念:** **预设误概念:**
- 误概念 M1「帮我做俄罗斯方块」就够了不需要写文档 - 误概念 M1「帮我做俄罗斯方块」就够了不需要 Plan Mode
- 误概念 M4需求文档要写得很长很全面 - 误概念 M4需求文档要写得很长很全面
**讲解与演示 (Teach & Demo): (5分钟)** **讲解与演示 (Teach & Demo): (7分钟)**
**师:** 现在我们正式开启 Plan Mode。Plan Mode 分三步走,我们先把三步过一遍,再动手。
**师:** **第一步——整理需求。** 把你想要的游戏写成一份需求文档。这一步的目标是:把你脑子里「模糊的感觉」变成「具体可以测试的文字」。就像设计师画图纸——不是说「大概这个形状」,而是标出每个尺寸。
**师:** **第二步——审核需求。** 写完不等于写好。让 AI 扮演一个审核工程师,专门找你文档里没说清楚的地方。它不帮你回答,只问问题——「这里如果出现了 XXX 情况,怎么处理?」
**师:** **第三步——确认需求。** 把审核工程师问的问题,一条条回答,补充进你的文档。这样你的需求文档才是「经过压力测试的版本」,可以提交给 AI 执行了。
**师:** 三步走完,才动手生成。听起来麻烦?我来给你算一笔账——
【投屏展示对比】
```
路径一(跳过 Plan Mode
说一句话 → AI 做出来 → 不对 → 让 AI 改 → 还不对
→ 再改 → 加乱了 → 从头来 → 又不对……(无限循环)
路径二Plan Mode 先行):
写需求文档 10分钟 → 压力测试 5分钟 → AI 做出来
→ 小幅微调 → 完成!
```
**师:** 路径一的「快」是假的快,路径二的「慢」是真的快。
**师:** 好,现在进入第一步。我给你们一个 Level 0 需求文档模板,里面有 5 个必须填的部分。
**师:** 现在我们要写一份需求文档。我给你们一个模板,里面有 5 个必须填的部分。
【投屏展示 Level 0 需求文档模板见第5节】 【投屏展示 Level 0 需求文档模板见第5节】
**师:** 这 5 个部分,每一个都是 AI 「必须知道才能做对」的信息。我们来看第一条——游戏区域:宽多少列、高多少行? **师:** 这 5 个部分,每一个都是 AI「必须知道才能做对」的信息。我带你们看一遍每个字段是什么意思——
**生:** 10×20
**师:** 这是默认值,但你可以改。重点是:你必须明确告诉 AI否则它自己决定。
**师:** 有一个原则:每条需求,你都要能说出「什么样的结果算做对了」。比如「消行规则:横向填满一整行就消除」——这条我怎么测试? **师:** 第一部分「游戏区域」——游戏画面有多大10列×20行是经典俄罗斯方块的标准尺寸你可以改也可以保持默认。这条如果不写AI 可能给你做个 6×6 的迷你版,也可能做个 20×40 的超高版。
**生:** 把一行填满,看它有没有消掉。
**师:** 对,这条可以被测试。好的需求就是这样——写完之后,你知道怎么验收。 **师:** 第二部分「方块移动规则」——注意这里有一个关键字段:「碰到左右边界」。这条要写清楚。什么意思?就是方块走到最边上了,再按左/右键,会发生什么?
**生:** (预期:就停下来不动 / 不能再往那边移了)
**师:** 对。但如果你不写AI 可能给你做出一个方块可以穿过边界、从另一侧出来的效果——像贪吃蛇穿墙那种。你想要这个效果吗?
**生:** (预期:不想!/ 哦那要写清楚)
**师:** 所以每一条都得写。第三部分「消行规则」,这里特别要注意「验收标准」这个概念——每条需求,你都要能说出「什么样的结果算做对了」。
**师:** 比如「消行规则:横向填满一整行就消除」——我怎么测试这条需求 AI 做没做对?
**生:** (预期:把一行填满,看它有没有消掉)
**师:** 对。能被测试的需求,才是写清楚了的需求。反过来,「游戏体验要流畅」——这条能测试吗?
**生:** (预期:不能……怎么叫流畅?)
**师:**「流畅」是感受不是能测试的需求。如果要写你得改成「初始速度每800毫秒下落一格」——这才是可以测试的。
【理解层:建立「需求可测试性」的直觉】 【理解层:建立「需求可测试性」的直觉】
**学生实践 (Practice): (13分钟)** **学生实践 (Practice): (11分钟)**
学生独立填写 Level 0 需求文档模板,完成 5 个必填部分 **师:** 现在开始填。5 个部分,把模板里的空格全填上。不确定的地方保持默认值也可以,但不能空着不填
> 教师走动观察重点: > 教师走动观察重点:
> - 是否有学生在「移动规则」里只写了「可以左右移动」,但没写「碰到边界怎么处理」? > - 是否有学生在「移动规则」里只写了「可以左右移动」,但没写「碰到边界怎么处理」?
> - 是否有学生在「消行规则」里写了消行但没写「消多行的得分是否不同」? > - 是否有学生在「消行规则」里写了消行但没写「消多行的得分是否不同」?
> - 这些漏洞不要直接告诉学生,留给下一步「压力测试」去发现 > - 是否有学生把「游戏体验流畅」这类不可测试的需求填进去了?
> - 这些漏洞先不要直接告诉学生,留给下一步「压力测试」去发现
**进度同步 (Checkpoint): (2分钟)** **进度同步 (Checkpoint): (2分钟)**
**师:** 5个部分都填完的举手。 **师:** 5个部分都填完的举手。
**师:**你觉得你的需求文档AI 能根据它做出你想要的游戏吗?
**** 应该可以 **** 好,先不着急提交。我来问你一个问题:按照你写的需求,一个不认识这个游戏的人拿到这份文档,能不能把一局游戏从头玩到结束——开始游戏、移动方块、消行、游戏结束?这个流程能跑通吗
**师:** 我们来测试一下你写的文档够不够清楚。
**生:** (预期:能?/ 好像……方块怎么消行没写清楚?/ 游戏结束之后怎么重新开始没写)
**师:** 对,就检查这一件事——游戏流程能不能跑通。先别想加什么特殊功能,那是后面的事。
【诊断点:学生的需求是否覆盖了「开始→玩→消行→升级→结束」这个基础流程】【理解层】
--- ---
**【分段二:AI 压力测试 → 完善需求文档】(15分钟)** **【分段二:Plan Mode 第二步:需求审核】(20分钟)**
*本段重点:用 AI 扮演审核工程师对需求做压力测试,区分「审需求」和「审代码」,发现自己看不见的漏洞*
**预设误概念:** **预设误概念:**
- 误概念 M2需求文档写完就可以直接提交了 - 误概念 M2需求文档写完就可以直接提交执行
- 误概念 M3AI 生成不对是 AI 的问题 - 误概念 M6需求审核是在挑代码的问题
**讲解与演示 (Teach & Demo): (3分钟)** **讲解与演示 (Teach & Demo): (5分钟)**
**师:** 接下来用一个技巧——让 AI 扮演挑剔的工程师,来「审问」你的需求文档。 **师:** 需求文档写完了,进入 Plan Mode 第二步——需求审核。在做这一步之前,我要告诉你们一个非常重要的规则:**审核必须在新窗口进行。**
【投屏展示「压力测试」提示词见第5节】
**师:** 注意这里:不是让 AI 帮你补全需求,是让 AI 问你问题。这两个完全不同——一个是 AI 替你决定,一个是 AI 帮你发现你自己没想到的地方 **师:** 为什么一定要新窗口?因为 AI 有一个特质——它不会主动承认自己的问题。你在同一个窗口里跟它说「帮我看看需求有没有问题」,它会被之前的对话「污染」,倾向于觉得「之前做得挺好的」。这叫**上下文污染**
**师:** 我来演示一次。
【教师用自己的需求文档执行压力测试,展示 AI 提出的问题列表】
**师:** AI 问了我哪些问题?有哪些是你们的需求文档里也没有答的? **师:** 打个比方——让同一个同学既写作文又给自己改错,他永远改不出大问题。必须换一个没看过你作文的同学来审,他才会发现真正的漏洞。换新窗口,就是换一个「什么都不知道」的新 AI 来看,它没有偏见,才会客观地找出问题。
**学生实践 (Practice): (10分钟)** **师:** 记住这条规则,以后做任何项目都要遵守:**写需求——一个窗口;审核需求——新窗口;执行生成——再新一个窗口。** 不同阶段,不同窗口,互不干扰。
【投屏展示「需求审核」提示词见第5节】
**师:** 现在的操作是:复制你的需求文档内容,打开一个全新的 Kimi 对话,把审核提示词粘贴进去。注意——审核的不是代码,是需求本身。审核工程师只负责问问题,不帮你回答,不帮你修改。
**师:** 我来演示一次。我把自己的需求文档粘贴进去,看 AI 会问什么问题。
【教师用自己的需求文档执行审核,投屏展示 AI 提出的问题列表】
**师:** 你们看AI 问了我这些问题——我们一起来分析分析:
**师:** 第一个问题:「方块旋转到边界时,如果旋转后的形状超出边界,是直接禁止旋转,还是自动向内移动?」——我的文档里有没有写这条?
**生:** (看文档)……没有……
**师:** 对,我没写。这个「踢墙旋转」问题我根本没想到。接着看——
**师:** 第二个问题:「如果方块落下时速度极快(玩家按了加速键),落地瞬间玩家还没松开方向键,方块会继续移动吗?」——我的文档里呢?
**生:** 也没写……
**师:** 再看第三个——「当积木消除多行时,是一行一行消,还是同时消除?视觉效果有没有要求?」——我写了得分,但消除的视觉顺序有没有说清楚?
**生:** 没说……
**师:** 最后一个——「游戏结束画面,除了「游戏结束」和得分,还需要「重新开始」按钮吗?」——这条很关键,游戏结束了玩家怎么重玩?
**生:** 对!要有重新开始。
**师:** 你看——四个问题,我的文档里一条都没写。这不是 AI 在刁难我,是它在帮我找漏洞。这就是「需求审核」的价值。
【识别层:直观感受审核的价值,澄清 M6 误概念——这些问题全是「需求层面」,跟代码无关】
**学生实践 (Practice): (12分钟)**
1. 学生把自己的需求文档粘贴进「压力测试」提示词,提交 Kimi 1. 学生把自己的需求文档粘贴进「压力测试」提示词,提交 Kimi
2. 把 AI 提出的问题复制到文档里 2. 把 AI 提出的问题复制下来
3. 逐条回答——每个问题写上自己的答案,补充进需求文档 3. 逐条读问题,判断哪些问题「我真的没想到」,哪些「我已经写了但 AI 没看清楚」
4. 对「真的没想到」的问题,写上自己的回答,补充进需求文档
> 教师走动观察:学生是否对某个问题感到惊讶——「这个我真的没想过」是好的信号 > 教师走动观察重点
> - 学生是否对某个问题感到惊讶——「这个我真的没想过」是好的信号,主动靠近问「这个问题你打算怎么回答?」
> - 学生是否对 AI 的问题感到不耐烦「这些问题没必要」——这是 M6 误概念的表现,需要介入
> - 学生是否在逐条认真回答,还是全部跳过直接提交——走过去让学生读出一条来分析
**进度同步 (Checkpoint): (2分钟)** **进度同步 (Checkpoint): (3分钟)**
**师:** AI 问了你最意外的是哪个问题? **师:** AI 审核出来,问了你最意外的问题是哪条?谁来说说
**生:** 分享1-2条
**** 如果不补上这条AI 会自己猜一个答案。那个答案不一定是你想要的。 **** (预期 A「方块落到底部前能不能再移动」我没想到
【识别层建立「每个没说清楚的地方AI 都会自己决定」的意识】
**生:** (预期 B「游戏结束要不要有重玩按钮」
**师:** 很好。这就是 Plan Mode 第二步的价值——你自己看不出来的漏洞,让 AI 来挖。
【诊断点:学生能否说出「因为 AI 问了这个问题,我补充了 XXX 这条需求」】【理解层】
**【分支A】若学生能说出具体被触发的漏洞**
**师:** 对,你补充了这条之后,需求文档就更完整了一步。这就叫「需求迭代」——不是一次写完,是一次次补充完善。
**【分支B】若学生说「AI 问的问题我都已经写了,没有漏洞」:**
**师:** 好,那你现在需求文档里,有没有写「方块旋转到边界时怎么处理」?
(引导学生发现一个真实存在的漏洞,避免学生误以为文档已经完整)
**【分支C】若学生觉得「AI 问的这些问题都不重要,不影响游戏」:**
**师:** 我们来测试一下——现在先不补充这条,等 AI 做出来,如果这个情况出现,游戏会怎样?
(用结果验证的方式让学生自己发现「小漏洞可以让整个游戏崩掉」)
**师:** 现在进入第三步:回答这些问题,把需求补充完整,然后提交生成。
--- ---
**【分段三:提交生成 → 验收 → 结果溯源】(20分钟)** **【分段三:提交生成 → 验收 → 结果溯源】(20分钟)**
*本段重点:建立「按需求逐条验收」的习惯;发现问题时用「结果溯源」找需求根源,而非直接改代码*
**预设误概念:** **预设误概念:**
- 误概念 M3生成完了就算完成了不需要验收 - 误概念 M3生成完了就算完成了不需要验收
- 误概念 M3结果不对直接改代码 - 误概念 M3结果不对直接改代码
**讲解与演示 (Teach & Demo): (3分钟)** **讲解与演示 (Teach & Demo): (5分钟)**
**师:** 需求文档完善好了,现在提交给 Kimi 生成。但是——生成完不等于完成。生成完之后要做一件事:按需求文档逐条验收。 **师:** 需求文档完善好了,现在提交给 Kimi 生成。但是——生成完不等于完成。生成完之后要做一件事:按需求文档逐条验收。
**师:** 验收方法:你需求文档里写了什么,就测什么。比如你写了「满一行消除」,就在游戏里把一行填满,看它消没消。 **师:** 验收方法:你需求文档里写了什么,就测什么。比如你写了「满一行消除」,就在游戏里把一行填满,看它消没消。比如你写了「碰到边界停止移动」,就在游戏里把方块推到最边上,按方向键,看它有没有停。
**师:** 我来示范验收清单是什么样的。你们跟着我一起来建——
【投屏在白板/文档上逐条写出来,让学生也在自己文档里写】
**师:** 第一条——「方块能左右移动」。怎么测?
**生:** 运行游戏,按左右键,看方块有没有动。
**师:**动了就打勾没动就标叉写上「实际表现XXX」。第二条——「方块能旋转」。第三条——「方块碰到左边界就停」。第四条——「满一行自动消除」。第五条——「消行后上方积木下移」。
**师:** 5条验收清单。注意——**每条都要你真的去操作,不能只是「看起来好像对」。** 看起来对,跟测试过是对,是不一样的。
**师:** 验收清单怎么记录?用三列格式:需求描述 → 实际结果 → 通过/不通过。比如:
```
消行规则:横向填满整行就消除
→ 实际测试:把最下一行填满,方块消失了,上方积木下移了
→ 通过 ✓
碰到左边界停止移动
→ 实际测试:方块移到最左边,继续按左键……还能继续移出去
→ 不通过 ✗ → 需要溯源
```
**师:** 写「不通过」的时候,把你看到的现象写清楚——「方块能移出边界」比「边界有问题」好得多,因为这是下一步溯源的线索。
【理解层:建立「验收 = 逐条测试」的意识,不是「感觉还行」】
**师:** 遇到「结果跟预期不一样」时,**先不要让 AI 改代码**。先做一个动作:找回需求文档,找到是哪一句话没说清楚。这叫结果溯源。 **师:** 遇到「结果跟预期不一样」时,**先不要让 AI 改代码**。先做一个动作:找回需求文档,找到是哪一句话没说清楚。这叫结果溯源。
**学生实践 (Practice): (14分钟)** **师:** 我给你们看一个具体例子——假如我测到这条:「方块可以移出右边界,一半跑出屏幕了」。我怎么溯源?
**师:** 打开需求文档,找「移动规则」这一节,看「碰到左右边界」这条。
【投屏展示需求文档】
**师:** 发现了——我写的是「碰到边界停止」但没说「停止」是指什么——是方块的中心碰到边界停还是方块的边缘碰到边界停AI 理解成了方块中心碰到边界才停,所以有半个方块出界了。
**师:** 这就是溯源的结果:找到了是需求里的哪句话没说清楚。现在怎么修?
**生:** (预期:把那条需求改得更清楚)
**师:**把需求改成「方块的任意一侧碰到边界就停止移动不能超出游戏区域」然后重新提交生成。不需要动代码需求说清楚了AI 会自己重新做对的。
【应用层:掌握「结果 → 溯源 → 修改需求 → 重新生成」的完整闭环】
**学生实践 (Practice): (12分钟)**
1. 把完善后的需求文档提交 Kimi 生成游戏 1. 把完善后的需求文档提交 Kimi 生成游戏
2. 运行游戏,对照需求文档逐条测试 2. 运行游戏,对照刚才建立的 5 条验收清单逐条测试
3. 遇到「结果跟预期不一样」时: 3. 遇到「结果跟预期不一样」时:
- 回到需求文档 - 回到需求文档
- 找到是哪条需求没说清楚,或者根本没写 - 找到是哪条需求没说清楚,或者根本没写
- 标注「这里有问题原因___」 - 在文档里标注「这里有问题原因___」
- 修改需求文档,重新提交生成第二版 - 修改需求文档,重新提交生成第二版
> 教师走动观察重点: > 教师走动观察重点:
> - 学生测试完一条直接跳过 → 走过去问「这条算做对了吗?你的需求是怎么写的?」 > - 学生测试完一条直接跳过 → 走过去问「这条算做对了吗?你的需求是怎么写的?对比一下
> - 学生说「不对」但直接让 AI 改代码 → 叫停,引导回到需求文档:「先找找是哪条需求没说清楚」 > - 学生说「不对」但直接让 AI 改代码 → 叫停,引导回到需求文档:「先找找是哪条需求没说清楚,找到了再来修改
> - 学生说「全部都对了!」但第一版生成很少能完全通过 → 走过去追问「你测了消行那条了吗?消两行得分对不对?」
**进度同步 (Checkpoint): (3分钟)** **进度同步 (Checkpoint): (3分钟)**
**师:** 谁的第二版比第一版有改善?具体改了什么,改完效果怎样? **师:** 谁的第二版比第一版有改善?具体改了什么,改完效果怎样?
【诊断点:学生是否能量化进步——不是「感觉好多了」,而是「之前 XXX 不对,改了需求之后 XXX 对了」】【应用层】 【诊断点:学生是否能量化进步——不是「感觉好多了」,而是「之前 XXX 不对,改了需求之后 XXX 对了」】【应用层】
**【分支A】若学生能说出具体改善** **【分支A】若学生能说出具体改善**
**师:** 这个进步是因为你把需求文档的 XXX 改得更具体了。记住这个感觉——这就是需求驱动输出。 **师:** 你找到了根源,改了需求,结果变对了——这个进步不是运气,是因为你走了溯源流程。记住这个感觉——这就是需求驱动输出。需求写清楚了AI 就做对了。
**【分支B】若学生说「改了但没变化」** **【分支B】若学生说「改了但没变化,还是不对」:**
**师:** 说明改的地方不是真正的根源。我们再来找——测试结果里还有哪里跟预期不一样 **师:** 说明改的地方不是真正的根源。我们再来找——你改了哪条需求?游戏里不对的现象是什么?一起来对照看看——你改的那条,跟这个现象有没有直接关系
【帮学生重新溯源,找到真正的问题所在 【帮学生重新溯源,找到真正对应的需求字段
**【分支C】若学生说「我全部验收通过了没有问题」**
**师:** 很厉害!那我问你——你验收的是哪 5 条?能说一遍吗?
(让学生逐条念,引导回顾验收过程,确认是真的测试过还是只是「感觉对」)
**师:** 如果真的都通过了,你现在已经有一个基础版可玩的俄罗斯方块了!那你可以进下一段——想想你想给它加什么功能,留到下节课来做。
--- ---
**【分段四:自由探索 + 下节课铺垫】(10分钟)** **【分段四:自由探索 + 下节课铺垫】(10分钟)**
*本段重点:在基础版可玩的基础上,允许学生做少量视觉微调;同时引导学生开始想下节课的扩展功能。只有基础版已经跑通的学生才进入本段,进度慢的学生继续完善分段三的最小闭环*
**学生实践 (Practice): (7分钟)** **学生实践 (Practice): (7分钟)**
游戏基本跑起来了之后,学生自由探索: 游戏基本跑起来了之后,学生自由探索:
@@ -235,6 +447,7 @@
- 或者调整一下速度和得分规则 - 或者调整一下速度和得分规则
**师:** 你现在手上有一个可以玩的俄罗斯方块了。下节课,你们可以给它加一个你自己想要的功能。现在开始想——你想给你的游戏加什么? **师:** 你现在手上有一个可以玩的俄罗斯方块了。下节课,你们可以给它加一个你自己想要的功能。现在开始想——你想给你的游戏加什么?
【诊断点:观察学生自然产生的功能需求,作为下节课教学素材】 【诊断点:观察学生自然产生的功能需求,作为下节课教学素材】
学生说出 1-2 个想加的功能(不要求写清楚,只是发散): 学生说出 1-2 个想加的功能(不要求写清楚,只是发散):
@@ -245,26 +458,50 @@
**进度同步 (Checkpoint): (3分钟)** **进度同步 (Checkpoint): (3分钟)**
**师:** 说说你想加什么,一句话就行。 **师:** 说说你想加什么,一句话就行。
(每人说一个,教师快速记录在白板/投屏上) (每人说一个,教师快速记录在白板/投屏上)
**师:** 下节课,你们每个人选一个功能,用今天学到的方法——写需求文档、压力测试、生成、验收——把这个功能加进去。 **师:** 下节课,你们每个人选一个功能,用今天学到的方法——写需求文档、压力测试、生成、验收——把这个功能加进去。
--- ---
**第三幕:反思 (Contemplate) — 10分钟** 🤔 **第三幕:反思 (Contemplate) — 10分钟** 🤔
*本幕目标:通过成果展示和互评,让学生复述 Plan Mode 流程,并从「需求层面」评价他人作品,而非只评价游戏好不好玩*
**【环节】成果展示 (6分钟)** **【环节】成果展示 (6分钟)**
**师:** 谁来展示一下你今天做的游戏? **师:** 谁来展示一下你今天做的游戏?不只是展示游戏——要说三件事:
邀请 2 名学生展示,展示重点不只是游戏本身,还要说: 投屏列出三个展示要点
- 你的需求文档里加了什么特别的设计?
- AI 压力测试问了你什么最意外的问题? ```
- 第一版和第二版有什么不同 1. 你的需求文档里,有没有加什么你自己特别设计的规则
2. AI 压力测试问了你什么最意外的问题?你最后怎么回答的?
3. 你第一版和第二版有什么不同?是溯源到了哪条需求?
```
(邀请 2 名学生上来展示,每人约 2-3 分钟)
**师:** (第一位展示后)你刚才说需求审核问了你「方块是否能踢墙旋转」——你怎么回答这个问题的?
**生:** (预期:我写了「不可以踢墙,碰到边界就禁止旋转」)
**师:** 好,你做了一个具体的设计决定。有没有人觉得另一种方案——可以踢墙——更好玩?
(让其他学生发表意见,引出「需求可以有不同的设计选择」这个意识)
**【环节】互评 (4分钟)** **【环节】互评 (4分钟)**
**师:** 看完 XXX 同学的游戏,你们能不能从他的需求文档里找出一个还没说清楚的地方? **师:** 看完 XXX 同学的游戏,我想让你们不只评价「好不好玩」,而是看他的需求文档——能不能找出还有没说清楚的一个地方?
【诊断点:学生是否建立了「挑剔工程师」的眼光】
【诊断点:学生是否建立了「挑剔工程师」的眼光,能从需求角度评价作品】【应用层】
**生:** (预期:「他的需求文档里消多行的得分没写具体」/ 「游戏暂停功能没有需求描述」)
**师:** 很好!这不是在批评他,这是工程师的职业习惯——总是看「还有什么没说清楚」。你刚才找到的这条,就是下节课可以迭代的方向。
**师:** 互评用一个结构:**一个优点**——需求文档里什么地方写得很具体、很清楚?**一个建议**——需求层面(不是游戏体验),还有什么地方可以更精准?
**师:** 今天最难描述清楚的规则是什么?你最后是怎么写清楚的? **师:** 今天最难描述清楚的规则是什么?你最后是怎么写清楚的?
@@ -274,16 +511,20 @@
**【环节】抽象总结 (3分钟)** **【环节】抽象总结 (3分钟)**
**师:** 今天我们做了什么? **师:** 今天我们用了一个贯穿整个课程的核心流程——Plan Mode。谁来说说 Plan Mode 的三步是什么?
**生:** 做了一个俄罗斯方块。
**** 对,但更重要的是用什么方法做的? **** (预期:整理需求、审核需求、确认需求……)
**生:** 写了需求文档,然后让 AI 做……
**师:** 对。我们今天学的这个方法,有一个名字——工程师思维。工程师做任何事情之前,都先把「要做什么」说清楚。说清楚了,才开始动手 **师:** 对。从今天开始,无论做什么项目,第一步永远是开启 Plan Mode。不管你多想直接动手都先走这三步
**师:** 今天这个能力,不只是做游戏用——以后让 AI 做任何事,需求越清晰,结果越接近你想要的。
**师:** 为什么因为需求写得越详细AI 执行越准确。需求写不清楚,执行过程中就会出问题,改来改去反而更慢。
**师:** 今天做游戏是这样以后做任何东西——网页、工具、应用——都是同一个流程。Plan Mode 是你跟 AI 合作的基础动作。
**【环节】下节预告 + 5分钟挑战 (2分钟)** **【环节】下节预告 + 5分钟挑战 (2分钟)**
**师:** 下节课,每人选一个你想加的功能,用同样的方法——写需求文档、压力测试、生成、验收——把它加进你的俄罗斯方块里。你的游戏会跟任何人的都不一样。 **师:** 下节课,每人选一个你想加的功能,用同样的方法——写需求文档、压力测试、生成、验收——把它加进你的俄罗斯方块里。你的游戏会跟任何人的都不一样。
**师:** 本周5分钟挑战想好你下节课要加的功能是什么在脑子里想一想这个功能如果要写需求文档最难描述清楚的是哪一条不用写想一想就行下节课说给我听。 **师:** 本周5分钟挑战想好你下节课要加的功能是什么在脑子里想一想这个功能如果要写需求文档最难描述清楚的是哪一条不用写想一想就行下节课说给我听。
--- ---
@@ -321,16 +562,32 @@
- 结束后显示___游戏结束画面 + 最终得分) - 结束后显示___游戏结束画面 + 最终得分)
``` ```
**「压力测试」提示词** **⚠️ 新窗口使用规则(必须遵守)**
```
本课三个阶段的窗口规则:
第一步:整理需求 → 窗口 A写需求文档用这个窗口
第二步:需求审核 → 窗口 B全新窗口不要用窗口A
第三步:执行生成 → 窗口 C全新窗口
原因AI 不会承认自己的问题。在同一个窗口里审核AI 会受
到之前对话的影响(上下文污染),很难客观找出真正的漏洞。
换新窗口 = 换一个全新的 AI 来看,没有偏见,更客观。
```
**Plan Mode 第二步:「需求审核」提示词:**
``` ```
我写了一份俄罗斯方块的需求文档,内容如下: 我写了一份俄罗斯方块的需求文档,内容如下:
[粘贴你的需求文档] [粘贴你的需求文档]
请你扮演一个非常挑剔的工程师,读完这份文档后, 请你扮演一个需求审核工程师,读完这份文档后,
找出所有你觉得「没有说清楚」「可以有多种理解」「遇到特殊情况不知道怎么处理」的地方 找出所有你觉得「没有说清楚」「可以有多种理解」「遇到特殊情况不知道怎么处理」的地方
用问题的形式列出来(每条一行,以问号结尾)。
注意:你审核的是需求本身,不是代码。现在还没有任何代码。
请用问题的形式列出来(每条一行,以问号结尾)。
不要帮我回答这些问题,不要帮我修改文档,只负责提问。 不要帮我回答这些问题,不要帮我修改文档,只负责提问。
``` ```
@@ -366,19 +623,35 @@
俄罗斯方块核心数据结构(教师理解用,不讲给学生):游戏核心是一个二维数组代表游戏区域,每个格子存 0或颜色值有方块。方块用形状矩阵 + 当前位置表示。学生不需要理解这些,只需要知道「我的需求文档决定了游戏的行为」。 俄罗斯方块核心数据结构(教师理解用,不讲给学生):游戏核心是一个二维数组代表游戏区域,每个格子存 0或颜色值有方块。方块用形状矩阵 + 当前位置表示。学生不需要理解这些,只需要知道「我的需求文档决定了游戏的行为」。
「踢墙旋转」技术说明教师理解用标准俄罗斯方块有「超级旋转系统SRS允许方块在边界旁旋转时自动内移。让 AI 默认生成的版本通常不包含这个细节。如果学生需求里没写AI 可能实现的是「碰到边界禁止旋转」或「可以踢墙」两种之一,建议教师在压力测试环节主动提示学生考虑这条规则。
**常见问题 FAQ** **常见问题 FAQ**
| 问题 | 应对 | | 问题 | 应对 |
|------|------| |------|------|
| 「Kimi 生成的代码打开没有反应」 | 检查文件是否保存为 .html 格式;用浏览器(不是记事本)打开 | | 「Kimi 生成的代码打开没有反应」 | 检查文件是否保存为 .html 格式;用浏览器(不是记事本)打开 |
| 「方块可以移动出边界」 | 需求文档里「碰到边界怎么处理」这条没写清楚,引导学生溯源并补充 | | 「方块可以移动出边界」 | 需求文档里「碰到边界怎么处理」这条没写清楚,引导学生溯源并补充「方块任意一侧碰到边界就停止移动,不能超出游戏区域」 |
| 「消行了但积木没有下移」 | 需求文档里消行后的处理没说清楚,引导溯源 | | 「消行了但积木没有下移」 | 需求文档里消行后上方积木全部下移一行」没有写,或者 AI 理解成消行后积木消失但不下移,引导溯源并在需求里明确写出「消行后上方所有积木整体下移填补空位」 |
| 「游戏结束了但还能继续操作」 | 需求文档里游戏结束条件对应的处理没写,引导补充「结束后禁止操作」 | | 「游戏结束了但还能继续操作」 | 需求文档里游戏结束条件对应的处理没写「结束后禁止玩家操作」,引导补充 |
| 「我想直接改代码」 | 「你能看懂这段代码是做什么的吗?如果不能,改了可能引发新的问题。先改需求文档,让 AI 重新生成更安全。」 | | 「我想直接改代码」 | 「你能看懂这段代码是做什么的吗?如果不能,改了可能引发新的问题。先改需求文档,让 AI 重新生成更安全。」 |
| 「AI 审核出来问了很多问题,我不知道怎么回答」 | 先问学生「这些问题里,哪一条你有自己的想法?」——通常能说出 2-3 条,剩下的保持默认值,在需求文档里写「保持默认行为」也是合法的回答 |
| 「需求文档写了但 AI 生成的完全没有按需求来」 | 检查提交时有没有把需求文档完整粘贴;也可能是 Kimi 单次生成的结果质量波动,重新提交一次,如果还不行,引导学生检查需求是否有歧义 |
| 「我的游戏做好了,同学的还差很多,我要做什么」 | 可以进入分段四自由探索,或者帮教师整理一份「你发现的 AI 自作主张细节」作为下节课的展示素材 |
| 「旋转之后方块超出边界怎么回事」 | 这是「踢墙旋转」的问题,引导学生在需求文档里补充「旋转后如超出边界,禁止该次旋转」或「旋转后自动内移一格」,选择哪个方案让学生自己决定 |
| 「游戏跑起来了但速度太快,根本玩不了」 | 引导溯源:「你的需求文档里初始速度写的是多少毫秒?」——如果填了 100 毫秒(很快),改成 800 毫秒(默认值);如果没填,这就是漏洞,补充进去 |
**课堂节奏控制(重要):**
本课的核心节奏:**先做出基础可玩版本,再做扩展**。教师要守住这条线,不能让学生在第一版都没跑通的情况下就开始想「加炸弹」「加 Boss」。
具体做法:
- 分段三开始前,教师快速巡场确认每个学生的需求文档覆盖了基础流程(开始→移动→消行→结束)
- 发现学生需求里加了复杂功能(比如道具、反重力):引导他先注释掉,等基础版跑通再加
- 分段四的自由探索,只有基础版已经可玩的学生才能进入
**课堂风险预案:** **课堂风险预案:**
- 如果 Kimi 一次生成就完全符合预期:恭喜学生,但仍要做压力测试——「这次 AI 猜对了,你能保证下次加新功能时也能猜对吗?」需求文档的价值在于每次都可预期,不只是修复当次问题。 - 如果 Kimi 一次生成就完全符合预期:恭喜学生,但仍要做需求审核——「这次 AI 猜对了,你能保证下次加新功能时也能猜对吗?」
- 如果学生进度差异很大:进度快的学生开始探索「想加什么功能」并尝试写第一版 Level 1 需求文档;进度慢的学生只要完成「生成 + 发现一个问题 + 溯源到需求」这个最小闭环即可。 - 如果学生进度差异很大:进度快的学生进入分段四自由探索;进度慢的学生只要完成「基础版可玩 + 发现一个需求漏洞 + 修复」这个最小闭环即可,不强求扩展功能
--- ---

View File

@@ -1,7 +1,7 @@
--- ---
课时: 7 课时: 7
主题: 魔幻俄罗斯方块(下)— 魔改升级 + 成果路演 主题: 魔幻俄罗斯方块(下)— 魔改升级 + AI 自动测试
核心能力: [拆解力, 共创力, 表达力] 核心能力: [拆解力, 共创力, 韧性力]
核心工具: [Kimi 2.5] 核心工具: [Kimi 2.5]
时长: 90分钟 时长: 90分钟
透明化层级: 过程层 透明化层级: 过程层
@@ -11,19 +11,19 @@
### 1. 课程目标 ### 1. 课程目标
**知识目标:** **知识目标:**
- 理解「功能扩展」的本质:在已有基础上,通过新一轮需求文档 + 生成 + 验收来增加功能 - 理解「自动化测试」的概念:不靠人工玩,让代码自己验证代码,更快更全面
- 理解「需求冲突」的概念:新功能的规则可能跟已有功能产生交互,需要提前想清楚 - 理解「测试覆盖」:需求文档里每一条规则,都应该有一条对应的测试
- 理解路演不只是展示结果,而是展示「设计决策」——你为什么这样设计 - 理解功能扩展的增量思维:新功能只写「增量」,不重写整体
**能力目标:** **能力目标:**
-独立完成「想法 → 需求文档 → 压力测试 → 生成 → 验收 → 迭代」完整流程(共创力) -把需求文档转化为测试条件,交给 AI 生成测试脚本(拆解力)
-在验收时主动测试功能之间的交互,发现潜在冲突(拆解力) -读懂测试脚本的 ✅❌ 结果,把失败的测试溯源到需求文档(韧性力)
-用 3 分钟路演清楚说明:加了什么功能、需求文档改了几版、遇到什么问题怎么解决的(表达力) -独立完成「需求 → 审核 → 生成 → 测试 → 修复」完整闭环(共创力)
**情感目标:** **情感目标:**
- 体验「每个人的游戏都不一样」带来的成就感和个性化自豪 - 体验「一键跑测试,马上知道哪里有问题」带来的效率
- 建立「我能用需求文档控制 AI 做出我想要的东西」的自信 - 建立「测试通过才算完成,不是能玩就算完成」的质量意识
- 感受从「做一个大家都一样的游戏」到「做一个只属于我的游戏」的跨越 - 感受从「每次手动点来点去找 bug」到「测试脚本自动找 bug」的升级
--- ---
@@ -33,20 +33,22 @@
| 概念 | 学生类比 | 认知层级 | | 概念 | 学生类比 | 认知层级 |
|------|---------|---------| |------|---------|---------|
| 功能扩展 | 在基础款手机上加功能——不是重新做一台手机,而是在已有基础上增加 | 理解层 | | 自动化测试 | 与其自己每次手动检查作业有没有写对,不如写一个「答案核对程序」,把你的答案输进去,自动判断对错 | 理解层 |
| 需求冲突 | 新加了一条规则跟原来的规则打架了——就像「所有人说话不准超过10秒」和「发言人可以不限时间」同时存在 | 应用层 | | 测试覆盖 | 需求文档里写了10条规则测试脚本就要测10件事——少测一条那条出了 bug 你就不知道 | 应用层 |
| 增量需求文档 | 只写「新加的部分」以及「新部分跟原有部分的交互规则」,不需要把全部需求重写一遍 | 理解层 | | 边界条件 | 不只测「正常情况」还要测「极端情况」——比如方块刚好在边角旋转、同时消4行、积木堆到最顶 | 应用层 |
| 路演 = 决策展示 | 路演不是说「我做了什么」,而是说「我为什么这样设计」——设计背后的思考才是最有价值的 | 迁移层 | | 增量需求文档 | 只写「新加的部分」,不重写整体——就像给手机升级系统,不是重新买一台手机 | 理解层 |
| 新窗口原则 | 让同一个人既写方案又审方案,他永远找不出大问题——审核和测试必须开新窗口,让没有上下文的新 AI 来做,才能客观找出漏洞 | 应用层 |
**典型误概念表:** **典型误概念表:**
| 编号 | 误概念 | 正确认知 | 激发策略 | | 编号 | 误概念 | 正确认知 | 激发策略 |
|------|--------|---------|---------| |------|--------|---------|---------|
| M1 | 加新功能要把整个游戏重新做一遍 | 在已有代码基础上增加功能,只需要写「新功能的需求文档」 | 演示「增量需求文档」的写法,只写新加的部分 | | M1 | 测试就是自己玩一遍,没出问题就算通过 | 手动测试只能发现你刚好测到的问题;自动测试每次都覆盖所有条件,不会遗漏 | 「你刚才玩的时候有没有特意测『同时消4行的得分是不是800』 |
| M2 | 新功能加进去就算完成了,不用测试已有功能 | 新功能可能跟已有功能冲突,必须测试两者的交互 | 举例:加了「炸弹」,炸弹爆炸后会不会触发消行?触发的话是几分? | | M2 | 测试脚本是程序员才会写的东西 | 把你的需求文档给 AIAI 就能帮你把每条需求变成一个测试 | 现场演示需求文档→AI→测试脚本5分钟完成 |
| M3 | 需求文档越来越长越好,把所有想法都写进去 | 本次迭代只写「这次新加的功能」,保持文档聚焦 | 「这次 AI 只需要知道新加什么,不需要把整个游戏重新理解一遍」 | | M3 | 加新功能要把整个游戏重新做一遍 | 增量需求文档只写新加的部分,在已有代码上加,不是重做 | 演示增量需求文档的写法 |
| M4 | 路演只要展示游戏好不好玩就行 | 路演的核心是展示你的思考:你加了什么、为什么加、需求文档改了几版、遇到什么问题 | 对比两种路演:一个只展示功能,一个讲设计决策,让学生投票哪个更精彩 | | M4 | 测试通过了就说明没有 bug | 测试只能覆盖你想到的情况;没覆盖的情况仍然可能有 bug——所以需求文档要写详细 | 「你的测试脚本有没有测『反重力期间消行』?」 |
| M5 | 没写清楚的需求可以让 AI 自己发挥 | 「让 AI 自己发挥」的部分你就失去了控制,结果可能跟你想的完全不同 | 「你让 AI 自己发挥了哪里?那里 AI 做出来的跟你想的一样吗?」 | | M5 | 测试失败说明代码写错了 | 测试失败可能是代码问题,也可能是需求文档没说清楚——先溯源,再决定改哪里 | 展示一个「测试失败但代码没错,是需求文档漏写了」的案例 |
| M6 | 需求审核、测试脚本生成可以在写需求的同一个窗口里做 | 上下文污染——AI 会受之前对话影响,不会客观审核自己生成的东西。审核和测试必须开新窗口 | 演示:同一窗口让 AI 审核自己写的需求,回答很保守;新窗口审核,问题暴露更多 |
--- ---
@@ -59,165 +61,350 @@
- 准备路演计时器3分钟倒计时 - 准备路演计时器3分钟倒计时
**教学资源:** **教学资源:**
- 教师准备:增量需求文档」提示词见第5节 - 教师准备:增量需求文档模板见第5节
- 教师准备:几个功能想法的参考清单(防止学生没有灵感,见第5节 - 教师准备:生成测试脚本的提示词(见第5节
- 教师准备:路演结构引导卡见第5节路演前发给学生 - 教师准备:一份提前生成好的测试脚本演示包含1个 ✅ 和1个 ❌,用于导入展示
- 学生资源第6课完成的俄罗斯方块 HTML 文件 - 教师准备功能灵感参考清单见第5节
- 教师准备路演结构引导卡见第5节
- 学生资源第6课完成的俄罗斯方块 HTML 文件 + Level 0 需求文档
**教师备课体验任务:** **教师备课体验任务:**
> 备课前,教师必须亲自完成以下操作: > 备课前,教师必须亲自完成以下操作:
> >
> 1. 选2-3个功能比如炸弹方块、速度加成、颜色主题各写一份增量需求文档,实际提交 Kimi 测试生成质量 > 1. 用自己的俄罗斯方块代码 + 需求文档, Kimi 生成测试脚本,实际运行并记录结果
> 2. 故意在一个功能的需求文档里漏写「与消行的交互规则,观察 AI 会怎么处理 > 2. 故意在需求文档里漏写一条规则,观察测试脚本是否能发现对应的 bug
> 3. 练习一遍 3 分钟路演,讲「设计决策」而不只是展示游戏 > 3. 准备一个演示用的「测试结果页面」包含至少1个 ✅ 和1个 ❌,用于课堂导入
> 4. 提前试跑「炸弹方块」功能的增量需求文档完整流程,记录 AI 审核会问什么问题
--- ---
### 4. 教学流程 ### 4. 教学流程
---
**第一幕:联系 (Connect) — 10分钟** 🔗 **第一幕:联系 (Connect) — 10分钟** 🔗
**【环节】上节课回顾 + 功能发布 (10分钟)** *本幕目标:用每人的功能构想激活学生兴趣,制造「今天不只是加功能,还要学一个新技能」的认知悬念*
**师:** 上节课结束前,我让你们想一个下节课要加的功能。现在每个人说出来——你想给你的俄罗斯方块加什么? **【环节】上节课回顾 (4分钟)**
【每人快速说一个,教师写在白板/投屏上】
**师:** 我们来看一下大家想加的功能。 **师:** 上节课结束前,我让你们想一个今天要加的功能,而且在出门前每人说了一句话。我们来回顾一下——上节课我们用了 Plan Mode它有几步谁来说
(读出清单)有没有一模一样的? 【诊断点:检测 Plan Mode 三步流程的记忆保持度】【识别层】
**【分支A】若有学生选了相同的功能** **生:** (预期:打开 Plan Mode → 写需求文档 → 让 AI 审核……)
**师:** 你们选了同样的功能,但你们的需求文档可能完全不一样——因为每个人对这个功能的设计可能不同。比如同样是「炸弹方块」,炸弹爆炸多大范围?爆炸后会不会消行?这些都是你自己决定的。
**【分支B】若所有人都选了不同的功能** **【分支A】若学生能说出三步(写文档、审核、执行)**
**师:** 大家选的都不一样——这节课结束,每个人的游戏都会跟别人的不一样。这就是今天最有意思的地方 **师:** 说得很准。今天还是这三步,但是有一点不一样——今天写的是「增量需求文档」,不是重新写整个游戏。等会儿我会解释区别在哪里
**师:** 来简单说一下——你想加的功能,最难描述清楚的是哪一条规则? **【分支B】若学生只说出一两步**
**** (各自说出预感最难写的部分) **** 没关系我帮你补一下。Plan Mode 三步:第一步,写需求文档,把你想做的东西说清楚;第二步,需求审核,让 AI 扮演审核工程师找漏洞;第三步,执行,生成代码。今天还是这三步,我们继续。
**师:** 很好,记住这个预感。等会写需求文档的时候,那条规则要特别仔细写。
【诊断点:学生上节课课后是否真的想了,还是现在才开始想】【识别层】 **师:** 好,现在说说你想加的功能——每人说一个,我把它们写在黑板上。
【每人快速说一个,教师写在白板/投屏上,列成一列】
**生:** (各自说出功能:炸弹方块、速度加成、冻结方块、彩虹消行……)
**师:** 大家看一下黑板——(指着功能列表)这里有六个不同的功能方向,没有两个人做一样的。你们今天各自在做一个完全不同的版本。
**师:** 我们来做一件有趣的事——互相评一下别人的功能想法。你觉得黑板上哪一个功能听起来最难描述清楚?
**生:** (讨论,指出某个功能)
**师:** 为什么觉得难?
**生:** (例如:「重力反转不知道要怎么写规则」「炸弹不知道爆炸范围算不算消行」)
**师:** 你们刚才说的这些「不知道」就是今天需求文档里必须写清楚的部分。不写清楚AI 就会自己决定,结果可能跟你想的完全不一样。
**【环节】引发认知冲突 + 悬念铺垫 (6分钟)**
**师:** 我再问你们——你刚才说想加的功能,你觉得最难描述清楚的规则是哪一条?比如你想加炸弹方块,「炸弹爆炸」这件事,你要怎么告诉 AI
【诊断点:探测学生对「把直觉转化为精确规则」的困难点认知】【理解层】
**生:** (各自说出难点——爆炸范围?爆炸后消行吗?得分怎么算?)
**师:** 很好,你已经在思考这个问题了。等会儿写需求文档的时候,那条规则要特别仔细。
**师:** 今天除了加功能,我们还会学一个新技能。上节课你们手动测试游戏——自己玩、自己找 bug对吗手动测试有一个问题一会儿让你们自己感受。
【故意不展开,制造悬念】
**师:** 我问你们——你的游戏有5条规则你在手动测的时候你能保证5条都测到了吗
**生:** (预期:不一定……有点难……)
**师:** 等会儿让你们亲身体验这个问题,我们先进建构。
**师:** 打开你上节课的游戏文件,确认能正常运行。我们进入建构阶段。
--- ---
**第二幕:建构 (Construct) — 65分钟** 🛠️ **第二幕:建构 (Construct) — 65分钟** 🛠️
**【分段一:写增量需求文档 + 压力测试】(20分钟)** ---
**【分段一:增量需求文档 + 需求审核】(15分钟)**
*本段重点:理解「增量」思维,写出可测试的需求文档,尤其是交互规则和验收标准*
**预设误概念:** **预设误概念:**
- 误概念 M1:加新功能要把整个游戏重做 - 误概念 M3:加新功能要把整个游戏重做
- 误概念 M3需求文档要把所有东西都写进去 - 误概念:验收标准可以写成「功能正常」这种模糊的话
**讲解与演示 (Teach & Demo): (5分钟)** **讲解与演示 (Teach & Demo): (5分钟)**
**师:** 今天写的需求文档,跟上节课不一样。上节课写的是「整个游戏的规则」,今天只写「新加的功能」。这叫增量需求文档 **师:** 今天继续用 Plan Mode。但今天写的是「增量需求文档」——只写新加的部分不重写整个游戏
【投屏展示增量需求文档提示词见第5节】
**师:** 有一个特别重要的部分——「与原有功能的交互规则」。我举个例子:你加了一个炸弹方块,炸弹爆炸了,爆炸范围里刚好有一整行被清空了——这算消行吗?算消行的话,得多少分? **师:** 类比一下——你手机升级系统,是重新买一台手机,还是只更新改变的那一部分?
**生:** ……这个我没想过。 **生:** (预期:只更新改变的部分)
**师:**,这就是功能交互。新功能和原有功能之间,总会有一些「撞在一起」的情况,需要提前想清楚 **师:**。增量需求文档就是这个意思:你的游戏核心逻辑不动,你只描述「要在这个基础上加什么」
【理解层:建立「功能之间有交互」的设计意识】
**师:** 我来演示一次——用「炸弹方块」功能写一份增量需求文档,然后做一次压力测试。 **师:** 增量需求文档有一个特别重要的部分,叫「与原有功能的交互规则」。我举一个具体例子——你加了炸弹方块。炸弹落地,爆炸了。爆炸之后刚好把整行都消干净了——这算消行吗?得多少分?是按炸弹规则算,还是按消行规则算,还是两个叠加?
【教师演示:写需求 → 压力测试 → AI 提问 → 补漏洞,重点展示「交互规则」这部分】
**学生实践 (Practice): (13分钟)** **师:** 如果你没写清楚AI 自己决定,结果可能跟你想的完全不一样。所以交互规则必须写。
1. 学生写自己的功能的增量需求文档(重点写清楚:功能规则 + 与消行/得分的交互规则) **师:** 还有一个字段叫「验收标准」。它的格式是这样的:
2. 执行「压力测试」提示词 **「[情况] → [预期结果]」**
3. 针对 AI 提出的问题,补充完善需求文档 比如:「炸弹落地时周围没有积木 → 爆炸动画播放,得分+50无消行」。
验收标准必须是可以测试的——能测试 = 说清楚了具体情况,说清楚了预期结果。不能写「功能正常运行」,那不是验收标准,那是废话。
> 教师走动观察重点: **师:** 我来考考你们——以下两条验收标准,哪条是可测试的?
> - 学生是否写了「交互规则」这一部分? - A「炸弹功能运行正常」
> - 对于「AI 问了什么」感到最意外的问题是什么? - B「炸弹落地后落点周围3×3格内的积木清除得分+50」
**生:** 预期B
**师:**B 是可测试的——它告诉了 AI「清除3×3范围」和「+50分」。A 是废话——AI 不知道「正常」是什么意思。你的验收标准必须全部像 B 这样写。
【投屏展示增量需求文档模板见第5节】
**学生实践 (Practice): (8分钟)**
1. 学生填写增量需求文档(重点:功能规则 + 交互规则 + 验收标准)
2. **⚠️ 开新窗口**:复制需求文档内容,打开全新的 Kimi 对话,执行「需求审核」提示词
- 不能在写需求的同一个窗口里审核——AI 会受上下文影响,不会客观指出问题
3. 回答 AI 的问题,把答案补充进需求文档(回到原来的窗口补充)
> 教师走动:检查以下三项——是否写了「与原有功能的交互规则」?验收标准是否是「情况 → 预期结果」格式验收标准条数是否至少3条
**进度同步 (Checkpoint): (2分钟)** **进度同步 (Checkpoint): (2分钟)**
**师:** AI 压力测试问了你最意外的问题是什么 **师:** AI 审核出了什么?有没有让你意外的问题?
**生:** (分享1-2条重点是「交互规则」相关的问题 **生:** (分享例如「AI 问我炸弹同时触发消行怎么算」
**师:** 这类问题,如果不写清楚,加进去的功能会跟原来的规则「打架」,产生奇怪的结果。
**【分支A】若学生说 AI 问出了没想到的问题:**
**师:** 这个问题你之前有想到吗?
**生:** 没有……
**师:** 所以 AI 审核是真的有用的——它问的问题不是废话,是你确实需要回答的问题。
**【分支B】若学生说 AI 没审出什么问题:**
**师:** 把你的需求文档发我看一下。(观察)——你的验收标准里有没有写边界情况?比如「功能触发时恰好也消行」这种情况?如果没有,让 AI 专门针对边界情况再审一遍。
**师:** 好,需求文档基本确认了,进入执行阶段。
--- ---
**【分段二:提交生成 → 测试交互 → 溯源迭代】(25分钟)** **【分段二:生成第一版 → 手动测试 → 感受痛点】(15分钟)**
*本段重点:先让学生亲身体验手动测试的局限,再自然引出「让电脑帮我们测」的需求*
**预设误概念:** **预设误概念:**
- 误概念 M2新功能加进去之后只测新功能就行 - 误概念 M1手动玩一遍没出问题就算通过
- 误概念 M5没写清楚的部分让 AI 自己决定 - 误概念:测试只需要测「好不好玩」,不需要逐条对照需求
**讲解与演示 (Teach & Demo): (3分钟)** **讲解与演示 (Teach & Demo): (2分钟)**
**师:** 把需求文档提交 Kimi有一个特别的要求——告诉 AI 「在已有代码基础上增加功能」,而不是「重新做一个游戏」 **师:**确认好的增量需求文档提交 Kimi基于上节课的代码增加功能。
【投屏展示「增量生成」提示词见第5节】 【投屏展示「增量生成」提示词见第5节】
**师:** 生成之后,验收的时候要测两件事: **师:** 生成之后,先手动测几条——自己玩一玩,看看新功能有没有按需求做出来。特别注意:不是随便玩,是对照着你的验收标准一条条测。
- 第一:新功能有没有按需求做出来
- 第二:原来的功能还正常吗(消行、得分、游戏结束,都要测)
**师:** 遇到不对的地方,还是同样的动作——溯源到需求文档,找到是哪条没说清楚,改需求,重新生成。 **学生实践 (Practice): (10分钟)**
**学生实践 (Practice): (19分钟)** 1. 提交增量需求文档 + 旧代码给 Kimi生成第一版
2. 打开游戏,手动测试:新功能触发了吗?原有消行/得分还正常吗?
3. 对照验收标准逐条检查,记录发现的问题
1. 把需求文档提交 Kimi基于已有代码增加新功能 > 教师走动观察故意不干预让学生自己感受「手动测试的局限」。观察是否有学生只是「玩了一会儿」而没有逐条对照验收标准是否有学生根本没有测「新功能和消行同时触发」的边界情况是否有学生测了3条就觉得「差不多了」
2. 验收:新功能测试 + 原有功能回归测试
3. 记录「结果跟预期不一样」的地方
4. 溯源到需求文档 → 修改 → 重新生成
5. 重复迭代,直到功能符合预期
> 教师走动观察重点:
> - 新功能和消行之间的交互是否符合需求文档的设计?
> - 是否有学生因为新功能导致原有功能出问题(回归 bug
> - 学生是否在需求文档里找到了回归 bug 的根源?
**进度同步 (Checkpoint): (3分钟)** **进度同步 (Checkpoint): (3分钟)**
**师:** 你加的新功能,有没有让原来的某个功能出问题?怎么溯源到需求文档的 **师:** 手动测了哪几条?对照你的验收标准,你测了几条
【诊断点:学生是否理解「新功能可能影响旧功能」,并能溯源到需求文档里的交互规则】【应用层】 **生:** 预期2条、3条……
**【分支A】若学生发现了回归 bug 并成功溯源:** **师:** 你的验收标准里一共有几条?
**** 非常好!你刚才发现的问题,在需求文档里是哪条交互规则没有写清楚? **** 5条……
**【分支B】若学生说「没有问题,一切正常」:** **师:** 你测了3条还有2条没测。你确定那2条没有问题吗?
**** 那我们来测一个边界情况——你的新功能触发的同时,刚好消了一行,得分是怎么算的? **** 不确定……
【主动暴露交互场景,测试是否真的没问题】
**师:** 还有一个问题——就算今天你把所有5条都测了下次你改了代码你还要重新手动测一遍。如果你做了第二版、第三版每次都要从头手动测一遍。需求文档越来越长要测的条数越来越多手动测试需要的时间也越来越长。
**师:** 有没有办法,让电脑帮我们测,而且每次都自动把所有条件测完?
**【分支A】若有学生说「那肯定有程序能做这个」**
**师:** 你说对了!这个东西叫测试脚本。我们现在就去做一个。
**【分支B】若学生面面相觑不知道答案**
**师:** 答案就是——用 AI 帮我们写一个测试脚本。这个脚本会自动运行你的游戏逻辑,逐条对照需求文档,告诉你哪条通过了、哪条失败了。我来给你们看一下它长什么样。
--- ---
**【分段三:路演准备】(10分钟)** **【分段三:AI 生成测试脚本 → 跑测试 → 修复】(25分钟)**
*本段重点:生成并运行测试脚本,学会读懂 ✅❌ 结果并溯源,建立「测试才算完成」的质量意识*
**预设误概念:** **预设误概念:**
- 误概念 M4路演只要展示游戏好不好玩就行 - 误概念 M2测试脚本是程序员才会写的
- 误概念 M5测试失败说明代码写错了不可能是需求问题
- 误概念:看不懂代码就没法用测试脚本
**讲解与演示 (Teach & Demo): (6分钟)**
**师:** 我来给你们看一个东西。
【投屏展示教师提前准备好的测试结果页面,显示 ✅❌ 列表】
**师:** 在看之前,先讲一个重要规则——**生成测试脚本必须开新窗口。** 这和上节课需求审核的原因一样AI 不会承认自己的问题。你在写游戏代码的同一个窗口里让 AI 生成测试,它会倾向于「测试通过」,因为它觉得自己写的代码没问题。换新窗口,让新的 AI 来测,它才会客观地发现问题。
**师:** 整个项目的窗口规则是这样的:写需求一个窗口、审核需求一个新窗口、生成代码一个新窗口、生成测试一个新窗口。**每个独立的任务,都在自己干净的窗口里做,避免上下文污染。**
**师:** 这是一个测试脚本跑出来的结果。你们来读一下——
- ✅ 消1行得分100分通过
- ✅ 消2行得分300分通过
- ✅ 消4行得分800分通过
- ❌ 炸弹爆炸得分期望50实际得到0
- ✅ 游戏结束条件:通过
- ❌ 炸弹+消行叠加得分期望150实际得到100
**师:** 它自动测了6条规则告诉我哪些通过了、哪些失败了还说明了失败的具体原因——期望是多少实际是多少。我没有玩游戏是代码自己测自己。
**师:** 你们说这个测试脚本是怎么工作的它是怎么知道「消1行应该得100分」的
**生:** (预期:从需求文档里知道的?)
**师:**测试脚本的工作方式就三步第一步构造一个具体的场景——比如「棋盘最底行全满了一行」第二步调用游戏里的消行函数第三步对比结果和需求文档里写的数字。需求文档说100分结果也是100分就是 ✅;结果是别的数字,就是 ❌。
**师:** 你不需要看懂每一行代码。你只需要:能读 ✅❌ 结果、能看懂失败原因、能判断是代码问题还是需求文档问题。
**师:** 怎么判断失败原因?方法是这样的——
**师:** 如果 ❌ 显示「期望50实际0」你先查需求文档文档里有没有写「炸弹爆炸得50分」
**师:** 如果需求文档写了但代码算出来是0那是代码问题——让 AI 修代码。
**师:** 如果需求文档漏写了这条规则,那是需求问题——先补需求文档,再重新生成代码。
【投屏展示「生成测试脚本」提示词见第5节】
**学生实践 (Practice): (15分钟)**
1. 把需求文档 + 游戏代码提交 Kimi生成 `test.html`
2. 双击打开 `test.html`,查看测试结果
3. 对每一个 ❌,按以下步骤处理:
- 读失败原因:「期望 X实际 Y」
- 查需求文档:这条规则文档里写清楚了吗?
- 如果是代码问题 → 告诉 Kimi「这个测试失败了[失败信息],帮我修复」
- 如果是需求问题 → 先补充需求文档,再让 AI 重新生成代码
4. 修复后重新跑测试,目标:全部 ✅
> 教师走动观察重点:
> - 学生是否看到 ❌ 就直接让 AI「修代码」而没有先判断是需求问题还是代码问题
> - 是否有学生测试结果全是 ✅,但游戏实际上还有问题?此时引导:「哪条 bug 对应的规则,在需求文档里没有写到?」
> - 是否有学生 `test.html` 打开是空白?提示使用「测试失败溯源」提示词
**进度同步 (Checkpoint): (4分钟)**
**师:** 你的测试脚本发现了几个 ❌?哪一个让你最意外?
【诊断点:学生是否发现了手动测试时没有发现的问题】【应用层】
**【分支A】若测试发现了手动没发现的 bug**
**师:** 这个 bug 你刚才手动玩的时候发现了吗?
**生:** 没有……
**师:** 这就是自动测试的价值——你没想到去测的地方,它帮你测了。想象一下,如果你发布了这个版本,别人玩到这个 bug 会怎么样?
**生:** 会觉得游戏有问题……
**师:** 但是因为你有测试脚本,你在发布之前就知道了。这就是「安全网」。
**【分支B】若测试全部通过**
**师:** 全部通过——说明你的需求文档写得很清楚AI 也执行得准确。但我再问一下:测试脚本测了几条?你的验收标准里有几条?
**生:** 测了5条文档里写了7条……
**师:** 有2条没有被测到。是不是那2条在生成测试脚本的时候AI 没找到对应的逻辑或者写得不够清楚没办法生成测试把那2条拎出来让 AI 单独补测。
**【分支C】若测试脚本打不开或全是错误**
**师:** 不要慌,这种情况很正常。我们用「最小化调试法」——不是让 AI 修整个测试脚本而是只请它「单独测一条最简单的规则比如消1行得100分」先让一条测试跑起来再逐步扩展。
---
**【分段四:有了安全网,放心魔改——第二版、第三版】(10分钟)**
*本段重点:建立「跑测试 → 确认安全 → 再改代码」的迭代习惯,鼓励学生大胆扩展*
**预设误概念:**
- 误概念:有了测试脚本,每次改完不用跑,「应该没问题」
- 误概念:第一版做到及格就行,没必要做第二版
**讲解与演示 (Teach & Demo): (3分钟)** **讲解与演示 (Teach & Demo): (3分钟)**
**师:** 接下来每个人做一个 3 分钟路演。但我们的路演跟一般的展示不一样——不是「看,我做了个游戏」,而是「我为什么这样设计」。 **师:** 你现在有了一个测试脚本。这个测试脚本是你的「安全网」。它的具体意义是什么?
【发放路演结构引导卡见第5节】
**师:** 路演要说三件事: **师:** 没有安全网的时候,你怕改出问题,所以不敢改太多。每次改完要自己手动测半天,万一改出新 bug 还得找半天。结果是——你不敢大胆加功能。
1. 我加了什么功能,这个功能是什么效果
2. 我的需求文档改了几版,每次改了什么
3. 遇到的最难的地方是什么,怎么解决的
**师:** 注意第二和第三点——这才是最有价值的部分。任何人都能展示一个游戏,但你经历了哪些思考和决策,是只有你有的 **师:** 有了安全网之后:每次改完代码,双击 `test.html`10秒钟看结果。全部 ✅,说明原有功能没被破坏,可以继续加。出现 ❌,马上知道哪里出问题,立刻修。这就是「安全网」的具体作用——让你敢放心改代码
**学生实践 (Practice): (7分钟)** **师:** 所以现在,有了安全网,我们才敢做更大胆的第二版。去做——比第一版更酷、更复杂。每加完一个功能,跑一遍测试,确认 ✅ 再继续加下一个。
学生准备路演,按三点结构整理思路(不需要写逐字稿,想清楚就行)。 **学生实践 (Practice): (5分钟)**
教师走动帮助学生回忆「需求文档改了几版」「哪里卡住了怎么解决的」。
1. 在第一版基础上继续扩展——加第二个功能,或者把第一个功能做得更完整
2. 每次生成新代码后,跑 `test.html`,确认没有破坏原有逻辑
3. 有余力的学生继续做第三版,目标:做出功能灵感清单里「⭐⭐⭐」的功能
> 教师走动鼓励学生跑测试发现有学生只靠手动玩来验证时提示「跑一遍测试脚本10秒就有答案比手动玩快多了。」
**进度同步 (Checkpoint): (2分钟)**
**师:** 做了第二版的举手。你加了什么?跑测试了吗?结果怎样?
【诊断点:学生是否形成「加功能 → 跑测试 → 检查结果」的迭代习惯】【应用层】
**【分支A】若学生说「跑了测试全部通过然后继续加」**
**师:** 这就是正确的迭代节奏。以后做任何项目都是这个节奏。
**【分支B】若学生说「加了功能但忘记跑测试」**
**师:** 现在跑一遍。——有没有 ❌?
**生:** (跑完结果)
**师:** 如果有 ❌,这就是「不跑测试的代价」——你可能在不知情的情况下,新加的功能破坏了原来的逻辑。
--- ---
**第三幕:反思 (Contemplate) — 10分钟** 🤔 **第三幕:反思 (Contemplate) — 10分钟** 🤔
**【环节】成果路演 (8分钟)** *本幕目标:通过路演和互评,让学生反思「测试覆盖」的本质,以及「需求文档质量 = 测试质量」的核心认知*
每位学生 3 分钟路演按人数调整6人小班每人约 1.5 分钟,重点展示设计决策)。 **【环节】成果路演 (7分钟)**
**师:** 每人路演后)你的需求文档改了几版?最关键的那次修改是什么? **师:** 每人1.5分钟路演6人小班。路演有引导卡大家拿一张。
【发放路演结构引导卡见第5节】
**【环节】互评 (2分钟)** **师:** 路演不是「打开游戏给大家看」——那只是演示,不是路演。路演要讲三件事:你加了什么功能、测试脚本发现了什么、你怎么修的。开始。
**师:** 刚才大家展示的游戏里,哪个功能的需求文档你觉得写得最清楚? (每人依次路演,教师计时。路演要点:)
**师:** 哪个功能的设计,是你之前没想到可以这样做的? - 展示最终版本游戏20秒演示
【诊断点:学生是否能评价「需求清晰度」,而不只是「游戏好不好玩」】 - 展示测试脚本的 ✅❌ 结果(告诉大家发现了几个 ❌)
- 重点讲:最意外的那个 ❌ 是代码问题还是需求问题?怎么修的?
- 一句话总结:「如果重来一次,需求文档哪里会写得不一样?」
> 教师观察:是否有学生的路演只说了「我加了炸弹方块」,但没有说测试相关的内容?提示:「测试脚本发现了什么?」
**【环节】互评与讨论 (3分钟)**
**师:** 刚才大家的路演里,哪一个测试脚本发现的问题最有价值?
**生:** (讨论)
**师:** 我们来做一个结构化互评——选一位刚才路演的同学,给他的作品说「一个你觉得做得好的地方」和「一个你觉得可以改进的地方」。
**生:** (互评,例如:「他的炸弹功能很酷,但是我觉得爆炸范围可以更大一点」)
**师:** 「更大一点」——具体怎么大?你能告诉他怎么写到需求文档里吗?
**生:** 尝试表达「爆炸范围从3×3改成5×5」
**师:** 这就是一条可测试的需求改进建议。他可以把这条加到需求文档里,然后让 AI 重新生成。
**师:** 有没有人的测试结果全是 ✅,但路演时我们发现游戏还是有问题?
**生:** (可能有学生举手)
**师:** 这说明什么?
**生:** (预期:测试没有覆盖到那个情况……)
**师:** 对。测试只能发现「你写进需求文档的问题」。你没写的,它不知道要测。这就是为什么需求文档要尽量详细——需求文档越完整,测试覆盖越全,漏掉的 bug 越少。
**师:** 给今天的工作做一个评分——需求文档 100 分、测试脚本 100 分、游戏功能 100 分,你自己的三项各给多少分?
(让学生自评,不需要出声,心里有数就好)
**师:** 哪位愿意说说自己的分?
**生:** 分享例如「需求文档70测试80功能90」
**师:** 三项里面,哪一项你觉得下次会做得更好?
**生:** (分享)
**师:** 记住这个答案。这就是你下一个项目要重点改进的地方。
--- ---
@@ -225,23 +412,44 @@
**【环节】抽象总结 (3分钟)** **【环节】抽象总结 (3分钟)**
**师:** 两节课,你们从零做了一个俄罗斯方块,还加了自己设计的功能。我们用的核心方法是什么 **师:** 两节课,我们走了一个完整的流程。谁来说说是哪几步
**生:** 写需求文档,让 AI 做,然后验收,有问题就改需求文档…… **生:** Plan Mode → 需求文档 → 审核 → 生成 → 测试 → 修复……
**师:** 对。这个流程有个名字:需求 → 设计 → 实现 → 验收 → 迭代。这是真实的工程师每天都在做的事情。
**师:** 你们今天做的,跟真实的产品开发,步骤上是完全一样的。
**师:** 最后一个问题:如果今天你的功能需求文档第一版就做对了,是因为你运气好,还是需求写得好? **师:** 对。这个流程有个名字:需求 → 实现 → 测试 → 修复。真实的工程师每天都在走这个循环。
**** 需求写得好? **** 今天最重要的一句话:**「测试通过才算完成,不是能玩就算完成。」**
**师:** 对。运气不可靠,清晰的需求才可靠。这是今天最重要的一句话 **师:** 能玩是 60 分,测试通过是 90 分,需求文档里每一条都有测试覆盖是 100 分
**【环节】下节预告 (2分钟)** **师:** 还有一件事——今天你们体验了什么是「安全网」。这不只是测试脚本的概念,这是一种工作方式:在做危险操作之前,先建立一个保护机制。以后你们做任何项目,都可以问自己:「我有没有安全网?」
**师:** 接下来的课程,你们会进入更大的项目。今天学到的方法——需求文档、压力测试、结果溯源——会一直用到。你们已经会了,后面只是把项目变得更复杂。 **师:** 最后,我想让你们想一个问题——今天学到的「需求文档 → 测试 → 修复」这个流程,除了做游戏,还能用在哪里?
**生:** (讨论:做网站?做 App……
**师:** 对。你们今天走的这个流程,和真实的工程师在公司里走的流程是一样的。只不过他们的项目更大、需求文档更长、测试脚本更复杂。但方法是一样的。
**【环节】下节预告 + 5分钟挑战 (2分钟)**
**师:** 接下来的课程你们会做更大的项目。今天学到的——Plan Mode、需求审核、自动测试——会一直用到只是项目变得更复杂。你们已经有了完整的工具箱。
**师:** 本周5分钟AI挑战找到你需求文档里一条没有被测试覆盖到的规则把它补进需求文档然后让 Kimi 给测试脚本加上这条测试,跑一下看是 ✅ 还是 ❌。下节课说出你补的是哪条规则,测试结果是什么。
--- ---
### 5. AI 助教使用指南 ### 5. AI 助教使用指南
**⚠️ 新窗口使用规则(必须遵守):**
```
本课四个阶段的窗口规则:
窗口 A写增量需求文档整理新功能需求用这个窗口
窗口 B需求审核全新窗口——AI 不会承认自己写的有问题)
窗口 C执行生成全新窗口——基于原代码增加新功能
窗口 D生成测试脚本全新窗口——让新 AI 客观测试)
核心原则:审核、测试必须换新窗口。在同一个窗口里又写又审
又测叫「上下文污染」——AI 会偏向为自己生成的内容辩护,
找不出真正的问题。
```
**增量需求文档模板(学生填写):** **增量需求文档模板(学生填写):**
``` ```
@@ -261,58 +469,139 @@
- 与得分的交互:[这个功能会影响得分吗?怎么影响?] - 与得分的交互:[这个功能会影响得分吗?怎么影响?]
- 与游戏结束的交互:[游戏结束时,这个功能有没有特殊处理?] - 与游戏结束的交互:[游戏结束时,这个功能有没有特殊处理?]
## 验收标准 ## 验收标准(每条都要能自动测试)
[我怎么测试这个功能做对了列出2-3条可以测试的情况] 1. [情况] → [预期结果]
2. [情况] → [预期结果]
3. [情况] → [预期结果]
``` ```
**「增量生成」提示词:** **「增量生成」提示词:**
``` ```
我已经有一个俄罗斯方块游戏(代码在下面)。 我已经有一个俄罗斯方块游戏(代码在下面)。
现在需要在这个基础上增加一个新功能,需求如下: 现在需要在这个基础上增加一个新功能,需求如下:
[粘贴你的增量需求文档] [粘贴增量需求文档]
要求: 要求:
1. 在已有代码基础上增加这个功能,不要改变原有功能的行为 1. 在已有代码基础上增加,不要改变原有功能的行为
2. 严格按照需求文档实现,不要添加文档里没有提到的内容 2. 严格按照需求文档实现,不要添加文档里没有提到的内容
3. 输出完整的 HTML 文件(内联 CSS 和 JS 3. 核心逻辑函数collides、clearLines、rotate 等)必须保持独立,
不要把逻辑和渲染混在一起(方便后续生成测试脚本)
4. 输出完整的 HTML 文件(内联 CSS 和 JS
已有代码: 已有代码:
[粘贴你的游戏 HTML 代码] [粘贴游戏 HTML 代码]
``` ```
**功能灵感参考清单(学生没有想法时参考):** **「生成测试脚本」提示词(本课核心):**
| 功能 | 一句话说明 |
|------|---------|
| 炸弹方块 | 特殊方块,落地后炸掉周围一圈 |
| 速度加成方块 | 碰到就让下落速度加快 5 秒 |
| 冻结方块 | 碰到就让当前方块暂停 3 秒不下落 |
| 彩虹模式 | 消行的时候屏幕闪一下彩虹色 |
| 双人模式 | 两个人在同一个游戏区域交替操控 |
| Boss 行 | 每隔 10 行出现一行不能被普通消行消掉的「Boss 行」 |
| 方块预言家 | 显示接下来 3 个方块而不只是 1 个 |
| 重力反转 | 消 4 行一次触发 5 秒重力反转,方块往上飞 |
**路演结构引导卡(课前发给学生):**
``` ```
的路演3分钟 有一个俄罗斯方块游戏,代码如下:
1. 我加了什么功能30秒 [粘贴游戏 HTML 代码]
我的需求文档如下:
[粘贴需求文档]
请帮我生成一个独立的 test.html 文件,要求:
1. 把游戏的核心逻辑函数(碰撞检测、消行、得分计算等)提取出来
2. 根据需求文档里的每一条规则,编写一个对应的测试
3. 每个测试构造一个具体的场景,调用函数,验证结果是否符合需求
4. 在页面上显示测试结果:✅ 通过 / ❌ 失败(失败时说明期望值和实际值)
5. 不需要任何外部依赖,双击 test.html 就能在浏览器里运行
重点测试的边界条件:
- 方块在左/右边界的碰撞
- 底部锁定
- 消1行/2行/4行的得分是否符合需求文档
- 消行后上方积木是否正确下移
- 游戏结束条件
- [新功能]的触发条件和结果
```
**测试脚本示例输出(教师用于课堂展示):**
```
俄罗斯方块测试结果
───────────────────────────────────
✅ 消1行得分 100 分 通过
✅ 消2行得分 300 分 通过
✅ 消4行Tetris得分 800 分 通过
✅ 游戏结束条件(积木超出顶行) 通过
❌ 炸弹方块落地得分 失败
期望50 实际0
→ 提示clearBomb() 函数未返回得分
✅ 消行后积木正确下移 通过
❌ 炸弹 + 消行叠加得分 失败
期望15050+100 实际100
→ 提示:爆炸触发消行时,炸弹得分未累加
───────────────────────────────────
6/8 通过 2/8 失败
```
**「测试失败溯源」提示词:**
```
我的测试脚本显示这个测试失败了:
测试名称:[测试名称]
期望结果:[期望值]
实际结果:[实际值]
我的需求文档里关于这条规则写的是:
[粘贴相关需求]
请帮我分析:
1. 是代码实现有问题?还是需求文档没有说清楚?
2. 如果是代码问题,具体是哪里需要修改?
3. 如果是需求问题,我需要在需求文档里补充什么?
```
**测试失败溯源示例对话:**
```
学生输入:
测试名称:炸弹方块落地得分
期望结果50
实际结果0
需求文档写的是:炸弹方块落地时,播放爆炸动画,得分+50
AI 回答:
这是代码问题。你的需求文档写得很清楚——「落地得分+50」。
问题出在 clearBomb() 函数:当前函数只处理了爆炸效果,
没有调用得分更新逻辑。建议修改:
在 clearBomb() 函数末尾加一行 score += 50 并调用 updateScore()。
```
**功能灵感参考清单:**
| 功能 | 复杂度 | 一句话说明 |
|------|--------|---------|
| 炸弹方块 | ⭐⭐ | 落地后炸掉周围3×3范围 |
| 速度加成方块 | ⭐⭐ | 碰到就让下落速度加快5秒 |
| 冻结方块 | ⭐⭐ | 碰到就让当前方块暂停3秒 |
| 彩虹消行 | ⭐ | 消行时屏幕闪彩虹色 |
| 方块预言家 | ⭐⭐ | 显示接下来3个方块 |
| Boss 行 | ⭐⭐⭐ | 每隔10行出现一行无法普通消除的行 |
| 重力反转 | ⭐⭐⭐ | 消4行触发5秒方块往上飞 |
**路演结构引导卡(路演前发给学生):**
```
我的路演每人约1.5分钟)
1. 我做了几个版本加了什么功能20秒
→ 演示给大家看 → 演示给大家看
2. 我的需求文档改了几版(1分钟 2. 需求文档改了几版(20秒
第一版写完之后,AI 问了我什么让我意外的问题? → AI 审核问了我什么让我意外的问题?
→ 我改了哪里?改完之后结果有什么变化?
3. 最难的地方1分钟 3. 测试脚本发现了什么30秒
我遇到的最难描述清楚的规则是什么 发现了几个 ❌
→ 最后是怎么写清楚的? → 最意外的 ❌ 是什么?是代码问题还是需求问题?怎么修的?
→ 有没有发现新功能让原来的功能出了问题?怎么解决的?
4. 如果重来一次(30秒 4. 如果重来一次(10秒
→ 需求文档哪里会写得不一样? → 需求文档哪里会写得不一样?
``` ```
@@ -322,35 +611,54 @@
**本课技术备注:** **本课技术备注:**
增量开发策略:把已有的 HTML 代码粘贴给 Kimi让它在基础上增加功能比重新生成稳定得多。如果 Kimi 倾向于重写整个游戏,在提示词里加「请务必基于我提供的代码修改,不要重新生成整个游戏」 测试脚本的原理:游戏的核心逻辑函数(`collides``clearLines``rotate`)是纯函数——给定输入,返回确定的输出,不依赖渲染。测试脚本把这些函数复制出来,构造特定的棋盘状态作为输入,调用函数,然后检查输出是否符合需求文档的描述。整个过程在浏览器里运行,不需要任何安装
功能复杂度控制:炸弹方块、速度加成、冻结方块是低复杂度功能,适合大多数学生。重力反转、双人模式是高复杂度功能,适合进度快的学生挑战。教师可以根据学生能力在发清单时口头引导 需求文档与测试的关系测试脚本的质量取决于需求文档的质量。需求文档里写「消行得分」但没写具体数字AI 就无法生成有意义的测试。这是本课的核心教学点——需求越具体,测试越有效
增量需求文档的代码实现原理教师需要了解增量功能是在已有代码的基础上「插入」新逻辑而非替换。Kimi 在生成增量代码时会查找合适的插入位置(比如 `clearLines()` 函数内部并添加新逻辑。如果原有代码结构混乱逻辑和渲染混在一起AI 会很难找到插入点导致生成失败。这就是为什么第6课的「增量生成」提示词里要求「核心逻辑函数保持独立」。
**常见问题 FAQ** **常见问题 FAQ**
| 问题 | 应对 | | 问题 | 应对 |
|------|------| |------|------|
| 「加了新功能之后原来的功能坏了」 | 检查需求文档里「与原有功能的交互规则」写了什么;如果没写,补上去重新生成 | | 「测试脚本打开是空白页」 | 检查 HTML 文件是否完整;让 Kimi 检查生成的代码有没有语法错误 |
| 「Kimi 加功能之后把整个游戏重写了,之前的功能不见了」 | 提示词里强调「在已有代码基础上修改」;或者把原有代码中关键的函数名告诉 Kimi要求保留 | | 「测试全部 ❌」 | 很可能是函数提取时出了问题;让 Kimi「只提取 clearLines 函数写一个最简单的测试」,逐步调试 |
| 「我想要的功能 AI 做不出来」 | 先检查需求文档是否足够具体;可以要求 AI「只实现这一个功能不要改其他任何部分」确实做不到的记录进 Backlog 留待下次 | | 「测试全部 ✅ 但游戏还是有 bug」 | 引导学生找到:哪条 bug 对应的规则,在需求文档里没有写到?这就是测试覆盖不足 |
| 「路演不知道说什么」 | 引导卡上的4个问题逐一回答就够了重点提醒「不需要说得很完整说你觉得最有意思的那件事就行」 | | 「Kimi 生成的测试脚本太复杂看不懂」 | 「你不需要看懂每一行代码,只需要看 ✅❌ 结果和失败原因」;理解结果比理解实现更重要 |
| 「时间不够,功能还没做完」 | 没关系,路演时说「我加了什么功能、需求文档写到第几版、还差什么没完成」也是完整的路演——这本身就是真实工程师的日常 | | 「测试失败但不知道是代码问题还是需求问题」 | 使用「测试失败溯源」提示词,让 AI 帮你判断 |
| 「AI 审核问了太多问题,不知道怎么回答」 | 让学生先回答最好回答的那一条,其他的暂时写「待定」,先生成第一版,跑测试的时候再补 |
| 「增量生成后原有消行功能不见了」 | 说明 Kimi 误修改了原有逻辑;把原版代码重新提交,强调「不要改变原有功能的行为」 |
| 「验收标准不知道怎么写」 | 用「情况 → 预期结果」句式套一遍:「[什么情况] → [应该看到什么]」;如果写不出来,说明还没想清楚这条规则 |
| 「第二版加完功能跑测试有 ❌,但不知道是新功能的问题还是旧功能出了问题」 | 先把新功能注释掉,跑测试,确认旧功能全部 ✅;再把新功能打开,重新跑,锁定是新代码引入的问题 |
| 「路演超时怎么办」 | 路演前说清楚:演示只需要展示「最酷的那一个功能」,不需要演示全部;测试部分只说「最意外的 ❌」 |
**课堂节奏控制:**
- 分段一15分钟最容易超时的环节是「AI 审核」——有学生会和 AI 进行很长的对话。给学生一个限制AI 审核最多回答3轮问题然后就进入执行。
- 分段二结束时,学生手动测完后,教师主动提问「你需求文档里有几条规则,你测了几条」——制造认知冲突,让学生自己感受手动测试的不完整性,再引出自动测试。
- 分段三的核心是「看懂 ✅❌ 结果并溯源」,不是「让每个学生都把测试写完」。进度慢的学生只要跑出测试结果、理解一个 ❌ 的原因即可。
- 分段四时间有限,对于进度慢的学生,「跑一遍测试确认第一版没问题」就算达标,不强求做第二版。
- 路演时间第三幕要严格控制在1.5分钟/人。如果有学生的功能特别复杂,教师可以帮助提炼:「你只需要说最意外的那个 ❌,其他的可以跳过。」
**课堂风险预案:** **课堂风险预案:**
- 如果多名学生选了同一个功能(比如炸弹方块):利用这个机会对比两个人的需求文档——相同的功能,两份需求文档有什么不同?最终生成的结果有什么不同?这是很好的教学素材 - 如果 Kimi 生成的测试脚本无法运行:教师用提前准备好的演示版本讲解概念,让学生理解「测试脚本是什么、能发现什么问题」即可,不强求每人都跑成功
- 如果某个学生的功能过于复杂(比如联机对战):引导学生做「最小可行版本」——先只实现最核心的规则,其余的加进 Backlog下次再加 - 如果学生进度差异大:进度快的学生尝试「给测试脚本加更多边界条件测试」;进度慢的学生重点完成「生成第一版 + 跑一次测试 + 理解结果」
- 如果网络不稳定导致 Kimi 响应慢:提前在教师电脑上缓存一份完整的增量生成 + 测试脚本生成示例,用于离线演示。
--- ---
### 7. 5分钟日常AI挑战 ### 7. 5分钟日常AI挑战
**本周挑战:** 再加一个功能,或者把这次没做完的功能完成 **本周挑战:** 给你的游戏补一条没有被测试覆盖的规则
**挑战说明:** 用同样的方法——写增量需求文档、压力测试、生成、验收——自己在家给游戏再加一个功能(或者完成这节课没完成的部分)。下节课展示 **挑战说明:** 找到你需求文档里一条没有被测试脚本覆盖到的规则,把它补进需求文档,然后让 Kimi 给测试脚本加上这条测试,跑一下看是 ✅ 还是 ❌
**下节课分享:** 选 2-3 位同学展示在家加的功能,重点说「需求文档是怎么写的」 **下节课分享:** 说出你补的是哪条规则,测试结果是什么
--- ---
### 8. 拓展任务 ### 8. 拓展任务
**拓展一(推荐):** 你的游戏加一个「暂停功能」——按 P 键暂停,再按 P 键继续;暂停时显示一个半透明遮罩 **拓展一(推荐):** 你的测试脚本覆盖所有需求文档里的规则——对照需求文档逐条检查,补上缺少的测试。目标:测试脚本的条数 = 验收标准的条数。
**拓展二(挑战):** 给你的游戏写一份「完整的 Level 0 + Level 1 需求文档合并版」——把上节课的需求文档和今天的增量需求文档合并成一份完整的文档,让任何人读了都能理解整个游戏的所有规则
**拓展二(挑战):** 在需求文档里加一个你自己想出来的「边界情况」,写清楚预期结果,让 AI 生成对应的测试看游戏能不能通过。参考方向「同时消4行 + 炸弹爆炸」这种极端叠加情况的得分是多少?
**拓展三(超级挑战):** 试着把你的增量需求文档写得让另一个同学能看懂——交换给同桌,让他/她用你的需求文档,在他/她的游戏上生成同样的功能。看看你的需求文档清不清楚。

View File

@@ -0,0 +1,602 @@
---
课时: 8
主题: 涂鸦PK— 画图工具 + 角色设计
核心能力: [拆解力, 审美力]
核心工具: [Trae IDE, Kimi]
时长: 90分钟
透明化层级: 过程层
适用路线: AICODE-06
---
### 1. 课程目标
**知识目标:**
- 理解「精准需求 > 冗余需求」:需求文档的价值在于可测试性,不在于篇幅长短
- 理解「新窗口审核」的本质:不同上下文的 AI 才能发现你看不见的漏洞
- 理解角色属性是「打法定位」的策略设计,不是随意填写的数字
**能力目标:**
- 能用 Plan Mode 三步窗口A整理→窗口B审核→窗口C执行完成画图工具的需求文档拆解力
- 能用自己生成的画图工具画出两帧角色帧1待机 + 帧2攻击差异度清晰可见审美力
- 能根据20分预算制选定自己的打法定位合理分配属性点拆解力
**情感目标:**
- 建立「工具是自己做的」的自豪感——不是用别人的软件,而是先造工具再用工具
- 发现「像素画」的审美乐趣小小的64×64格子里也能表达角色个性
- 体验「打法定位」带来的策略感:我的角色是有设计意图的,不是随便画的
---
### 2. 核心概念与误概念预设
**核心概念认知层级:**
| 概念 | 学生类比 | 认知层级 |
|------|---------|---------|
| Spritesheet精灵图集 | 动画书里的一页纸上排了多幅连续图快速翻动就成了动画Spritesheet 就是把所有帧拼成一张图,程序按顺序裁取每一帧 | 识别层 |
| 帧Frame | 动画里每一张静止的画面;两帧之间的差异越大,动画的动感越强 | 识别层 |
| 属性预算制 | 创建角色时只有20个技能点花在HP多了ATK就少——这就是策略不是加法题 | 理解层 |
| 需求可测试性 | 「字要好看」不可测试「字体大小16px、颜色白色」可以测试。只有写得出来测试步骤的需求才是写清楚了的需求 | 应用层 |
| 打法定位 | 篮球队里有中锋/控卫/得分手,分工不同;坦克型靠血厚扛伤害,刺客型靠高攻一击必杀——每种打法都有对应的属性分配 | 应用层 |
**典型误概念表:**
| 编号 | 误概念 | 正确认知 | 激发策略 |
|------|--------|---------|---------|
| M1 | 需求写得越多越好,越详细越安全 | 冗余需求会制造更多歧义和 bug精准的需求才是关键——每条需求都要能写出测试步骤 | 展示一份100行需求文档生成的混乱代码 vs 精准15行需求生成的干净代码问「这条需求你怎么测试它对不对」 |
| M2 | 让 AI 直接写代码更快,需求文档浪费时间 | 没有需求文档,验收时你不知道从哪里查;出了 bug 无法溯源,只能靠直觉瞎改 | 问:「如果 AI 做出来画布不是64×64你怎么知道是哪条需求没说清楚」 |
| M3 | 需求审核就是自己再读一遍 | 新窗口审核:上下文不同的 AI 能发现你看不见的漏洞;自己审等于让同一个人既出题又判卷 | 演示:把需求文档发给同一窗口的 AI它说「没问题」发给新窗口它提了3个你没想到的问题 |
| M4 | 两帧画一样就行,动画不重要 | 帧1待机姿势 + 帧2攻击姿势差异越大动画越有冲击力帧2完全复制帧1等于没有动画 | 打开 demo-3-animation展示两帧相同 vs 帧2明显前冲的动画对比让学生自己感受差距 |
| M5 | 属性随便填,反正都是数字 | 属性是角色的「打法定位」这是策略设计不是填表格20分预算是有限资源怎么分配决定了这个角色的战斗风格 | 类比:篮球队里有中锋/控卫/得分手,不是每个人都练同一个技能;问「你是想先手打爆对方,还是慢慢耗死对方?」 |
---
### 3. 教学准备
**工具与环境:**
- 每台电脑已登录 Kimi网页版网络正常
- 每台电脑装有 Trae IDE可以创建 HTML 文件并用浏览器预览
- 教师电脑准备好 demo-pk 目录下的两个 demo 文件:
- `demo-1-draw-tool.html`(完整画图工具,备用展示)
- `demo-3-animation.html`角色动画展示Connect 环节使用)
**教学资源:**
- 教师准备:打开 `demo-3-animation.html`,导入一个提前画好的临时涂鸦角色 Spritesheet用于 Connect 导入
- 教师准备「画图工具需求文档模板」文字版见第5节投屏展示
- 教师准备一份「属性分配表」展示四种打法定位见第5节
- 学生资源:上节课完成的俄罗斯方块作品(精神延续,不需要文件)
**教师备课体验任务:**
> 备课前,教师必须亲自完成以下操作:
>
> 1. 走完完整的 Plan Mode 三步用窗口A整理画图工具需求文档 → 窗口B审核记录 AI 问了哪些你没想到的问题)→ 窗口C生成画图工具
> 2. 用生成的画图工具画一个角色帧1待机 + 帧2攻击感受64×64像素格子的限制和乐趣
> 3. 故意画一个「两帧完全相同」的角色,导出预览,体验「没有动画感」的效果——备用作为 M4 反例
> 4. 验证 `demo-3-animation.html` 的导入功能是否正常,准备一张 128×64 的 Spritesheet PNG
---
### 4. 教学流程
---
**第一幕:联系 (Connect) — 10分钟** 🔗
*本幕目标:用上周挑战回顾激活工程思维记忆;用 demo-3-animation 的动画展示制造「哇!」的感受,建立今天的项目目标*
**【环节】上节课挑战回顾 (3分钟)**
**师:** 上节课我布置了一个5分钟挑战——给测试脚本补一条没有被覆盖的规则。谁补了补了什么
**生:** (预期 A我补了一条测试「方块碰到左边界不能出去」
**师:** 具体怎么补的?你是发现了哪条规则测试脚本没写?
**生:** (预期:测试脚本里没有检查边界,我就加了一个模拟按键到最左边的测试)
**师:** 很好——你不只是「加了一条」,你先发现了漏洞,再去补。这就是工程师的思维方式:不是随便加,而是找到没覆盖的地方补上去。
【诊断点:学生能否清晰描述「我发现了什么漏洞 → 我用什么方法补上去了」,而不只是「我加了一条测试」】【理解层】
**【分支A】若学生能说出具体的漏洞和补法**
**师:** 这条补得很到位。以后做任何项目,验收脚本里「没写到的地方」就是最容易漏 bug 的地方。
**【分支B】若学生说「我没有补不知道怎么补」**
**师:** 没关系,今天我们马上会用到同样的思路——先写需求,需求就是验收的标准。你今天会自己搞清楚这个流程。
**【分支C】若没有学生做了挑战**
**师:** 好,我来快速过一遍——测试脚本的核心作用是什么?
**生:** (预期:验证游戏规则有没有做对)
**师:** 对。「没被覆盖的规则」就是「测试脚本没想到的情况」。记住这个概念,今天会继续用到。
---
**【环节】情景导入 (7分钟)**
**师:** 上节课我们用 Plan Mode 完成了俄罗斯方块。今天我们开启一个全新项目——涂鸦PK。
**师:** 先不解释「PK」是什么我先让你们看一个东西。
【投屏打开 demo-3-animation.html】
**师:** 这是一个角色动画展示器。你们看——这两个角色在做什么?
**生:** (预期:在动!/ 一个在走路,一个在打架)
**师:**这是两帧动画——帧1是待机状态帧2是攻击状态快速交替播放就有了动感。但是这两个角色是我临时画的我花了大概5分钟。
**师:** 现在,我要把这个临时角色换成你们自己画的角色。
【教师演示导入提前准备的临时涂鸦 Spritesheet动画播放起来】
**师:** 这个动画里的角色,是用一个 HTML 画图工具做的。这个画图工具——是我们今天自己写出来的。
**师:** 今天的任务,两件事:第一,用 Plan Mode 做出一个画图工具第二用这个工具画你自己的战斗角色——帧1待机帧2攻击。
**师:** 上节课用的 Plan Mode 三步,还记得吗?
**生:** 预期记得窗口A整理需求窗口B审核窗口C执行
**师:** 对,一模一样的流程。只是这次做的不是游戏,而是工具。先造工具,再用工具。
【诊断点:学生是否能自己说出 Plan Mode 三步,而不是被教师提示】【识别层】
---
**第二幕:建构 (Construct) — 65分钟** 🛠️
*本幕目标:完整走完 Plan Mode 三步生成画图工具;逐条验收画图功能;用生成的工具画出两帧角色并填写属性 JSON*
---
**【分段一Plan Mode — 画图工具需求文档】(25分钟)**
*本段重点引导学生从「我要做什么」出发用窗口A整理需求文档再用窗口B新窗口审核工程师发现漏洞补充完整后准备提交*
**预设误概念:**
- 误概念 M1需求写得越多越好越长越安全
- 误概念 M2直接让 AI 写代码更快,不需要文档
- 误概念 M3需求审核就是自己再读一遍不用开新窗口
**讲解与演示 (Teach & Demo): (8分钟)**
**师:** 开始 Plan Mode 第一步——整理需求。我们要做的画图工具有哪些功能?先不急着写,我们来想一想,这个工具要能做什么。
**师:** 用这个画图工具,最后要交出一张图。这张图有什么要求?
**生:** (预期:要是方块组成的 / 要有两帧 / 颜色要能选)
**师:** 我来把这些需求点梳理一下一共6个部分。你们跟着我一起确认每个部分的内容然后我们用 AI 整理成规范文档——
**师:** 第一部分——画布大小。我们用 64×64 像素,但显示的时候放大 8 倍显示,这样格子就有 512×512 的大小,不会太小看不清。
**师:** 第二部分——工具。需要三个:画笔(点击涂色)、橡皮(把像素变透明或白色)、填充桶(把一片同色区域一起换色)。
**师:** 第三部分——调色板。至少 16 种颜色,支持自定义颜色选择器。
**师:** 第四部分——多帧编辑。这是最关键的——我们有两帧帧1是待机姿势帧2是攻击姿势可以切换编辑而且可以把帧1复制到帧2作为修改起点。
**师:** 第五部分——动画预览。在工具里就能看到两帧切换的动画效果,不用每次都导出去看。
**师:** 第六部分——导出。生成一张 128×64 的 PNG 图片,两帧横向拼在一起,就是 Spritesheet。
**师:** 这 6 个部分,每一个都有一个关键问题:「怎么测试它做对了?」这叫需求可测试性。比如「画布 64×64 像素」——怎么测试?
**生:** (预期:在画布上画一条线,看有没有 64 格 / 在画布最边上点看坐标是不是6363
**师:** 对,能说出测试步骤,说明需求写清楚了。如果是「颜色要好看」——怎么测试「好看」?
**生:** (停顿)……好像测不了……
**师:** 对,「好看」不可测试。所以需求文档里不能出现「好看」「流畅」「方便」这类词。要写具体的、能测试的描述。
【理解层:建立「需求可测试性」的直觉,澄清 M1 误概念】
**师:** 现在看一下今天用的三步流程——
**师:** 窗口A——打开 Kimi把这6个需求要点告诉 AI请它整理成规范的需求文档五部分格式功能描述/触发条件/功能规则/交互规则/验收标准)。
**师:** 窗口B——全新 Kimi 对话,把文档发给「审核工程师」,让它只问问题不给答案,找出所有描述不清楚的地方。
**师:** 窗口C——再新一个 Kimi 对话,把补充完整的需求文档发给它,生成画图工具的 HTML 代码。
**师:** 我来演示窗口A这一步。
【教师打开 Kimi用保底提示词一见第5节提交投屏展示 AI 整理出来的需求文档结构】
**师:** 你看 AI 给我整理出来的格式——每个功能都有「功能规则」和「验收标准」。你们对比一下AI 加的这些验收标准,我在口头说的时候有没有提到?
**生:** (预期:有几条我没说 / 填充桶的那条我没想到边界情况)
**师:**AI 帮我补了一些我没说清楚的细节。但注意——AI 整理的内容可能有你不想要的,也可能漏掉你想要的。整理完之后要自己过一遍,确认每条都是你真正想要的。
【应用层学生开始区分「AI 帮我补充的」和「我自己想要的」】
**学生实践 (Practice): (12分钟)**
学生操作:
1. 打开 Kimi新建对话窗口A
2. 根据教师给出的保底提示词一,补充自己想要的特殊需求,提交
3. 拿到 AI 整理的需求文档,自己读一遍,圈出「这条我没提到的」和「这条说法我不认可的」
4. 打开新的 Kimi 对话窗口B把文档和审核提示词一起提交
> 教师走动观察重点:
> - 学生在窗口A拿到文档后有没有真的读了一遍还是直接复制粘贴给窗口B「读一遍」是诊断这步的关键行为。
> - 窗口B的 AI 问了哪些问题?有没有学生对某个问题感到惊讶——「这个我真的没想过」是好信号。
> - 是否有学生在同一个对话窗口里同时做整理和审核?这是 M3 误概念的表现,需要介入。
**进度同步 (Checkpoint): (5分钟)**
**师:** 窗口B审核出来了AI 问了你哪个最意外的问题?谁来说一下?
**生:** (预期 A它问我「橡皮是把像素变透明还是变白色」我没想过这两种不一样
**师:** 好——这两种有什么区别?
**生:** (预期:透明就是真的没颜色,白色就是变成白色格子)
**师:** 对,区别很重要。如果背景是黑色的,你用橡皮想抹掉一块,变成「白色」就是留了个白点,变成「透明」才是真正删掉。你准备选哪个?
**生:** 透明!
**师:** 那就在你的需求文档里补上「橡皮擦除后像素变为透明alpha=0」。
【诊断点:学生能否说出一条「因为 AI 审核问了这个问题我补充了这条描述」而不是「AI 问了很多,我不知道填哪些」】【理解层】
**【分支A】若学生能说出具体补充了什么**
**师:** 很好。需求每补充一条,代码出问题的概率就降低一点点。这就是 Plan Mode 的价值。
**【分支B】若学生说「AI 问了很多但我觉得都不重要」:**
**师:** 好,拿出来看看。最意外的那条是哪个?我们一起判断——这条如果不补,代码生成出来会发生什么?
(引导学生预测后果,建立「需求漏洞→代码行为异常」的因果感)
**【分支C】若学生在同一窗口做了整理和审核**
**师:** 我注意到你在同一个对话框里做的审核。还记得上节课说的「上下文污染」是什么意思吗?现在再开一个全新对话,把文档重新发过去,对比一下这次 AI 问的问题和刚才有没有不同。
---
**【分段二窗口C生成画图工具 → 逐条验收 → 溯源修复】(20分钟)**
*本段重点:提交需求文档生成画图工具;建立「按需求逐条验收」的习惯;遇到问题先溯源需求再修改*
**预设误概念:**
- 误概念 M2直接改代码比溯源需求更快
- 误概念:看起来能用就算完成,不需要逐条验收
**讲解与演示 (Teach & Demo): (3分钟)**
**师:** 需求文档补完了进入窗口C——提交生成。把补充完整的需求文档粘贴进新的 Kimi 对话,加上保底提示词三,提交。
**师:** 生成完之后,你会拿到一段 HTML 代码。把这段代码复制出来,在 Trae 里新建一个 `draw-tool.html` 文件,粘贴进去,保存,用浏览器打开。
**师:** 打开之后——不要急着画。先做一件事:用你的需求文档,逐条验收。验收方式:你需求文档里写了什么,就测什么。
**师:** 我给你们一个最快的验收清单——五条最关键的:
【投屏展示验收清单】
```
1. 画笔工具:选颜色,点击画布能涂色
2. 橡皮工具:点击有颜色的格子,变为透明/白色
3. 填充桶:点击一片同色区域,整体换色
4. 多帧切换点「帧1」「帧2」Tab画的内容各自独立
5. 动画预览:点播放,两帧交替显示
```
**师:** 这五条,每条真的去操作一次,不能只「看起来好像对」。有一条不通过就标叉,写下「实际看到了什么现象」。
【应用层:建立「验收 = 逐条测试 + 记录现象」的习惯】
**学生实践 (Practice): (13分钟)**
学生操作:
1. 在 Trae 新建 `draw-tool.html`,粘贴 AI 生成的代码,浏览器打开
2. 按五条验收清单逐条测试,记录「通过✓」或「不通过✗ + 实际现象」
3. 遇到「不通过」先不改代码——找回需求文档,找到是哪条需求没说清楚,标注出来
4. 把找到的漏洞描述清楚(「步骤→预期→实际」格式),让 AI 针对性修复
> 教师走动观察重点:
> - 有没有学生遇到 bug 直接问 AI「帮我改这个 bug」而没有先溯源需求主动走过去问「你知道是哪条需求没说清楚吗
> - 有没有学生卡在代码生成失败(如 AI 输出不完整)?准备好备用的 demo-1-draw-tool.html但先引导学生尝试重新生成
> - 验收清单有没有学生不知道「如何测试填充桶」?示范:先用画笔画一个封闭区域,再用填充桶点内部
**进度同步 (Checkpoint): (4分钟)**
**师:** 五条验收,全部通过的举手。
**师:** 好。有没有人验到「不通过」的?说一下你发现了什么。
**生:** (预期 A帧1和帧2切换但画的内容是共用的切换没效果
**师:** 好,你溯源到需求文档了吗?哪条需求没写清楚?
**生:** (预期:我写了「可以切换帧」,但没写「两帧的数据是独立存储的」)
**师:** 对,这就是漏洞。「切换帧」和「数据独立」是两件事,你只说了一件。现在补上这条,用「步骤→预期→实际」格式描述给 AI让它修复。
【诊断点:学生能否从「出了 bug」出发溯源到「是哪条需求没写清楚」而不是直接「AI 帮我改一下」】【应用层】
**【分支A】若学生完成了溯源并修复了 bug**
**师:** 这是真正的工程师思维——不是改代码,是改需求,然后重新生成。
**【分支B】若学生直接把代码发给 AI 说「帮我改」:**
**师:** 先停一下。你现在让 AI 改的是代码,但 AI 不知道你要的是什么效果。你能不能先找到需求文档里「两帧切换」那一条,把它描述得更清楚再提交?
**【分支C】若 AI 生成的代码完全无法运行(语法错误):**
**师:** 遇到这种情况,先用「步骤→预期→实际」描述给 AI「我打开 HTML 文件,浏览器显示空白/报错,期望看到画图工具界面」。让 AI 自己排查哪里有问题。如果还不行,可以用老师电脑上的备用 demo 继续后面的步骤——画角色才是今天更重要的部分。
---
**【分段三用工具画角色帧1待机 + 帧2攻击+ 填属性JSON】(20分钟)**
*本段重点用画图工具完成角色设计理解两帧差异的审美原则用20分预算制选定打法定位并填写属性*
**预设误概念:**
- 误概念 M4两帧画一样就行动画不重要
- 误概念 M5属性随便填反正都是数字
**讲解与演示 (Teach & Demo): (5分钟)**
**师:** 画图工具做好了。现在正式开始画你的战斗角色。
**师:** 先确认一件事——帧1和帧2要画什么
**师:** 帧1是「待机」姿势——就是角色站在那里什么都没发生。这一帧决定了角色的整体外形和配色。
**师:** 帧2是「攻击」姿势——角色在出击的那一瞬间。这一帧决定了动画的冲击感。
**师:** 我让你们看一个对比。
【投屏打开 demo-3-animation.html分别导入两个角色一个帧1和帧2完全一样一个帧2明显前冲】
**师:** 哪个更有打击感?
**生:** 第二个!第一个完全没动……
**师:**帧1和帧2差异越大动画的冲击感越强。帧2复制帧1不修改等于没有动画。攻击帧要做什么肢体前冲、武器伸出去、颜色变化——让角色看起来「正在做某件事」。
【识别层建立「帧1待机 vs 帧2攻击」的视觉差异意识澄清 M4 误概念】
**师:** 画完之后我们要给角色填属性。属性系统用的是「20分预算制」——你有20分分配完就没了怎么分是你的策略。
**师:** 四个属性:
【投屏展示属性表】
```
HP生命值1分 = 10血 范围 3-10分
ATK攻击力1分 = 5伤害 范围 2-10分
DEF防御力1分 = 3减伤 范围 0-6分
SPD速度1分 = 1速度值 范围 1-6分
```
**师:** 我给你们四种打法定位参考,你选一种,或者自己设计——
```
坦克型HP 8 + ATK 4 + DEF 6 + SPD 2 = 20分耗死对方
刺客型HP 4 + ATK 10 + DEF 0 + SPD 6 = 20分一击毙命
平衡型HP 5 + ATK 5 + DEF 4 + SPD 6 = 20分没有弱点
速攻型HP 6 + ATK 7 + DEF 1 + SPD 6 = 20分先手压制
```
**师:** 注意——你的角色外观要和你的打法定位「匹配」。刺客型画个超大的圆圆的胖角色,感觉不对;坦克型画个细细的小人,也感觉不对。角色的视觉设计要传达出它的战斗风格。
**师:** 还有一个特技系统——每个角色可以选一个特技,不占预算:
```
🔥燃烧:每回合额外-5血持续3回合
🛡护甲:首次受击伤害归零
⚡连击25%概率攻击两次
💉吸血攻击回复伤害30%血量
❄️冰冻:每场一次,对方跳过下回合
🎯穿透:一次攻击无视防御
```
**师:** 选好了吗?现在开始画。先定打法定位,再动手画角色外形。
【应用层:学生开始从策略角度设计角色,而不是「随便画一个」】
**学生实践 (Practice): (12分钟)**
学生操作:
1. 在画图工具里,先选定自己的打法定位(坦克/刺客/平衡/速攻,或自定义)
2. 在帧1画待机姿势——确定角色外形、配色、整体风格
3. 点「复制帧1到帧2」在帧2的基础上修改攻击姿势肢体前冲、武器伸出、颜色变化等
4. 预览动画确认帧1和帧2有明显差异
5. 完成后,在 Trae 新建一个 `character.json`,填写属性数据:
```json
{
"name": "你的角色名",
"hp": 50,
"atk": 25,
"def": 12,
"spd": 4,
"special": "⚡连击",
"description": "一句话描述你的打法"
}
```
> 教师走动观察重点:
> - 有没有学生帧2完全没改只是复制了帧1走过去问「攻击的时候角色在做什么动作
> - 有没有学生分配完属性后发现「总分超了20分」引导他重新算一下
> - 关注速度慢的学生如果卡在画角色帧2可以暂时只改一两个像素保证完成基本的两帧结构细节可以之后优化
**进度同步 (Checkpoint): (3分钟)**
**师:** 谁的帧2跟帧1有明显不同的把动画预览给大家看一下。
**生:** (学生展示动画预览)
**师:** 帧2里角色做了什么动作
**生:** (预期:他的手臂伸出来了 / 整个身体往前倾了)
**师:** 这就是攻击感。你们的打法定位是什么型?
**生:** 刺客型!
**师:** 刺客型——高攻低血。碰到坦克型怎么办?
**生:** (预期:先手攻击!速度快,先打对方)
**师:** 好,这说明你的策略是清晰的。你知道你的角色能赢谁,也知道怕谁——这才是策略设计。
【诊断点:学生能否说出自己的打法定位,并能解释「这个定位的优势和弱点」,而不只是「我觉得这个好看」】【应用层】
---
**第三幕:反思 (Contemplate) — 10分钟** 🤔
*本幕目标2-3名学生展示角色并说出打法定位的策略思考让其他学生从「对抗」角度给出评价*
**【环节】成果展示 (6分钟)**
**师:** 现在我们来做一个角色路演。每个人只说三件事:第一,你的角色叫什么名字;第二,你的打法定位是什么型;第三,你的特技是什么,为什么选这个。
**师:** 谁先来?
【1-2名学生上来展示角色动画说出三件事】
**师:** (对展示的学生)你是刺客型,特技选了「穿透」。你的思路是什么?
**生:** (预期:因为刺客型攻击高,加上穿透直接无视防御,坦克型也扛不住)
**师:** 这个搭配有意思——高攻 + 穿透,专门克制高防御的坦克。但是你血量很少,碰到先手比你快的角色怎么办?
**生:** (预期:就……输了? / 要速度快才行)
**师:** 所以你的角色有一个明显弱点。这不是坏事——有弱点才有对抗的乐趣。
**【环节】互评与讨论 (4分钟)**
**师:** 大家来评价刚才展示的这个角色——一个优点 + 一句「如果我拿XX型去打他我的策略是」。
**生:** (预期:优点是攻击穿透很厉害;如果我用速攻型,速度比他快,先手打他,因为他血少可能一下就输了)
**师:** 很好——你在想「反制策略」了。这就是涂鸦PK的乐趣不只是画角色而是设计一个有自己思考的战斗方案。
**师:** 帧2动画有一个想问的——你们觉得帧2改变得最大、最有攻击感的是哪个角色
【学生投票或说出谁的】
**师:** 为什么你觉得那个最有攻击感?
**生:** 预期因为他帧2整个身体都往前扑了和帧1差别很大
**师:** 对——**差异越大,冲击感越强。** 这是今天审美设计的核心原则,记住它。
---
**第四幕:延续 (Continue) — 5分钟** 🚀
**【环节】抽象总结 (3分钟)**
**师:** 今天我们做了两件事:先用 Plan Mode 做出了画图工具然后用工具画了角色、填了属性。我来问你们——今天用的三步流程窗口A整理、窗口B审核、窗口C执行和上节课做俄罗斯方块时有什么一样的地方
**生:** (预期:流程完全一样 / 都是先写需求再生成)
**师:** 对——**流程是可以迁移的。** 这套流程不只用来做游戏,不只用来做画图工具。以后你们要做任何稍微复杂一点的项目,都可以用这三步。
**师:** 今天还有一个新东西——「先造工具,再用工具」。你们不是下载别人的画图软件来用,而是自己写了一个。这个思路以后还会用到:当你发现某个工具不好用,或者根本没有,你可以自己造。
**师:** 今天的「审美力」训练,核心是什么?
**生:** 预期帧1和帧2差异要大 / 攻击帧要有动感)
**师:**帧2差异越大动画越有冲击感。这条原则不只用在像素画——你以后做任何动画、做任何交互「差异对比产生冲击感」都是一条通用的审美原则。
【迁移层:将「先造工具」「差异产生冲击感」提炼为可迁移的思维模型】
**【环节】下节预告 + 5分钟挑战 (2分钟)**
**师:** 下节课——涂鸦PK。你们今天画好的角色和属性会被真正用来对战。我们要搭建一个战斗引擎让你的角色和同学的角色打起来看看谁的策略赢。
**师:** 想一想——你的打法定位,碰到哪种对手会输?下节课我们来验证。
**师:** 本周 5 分钟挑战:用你今天做的画图工具,帮一个同学画一个角色,然后对比和他自己画的有什么不同——外形、配色、攻击姿势,哪里不一样?拍照或截图,下节课分享。
---
### 5. AI助教使用指南
**教师演示用提示词需求文档整理窗口A**
```
你是一个需求分析师。我要做一个HTML5像素画图工具功能是64×64画布、8倍缩放显示、支持画笔/橡皮/填充桶、有调色板、两帧编辑帧1待机/帧2攻击、可复制帧1到帧2、有动画预览、能导出128×64 Spritesheet PNG。请把这些需求整理成规范的需求文档包含功能描述、触发条件、功能规则、交互规则、验收标准五个部分。
```
**审核用提示词窗口B——新窗口不要在整理窗口里使用**
```
你是一个挑刺工程师。下面是一份画图工具的需求文档,请只提出你觉得描述不清楚或有遗漏的问题(每个问题一行),不要给答案,不要写代码:
[粘贴需求文档]
```
**学生保底提示词生成画图工具窗口C**
```
请帮我用HTML5 Canvas做一个像素画图工具64×64像素画布、8倍缩放显示、支持画笔/橡皮/填充桶、有颜色调色板至少16色、有两帧帧1待机/帧2攻击可切换编辑、可复制帧1到帧2、有动画预览循环播放、能导出128×64 Spritesheet PNG。单文件HTML不用任何外部库。
```
**进阶提示词(属性 JSON 生成辅助):**
```
我的角色是刺客型打法是先手高爆发。20分预算制HP/ATK/DEF/SPD各有范围限制HP:3-10ATK:2-10DEF:0-6SPD:1-6。请帮我给出一个满足20分总预算、符合刺客型定位的属性分配方案并说明每个属性为什么这样分配。
```
**属性分配参考表(投屏展示用):**
| 打法定位 | HP | ATK | DEF | SPD | 合计 | 核心优势 |
|---------|----|----|-----|-----|-----|---------|
| 坦克型 | 8 | 4 | 6 | 2 | 20 | 血厚扛伤害,慢慢耗死对方 |
| 刺客型 | 4 | 10 | 0 | 6 | 20 | 先手高爆发,一击毙命 |
| 平衡型 | 5 | 5 | 4 | 6 | 20 | 无明显弱点,应对各种对手 |
| 速攻型 | 6 | 7 | 1 | 6 | 20 | 先手压制,中等爆发 |
---
### 6. 教师指南
**本课技术备注:**
- **Spritesheet 格式**128×64 PNG左半0-63像素宽是帧1右半64-127像素宽是帧2。对战引擎会按这个坐标裁取每帧。导出时必须确保两帧横向拼接否则下节课的战斗引擎无法正确读取。
- **Canvas 像素操作**:画图工具底层用 `getImageData/putImageData` 操作像素数组。每个像素有 RGBA 四个值各0-255。橡皮设为「透明」等于把该像素的 alpha 通道设为 0。这部分不需要传达给学生但教师需要了解以便解释「橡皮变透明 vs 变白色」的区别。
- **填充桶(洪水填充)**:用 BFS 或 DFS 算法扩展同色区域。如果学生报告「填充桶没有反应」,可能是起点像素和周边颜色完全相同导致;让学生先用画笔画一个封闭区域再测试。
- **帧数据独立存储**:每帧用一个独立的 `Uint8ClampedArray` 存储像素数据。切换帧时先保存当前帧数据,再加载目标帧数据。如果生成的代码两帧共用数据,需要在需求里补充「每帧像素数据独立存储,互不影响」。
- **导出 PNG**:用 `canvas.toDataURL('image/png')` 获取 base64 编码后触发下载。导出前需要把两帧数据拼接到一个 128×64 的临时 canvas 上再导出。
**常见问题 FAQ**
| 问题 | 应对 |
|------|------|
| 「AI 生成的代码报错,打开是空白页」 | 让学生把报错信息(浏览器 F12 控制台)截图给 AI用「步骤→预期→实际」描述问题让 AI 修复 |
| 「橡皮擦了但颜色还在」 | 先确认是否切换到了橡皮工具;如果工具选对但无效,可能是代码把橡皮当成了「白色画笔」,需要溯源需求文档里橡皮的功能描述 |
| 「填充桶整个画面都变色了」 | 这是填充桶「溢出」问题——起点像素周围没有封闭边界,颜色扩散到整个画布。让学生先用画笔封闭区域再用填充桶 |
| 「帧1和帧2切换后内容一样」 | 需求文档里需要补充「两帧像素数据独立存储」;溯源后让 AI 修复数据存储逻辑 |
| 「导出的 PNG 只有一帧」 | 需求里「128×64 Spritesheet」的描述可能没说清楚是「两帧横向拼接」补充描述后重新生成导出功能 |
| 「20分分配不够不知道怎么算」 | 给学生纸笔,列出四个属性的分值,加减法算一遍;主要目的是让学生思考优先级,不是数学练习 |
| 「帧2不知道怎么画才有攻击感」 | 给三个具体操作方向1身体/手臂前倾2把武器或特效从帧1的位置移动到「伸出去」的位置3帧2加一些颜色特效爆炸光晕、冲击线条 |
**课堂风险预案:**
- **AI 服务不可用**:使用 `demo-1-draw-tool.html` 作为备用画图工具,跳过 Plan Mode 生成步骤,直接让学生用 demo 画角色。Plan Mode 三步可以在白板上演示讲解,不需要真的生成。
- **学生进度差异过大**:画图工具生成完成的学生直接开始画角色;还在 Plan Mode 阶段的学生可以在分段三时使用教师备用的 demo-1-draw-tool.html 画角色,保证每位学生都能产出两帧角色。
- **属性分配争议(学生觉得某个定位「必胜」)**:预先告知下节课会有实际对战,不同对手克制关系不同,没有「必胜型」。这个问题留到实战后讨论,今天重点是把角色画完。
---
### 7. 5分钟日常AI挑战
**本周挑战:** 用画图工具帮同学画一个角色,然后对比差异
**挑战说明:**
用你今天做好的画图工具帮一个同学家人或朋友也行画一个角色帧1待机 + 帧2攻击然后让他自己也画一个版本对比两个版本有什么不同外形、配色、攻击姿势哪里不一样想一想为什么会不一样是因为你们对「好看」的标准不同还是因为对「攻击感」的理解不同
**下节课分享:** 下节课选2-3位同学展示「你画的版本」和「同学画的版本」说出最大的一处差异是什么。
---
### 8. 拓展任务
**拓展一(推荐):** 设计一个「克制链」——你的角色克制哪种打法定位的对手被哪种打法定位克制用文字写出来「我是XX型我克制XX型因为……我怕XX型因为……。」然后想想下节课对战时要不要调整属性。
**拓展二(挑战):** 给你的角色再画一帧「受伤帧」——当角色被攻击时显示的状态帧3。试着修改画图工具的代码让它支持三帧编辑并在动画预览里加入「受伤」动画。

View File

@@ -0,0 +1,605 @@
---
课时: 9
主题: 涂鸦PK— 基础对战系统
核心能力: [拆解力, 韧性力]
核心工具: [Trae IDE, Kimi]
时长: 90分钟
透明化层级: 过程层
适用路线: AICODE-06
---
### 1. 课程目标
**知识目标:**
- 理解「边界情况」的概念:任何系统都有正常路径以外的边缘场景,这些场景必须在需求里明确定义,否则 AI 的行为不可预测
- 理解「独立窗口审核」原则:需求文档、边界审核、代码生成、测试验证各用独立会话,防止 AI 自我偏袒
- 理解「测试脚本」的价值:用代码来验证代码,比人工点击效率高十倍,且可重复执行
**能力目标:**
- 能用 Plan Mode 整理战斗系统需求文档,并用窗口 B 发现边界情况(拆解力)
- 能把自己的 Spritesheet 导入对战系统,完成逐条验收,遇到 bug 能描述「步骤→预期→实际」(韧性力)
- 能让 AI 生成测试脚本,读懂「✅通过/❌失败」的输出,并定位到需求文档中对应的条款
**情感目标:**
- 建立「测试不是挑刺,是保护」的意识——有 bug 的游戏比没有游戏更让人沮丧,测试帮你在发布前修好它
- 体验「让 AI 测试 AI 写的代码」的效率感,而不是自己一条条人工验证
- 对「今天的角色真的打起来了」产生真实的成就感为第10课加动画做好期待
---
### 2. 核心概念与误概念预设
**核心概念认知层级:**
| 概念 | 学生类比 | 认知层级 |
|------|---------|---------|
| 边界情况 | 游戏规则书上写了「普通攻击」怎么算,但没写「两个人同时出手」怎么判——比赛现场这种情况真出现了,裁判不知道怎么判,整场比赛乱掉 | 理解层 |
| 独立窗口审核(新窗口原则) | 作文让同学评改:写作文的同学自己改,永远改不出大问题;换一个没看过你作文的同学来改,他会发现真正的漏洞 | 应用层 |
| 测试脚本 | 乐高玩具的「质检机器人」——每次按一个按钮,它自动把所有零件逐一检查一遍,告诉你哪个零件不对;比你自己一个个用眼睛看快一百倍,而且不会看漏 | 应用层 |
| 伤害公式 | 攻击力是出拳力气,防御力是护甲厚度,伤害 = 出拳力气 护甲厚度,最少也会有 1 点伤害(护甲再厚也会有点疼) | 识别层 |
| 先手机制 | 速度值高的人先出手,就像运动会百米跑步,反应快的人先起跑——如果两人反应时间一样,就抛硬币决定 | 识别层 |
**典型误概念表:**
| 编号 | 误概念 | 正确认知 | 激发策略 |
|------|--------|---------|---------|
| M1 | 游戏能跑就行,不用测试 | 有 bug 的游戏比没有游戏更难受;测试发现的 bug 才是可修复的 bug | 问:「你有没有玩过一个游戏,有个 bug 特别烦但一直不修?你是什么感受?」 |
| M2 | 测试脚本要自己写,太难了 | 测试脚本也可以让 AI 写,你只需要描述「我要验证什么结果」 | 演示:把「验证 ATK=15, DEF=5 时伤害=10」告诉 AI它直接给出测试代码 |
| M3 | 同速的情况 AI 会自动处理好 | 边界情况必须在需求里明确,否则 AI 行为不可预测(可能每次随机,也可能直接报错) | 问:「如果两个角色速度一样,谁先出手?你需求里写了吗?」 |
| M4 | 伤害公式在脑子里,不用写进需求 | 代码里的公式必须和需求文档一致,否则测试脚本无法验证;口头约定不算 | 演示需求里写「ATK 减 DEF」代码里却写成「ATK 乘以 DEF」逻辑完全不同 |
| M5 | 格挡和攻击同帧不会有问题 | 行动顺序是战斗公平性的核心;「同时出手」必须在需求里明确谁的优先级高 | 类比:石头剪刀布如果两人同时出,规则说谁赢?——必须有一个规则,不能靠 AI 猜 |
---
### 3. 教学准备
**工具与环境:**
- 每台电脑已安装 Trae IDEKimi 已登录,网络正常
- 每台电脑浏览器可以正常打开本地 HTML 文件
- 每台电脑上已有上节课完成的 Spritesheet PNG 文件128×64帧1待机+帧2攻击
- 每台电脑上已有上节课完成的角色属性 JSONhp/atk/def/spd/skill 字段20分预算制
- 投影可切换至任意学生屏幕
**教学资源:**
- 教师准备demo-2-pk-battle.html可直接运行的战斗演示开课前准备好用于 Connect 展示和 Contemplate 演示)
- 教师准备战斗需求文档范例见第5节完整版供学生卡壳时参考
- 教师准备测试脚本的预期输出截图4条全部 ✅ 通过的版本,用于对比)
- 学生资源:上节课完成的 Spritesheet PNG 和角色属性 JSON若有学生未完成教师提供默认模板
**教师备课体验任务:**
> 备课前,教师必须亲自完成以下操作:
>
> 1. 打开 demo-2-pk-battle.html完整打一局记录下「格挡+重击同回合」时伤害是怎么算的,以及「特技用完后再点特技按钮」的实际行为
> 2. 在窗口 A 整理一份战斗需求文档,然后在全新窗口 B 用审核提示词跑一遍,记录 AI 问出来的边界问题里哪几条最让你意外
> 3. 在窗口 D 生成测试脚本,验证伤害公式和先手机制,确认本课所有测试用例都能通过
> 4. 故意把伤害公式改成「ATK × DEF」重跑测试脚本截图「❌失败」的输出备课时展示用
---
### 4. 教学流程
---
**第一幕:联系 (Connect) — 10分钟** 🔗
*本幕目标:让学生展示上节课做的角色,说出打法定位和取胜策略,激活策略意识;教师演示可以运行的对战系统,建立「今天目标」的画面感*
**【环节】角色展示 + 打法定位 (7分钟)**
**师:** 上节课结束前,每个人都交了两样东西:一个 Spritesheet PNG一份角色属性 JSON。今天我们用这两样东西做什么
**生:** (预期:做游戏?让它打起来?)
**师:** 对。上节课大家做了什么,记得吗?
**生:** (预期:画了自己的角色 / 设计了角色属性)
**师:** 对。你们每个人手里有一个属于自己的角色:有自己画的图,有自己定的属性,还有自己选的特技。今天不是来欣赏它的——今天要让它真的打起来。
**师:** 在打之前,我先问每个人一个问题。谁来第一个回答——你的角色是什么打法定位?为什么你的角色应该赢?
【点一个学生,把他的 Spritesheet 投屏展示】
**生:** (预期 A「我是高攻击型我的 ATK 是 16我可以直接打穿对面」
**生:** (预期 B「我是高防御型我血多耗死对手」
**生:** (预期 C「我速度最快我先手每次都比对面早打」
**师:** 很好。你说的「先手」——如果两个角色速度值一样怎么办?
**生:** (预期:不知道 / 随机?/ 谁编号小谁先?)
**师:** 这就是我们今天第一件事要想清楚的——你脑子里的规则必须写成文字AI 才能按你想的来做。
【诊断点:学生能否意识到「打法逻辑」和「代码规则」之间有一层需要翻译的过程】【识别层】
**【分支A】若学生对打法定位有清晰的想法**
**师:** 很好,待会儿写需求文档的时候,你的打法定位就是你的出发点——你的角色是高攻击,那你的伤害公式、重击倍率、特技效果就都要围绕「攻击压制」来设计。
**【分支B】若学生说「我随便什么定位都行」**
**师:** 没关系,我们来帮你定位。你的 ATK 最高,还是 DEF 最高,还是 SPD 最高?
(引导学生从属性数值反推定位,而不是空想)
**【环节】展示今天目标 (3分钟)**
**师:** 我先给大家看一个东西。
【投屏打开 demo-2-pk-battle.html运行一局对战】
**师:** 你们看——左边是玩家角色,右边是 AI 对手,有血条,有行动按钮,有战斗日志。这就是今天下课前你们要做到的东西。
**师:** 但注意——你们做出来的版本,角色是你们自己画的,属性是你们自己定的。你的角色放在左边,跟 AI 对手打。今天不做动画,角色还是用矩形占位,但战斗逻辑是真实的。下节课我们把图换上去、加动画。
**师:** 今天的核心是:战斗规则要真的是你想的规则,而不是 AI 随机猜的规则。怎么保证?用 Plan Mode。
**师:** 今天我们会开 4 个窗口——窗口 A 写需求、窗口 B 审核边界、窗口 C 生成代码、窗口 D 做测试。这 4 个窗口,每个只做一件事,互相不干扰。记住这个结构,以后你做任何大一点的项目都可以用。
---
**第二幕:建构 (Construct) — 65分钟** 🛠️
*本幕目标:走完「需求文档→边界审核→代码生成→导入 Spritesheet→验收→测试脚本验证」完整闭环强化多窗口协作和边界意识*
---
**【分段一Plan Mode — 战斗系统需求文档】(20分钟)**
*本段重点:学生在窗口 A 整理战斗系统需求,在窗口 B 用「边界审核」提示词发现漏洞,补充进文档*
**预设误概念:**
- 误概念 M3同速情况 AI 会自动处理好,不用写
- 误概念 M4伤害公式在脑子里不用写进需求文档
- 误概念 M5格挡和攻击同帧不会有问题
**讲解与演示 (Teach & Demo): (7分钟)**
**师:** 我们先开窗口 A——这个窗口只做一件事整理需求文档。
**师:** 今天的需求文档比俄罗斯方块简单一点,但有一个新的挑战——这个系统里有很多「边界情况」。什么叫边界情况?就是不在「正常流程」里,但真实对战时会出现的场景。
**师:** 我给你们举几个例子。正常流程是什么玩家点攻击AI 点攻击,互相扣血,谁血量归零谁输。这是主路径,好想,好写。
**师:** 但是——如果两个角色速度值完全相同,谁先出手?这条你有没有想过?
**生:** (预期:没想过 / 随机?)
**师:** 还有——特技每场只能用一次。如果玩家特技已经用完了,再点「特技」按钮,游戏会怎样?按钮灰掉不能点?还是提示「已用完」?还是直接变成普通攻击?
**生:** (预期:好像要变灰 / 要提示吧)
**师:** 对。还有——格挡时受到的伤害减少 50%那对方用的是重击伤害×1.8),格挡减伤是基于重击后的伤害算,还是基于原始伤害算?
**生:** (预期:有区别吗……?哦,区别很大!)
**师:** 这些都是边界情况。这些情况如果你没有在需求里写清楚AI 会自己猜一个——可能猜得跟你想的不一样,可能每次结果还不同,可能直接崩掉。
**师:** 所以今天需求文档要写两轮。第一轮是窗口 A把主路径写出来。第二轮是窗口 B让 AI 扮演审核工程师,专门找你没考虑到的边界情况。
【投屏展示战斗需求文档的五个核心模块】
**师:** 战斗系统需求文档有五个必填模块。我过一遍每个的关键要点——
**师:** 模块一先手机制。你必须写清楚两条第一SPD 高的先出手——这好理解;第二,**同速时怎么处理**——你必须选一个方案,写进文档。你可以写「同速时随机决定」,也可以写「同速时玩家先手」,但必须选一个,不能空着。
**师:** 模块二伤害公式。写「ATK 减 DEF最低造成 1 点伤害」。注意「最低 1 点」这个细节必须写——如果不写当防御值大于攻击值时AI 可能算出负数伤害,变成治疗对手了。
**师:** 模块三行动类型。四种普通攻击、重击伤害×1.8)、格挡(本回合受伤减少 50%)、特技(每场只能用一次,效果来自角色 JSON 的 skill 字段)。
**师:** 模块四胜负判定。HP≤0 立即判输,游戏结束。
**师:** 模块五AI 对手决策逻辑。AI 对手不是乱打的——写清楚它的决策规则HP 低于 30% 时用特技25% 概率重击;否则普通攻击。
**师:** 我来快速演示一下这五个模块写出来是什么样的。
【投屏展示教师自己的需求文档草稿,只展示模块一和模块二,让学生对格式有直观感受,不逐字念,只指出关键字段】
**师:** 你们看模块一的「同速处理」我写了「同速时随机决定Math.random() < 0.5 为玩家先手」。你们不需要写代码,但要写清楚「同速时谁先」——随机、或者固定玩家先手,选一个写进去。
**师:** 模块二里,我特别标了「最低 1 点」并且加了验收标准:「当 ATK=3, DEF=8 时,伤害=1 而不是负数」。这就是一条可测试的需求——能写出具体数字的需求,才是真正写清楚了的需求。
**师:** 这五个模块写完,是你的「主路径」。然后我们开窗口 B让 AI 来挑刺。
**学生实践 (Practice): (10分钟)**
**师:** 现在打开 Kimi开一个新对话这是窗口 A。用我刚才说的五个模块把你的战斗系统需求文档写出来。不确定的地方先写一个版本待会儿窗口 B 会帮你发现问题。
> 教师走动观察重点:
> - 是否有学生跳过「同速处理」这条?走过去问:「你这里写了 SPD 高的先手,那两人 SPD 一样怎么办?」
> - 是否有学生漏写「最低 1 点伤害」?问:「如果对手 DEF 比你 ATK 还高,伤害是负数还是零?你想要哪个?」
> - 是否有学生把 AI 对手的决策逻辑写得很复杂(超过 3 条提醒「AI 对手逻辑先简单,战斗规则才是重点」
**进度同步 (Checkpoint): (3分钟)**
**师:** 五个模块都填了的举手。
**师:** 好,现在开窗口 B——新开一个 Kimi 对话,把你写好的需求文档整个复制过去,然后加上这句话:「你是一个游戏测试工程师,请列出这份需求文档里所有的边界情况和异常情况,只列问题,不需要解答。」提交。
【诊断点:学生是否能把「用窗口 B 挑需求漏洞」理解为「需求还没写完」,而不是「可以不管这些问题」】【理解层】
**【分支A】若学生窗口 B 得到了边界问题列表:**
**师:** 好,先把这些问题读一遍。你觉得哪条最重要——就是如果不处理,对战时一定会出问题的那条?
(让学生自己判断优先级,而不是逐条补充所有问题)
**【分支B】若学生说「AI 没问出什么有用的问题」:**
**师:** 我来帮你加一条——「同速时谁先出手」这条你文档里怎么写的?
(从教师预设的边界情况里选一条,帮学生发现漏洞)
**【分支C】若学生觉得「这些边界情况不重要先做再说」**
**师:** 好,我们来做个实验——你先做,遇到边界情况的时候我们再说。
(让学生先继续,在分段二验收时用真实 bug 来说明边界问题的后果)
**师:** 窗口 B 出来的问题,不需要全部解决——先挑最重要的 3 条,补充到你的需求文档里。什么叫「最重要」?就是如果不写清楚,对战时一定会出现的场景。「同速先手」、「特技用完后再点」、「格挡+重击叠加」——这三条必须在文档里有明确答案。
**师:**5分钟把窗口 B 给你的问题里最关键的几条回答了,补到需求文档里。
> 教师走动:重点帮学生判断哪条边界问题「必须解决」。判断标准——对战时能触发的频率高不高?触发了但没处理会崩游戏还是只是显示不对?优先处理会崩游戏的。
---
**【分段二:生成 PK 系统 → 导入 Spritesheet → 逐条验收】(25分钟)**
*本段重点:在窗口 C 生成战斗系统骨架,替换为自己的角色数据,验收核心功能*
**预设误概念:**
- 误概念 M1能跑就行不用验收每一条
- 误概念 M4遇到 bug 直接让 AI 改代码,不回需求文档溯源
**讲解与演示 (Teach & Demo): (5分钟)**
**师:** 需求文档确认好了,进入执行阶段。打开 Trae IDE新开一个文件命名为 pk-battle.html。
**师:** 这次生成分两步走。**第一步**:先生成「带占位图的骨架」——角色先用彩色矩形代替,重点让战斗逻辑跑通。**第二步**:骨架验收通过后,再把你的 Spritesheet 换进去。
**师:** 为什么要分两步?如果你一开始就把 Spritesheet 传进去,战斗逻辑有 bug 的话,你不知道是逻辑问题还是图片问题——两个问题混在一起,很难调试。先把逻辑跑通,再换图,问题清晰。
**师:** 现在开窗口 C——这个窗口专门做代码生成。把你的需求文档复制进去加上保底提示词提交。
【投屏展示学生保底提示词见第5节教师演示提交过程展示骨架生成结果】
**师:** 生成出来之后,把代码复制到 Trae IDE 里的 pk-battle.html保存用浏览器打开开始验收。
**师:** 验收怎么做?还是我们之前说的三步——「步骤→预期→实际」。你做了一个操作,你期望看到什么,实际看到了什么。如果期望和实际不一样,记下来,这就是一个 bug。
**师:** 验收清单,我们一起来定:
```
验收清单(至少验这 5 条):
1. 普通攻击 → 对手 HP 减少,减少量 = ATK - DEF最低 1
2. 格挡 → 下一次受到伤害减少 50%
3. 重击 → 伤害比普通攻击×1.8
4. 特技按钮 → 用完一次后变灰或显示「已用」
5. HP≤0 → 游戏结束,显示胜负
```
**学生实践 (Practice): (17分钟)**
第一步8分钟用保底提示词在窗口 C 生成战斗骨架,复制到 Trae IDE浏览器打开
第二步4分钟按验收清单逐条点击验收记录「通过/不通过」
第三步5分钟把验收通过的骨架里的角色数据替换为自己的角色属性 JSON把矩形占位符的颜色改成自己角色的代表色
> 教师走动观察重点:
> - 学生是否在逐条验收,还是点一下「感觉可以」就算通过?走过去问:「第 3 条重击倍率你验了吗?怎么确认它是 1.8 倍?」
> - 学生遇到 bug 时,是直接跟 AI 说「帮我修复」,还是先回需求文档确认这条需求怎么写的?
> - 有学生进展很快的:引导他多做一步——把 Spritesheet 也导入,用 `drawImage` 替换矩形,不需要动画,只是让图片显示出来
**进度同步 (Checkpoint): (3分钟)**
**师:** 谁来说一条验收结果——哪条通过了,哪条没通过?
**生:** (预期 A「普通攻击通过了格挡没通过格挡好像没有减伤」
**生:** (预期 B「特技用完之后按钮没变灰还能继续按」
**师:** 格挡没有减伤——这个 bug 我们用「步骤→预期→实际」来描述。步骤是什么?
**生:** 点格挡,然后对面攻击我。
**师:** 预期是什么?
**生:** 受到的伤害应该是正常伤害的 50%。
**师:** 实际是什么?
**生:** 受到的伤害跟没格挡一样。
**师:** 对。这个描述发给 AI加上一句「请检查需求文档里的格挡逻辑修复这个 bug」。注意——不是说「帮我改代码」是「检查需求文档对应的逻辑」让 AI 溯源。
【诊断点:学生是否会用「步骤→预期→实际」三要素描述 bug而不是只说「格挡不对」】【应用层】
**【分支A】若学生能准确描述 bug 三要素:**
**师:** 这就是专业的 bug 报告。真正的软件工程师报 bug 就是这个格式,你今天已经在用了。
**【分支B】若学生描述模糊「格挡感觉不太对」**
**师:** 「感觉不对」AI 不知道从哪里改。你告诉我——你点了格挡之后,对面攻击你,你的 HP 掉了多少?你预期应该掉多少?
(帮学生把感受翻译成具体数字)
---
**【分段三:窗口 D 生成测试脚本 → 验证核心逻辑】(15分钟)**
*本段重点:在独立窗口 D 生成测试脚本,运行 4 条测试用例,读懂「✅通过/❌失败」输出,体验「让 AI 测试 AI」的效率*
**预设误概念:**
- 误概念 M2测试脚本要自己写太难了我不会代码
- 误概念 M1能跑就行不需要写测试
**讲解与演示 (Teach & Demo): (5分钟)**
**师:** 你们刚才用手工逐条验收——点按钮,看结果。这样做是对的,但有一个问题:下次你改了一段代码,你要把所有条目再点一遍吗?
**生:** (预期:要的 / 好累 / 不想点)
**师:** 有一个更高效的方法——**测试脚本**。你告诉 AI「我要验证哪些规则预期结果是什么」AI 写一个 HTML 文件,你一打开,它自动跑完所有验证,每条显示「✅通过」或者「❌失败+原因」。
**师:** 这叫「用代码测试代码」。以后你每次改完代码打开测试脚本跑一遍5 秒看完所有结果,知道改了哪里有没有破坏原来的逻辑。
**师:** 注意——测试脚本必须在全新窗口 D 生成。为什么?因为如果你在窗口 C 生成测试脚本AI 知道自己刚刚写了什么代码,它的测试会偏向「帮自己的代码辩护」——故意写成能通过的测试,而不是真正验证逻辑是否正确。
**师:** 独立窗口 D让一个「什么都不知道」的 AI 来写测试,它才会真的按需求文档来验证。
【投屏展示窗口 D 测试脚本提示词见第5节演示把需求文档粘贴进去提交】
【投屏展示测试脚本的运行结果4条全部✅效果直观】
**师:** 这 4 条测试验证了什么——
```
测试 1ATK=15, DEF=5 → 伤害=10 ✅
测试 2ATK=5, DEF=10 → 伤害=1最低
测试 3SPD=8 vs SPD=5 → SPD=8 先手 ✅
测试 4ATK=10, DEF=2, 重击 → 伤害=14 ✅
```
**师:** 现在我把伤害公式故意改错——把「ATK - DEF」改成「ATK × DEF」再跑一遍。
【演示:故意修改代码,重跑测试脚本,出现 ❌ 失败】
**师:** 你们看——测试脚本立刻告诉我「测试1失败期望10实际75」。这就是测试脚本的价值改完代码立刻能发现问题不用自己重新点一遍。
**学生实践 (Practice): (8分钟)**
**师:** 现在你们来做。打开全新窗口 D用测试脚本提示词把你的需求文档粘贴进去生成测试脚本保存为 pk-test.html浏览器打开看结果。
> 教师走动观察重点:
> - 是否有学生在窗口 C 生成测试脚本(违反独立窗口原则)?立刻叫停,强调「必须是窗口 D全新对话」
> - 学生的测试结果有 ❌ 吗?引导他们找对应的需求条款,而不是直接改代码
> - 进度快的学生:引导拓展任务——「让 AI 帮你写一条需求里没有的边界测试,看是否通过」
**进度同步 (Checkpoint): (2分钟)**
**师:** 谁的 4 条测试全部通过的举手。
**师:** 有没有哪条测试失败了的?能说一下是哪条,失败的原因是什么?
**生:** 预期「测试3失败了说期望8先手但实际是5先手」
**师:** 这就是测试脚本的价值——它告诉你具体哪里不对。现在打开你的需求文档,找「先手机制」那条,看看写的是什么,再对照你生成的代码,找差异。这才是正确的调试方式。
【诊断点:学生是否能从「❌失败」的输出里,找到对应的需求文档条款进行溯源】【应用层】
**【分支A】若学生能自己完成溯源**
**师:** 你刚才做的就是「测试驱动调试」——测试报告告诉你哪里不对,你去需求里找根因,然后修代码。这是专业开发者的工作方式。
**【分支B】若学生看到 ❌ 直接去改代码而不溯源:**
**师:** 等等——你要改什么?测试说失败,但测试是按需求文档验证的。先问:你的需求文档里这条是怎么写的?再问:代码里是不是按这条写的?最后才改代码。
**【分支C】若学生的测试脚本全部通过但游戏实际有 bug**
**师:** 这很有趣——测试通过但游戏有 bug说明两件事之一第一测试脚本用的是独立实现和游戏代码逻辑不一样第二你的 bug 不在测试覆盖的范围里——是一个「需求没写到的边界情况」。你知道是哪种吗?
(引导学生区分「需求定义的 bug」和「需求未覆盖的 bug」
---
**第三幕:反思 (Contemplate) — 10分钟** 🤔
*本幕目标:两学生角色现场对战演示(投屏全班围观),展示「同一规则、不同角色」下的策略差异*
**【环节】角色对战演示 (6分钟)**
**师:** 好,现在我们来见证今天最重要的时刻——让两个同学的角色真的打一场。
【选两个进度最快的学生,把他们的 pk-battle.html 分别投屏,或者其中一个学生的对战画面投屏全班观看】
**师:** 我们先看左边——这个角色是什么打法?
**生(观察者):** (预期:高攻击型 / 速度型 / 防御型)
**师:** AI 对手用的是默认属性。先看第一回合谁先出手——
【运行对战,全班实时观看,教师边打边念战斗日志】
**师:** 看到没先手机制起作用了——SPD 高的先出手。现在玩家用重击——
【展示重击效果,强调伤害计算是按需求文档的公式来的】
**师:** 这场对战里有哪条规则你们觉得体现得最清楚?
**生:** (预期:先手 / 格挡减伤 / 特技限制次数)
**【环节】互评与讨论 (4分钟)**
**师:** 现在回顾今天的工作。今天你们打开了几个窗口,每个窗口做什么?
**生:** (预期:窗口 A 写需求,窗口 B 找边界,窗口 C 生成代码,窗口 D 做测试)
**师:** 对。这四个窗口,缺一个会出什么问题?比如如果跳过窗口 B——
**生:** (预期:边界情况没想到,游戏对战时可能出 bug
**师:** 如果窗口 D 不开新窗口,在窗口 C 里让 AI 测自己的代码——
**生:** 预期AI 会偏袒自己,测试结果不可信)
**师:** 今天遇到最大的挑战是什么?
**生:** (预期 A「边界情况想不到」/ 预期 B「看懂测试失败的原因」/ 预期 C「导入 Spritesheet」
**师:** 你们今天写需求、审核、生成、验收、测试——5 个步骤。这 5 步,哪一步对你来说最有收获?
**生:** (开放回应,教师倾听,不评判)
**师:** 我来说说我的观察——今天做得最好的事,是大家在遇到 bug 时,第一步不是「让 AI 帮我改」,而是先问「哪条需求没说清楚」。这个习惯,今天有人做到了,有人还在培养。没关系——这个习惯养成了,你做任何项目都会比别人少踩很多坑。
---
**第四幕:延续 (Continue) — 5分钟** 🚀
**【环节】抽象总结 (3分钟)**
**师:** 今天我们做了一件很重要的事——让「脑子里的规则」变成「代码里真正执行的规则」,中间经过了几个步骤?
**生:** (预期:写需求、找边界、生成代码、验收、测试脚本)
**师:** 对。这个流程不只是做游戏用的——以后你做任何项目,只要涉及「规则」,都要走这个流程。规则越复杂,「找边界」这步就越重要。
**师:** 今天你们掌握的能力有一个名字,叫**「测试驱动验证」**——先定规则,再写代码,再用测试检查代码是不是真的按规则跑。这是真实开发团队的工作方式,不是只有高手才能用的,你们今天已经用上了。
**师:** 还有一个能力——「边界意识」。以后遇到任何系统你的第一反应不是「正常情况怎么用」而是「边界情况、异常情况怎么处理」。这个意识让你的系统更稳定bug 更少。
**师:** 边界意识不只是写代码时用的。你们有没有玩过一个游戏,有个 bug 特别烦——比如某个角色在特定情况下无敌,或者某个技能叠加 20 层秒杀对手——这些 bug 从哪里来?就是设计师在写规则的时候,没有考虑到「如果玩家这样做怎么处理」。你们今天练习的,就是这个能力。
**【环节】下节预告 + 5分钟挑战 (2分钟)**
**师:** 现在你们的战斗系统能跑,逻辑是对的,但角色还是矩形占位符。下节课我们做什么?
**生:** (预期:换成自己的图?/ 加动画?)
**师:** 对——下节课我们把你的 Spritesheet 真正加进战斗系统,受击有闪烁效果,攻击有前摇后摇,死亡有倒下动画。你的角色会真的「活起来」。
**师:** 本周 5 分钟 AI 挑战——让 AI 帮你写一条需求文档里没有的边界情况测试,提交到测试脚本里,看是否通过。如果通过了,说明你的代码比需求文档还严格;如果没通过,说明你发现了一个新 bug。下节课分享。
**师:** 最后一件事——在 pk-battle.html 的顶部注释里,写上你的角色名、打法定位一句话总结,还有今天你发现的最有意思的一条边界情况。格式随意,只要下节课你打开文件,还能想起今天做了什么。
---
### 5. AI助教使用指南
**教师演示用提示词(窗口 A需求文档整理**
```
你是一个需求分析师。我要做一个基于 HTML/CSS/JS 的回合制战斗游戏,规则如下:
- 先手机制SPD 高的角色先出手;同速时随机决定(或编号小的先手,明确规定一种)
- 伤害公式ATK - DEF最低造成 1 点伤害
- 行动类型:普通攻击(按公式)/ 重击伤害×1.8/ 格挡本回合受伤减少50%/ 特技每场只能用一次效果来自角色JSON的skill字段
- 胜负判定HP≤0 判输,立即结束
- AI对手决策HP低于30%用特技25%概率重击;否则普通攻击
请把这些需求整理成规范需求文档,包含:功能描述、触发条件、功能规则、边界情况说明、验收标准。
```
**边界审核用提示词(窗口 B独立新对话**
```
你是一个游戏测试工程师。以下是战斗系统需求文档请列出所有你能想到的边界情况和异常情况例如同速时谁先出手特技已用完再点会怎样格挡叠加重击怎么算双方同时HP≤0怎么判只列问题不需要解答
[粘贴需求文档]
```
**测试脚本生成(窗口 D独立新对话**
```
以下是我的战斗系统需求文档:
[粘贴需求文档]
请帮我写一个 JavaScript 测试脚本(单文件 HTML验证以下4条规则
1. 伤害公式ATK=15, DEF=5 → 伤害=10ATK=5, DEF=10 → 伤害=1最低1点
2. 先手判定SPD=8 vs SPD=5 → SPD=8 的先出手
3. HP≤0 → 游戏结束标志为 true
4. 重击倍率ATK=10, DEF=2, 重击 → 伤害=(10-2)×1.8=14取整
每条测试显示"✅通过"或"❌失败期望XXX实际XXX"。用纯 HTML/JS不需要 Phaser.js。
```
**学生保底提示词(窗口 C生成战斗系统骨架**
```
请帮我用纯 HTML/CSS/JS单文件做一个回合制战斗游戏
角色数据从 JS 对象读取字段name/hp/maxHp/atk/def/spd/skill
- 玩家角色在左边AI 对手在右边
- 先用彩色矩形代替角色图片(稍后替换)
战斗规则:
- 先手SPD 高的先出手;同速时随机
- 伤害公式ATK - DEF最低 1 点
- 四种行动按钮:普通攻击 / 重击×1.8/ 格挡(本回合受伤-50%/ 特技(每场限用一次)
- AI对手逻辑HP<30%用特技25%概率重击;否则普通攻击
- HP≤0 判输,显示胜负结果,提供「再来一局」按钮
界面要求:
- 双方头顶有 HP 血条(数值显示)
- 底部有战斗日志最近3条
- 特技用完后按钮变灰不可点击
只给骨架,角色图片先用矩形占位,单文件 HTML。
```
**进阶提示词(替换 Spritesheet**
```
以下是我的 pk-battle.html 中绘制角色的代码片段:[粘贴相关代码]
请修改角色绘制逻辑,把左边玩家角色的矩形替换为 Spritesheet 图片显示:
- Spritesheet 文件名player-sprite.png128×64 像素
- 帧1x=0, y=0, w=64, h=64待机状态
- 帧2x=64, y=0, w=64, h=64攻击状态
- 平时显示帧1玩家攻击时显示帧2 持续 300ms 后恢复帧1
- 图片要等比缩放,显示在原来矩形的位置
```
---
### 6. 教师指南
**本课技术备注:**
- **Spritesheet 导入**`new Image()``onload``ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)`。sx/sy 是帧在图片中的起始坐标sw/sh 是帧的宽高dx/dy 是画布上的目标位置dw/dh 是目标大小。教师需要理解这 8 个参数的含义学生不需要记只需知道「帧1=x0帧2=x64」。
- **回合制 vs 实时战斗**:本课做的是回合制(玩家点一次按钮 = 一个回合)。教师不要混淆,不要和「帧循环/requestAnimationFrame」扯在一起那是下节课动画的内容。
- **先手机制的实现**:在回合开始时比较 SPD高的先执行攻击逻辑低的后执行。同速时 `Math.random() < 0.5` 决定。这个逻辑放在「玩家点击行动按钮」的事件处理函数里。
- **格挡状态**:格挡是一个「状态标记」(如 `isBlocking = true`),在本回合受到伤害时检查这个标记,伤害减半后重置标记。注意:格挡对「下一次受到的攻击」生效,还是「本回合」生效,需在需求里明确(本课设计为「格挡后敌方先手攻击时生效」)。
- **测试脚本独立窗口**:这是本课最重要的原则。测试脚本里的战斗逻辑函数是「从需求文档重新实现」的独立版本,不引用 pk-battle.html 的代码——所以必须在独立窗口生成,让 AI 按需求文档写,而不是看着自己的代码写测试。
**常见问题 FAQ**
| 问题 | 应对 |
|------|------|
| 「特技按钮用了还能继续按」 | 引导学生检查需求文档里「特技每场限用一次」的验收标准写了什么,再在代码里找特技的触发逻辑,看有没有加 `skillUsed` 标记 |
| 「测试脚本生成出来全部通过,但游戏实际打起来伤害不对」 | 说明测试脚本和游戏代码用的是两套独立实现。先确认测试脚本按需求文档写,再对照游戏代码里的实现,找两者差异 |
| 「Spritesheet 图片不显示,一片黑」 | 99% 是路径问题——图片文件和 HTML 文件要放在同一个目录,路径写相对路径。让学生把 HTML 和 PNG 放到同一个文件夹再刷新 |
| 「重击的伤害计算和我预期不一样」 | 让学生打开战斗日志,找重击那行,用计算器手算一遍:(ATK - DEF) × 1.8,取整,对比日志里的数值 |
| 「AI 对手一直用特技,根本不普通攻击」 | AI 对手的决策逻辑里「HP<30%用特技」的判断可能有 bug——HP 初始值设置不对,或者条件写反了。引导学生检查 AI 对手的 maxHp 和当前 hp 的比较逻辑 |
| 「窗口 B 没问出什么问题」 | 提示学生在提示词末尾加一句:「请特别关注:同速先手、特技用完再点、格挡叠加重击、双方同时归零 这四个场景」 |
| 「测试脚本里 ✅ 全通过,但我不知道这说明什么」 | 说明你的战斗逻辑函数在这 4 种情况下是正确的。测试脚本是「护栏」——以后你改代码,再跑一次,如果还是全通过,说明你没有破坏原来的逻辑 |
| 「怎么知道 AI 对手「25%概率重击」是不是真的 25%?」 | 这是个好问题。严格测试需要跑 100 次统计分布,但我们的课堂不做这个。你可以告诉 AI「请解释这段 AI 决策代码里重击概率是如何实现的」,让 AI 用中文解释给你看 |
**课堂风险预案:**
- **如果 AI 服务不可用Kimi 宕机)**:切换到 Trae IDE 的内置 AI 对话,或使用教师备用的 demo-2-pk-battle.html 直接展示;学生完成需求文档部分,代码生成推迟
- **如果学生进度差异过大**:进度快的学生做拓展(导入 Spritesheet + 写边界测试);进度慢的学生保证完成「战斗骨架能跑通 + 普通攻击验收通过」即可,其余留作课后
- **如果有学生上节课的 Spritesheet 未完成**:提供默认占位图(单色 128×64 PNG确保不因图片问题卡住当天进度
---
### 7. 5分钟日常AI挑战
**本周挑战:** 让 AI 帮你写一条「需求文档里没有的」边界测试,看看能不能通过
**挑战说明:**
打开窗口 D全新对话告诉 AI「我的战斗系统需求文档里有这些规则[粘贴你的需求文档]。请帮我写一条我没有明确写进需求的边界测试——就是那种「正常情况不会想到,但真实对战时可能出现」的场景。」把这条新测试加入你的 pk-test.html运行看是通过还是失败。把结果截图或描述带到下节课分享。
**下节课分享:** 下节课选 2-3 位同学展示——「我写的新边界是什么,测试结果如何,有没有发现 bug」
---
### 8. 拓展任务
**拓展一(推荐):** 把你的 Spritesheet 真正导入 pk-battle.html`drawImage` 替换矩形占位符;不需要动画,只要图片能显示在正确位置。验收标准:打开游戏,左边角色是你自己画的图,不是矩形。
**拓展二(挑战):** 给你的战斗系统加一条新的行动类型:「反击」——格挡时如果对方用了重击,本回合额外反弹 30% 伤害给对方。先把这条规则写进需求文档,在窗口 B 审核边界情况(格挡+反击叠加怎么算?反击触发条件是什么?),然后在窗口 C 生成新代码,在窗口 D 补充测试用例,确保测试通过。
**拓展三(终极挑战):** 做一个双人对战模式——两个玩家分别控制左边和右边的角色,轮流点行动按钮。需求文档里要写清楚:轮次如何切换、一方操作完后如何提示另一方、胜负后如何重新开始。先写需求,再生成,再测试。
---
> 本教案遵循穹狼科创 SDDT + 4C 教学方法论编写,配合 `教学方法论规则.md` 和 `标准教案模板.md` 使用。

View File

@@ -0,0 +1,621 @@
---
课时: 10
主题: 涂鸦PK— 动画 + 音效 + 特技
核心能力: [审美力, 提问力]
核心工具: [Trae IDE, Kimi]
时长: 90分钟
透明化层级: 过程层
适用路线: AICODE-06
---
### 1. 课程目标
**知识目标:**
- 理解「游戏感Game Feel」的概念每次操作必须有视觉反馈玩家才知道自己的行动生效了
- 理解「增量需求」的写法:在已有系统上叠加新功能,只描述新增部分,避免 AI 误改已验收的代码
- 理解 Web Audio API 的核心思想:用纯代码合成音效,不需要任何音频文件
**能力目标:**
- 能用「感觉语言」描述动画需求(「快速冲过去然后弹回来」),让 AI 把感觉翻译成代码参数(审美力)
- 能写出增量需求文档,只描述新增的动画/音效,不重写已有战斗逻辑(提问力)
- 能在 AI 生成代码后,通过视觉对比和试玩,判断动画「感觉对不对」并给出精准修改意见(审美力)
**情感目标:**
- 对「同样的游戏,有没有动画,体验天差地别」产生真实感受——建立「视觉反馈是游戏的灵魂」的审美直觉
- 体验到「我说出一种感觉AI 把它变成了代码」的成就感,增强自然语言驱动开发的自信
- 对下节课的班级对战产生期待感
---
### 2. 核心概念与误概念预设
**核心概念认知层级:**
| 概念 | 学生类比 | 认知层级 |
|------|---------|---------|
| 游戏感Game Feel | 打篮球投篮时「嗖」一声进了网——同样进了,但有没有这个声音感觉完全不一样 | 理解层 |
| 增量需求 | 你去装修只告诉工人「加一个书架」,不需要重新描述整个房子的结构 | 理解层 |
| 感觉语言→参数翻译 | 给厨师说「要辣一点」而不是「加5克辣椒粉」——厨师懂怎么翻译成具体操作 | 应用层 |
| Web Audio 合成音效 | 用嘴巴「嗖——」发出声音,不需要找一个录好的「嗖」的音频文件 | 理解层 |
| 特技视觉标记 | 超市商品的标签——没有标签你看不出这是打折商品;特技没有特效,玩家感知不到它生效了 | 应用层 |
**典型误概念表:**
| 编号 | 误概念 | 正确认知 | 激发策略 |
|------|--------|---------|---------|
| M1 | 动画只是装饰,不影响游戏质量 | 动画是「游戏感」的核心:每次操作必须有视觉反馈,玩家才知道自己的行动生效了 | 展示对比无动画只有数字变化vs 有动画(角色冲刺),问学生哪个更爽 |
| M2 | 音效需要找音频文件,太麻烦了 | Web Audio API 用纯代码合成音效,零素材依赖,浏览器内置支持 | 演示:三行代码合成一个攻击音效,不需要任何音频文件 |
| M3 | 增量需求要把整个需求文档重写一遍 | 增量需求只写新增的部分;已验收的功能不重写,避免 AI 误改 | 类比:装修只说「加书架」,不需要重新描述整个房子 |
| M4 | 动画描述必须用技术参数duration: 200ms | 先用感觉描述「快速冲过去然后弹回来」AI 把感觉翻译成参数;感觉描述比参数描述更准确 | 让学生先说出感觉,再看 AI 翻译的参数,对比哪种方式更自然 |
| M5 | 特技效果只要触发就行,不需要视觉区分 | 特技必须有独特的视觉效果,玩家才能感知到特技生效了(如燃烧 = 橙色粒子,冰冻 = 蓝色闪烁) | 问:「如果燃烧特技触发了但什么都没发生,你怎么知道它触发了?」 |
---
### 3. 教学准备
**工具与环境:**
- 每台电脑已安装 Trae IDE可正常打开上节课第9课的战斗游戏文件
- 每台电脑浏览器可正常播放 Web AudioChrome/Edge 均支持,确认未被系统静音)
- 投影可切换至任意学生屏幕
- 准备两个演示文件:`demo-2-pk-battle.html`(无动画版)和 `demo-3-animation.html`(有动画版)
**教学资源:**
- 教师准备:`demo-3-animation.html` 在本机调试好,各按钮动画流畅可用
- 教师准备「动画增量需求」保底提示词见第5节、「音效合成」保底提示词见第5节
- 教师准备Web Audio 三行代码片段(在 Kimi 里提前验证过能正常合成音效)
- 学生资源上节课第9课完成的战斗游戏 `index.html`,血条/行动/胜负判定已可用
**教师备课体验任务:**
> 备课前,教师必须亲自完成以下操作:
>
> 1. 打开 `demo-2-pk-battle.html`,实际点一遍「普通攻击、重击、格挡、特技」四个按钮,感受没有动画时的体验——记录「哪里感觉很空洞」
> 2. 打开 `demo-3-animation.html`,同样点一遍四种行动,对比感受差距——这是课堂对比演示的素材
> 3. 用「动画增量需求」保底提示词向 Kimi 提交一次,拿到动画代码片段,在自己的测试文件里跑通——记录可能遇到的报错
> 4. 写一段 Web Audio 代码合成「攻击音效」,在浏览器里验证能正常播放(注意 Chrome 需要用户交互后才能触发音频)
> 5. 确认学生电脑音量未被静音Web Audio 可正常发声
---
### 4. 教学流程
---
**第一幕:联系 (Connect) — 10分钟** 🔗
*本幕目标:通过无动画 vs 有动画的真实对比,让学生切身感受「游戏感」的差距,建立「视觉反馈是游戏的灵魂」的直觉*
**【环节】上节课回顾 (2分钟)**
**师:** 上节课我们做了什么?谁来说一下我们的战斗系统现在能做什么?
**生:**(预期:有血条、可以攻击、有胜负、有特技)
**师:** 对,上节课我们还跑了测试脚本,伤害公式和胜负判定都验证通过了。今天我问你们——你们觉得现在这个游戏,好玩吗?好看吗?
**生:**(预期:还行 / 感觉有点无聊 / 就是点按钮数字变化)
**师:** 嗯。现在先别回答,我让你们看两个版本——看完再说。
**【环节】对比演示——无动画 vs 有动画 (8分钟)**
【投屏展示 demo-2-pk-battle.html教师操作】
**师:** 这是我们现在的版本。我点一下「普通攻击」——
【点击普通攻击,血条数字变化,但角色没有任何动作】
**师:** 发生了什么?
**生:**(预期:血量少了 / 没什么感觉 / 就是数字变了)
**师:** 对,血条从 100 变成了 85。游戏逻辑是对的。那感觉呢
**生:**(预期:感觉很平淡 / 不知道攻击有没有打出去 / 没什么反应)
**师:** 好,这个感受记住。现在看第二个版本。
【切换投屏到 demo-3-animation.html】
**师:** 同样的游戏,我点「普通攻击」——
【点击普通攻击,角色冲刺向对方,对方受击抖动,音效响起】
**师:** 现在感觉怎么样?
**生:**(预期:爽多了!/ 有打击感 / 感觉真的在打架)
【继续演示重击:角色蓄力放大→猛冲,屏幕震动】
【演示格挡:角色后退,护盾闪烁】
【演示特技:粒子效果爆发】
**师:** 这两个游戏,逻辑是完全一样的。血条、伤害、胜负——一模一样。唯一的区别是什么?
【诊断点:学生是否能说出「动画」「视觉反馈」「音效」等关键词】【识别层】
**【分支A】若学生说出「动画」「特效」「声音」**
**师:**动画、音效、视觉反馈。这些加在一起有一个专门的词——叫做「游戏感Game Feel」。什么叫游戏感就是每次你操作都有看得见、听得到的反馈——你的动作「生效了」。没有游戏感的游戏就算逻辑完美玩起来也像在操作表格。
**【分支B】若学生说「好看多了但说不出为什么」**
**师:** 你说好看多了,我问你——当你点普通攻击的时候,第一个版本你看到了什么?
**生:**(预期:数字变了)
**师:** 第二个版本呢?
**生:**(预期:角色冲过去了、对方抖了)
**师:** 对。第一个版本你的操作「发生了」但你看不到。第二个版本你的操作「生效了」,你看得到、感受得到。这种「每次操作都有反馈」的体验,叫「游戏感」。今天我们的任务就是把游戏感加到你们的战斗系统里。
**师:** 今天三件事:第一,给每种行动加动画;第二,加上音效;第三,让特技有独特的视觉标记。加完之后,你的战斗游戏就「活了」——可以参加下节课的班级对战了。
---
**第二幕:建构 (Construct) — 65分钟** 🛠️
---
**【分段一:动画系统——用感觉描述需求】(20分钟)**
*本段重点:学会用「感觉语言」而不是技术参数来描述动画;学会写增量需求(只写新增的动画部分,不重写已有战斗逻辑)*
**预设误概念:**
- 误概念 M3增量需求要把整个需求文档重写一遍
- 误概念 M4动画描述必须用技术参数duration: 200ms
**讲解与演示 (Teach & Demo): (5分钟)**
**师:** 我们要给战斗系统加动画。但加动画不是从头重写游戏——我们用「增量需求」的方式,只告诉 AI 「新加什么」,已有的代码不动。
**师:** 有人知道为什么不能重写吗?
**生:**(预期:不知道 / 会改坏 / 太麻烦)
**师:** 你上节课的战斗逻辑,血条、伤害、胜负——都测试通过了,是吧?如果我现在把整个需求文档重写一遍提交给 AIAI 很可能会「重新理解」你的战斗逻辑,把你已经调好的伤害公式改掉,或者把你的特技系统换一种方式实现。这叫「误改」。增量需求就是告诉 AI「已有的代码已经验收通过你只需要在这个基础上加动画不要动其他东西。」
**师:** 就像你家装修完了,现在只想加一个书架——你不会让装修工人重新来一遍,你只说「加一个书架,其他不动」。
**师:** 然后——动画要怎么描述?我问你:你觉得「普通攻击」的感觉应该是什么样子的?
**生:**(预期:冲过去 / 快速冲向对方 / 用力打一下)
**师:** 好,「快速冲向对方」——再具体一点,冲过去之后呢?
**生:**(预期:然后回来 / 弹回来)
**师:**「快速冲向对方然后弹回来」——这就是动画描述。不需要说「x偏移100像素duration 200msease Power2」——你只需要说出感觉AI 会把感觉翻译成参数。
【投屏展示对比】
```
❌ 技术参数描述:
普通攻击x += 100, duration: 200, ease: 'Power2', yoyo: true
✅ 感觉描述:
普通攻击:攻击方快速冲向对方,然后弹回原位,感觉轻快有力
```
**师:** 我们来做个小练习——每个人说出四种行动的感觉,不要用技术词汇,就说你脑子里的画面。
【投屏展示四种行动每个留5秒空白让学生思考】
| 行动 | 你脑子里的画面是什么? |
|------|---------------------|
| 普通攻击 | |
| 重击 | |
| 格挡 | |
| 受击 | |
**师:** 说一下,「重击」你觉得是什么感觉?
**生:**(预期:很用力冲过去 / 先蓄力然后猛冲 / 会震动)
**师:** 「先蓄力然后猛冲」——太好了这就是完美的感觉描述。AI 看到这个会知道要做两段动画:第一段蓄力(角色变大),第二段冲刺(快速位移)。
**学生实践 (Practice): (10分钟)**
在窗口 A 里,学生用感觉语言写出增量需求,提交给 Kimi获取动画代码。
> 教师走动观察谁停下来不动超过2分钟立刻过去观察他们写的感觉描述是否太技术化「角色向右移动100px」还是真的在用感觉语言
**学生写增量需求的结构提示(投屏展示):**
```
我已有一个用 HTML/JavaScript 写的战斗游戏,战斗逻辑已经验收通过。
现在我要新增动画系统,只描述新增部分,请不要修改已有的战斗逻辑。
普通攻击动画:[你的感觉描述]
重击动画:[你的感觉描述]
格挡动画:[你的感觉描述]
受击动画:[你的感觉描述]
死亡动画:[你的感觉描述]
```
**进度同步 (Checkpoint): (5分钟)**
**师:** 谁来分享一下你写的感觉描述?用感觉语言写的举手。
【诊断点:检验学生是否真的用了感觉语言,而不是技术参数】【理解层】
**【分支A】若学生写出了好的感觉描述「像被闪电劈中一样抖动」**
**师:** 这个描述太棒了!我们来看 AI 怎么翻译——
【投屏展示 Kimi 返回的参数,对应学生的感觉描述】
**师:** 你说「像被闪电劈中一样抖动」AI 翻译成了「x 方向快速来回震荡5次每次10像素总时长150毫秒」。感觉对吗
**【分支B】若学生的描述还是偏技术「角色向右移动」**
**师:** 我来问你——你玩游戏里打人,那个「被打」的感觉是什么?能用一个词或一句话描述吗?
**生:**(预期:抖了一下 / 像撞墙一样 / 闪了一下)
**师:**把这个感觉写进去比说「向右移动」更准确——AI 更能理解你真正想要的效果。
**【分支C】若学生 AI 返回的动画代码与感觉不符:**
**师:** 你看一下动画效果,跟你想的感觉一样吗?哪里不对?
**生:**(预期:太慢了 / 感觉不够有力 / 角色跑的方向不对)
**师:** 好,现在你就直接告诉 AI「重击感觉不够震撼蓄力时间太短了冲刺速度也不够快能不能把蓄力时间加长一倍冲刺速度加快」——这就是用感觉来迭代。
---
**【分段二:音效系统——描述声音感觉】(20分钟)**
*本段重点:用三行代码演示 Web Audio 合成原理;学会用「声音感觉」而不是技术参数描述音效需求;实现五种基础战斗音效*
**预设误概念:**
- 误概念 M2音效需要找音频文件太麻烦了
- 误概念 M4延伸描述音效也要用技术参数
**讲解与演示 (Teach & Demo): (5分钟)**
**师:** 现在我们加音效。有人觉得加音效很麻烦的举手——要找音频文件、下载、导入……
**生:**(预期:举手 / 点头)
**师:** 告诉你们一个秘密——我们不需要任何音频文件。我们用代码「生成」声音。
【投屏展示,教师打开浏览器控制台】
**师:** 这是 Web Audio API——浏览器内置的音频引擎。我现在在控制台里写三行代码让浏览器发出一个「攻击音效」。
【现场演示,逐行输入并解释】
```javascript
const ctx = new AudioContext();
const osc = ctx.createOscillator();
osc.frequency.value = 300; // 音调——300 是中等音高
osc.type = 'sawtooth'; // 波形——锯齿波比较「粗糙有力」
const gain = ctx.createGain();
gain.gain.value = 0.3; // 音量
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
setTimeout(() => osc.stop(), 80); // 只响 80 毫秒
```
**师:** 听到了吗?这是攻击音效——短促有力,像轻击。全部代码就这几行,不需要任何文件。
**师:** 和动画一样,音效也用感觉来描述。我来问你——「重击」的声音是什么感觉?
**生:**(预期:低沉 / 爆炸感 / 震动 / 轰的一声)
**师:** 「低沉震撼,像爆炸」——这就是感觉描述。告诉 AI 这个感觉AI 会帮你调频率、波形、时长。你不需要知道「锯齿波还是方波」——你只需要知道「你要什么感觉」。
【投屏展示感觉→参数对应表】
| 行动 | 声音感觉(你来填) | AI 可能翻译成 |
|------|----------------|-------------|
| 普通攻击 | 短促有力,像轻拍 | 频率300Hz锯齿波80ms |
| 重击 | 低沉震撼,像爆炸 | 频率150Hz锯齿波150ms |
| 格挡 | 硬邦邦,像金属碰撞 | 频率800Hz方波50ms |
| 受击 | 痛感,短促的「嗷」 | 频率400→150Hz扫频120ms |
| 胜利 | 欢快,像叮当 | 三音符523→659→784Hz正弦波 |
**学生实践 (Practice): (10分钟)**
学生打开窗口 B独立于战斗游戏用感觉描述让 Kimi 生成五种音效代码。验证每种音效能正常播放后,再把音效函数添加进战斗游戏的对应行动里。
> 教师走动观察重点:
> 1. 学生在描述音效时是否用了感觉语言还是乱写参数
> 2. 音效代码添加进战斗游戏后,有没有整合进「行动触发」时机(不能只是独立的函数,要在攻击时自动调用)
**进度同步 (Checkpoint): (5分钟)**
**师:** 谁来演示一下?点一个行动,我们听听音效。
【让1-2位学生上来演示战斗播放音效】
【诊断点:音效是否在正确的时机触发(点攻击时响,而不是游戏开始时就响)】【应用层】
**【分支A】若音效在正确时机触发且效果合理**
**师:** 注意听——每次点攻击有没有声音?每种行动的声音有没有区别?
【让全班听对比,引导学生注意重击和普通攻击的音效差异】
**师:** 好,把这个感觉记住——有音效和没音效,游戏感差距很大。
**【分支B】若音效能响但时机不对比如每隔一秒自动响**
**师:** 音效响了,但是时机有点问题——应该是点攻击时响,而不是自动响。你的音效函数是在哪里被调用的?
**生:**(查代码)……好像放到外面了?
**师:** 对,你需要把 `playSound_attack()` 这个函数的调用,放进你处理「普通攻击」按钮点击的那个函数里——告诉 AI「请把音效调用放在行动触发时不要自动播放」。
**【分支C】若音效完全没有声音Chrome 浏览器 AudioContext 未激活):**
**师:** 先检查电脑音量,再检查——在 Chrome 里,必须是用户点击页面之后,音频才能播放。你有没有先点一下游戏页面,再点攻击按钮?
【引导学生先点一下页面,激活 AudioContext然后再试】
---
**【分段三:特技视觉效果完善 + 平衡性调整】(15分钟)**
*本段重点:给每个特技加上独特的视觉标记(粒子/颜色/光效),让玩家能感知特技「生效了」;同时微调特技触发概率让对战更平衡*
**预设误概念:**
- 误概念 M5特技效果只要触发就行不需要视觉区分
**讲解与演示 (Teach & Demo): (4分钟)**
**师:** 最后一段——特技。你们的特技现在已经有效果了,对吧?但我问你——燃烧特技触发了,你怎么知道它触发了?
**生:**(预期:有文字提示 / 看日志 / 对方血量变化快)
**师:** 靠文字提示来告诉玩家「特技触发了」——这叫什么?这叫「说给玩家听」。而好的游戏是「让玩家感受到」。
**师:** 每个特技必须有专属的视觉标记——就像每种英雄技能有自己的颜色。我来给你们看几个方向:
【投屏展示特技视觉效果参考】
```
🔥 燃烧:目标角色持续冒出橙红色小粒子,向上飘散
🛡 护甲:角色周围出现蓝色光圈,护盾吸收伤害时闪烁
⚡ 连击:第二次攻击时黄色闪光从角色身上爆发
💉 吸血:攻击时绿色光线从对方身上流向自己
❄️ 冰冻对方角色整体变蓝tint 0x4444ff出现碎冰粒子
🎯 穿透:攻击线变红色,穿过对方护盾
```
**师:** 你的游戏里有哪种特技?现在打开需求文档,给你的特技加一条「视觉效果描述」——用感觉语言,告诉 AI 特技触发时玩家应该「看到什么」。
**学生实践 (Practice): (8分钟)**
学生在窗口 A增量需求里补充特技视觉效果描述提交给 Kimi 生成代码;同时,根据上节课测试结果,检查特技触发概率是否合理(可调整概率,让对战更有趣),把最终版战斗游戏跑通。
> 教师走动重点:检查学生的特技视觉效果是否真的有视觉差异,而不是所有特技都做成「同一种颜色的粒子」
**进度同步 (Checkpoint): (3分钟)**
**师:** 好,现在所有人试玩一下自己的战斗游戏——从开始到有人胜利,完整走一遍。有动画、有音效、有特技视觉效果的请举手。
【诊断点:完整游戏循环(动画+音效+特技视觉)是否全部正常运行】【应用层】
**【分支A】若学生完整游戏循环正常**
**师:** 很好你的涂鸦PK已经有游戏感了。下节课的班级对战就用这个版本。给自己鼓个掌。
**【分支B】若学生动画有但音效没有或相反**
**师:** 没关系,少一项我们继续加。现在缺的是哪一个?告诉我——动画、音效、特技视觉,哪个没做完?
【针对性引导完成缺失的部分,优先保证动画完整,音效可以只有三种基础音效也算通过】
**【分支C】若学生整个游戏运行出错**
**师:** 现在打开浏览器的控制台F12看一下有没有红色的报错信息把报错信息截图或者复制下来告诉 AI「我在运行时遇到了这个报错请帮我定位问题」——这是你们最后一次用 AI 调试,我们在课堂上解决。
---
**第三幕:反思 (Contemplate) — 10分钟** 🤔
**【环节】成果展示 (6分钟)**
**师:** 好,时间到。谁愿意来展示一下今天的战斗游戏?要展示三件事:第一,你的角色是谁;第二,你加了什么样的动画;第三,你设计了什么特技的视觉效果。
【邀请 2-3 名学生展示,每人 1.5 分钟】
**师:** 展示的时候,先说你「用感觉描述了什么」,再演示效果——让大家看你的感觉描述和 AI 翻译出来的代码,差距有多大。
【第一位学生展示】
**师:** 你说「重击感觉像被大锤砸到」AI 做出来的效果,符合你的感觉吗?
**生:**(预期:基本符合 / 比我想的还要好 / 有一点不对但我改了)
**师:** 好——改了几次才满意?
**生:**(预期:两三次)
**师:** 两三次迭代就做到满意——这就是「用感觉描述 + 迭代优化」的效率。
**【环节】互评与讨论 (4分钟)**
**师:** 谁来给刚才展示的游戏说「一个优点 + 一个改进建议」?要具体说,不能只说「很好看」。
**生:**(预期:动画很流畅,但是音效重击的声音感觉不够震撼)
**师:** 好,「不够震撼」——怎么改?
**生:**(预期:声音低一点 / 时间长一点 / 加个爆炸效果)
**师:** 对,这就是具体的改进建议。今天我们训练的核心能力是什么?
**生:**(预期:动画 / 音效 / 特效)
**师:** 这些都是工具。背后的能力是——**审美力**:用感觉描述「好不好看、好不好听」,让 AI 把你的感觉变成代码。审美力不是说「画画好看」,是说「你能不能清楚地描述一个体验,并且判断 AI 做出来的是不是你要的」。
---
**第四幕:延续 (Continue) — 5分钟** 🚀
**【环节】抽象总结 (3分钟)**
**师:** 今天三件事,我们来总结一下。第一件是什么?
**生:**(预期:动画 / 加了动画)
**师:** 对,我们加了动画。背后的方法是什么?
**生:**(预期:用感觉描述 / 感觉语言)
**师:** 对——「用感觉描述需求」。不用技术参数直接说你脑子里的画面AI 来翻译。这个能力不只能用在游戏开发里——你想做一个网页、一个动效、一个演示文稿都可以这样描述「我想要一种……的感觉」AI 来实现。
**师:** 第二件事,音效。背后的发现是什么?
**生:**(预期:不需要音频文件 / 代码可以直接合成声音)
**师:** 对——Web Audio API 用代码合成声音。零文件,浏览器内置。这说明一件事:很多你以为「需要素材」的东西,代码都可以直接生成。
**师:** 第三件事,特技视觉效果。核心原则是什么?
**生:**(预期:要让玩家看得见 / 每种特技要有独特的效果)
**师:** 对——「让玩家感受到,而不是告诉玩家」。这是所有好游戏的共同原则。
**【环节】下节预告 + 5分钟挑战 (2分钟)**
**师:** 下节课——班级大乱斗。你们的战斗游戏会互相对战,不同角色、不同特技。在对战之前,你有机会再调整一次你的特技参数——如果你觉得你的角色太弱,可以偷偷增强一点……
**生:**(预期:笑声 / 期待)
**师:** ……但是太强了也不行,因为我会检查公平性。
**师:** 本周的5分钟挑战给你的一个特技加一个你自己设计的视觉特效——不用上课说的那些完全自己想。描述感觉让 AI 实现,下节课展示。
---
### 5. AI助教使用指南
**教师演示用提示词(动画增量需求):**
```
我已有一个用 HTML + JavaScript 写的战斗游戏,代码在下面。
战斗逻辑(血条、行动、伤害、胜负判定)已经验收通过,请不要修改。
现在我要新增动画系统,只描述新增的动画部分:
- 普通攻击攻击方快速冲向对方100像素然后弹回来感觉轻快有力
- 重击攻击方先膨胀变大1.2倍蓄力感然后猛冲向对方150像素对方水平抖动3次
- 格挡格挡方向后退50像素同时闪烁两次半透明/不透明切换,护盾感)
- 受击被击方左右快速抖动±10像素3次同时变红后恢复原色
- 死亡旋转360度同时缩小到0并淡出600毫秒内完成
- 胜利上下弹跳3次-30像素/次200毫秒/次)
请只输出新增的动画相关代码,以及在哪里插入。不要改动现有代码结构。
```
**教师演示用提示词(音效合成,窗口 B**
```
我要用 Web Audio API 合成战斗游戏音效,全部在 HTML 里实现,不需要任何音频文件。
请帮我写以下音效函数(每个独立的 function音效感觉描述如下
1. playSound_attack()短促有力像轻击金属80毫秒
2. playSound_heavy()低沉震撼像远处爆炸有一点回响感150毫秒
3. playSound_block()清脆硬邦邦像剑挡住了另一把剑50毫秒
4. playSound_hit()短促的痛感像「嗷」一声120毫秒
5. playSound_win():欢快的三音符上升,像「叮叮叮」,音调一个比一个高
每个函数调用时直接播放,不需要预加载。请输出可以直接放进 HTML 的完整代码片段。
```
**学生保底提示词(动画增量需求,窗口 A**
```
我已有一个用 HTML/JavaScript 写的战斗游戏(代码如下)。
战斗逻辑已验收,请不要修改已有代码。
在此基础上新增动画:
- 普通攻击:角色冲出去然后弹回
- 重击:先蓄力放大再猛冲,对方抖动
- 格挡:角色后退并闪烁
- 受击:角色抖动并变红
- 死亡:旋转消失
请只添加动画相关代码,告诉我在哪里插入。
```
**学生保底提示词(音效合成,窗口 B**
```
请帮我用 Web Audio API 合成以下游戏音效,写成 HTML 代码片段,每个音效一个函数:
1. 普通攻击:短促有力的金属碰撞声
2. 重击:低沉震撼的爆炸声
3. 格挡:清脆的金属阻挡声
4. 受击:短促的痛苦音
5. 胜利:三音符上升的欢快音
函数名分别为 playSound_attack、playSound_heavy、playSound_block、playSound_hit、playSound_win。
调用时直接播放,不需要任何音频文件。
```
**进阶提示词(特技粒子效果):**
```
我的战斗游戏有一个「燃烧」特技,触发时目标会持续受到灼烧伤害。
请帮我用 Canvas 2D API不用任何外部库实现
- 燃烧状态下,目标角色周围持续出现橙红色小粒子
- 粒子从角色身体向上飘散透明度随时间降低到0后消失
- 燃烧持续3秒期间粒子效果一直在
请只给我新增的粒子系统代码,以及调用时机说明。
```
---
### 6. 教师指南
**本课技术备注:**
**关于 Canvas 动画实现方式(不用 Phaser 的情况下):**
本课的战斗游戏基于 HTML Canvas 2D 直接绘制(无 Phaser动画通过 `requestAnimationFrame` 循环和时间差deltaTime驱动。动画「缓动感」来自 lerp线性插值每帧把当前位置向目标位置靠近一小步产生「减速停下」的感觉。AI 生成的代码通常会用 `let t = 0; t += deltaTime / duration` 的方式控制进度0到1之间变化再套上缓动函数`1 - (1-t)^3`)产生弹性效果。
**关于 Web Audio API 的自动播放限制:**
Chrome/Edge 要求页面必须有用户交互(点击/键盘输入)后,才允许 `AudioContext` 播放声音。如果游戏一打开就尝试播放,会被浏览器拦截(报错:`AudioContext was not allowed to start`)。解决方案:把第一个 `AudioContext` 的创建放在用户点击按钮之后,或者用 `ctx.resume()` 在点击时激活。AI 生成的代码不一定会处理这个问题,需要教师在课堂提醒。
**关于增量需求的上下文传递:**
让学生在「增量需求提示词」里把整个现有代码粘贴进去Kimi 才能根据现有代码结构插入新代码。如果不提供现有代码Kimi 会自己猜代码结构,生成的插入位置可能不对。提醒学生:「把你的战斗游戏代码全选复制,粘贴到提示词末尾」。
**关于特技粒子效果的性能:**
粒子效果如果每帧创建大量 DOM 元素,会导致游戏卡顿。告诉学生用 Canvas 绘制粒子(在 `requestAnimationFrame` 里画圆点),不要让 AI 创建 `<div>` 元素来做粒子。如果学生的游戏明显卡顿,检查是否有粒子数组无限增长的问题(每帧加粒子但不清除),引导学生告诉 AI「粒子消失后从数组里删除」。
**常见问题 FAQ**
| 问题 | 应对 |
|------|------|
| 「动画代码加进去之后,血条不显示了」 | 很可能是 Canvas `clearRect` 的调用顺序被改了,告诉 AI「加了动画代码后血条不显示请检查 clearRect 和绘制顺序是否被修改」 |
| 「音效能响但是每次攻击响很多次」 | `playSound_attack` 函数被错误地放在了 `requestAnimationFrame` 里,每帧都在调用;告诉 AI「音效每次行动只应该播放一次」 |
| 「死亡动画结束后角色还留在屏幕上」 | 死亡动画只改了透明度,没有在动画结束后将角色标记为「已死亡」并停止绘制;引导学生告诉 AI「动画结束后停止绘制该角色」 |
| 「特技粒子效果触发一次后就一直在了」 | 粒子系统的「存活时间」逻辑没有实现,粒子永远不消失;告诉 AI「每个粒子有3秒存活时间时间到了就从数组里移除」 |
| 「我的重击蓄力动画太慢了游戏变拖沓」 | 这是审美判断问题,直接让学生告诉 AI「蓄力时间改成 150ms冲刺时间改成 80ms感觉更爽快」 |
| 「Chrome 没有声音」 | 先确认系统音量;再确认是否先点击了游戏页面;最后检查 `AudioContext` 是否在按钮点击后创建 |
**课堂风险预案:**
- **如果 Kimi 返回的代码加进去运行报错:** 让学生把报错信息复制给 Kimi提示词格式「我按你说的把代码加进去了运行时出现了这个报错[报错信息],原来的代码是 [原代码],请帮我找到原因并给出修正后的代码」
- **如果学生进度差异过大(有人全部做完有人还没做动画):** 做完的学生进入拓展任务(给特技加粒子效果 + 调平衡性);做到一半的学生优先完成动画,音效用保底提示词一步到位
- **如果整体时间不够25分钟建构还剩两段没做完** 合并分段二和分段三,音效只做三种基础音效(攻击/受击/胜利),特技视觉效果作为课后挑战
---
### 7. 5分钟日常AI挑战
**本周挑战:** 给你的一个特技,设计一个全新的视觉特效
**挑战说明:**
选你游戏里你最喜欢的一个特技,想象它触发时应该「看起来像什么」——可以是粒子爆炸、颜色变化、屏幕震动、时间静止特效……只要用感觉语言描述出来,告诉 AI 实现。要求:这个特效必须和课上做的不一样,完全是你自己的设计。
**下节课分享:** 下周课上选 2-3 位同学展示特技特效挑战成果说明自己是怎么描述感觉的AI 实现了几轮才满意。
---
### 8. 拓展任务
**拓展一(推荐):屏幕震动特效**
重击时不只是角色抖动——整个游戏画布轻微震动一下Canvas 偏移 ±5 像素,持续 200ms。这种效果叫「屏幕震动Screen Shake是格斗游戏的经典手法。用感觉描述让 AI 实现:「重击时整个画面抖一下,像相机被猛地推了一把」。
**拓展二(挑战):行动预判音效**
在角色攻击「蓄力」阶段播放一个低频「嗡——」的充能音效,攻击命中时再播放冲击音效——两段音效组合,制造出「蓄力→爆发」的完整听觉体验。提示:需要两个 Web Audio 函数,第一个在蓄力开始时调用,第二个在命中时调用,注意时机控制。
---
### 附:本课核心教学框架速查
**四幕时间分配(合计 90 分钟):**
| 幕 | 内容 | 时长 |
|----|------|------|
| 联系 Connect | 对比演示无动画 vs 有动画,引出「游戏感」 | 10 分钟 |
| 建构 · 分段一 | 感觉语言描述动画,写增量需求 | 20 分钟 |
| 建构 · 分段二 | 用代码合成音效,整合进战斗行动 | 20 分钟 |
| 建构 · 分段三 | 特技视觉标记完善 + 平衡性微调 | 15 分钟 |
| 反思 Contemplate | 成果展示 + 感觉描述回顾 + 互评 | 10 分钟 |
| 延续 Continue | 抽象总结三件事 + 预告班级对战 + 挑战发布 | 5 分钟 |
**本课核心能力训练路径:**
```
审美力路径:
无动画体验(识别层)
→ 用感觉描述动画/音效需求(理解层)
→ 对比 AI 输出和自己预期,定位差距(应用层)
→ 精准迭代修改,达到满意(迁移层)
提问力路径:
理解增量需求的必要性(识别层)
→ 只写新增部分的提示词,不重写全局(理解层)
→ 在已有代码基础上定向追加功能(应用层)
```
**关键词速查(下课前可口头检查学生是否能解释):**
- 游戏感Game Feel每次操作都有视觉/听觉反馈,让玩家感受到行动生效
- 增量需求:在已验收代码基础上只描述新增部分,避免 AI 误改
- Web Audio API浏览器内置音频引擎用代码合成声音无需音频文件
- 感觉语言:用描述体验和画面感的词汇提需求,而非技术参数

View File

@@ -0,0 +1,599 @@
---
课时: 11
主题: 涂鸦PK— 班级锦标赛
核心能力: [表达力, 共创力]
核心工具: [Trae IDE, Kimi]
时长: 90分钟
透明化层级: 结果层
适用路线: AICODE-06
---
### 1. 课程目标
**知识目标:**
- 理解「数据驱动设计」的核心思想:角色数据与游戏代码分离,加新角色只需加文件,代码不用改
- 理解角色选择界面是「数据读取 + UI 展示」的组合,而非把所有角色写死在代码里
- 理解路演的核心是「设计决策」而非「功能列表」:说清楚「为什么这样做」比「做了什么」更有价值
**能力目标:**
- 能用增量需求文档描述 roles 系统并经过窗口B审核、窗口C执行共创力
- 能在班级锦标赛中用语言分析胜负原因,做出有依据的复盘(表达力)
- 能按「展示角色→设计意图→精彩瞬间→设计复盘」结构完成3分钟路演表达力
**情感目标:**
- 感受到「自己的角色和全班的角色一起对战」的真实社交激励
- 建立「输了是迭代的起点」而非「输了说明设计失败」的成长心态
- 对「数据和代码分离」产生直觉性认同——这是真实工程师的设计方式
---
### 2. 核心概念与误概念预设
**核心概念认知层级:**
| 概念 | 学生类比 | 认知层级 |
|------|---------|---------|
| 数据驱动设计 | 游戏皮肤系统——你换皮肤不需要重新下载整个游戏,皮肤是「数据」,游戏逻辑是「代码」,两者分离 | 理解层 |
| 硬编码 | 把菜单直接印在餐厅墙上——想加一道菜要重新刷墙 | 识别层 |
| roles 系统 | 班级花名册——班里来了新同学,只需要在花名册上加一行,不需要改班级的所有规则 | 理解层 |
| 增量需求 | 在已有房子里加一个房间——不是推倒重建,而是在现有结构上扩展 | 应用层 |
| 路演设计决策 | 苹果发布会不说「这个手机有摄像头」,而是说「为什么这个摄像头改变了拍照方式」 | 应用层 |
**典型误概念表:**
| 编号 | 误概念 | 正确认知 | 激发策略 |
|------|--------|---------|---------|
| M1 | 加新角色需要改代码 | 数据驱动设计:加一个 json 文件 = 加一个角色,代码不用改 | 演示:不改一行代码,只在 roles/ 文件夹里加一个新 json新角色就出现在选择界面 |
| M2 | 路演就是说「我做了什么功能」 | 路演要说「我为什么这样设计」——设计决策比功能列表更有价值 | 类比苹果发布会:发布会不说「手机有摄像头」,而是说「为什么这颗摄像头改变了拍照方式」 |
| M3 | 属性随便分的角色不可能赢认真分的 | 「奇怪」的属性分配有时会产生意外的克制效果;游戏平衡比绝对强度更重要 | 展示一个「全部堆 HP」的坦克型角色用消耗战赢了「全部堆 ATK」的爆发型角色 |
| M4 | 输了说明自己设计失败了 | 输了是迭代的起点;「如果重来属性怎么分」才是最有价值的问题 | 强调:世界上最厉害的游戏设计师也在持续迭代,没有「一次设计就完美」的 |
---
### 3. 教学准备
**工具与环境:**
- 每台电脑已安装 Trae IDE第10课的战斗游戏项目可以正常运行
- 每台电脑有学生自己的角色图片(.png和角色数据.json
- 投影可切换至任意学生屏幕,用于锦标赛投屏
- 教师电脑预建好 `roles/` 文件夹结构(用于演示数据驱动)
**教师备课必做(课前):**
> 1. 提前向每位学生收集角色文件(角色名.png + 角色名.json放入教师电脑 `roles/` 文件夹
> 2. 在教师电脑上跑一遍 roles 系统生成流程,确保角色选择界面正常读取所有角色
> 3. 准备好「花名册投屏」:一个展示全班角色名字 + 特技的页面或截图
> 4. 准备好锦标赛对阵表(单淘汰制,根据班级人数提前画好括号)
> 5. 备用方案:如果 roles 系统生成失败手动在代码里加角色选择下拉菜单15分钟内可完成
**教学资源:**
- 教师准备:全班 roles/ 文件夹(含所有 png + json
- 教师准备:锦标赛对阵表(投屏用)
- 教师准备保底提示词见第5节
- 学生资源第10课的战斗游戏项目文件index.html
---
### 4. 教学流程
---
**第一幕:联系 (Connect) — 10分钟** 🔗
*本幕目标:用「角色花名册」制造全班兴奋感,引出数据驱动概念,建立「今天是全班大乱斗」的期待*
**【环节】角色花名册 + 今日导入 (10分钟)**
**师:** 课前我收集了大家的角色文件。我现在打开,给大家看看今天的「选手花名册」。
【投屏展示花名册——所有角色的名字、HP/ATK/DEF/SPD 属性、特技名称】
**师:** 你们来数一数,今天一共有多少个角色参赛?
**生:** 数人数……XX 个!
**师:** XX 个角色,今天全部都要上场对战。谁是最强的?我们今天锦标赛见真章。
【停顿 3 秒,让兴奋感发酵】
**师:** 但我现在遇到一个问题。大家的游戏现在只能用「硬编码」的两个角色。什么叫硬编码?就是角色数据直接写死在代码里,像这样——
【投屏展示代码片段】
```javascript
// 角色数据写死在代码里
const player = { name: '章鱼怪', hp: 80, atk: 15, def: 5, spd: 8 };
```
**师:** 这种写法,想换一个角色,要去代码里改数字。想加一个新角色,要再写一段代码。如果班里有 8 个角色要互相打,要改多少次代码?
【识别层:让学生感受到硬编码的局限性】
**生:** (预期:很多次……要改来改去)
**师:** 对,非常麻烦。今天我们要做一件事——用「数据驱动」的方式重新设计这个系统。做完之后,加新角色只需要加一个 json 文件,代码一行都不用改。我们先做好这个系统,然后——锦标赛开始。
【诊断点:学生是否感受到「数据驱动」和「硬编码」的差别,还是觉得无所谓】【识别层】
**【分支A】若学生问「数据驱动是什么意思」**
**师:** 你知道游戏皮肤吗?比如王者荣耀,你换一个皮肤,不需要重新下载整个游戏对吧?皮肤就是「数据」,游戏逻辑是「代码」,两个东西分开存放。我们今天就是要把角色数据从代码里分离出来,放进独立的 json 文件。
**【分支B】若学生觉得「改代码也没什么反正能跑」**
**师:** 如果你做的游戏将来要卖给别人,需要经常更新角色,每次更新都要改代码,那维护成本是很高的。数据驱动就是为了让「扩展」变得简单——这是真实游戏公司的标准做法。
---
**第二幕:建构 (Construct) — 65分钟** 🛠️
*本幕目标:完成 roles 系统(数据驱动角色选择);举办班级锦标赛*
---
**【分段一roles 系统——数据驱动设计实战】(20分钟)**
**预设误概念:**
- 误概念 M1加新角色需要改代码核心要破除的误概念
- 学生可能认为「从文件夹读取 json」很难实现需要很复杂的代码
**讲解与演示 (Teach & Demo): (5分钟)**
**师:** 现在我们用三个窗口来完成这个系统。还记得三窗口原则吗?
**生:** 预期窗口A写需求窗口B审核窗口C执行
**师:** 对。今天的需求是「增量需求」——不是从头做,而是在现有战斗游戏上加一个新模块。我先给大家看增量需求文档长什么样。
【投屏展示增量需求文档内容,逐条讲解】
**师:** 这份需求文档有四条核心要求。我来念一遍:
第一,从 roles/ 文件夹读取所有 .json 文件,每个文件包含 name、hp、atk、def、spd、skill 字段。
第二,显示角色选择界面:左边选玩家角色,右边选 AI 角色,每个角色显示名字和属性。
第三,有「开始战斗」按钮,点击后进入战斗,加载选中角色对应的同名 .png 作为 Spritesheet。
第四,不需要重写整个游戏——只增加角色选择这一层,原来的战斗逻辑不动。
**师:** 注意最后一条——「不重写整个游戏」。这就是增量需求的核心。我们不推倒重来,我们在现有基础上扩展。
现在我演示一下最重要的一步——数据驱动设计的关键代码思路。
【投屏展示两种写法对比】
```
硬编码方式(现在):
代码里直接写 { name: '角色A', hp: 80, atk: 15 ... }
→ 想加角色:改代码
数据驱动方式(今天要做):
读取 roles/角色A.json → 代码只负责读取,不存储数据
→ 想加角色:在 roles/ 文件夹里加一个新 json 文件,代码不变
```
**师:** 看到区别了吗现在窗口A的需求文档大家已经看到了我们直接去窗口B审核。
**学生实践 (Practice): (12分钟)**
【教师打开窗口B新的 Kimi 对话),投屏展示审核提示词】
**师:** 窗口B的提示词是——
```
你是一个严格的需求审核工程师。我有一份增量需求文档,请找出其中可能的漏洞和没说清楚的地方。每个问题单独列出来,只问问题,不给解决方案。
需求文档:
[粘贴增量需求内容]
```
**师:** 大家现在把窗口B打开把这个提示词加上你自己的增量需求提交审核。我来演示一遍你们同步做。
> 教师同步演示窗口B审核过程投屏展示 AI 找出的问题roles/ 文件夹里没有 png 文件只有 json 怎么处理?同一个角色能选两次吗?)
**师:** AI 找到了这几个问题。大家对照一下你的窗口B找到了什么问题
**生:** (预期:找到类似问题,或发现自己的需求文档有遗漏)
**师:** 好,把 AI 问的问题答案补充进需求文档。然后开窗口C执行。窗口C的提示词见屏幕。
【投屏展示保底提示词,学生复制使用】
> 教师巡视:谁的屏幕 3 分钟没变化就主动过去。重点关注窗口C是否用了「只输出新增/修改的代码」指令,避免 AI 重写整个游戏。
**进度同步 (Checkpoint): (3分钟)**
**师:** 谁的角色选择界面已经出来了?截图或者展示屏幕。
【让 1-2 位学生展示屏幕】
**师:** 你试试——现在不改一行代码,把一个新的 json 文件放进 roles/ 文件夹,刷新页面,新角色出现了吗?
【诊断点:验证学生是否真正实现了数据驱动,而不只是在选择界面里把角色名字硬编码进去】【应用层】
**【分支A】若角色自动出现了**
**师:** 这就是数据驱动。你的代码现在可以无限扩展角色,不需要碰代码本身。这是真实游戏公司的标准做法。
**【分支B】若角色没有自动出现角色名写死在选择界面里**
**师:** 看一下你的选择界面代码,角色名是直接写在 HTML 里还是从文件里读的?如果是写在 HTML 里,说明还是硬编码,需要让 AI 改成读取 json 文件的方式。提示词这样说:「我的角色选择界面现在是把角色名硬编码在 HTML 里的,请改成从 roles/*.json 文件里读取,动态生成选择列表。」
**【分支C】若 AI 把整个游戏重写了:**
**师:** 这是常见的情况——AI 有时候会把整个代码重写一遍。发现这个情况后,告诉 AI「请不要重写整个游戏只输出 roles 系统的新增部分,我自己复制进去。」然后把新增部分手动粘贴到原来的代码里。
---
**【分段二:班级锦标赛——全班大乱斗】(30分钟)**
**预设误概念:**
- 误概念 M3属性随便分的角色不可能赢认真分的
- 误概念 M4输了说明自己设计失败了
**讲解与演示 (Teach & Demo): (3分钟)**
**师:** roles 系统已经完成了。现在游戏可以选任意角色对战了。我们正式开始今天的班级锦标赛!
【投屏展示锦标赛对阵表】
**师:** 规则说明。单淘汰赛制。每场对战一方选自己的角色另一方选自己的角色战斗5回合。5回合后谁 HP 多谁赢;如果有一方 HP 归零,提前结束。每场大约 2-3 分钟。赢的人晋级,输的人直接进入路演准备。
**师:** 在每场开始之前,我会给大家介绍两个角色的属性和打法,然后问你们——你们觉得谁会赢?
**师:** 有没有问题?
> 如果学生有问题,简短回答;如果没有,直接开始第一场
**学生实践 (Practice): (25分钟)**
【锦标赛主持逐字稿,每场 2-3 分钟】
**第一场开始前:**
**师:** 第一场对阵:[角色A] vs [角色B]。我来介绍一下这两位选手——
[角色A] 是 [学生名] 设计的,[角色名],属性是 HP[数字]、ATK[数字]、DEF[数字]、SPD[数字],特技是[特技名]。从属性看,这是一个[分析高ATK爆发型 / 高HP坦克型 / 高SPD速攻型]的角色。
[角色B] 是 [学生名] 设计的,[角色名],属性是 HP[数字]、ATK[数字]、DEF[数字]、SPD[数字],特技是[特技名]。这是一个[分析]的角色。
你们觉得谁会赢?举手投票——投 [角色A] 的举手……投 [角色B] 的举手……
好,开始!
【由第一个出战的学生上台操作,或者由教师代操,全班围观投屏】
**战斗进行中(教师解说):**
**师:** 第 1 回合,[角色A] 先手——攻击了 [数字] 点伤害。[角色B] 现在剩 HP [数字]。
**师:** [角色B] 反击……特技触发了![特技效果]。现在形势[分析]。
**师:** 大家注意这一回合——[角色A] 的 DEF 是 [数字],所以对方每次攻击被减了 [数字] 点。这就是 DEF 的价值。
**战斗结束后:**
**师:** [赢方角色] 赢了![学生名] 晋级。
**师:** 分析一下——[赢方角色] 赢的原因是什么?是属性设计好,还是特技时机对,还是单纯运气?
**生:** (预期:因为 HP 多扛住了 / 因为特技打了两次 / 因为 ATK 高秒杀了)
**师:** 对。[具体分析赢的原因]。[输方角色] 输了——如果重来,你觉得属性怎么分会更好?
**生(输方学生):** (预期:我应该把 DEF 加高一点 / 我应该多加 SPD 先手)
**师:** 这个分析很好。记住这个想法,等会儿路演的时候说出来。
【继续下一场,同样流程】
**半决赛/决赛前:**
**师:** 现在进入[半决赛/决赛][角色A] 和 [角色B],这两位已经赢过了[X]场。来介绍一下他们的战绩——
**师:** [角色A] 在上一场用[什么策略]赢了[谁]。[角色B] 在上一场用[什么方式]赢了[谁]。这场对阵,你们觉得——
**决赛结束后:**
**师:** 恭喜[冠军角色][学生名] 拿到今天锦标赛冠军!
**师:** 但我要说一件事——今天冠军不是「最聪明的人」,而是「今天这个对阵顺序下最幸运的人」。如果换一个对阵顺序,冠军可能完全不同。游戏平衡性比绝对强度更重要——这是游戏设计师最核心的命题。
**进度同步 (Checkpoint): (2分钟)**
**师:** 锦标赛结束了。在进入路演之前,我问大家一个问题:你们觉得,今天的冠军角色「设计得最好」吗?
【诊断点:学生是否理解「赢得比赛」和「设计得好」是两件事,随机因素(对阵顺序)影响结果】【理解层】
**【分支A】若学生说「冠军设计最好」**
**师:** 如果把今天的对阵顺序换一下,让冠军第一场就对上另一个强角色,他还会赢吗?不一定。所以赢比赛不等于设计最好——游戏平衡设计是一门科学,不是谁 ATK 最高谁就赢。
**【分支B】若学生说「运气成分很大」**
**师:** 对,你说到一个重点。同样属性的两个角色打十场,赢的次数可能各五场。所以好的游戏设计要让不同风格的角色都有赢的可能,而不是让一种打法通吃所有。这叫「策略多样性」。
---
**第三幕:反思 (Contemplate) — 15分钟** 🤔
*本幕目标每人完成3分钟路演训练「说设计决策」而非「说功能列表」*
**【环节】班级路演 (15分钟)**
**师:** 现在进入路演环节。每个人 3 分钟,包括展示和提问。路演有四段固定内容:
【投屏展示路演结构】
```
第1段30秒展示角色
说:「我的角色叫 XX是一个 XX 型,属性分配是……」
第2段30秒设计意图
说:「我选这个打法是因为……我觉得用 XX 特技可以克制……」
第3段60秒精彩瞬间
说:「最让我印象深刻的是和 XX 的对战,那一场……」
第4段60秒设计复盘
说:「如果重来,我会把 XX 调低一点,把 XX 调高……因为我发现……」
```
**师:** 注意——路演不是说「我的角色有什么功能」。路演是说「我为什么这样设计」。这两件事差很多。我来举个例子:
错误示范:「我的角色叫章鱼怪,它有 HP 80、ATK 15、DEF 5、SPD 8特技是连击。」
正确示范:「我叫它章鱼怪,我把 HP 设高是因为我想让它能扛住对方的连续攻击,用消耗战的方式赢。特技选连击是因为连击在对方 HP 低的时候效果最好,可以一次清掉。」
看到区别了吗?第一个说了什么,第二个说了为什么。
**师:** 好,谁先来?
【6-8人小班建议全员路演学生逐一路演教师计时到 3 分钟给提示】
**路演示例教师可以第一个先做一遍示范30秒内**
**师:** 我先给大家示范一遍格式。如果我设计了一个角色,我会这样路演——
「我的角色叫石甲龟是一个防御型角色。HP 设了 100DEF 设了 20ATK 只有 5——我知道这样攻击力很弱但我的想法是用超高的防御力把对方的输出全部挡掉靠持久战赢。特技我选了「反弹」——把对方一部分伤害反还给它。最让我印象深刻的是和高 ATK 角色对战,那一场打了整整 5 回合,对方 ATK 30 打在我身上每次只剩 10 点,我靠反弹慢慢把它磨死了。如果重来,我会把 SPD 调高一点,因为我发现先手权在某些情况下比防御更重要——有几场因为后手被秒杀了。」
看到了吗?每一个数字背后都有理由,每一个设计决策都可以说出来。好,现在轮到大家了。谁第一个?
【学生逐一路演,教师计时,到 3 分钟给提示】
**每位学生路演后,教师引导互评:**
**师:** [学生名] 路演完了。谁来给一个「一个优点 + 一个改进建议」?
**生:** (预期:优点是……建议是……)
**师:** 好。[学生名],你对这个建议怎么看?如果你的下一个版本按照这个建议改,你觉得会怎样?
**生:** (预期:可能会更好 / 但是我担心……/ 我觉得还有另一个问题是……)
**师:** 好,这就是设计思维——不是「改了一定变好」,而是「改了会带来什么新的权衡」。
**学生路演引导词(当学生卡壳时使用):**
若学生在「设计意图」卡壳:
**师:** 你当时为什么把 HP 设成这个数字?是随机的还是有理由的?
若学生在「精彩瞬间」卡壳:
**师:** 锦标赛里你最紧张的是哪一场?或者最意外的是哪一场?就说那一场。
若学生在「设计复盘」卡壳:
**师:** 你输的那场,对方哪个属性让你最难受?如果把你的属性分配改一下,能不能针对性地克制它?
若学生整体准备不足:
**师:** 好,我给你 30 秒想一下。有一个问题你一定能回答:你和谁打的那一场,最让你印象深刻?说那一场就行。
**路演结束后的整体反思问题:**
**师:** 好,大家都路演完了。我有一个问题——你们今天路演,最难说的是哪个部分?是「展示角色」还是「设计意图」还是「设计复盘」?
【诊断点:学生是否认为「说为什么」比「说是什么」更难,反映其对设计思维的认知层级】【理解层】
**【分支A】若学生说「最难的是设计复盘」**
**师:** 对,复盘最难。因为复盘要求你承认「我的设计有问题」,然后还要找到具体是什么问题,还要想出怎么改。这个能力叫「反事实推理」——世界上所有的工程师和设计师都在练这个能力。你们今天已经开始练了。
**【分支B】若学生说「最难的是设计意图因为我当时随便分的」**
**师:** 没关系,随便分也有设计意图——「我不知道怎么分,所以平均分」本身就是一个决策。下一次你设计角色的时候,在分属性之前先写一句话:「我要做一个什么风格的角色,要克制什么打法」,有了这句话,属性分配就有了方向。
---
**第四幕:延续 (Continue) — 5分钟** 🚀
**【环节】四课主线总结 (3分钟)**
**师:** 最后一个问题——锦标赛结束后,你们觉得游戏平不平衡?有没有某种打法「必赢」?
**生:** (预期:有一点不平衡 / 感觉高 ATK 的更容易赢 / 感觉差不多)
**师:** 游戏平衡是永远做不到「完美」的——哪怕是王者荣耀这种级别的游戏,每个版本也要出「平衡性调整」。你们今天做的游戏也一样,会有某些打法偏强。这是正常的。重要的是你们发现了这个问题,发现了就可以迭代。
好,进入总结。
**师:** 今天是涂鸦 PK 系列的最后一课。我们来回顾一下这四课做了什么——
【投屏展示四课主线总结】
```
第8课需求文档 → 画图工具
你用工程流程做了一个工具
第9课需求文档 → 战斗系统 + 测试验证
你用数据验证了你设计的规则
第10课增量需求 → 动画 + 音效
你用「感觉描述」扩展了系统
第11课数据驱动 → 角色系统 + 锦标赛
你让系统可以无限扩展
```
**师:** 但这四课里,你们学到的不是游戏。
你们学到的是——如何把一个想法,变成可以被验证、可以被扩展、可以被迭代的系统。
需求文档让你在动手之前想清楚。
测试验证让你知道自己做的对不对。
增量需求让你在已有基础上扩展,而不是推倒重来。
数据驱动让你的系统可以无限成长,不被代码本身限制。
**师:** 这四件事,就是工程师做事的方式。你们今天用这个方式,做出了一个可以全班对战的游戏。
**【环节】下节预告 + 5分钟挑战 (2分钟)**
**师:** 下节课,我们开始新的项目。你们在前面学到的这些能力——需求文档、增量迭代、数据驱动——会在接下来的项目里继续用到。
**师:** 本周 5 分钟 AI 挑战——给你的角色设计一个「2.0 版本」:改变属性分配,写下这次你要克制哪种打法,以及你的设计理由。不需要改代码,只需要改 json 文件里的属性数值,然后写一段话说清楚「我为什么这样改」。下节课分享。
---
### 5. AI助教使用指南
**窗口A——增量需求文档模板教师投屏用**
```
增量需求文档 v1.0
项目涂鸦PK战斗游戏
新增功能roles 角色选择系统
1. 从 roles/ 文件夹读取所有 *.json 文件
- 每个 json 包含字段name, hp, atk, def, spd, skill
- 如果文件夹为空或读取失败,显示提示信息「暂无角色,请检查 roles/ 文件夹」
2. 战斗开始前显示角色选择界面
- 左侧:玩家角色列表(从 json 文件读取,动态生成)
- 右侧AI 对手角色列表(同上)
- 每个角色显示名字 + HP/ATK/DEF/SPD 数值
- 两侧可选同一个角色(允许镜像对战)
3. 选择确认
- 点击「开始战斗」按钮进入战斗
- 战斗时加载对应的同名 .png 文件作为角色图片
- 如果 .png 不存在,使用默认占位图
4. 不修改原战斗逻辑
- 只新增角色选择这一层
- 原来的战斗循环、伤害计算、特技逻辑保持不动
```
**窗口C——保底执行提示词学生直接使用**
```
我已有一个 Phaser.js 战斗游戏,两个角色数据现在是硬编码的。
现在我要加角色选择系统:
1. 从 roles/ 文件夹读取所有 *.json 文件
(每个文件包含 name/hp/atk/def/spd/skill 字段)
2. 显示角色选择界面:左边选玩家角色(列表),右边选 AI 角色(列表)
3. 每个角色显示名字和属性数值
4. 选好后点「开始战斗」进入游戏,加载对应的同名 .png 作为角色图片
5. 如果 .png 不存在,显示默认占位图
只输出新增/修改的代码部分,不要重写整个游戏。
在代码里加注释说明每段新增代码的作用。
```
**路演辅助提示词(给路演准备不足的学生):**
```
我的游戏角色数据是:
名字XX
HP=[数字], ATK=[数字], DEF=[数字], SPD=[数字]
特技:[特技描述]
在班级锦标赛中,我赢了[X]场,输了[X]场。
帮我用 3 个问题引导我做「设计复盘」——只问问题,不给答案。
```
**进阶提示词(给完成基础任务、想进一步扩展的学生):**
```
我的角色选择系统已经完成了。现在我想加一个「角色详情」功能:
点击角色卡片时,弹出一个详情面板,显示:
1. 角色大图(放大版的 .png
2. 属性条形图HP/ATK/DEF/SPD 各用一条进度条表示满分100
3. 特技说明(从 json 里读取 skill 字段,格式化显示)
4. 「选择此角色」按钮
只输出新增代码,不改变现有的选择界面逻辑。
```
---
### 6. 教师指南
**本课技术备注:**
**关于浏览器无法直接读取本地文件夹的问题:**
浏览器出于安全原因,无法用 `fetch('roles/角色.json')` 直接读取本地文件夹(会报 CORS 错误)。解决方案:
- 方案A推荐用 Trae IDE 的本地服务器功能启动项目Trae 内置 Live Server自动解决 CORS 问题)
- 方案B让 AI 生成一个简单的 Node.js 本地服务器(`node server.js`),在本地 8080 端口提供文件服务
- 方案C备用改为手动列出所有角色文件名`Promise.all` 批量 fetch避免了「自动扫描文件夹」的需求但需要每次加新角色时在列表里加一个文件名
**备课时务必先在 Trae 里跑通,确认本地服务器正常。**
**关于 Phaser.js 动态加载 Spritesheet**
Phaser 的 `this.load.spritesheet()` 需要在 `preload()` 阶段调用,不能在游戏运行时动态加载。如果 AI 生成的代码在 `create()` 里加载图片,会报错。解决方案:提示词加上「角色图片需要在 preload 阶段加载,请用 scene.restart() 重新启动场景来实现动态切换角色」。
**锦标赛组织 FAQ**
| 问题 | 应对 |
|------|------|
| roles 系统没做好,无法进行锦标赛 | 退回到手动改代码换角色,每场由教师提前改好对应角色的数据,坚持锦标赛进行 |
| 某位学生的角色文件损坏/无法加载 | 临时用教师提前准备的备用角色文件替换 |
| 6人以下小班如只有4人| 改为双败赛制(输两场才淘汰),增加对战场次 |
| 学生对结果强烈不满认为Bug导致输了| 认可情绪说「你可以在5分钟挑战里设计2.0版本,下次来复仇」 |
| 路演时学生只说功能不说设计意图 | 用引导词打断:「等等,你刚才说了做了什么。现在告诉我,你为什么这样做?」 |
**路演控时技巧:**
- 用手机计时3分钟倒计时给学生看
- 到2分30秒时轻声提醒「还有30秒」
- 如果学生还没到「设计复盘」环节,可以跳过「精彩瞬间」直接问:「如果重来你会怎么改?」
- 互评限时 30 秒:「一个优点,一个建议,简短说」
- 路演顺序建议:冠军最后一个路演,制造节奏感;输了第一场的同学第一个路演,减少等待焦虑
**关于「数据驱动设计」的深度追问(给理解快的学生):**
如果有学生问「为什么要把数据和代码分开,直接写在代码里不是更方便吗?」,这是一个很好的问题,可以这样回答:
「当你只有 2 个角色的时候,写死在代码里确实更方便。但当你有 100 个角色的时候呢?当别人也要给你的游戏加角色的时候呢——你要让他改你的代码吗?数据和代码分离,是为了让扩展和维护变得容易。这个原则不只在游戏里,几乎所有的软件系统都在用——数据库、配置文件、皮肤系统,都是同一个思路。」
**锦标赛后的班级文化建立:**
锦标赛不是为了分出高下,是为了制造「真实战斗测试」的场景。建议在锦标赛结束后强调:
- 不设「最强角色」称号,只设「冠军」(今日锦标赛冠军,不代表绝对最强)
- 鼓励「最有创意设计」:给属性分配最特别、最有设计理由的角色点名表扬
- 鼓励「最佳复盘」:在路演中,给复盘最到位、最有具体改进方案的同学点名表扬
**课堂风险预案:**
- 如果 Trae 本地服务器启动失败:切换到 Kimi 直接生成 HTML不依赖本地服务器
- 如果锦标赛时间不够roles 系统做得慢):缩减锦标赛场次,只打半决赛 + 决赛(两场)
- 如果进度差异过大有学生roles系统5分钟做完有学生20分钟还没做好先完成的学生做进阶任务角色详情弹窗帮进度慢的学生看报错
---
### 7. 5分钟日常AI挑战
**本周挑战:** 给你的角色设计「2.0 版本」
**挑战说明:**
打开你角色的 json 文件修改属性数值设计一个新版本。要求写一段话3-5句说清楚三件事
1. 你改了哪些属性,改成了什么数字
2. 这次你想克制哪种打法(比如:克制高 ATK 爆发型 / 克制高 SPD 速攻型)
3. 你觉得这样改会带来什么新的弱点
不需要跑代码验证,只需要写出「设计意图」。
**下节课分享:** 下周课上选 2-3 位同学分享「我的 2.0 设计方案」,其他同学判断:这个改动真的能克制目标打法吗?
---
### 8. 拓展任务
**拓展一(推荐):角色详情卡**
在角色选择界面里,给每个角色加一个「详情」功能:点击角色名字时,弹出一个小面板,用条形图显示 HP/ATK/DEF/SPD 的比例关系满分100的进度条让玩家在选角色前能直观对比属性强弱。
提示:条形图可以用 HTML 的 `<progress>` 标签,或者用 CSS 的 `width` 百分比实现。
**拓展二(挑战):战斗回放摘要**
每场战斗结束后自动生成一段「战斗日志」记录关键回合如特技触发、HP 归零)的文字摘要。格式参考:
```
第3回合章鱼怪特技「连击」触发对火焰鸟造成双倍伤害 28 点
第5回合火焰鸟 HP 归零,章鱼怪以 HP 34 获胜
```
这个功能需要在战斗循环里加「事件记录」逻辑,是真正的功能扩展挑战。

View File

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>涂鸦角色画图工具</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
font-family: Arial, sans-serif;
color: #fff;
padding: 20px;
gap: 16px;
}
h1 { font-size: 22px; color: #ffd700; letter-spacing: 2px; }
.app { display: flex; gap: 24px; align-items: flex-start; }
/* 左侧画板 */
.left { display: flex; flex-direction: column; gap: 10px; }
.frame-tabs { display: flex; gap: 0; }
.tab {
padding: 8px 20px;
background: #2a2a4a;
border: 2px solid #444;
cursor: pointer;
font-size: 13px;
color: #aaa;
border-bottom: none;
}
.tab:first-child { border-radius: 8px 0 0 0; }
.tab:last-child { border-radius: 0 8px 0 0; }
.tab.active { background: #3a3a6a; border-color: #ffd700; color: #ffd700; }
#display-canvas {
display: block;
cursor: crosshair;
border: 2px solid #ffd700;
image-rendering: pixelated;
}
.tools { display: flex; gap: 8px; flex-wrap: wrap; }
.tool-btn {
padding: 7px 14px;
background: #2a2a4a;
border: 2px solid #555;
border-radius: 6px;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: all 0.1s;
}
.tool-btn.active { border-color: #ffd700; background: #3a3a6a; color: #ffd700; }
.tool-btn:hover:not(.active) { border-color: #888; }
.palette { display: flex; flex-wrap: wrap; gap: 4px; max-width: 520px; }
.swatch {
width: 26px; height: 26px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: transform 0.1s;
}
.swatch:hover { transform: scale(1.2); }
.swatch.active { border-color: #fff; transform: scale(1.2); }
/* 右侧面板 */
.right { display: flex; flex-direction: column; gap: 12px; min-width: 160px; }
.panel {
background: #16213e;
border: 1px solid #0f3460;
border-radius: 10px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
}
.panel h3 { font-size: 12px; color: #ffd700; text-transform: uppercase; letter-spacing: 1px; }
#preview-canvas {
image-rendering: pixelated;
align-self: center;
border: 1px solid #333;
background: #0a0a1a;
}
.btn {
padding: 9px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
width: 100%;
transition: opacity 0.1s;
}
.btn:hover { opacity: 0.85; }
.btn-gold { background: #ffd700; color: #1a1a2e; }
.btn-blue { background: #0f3460; color: #fff; border: 1px solid #1a5276; }
.btn-red { background: #c0392b; color: #fff; }
.hint {
font-size: 11px;
color: #556;
line-height: 1.7;
}
.size-tag { font-size: 11px; color: #556; text-align: center; }
</style>
</head>
<body>
<h1>🎨 涂鸦角色画图工具</h1>
<div class="app">
<div class="left">
<div class="frame-tabs">
<div class="tab active" id="tab0" onclick="switchFrame(0)">帧1 · 待机</div>
<div class="tab" id="tab1" onclick="switchFrame(1)">帧2 · 攻击</div>
</div>
<canvas id="display-canvas"></canvas>
<div class="tools">
<button class="tool-btn active" id="btn-pen" onclick="setTool('pen')">🖊 画笔</button>
<button class="tool-btn" id="btn-eraser" onclick="setTool('eraser')">⬜ 橡皮</button>
<button class="tool-btn" id="btn-fill" onclick="setTool('fill')">🪣 填充</button>
<input type="color" id="custom-color" value="#ffffff"
onchange="setColor(this.value)"
title="自定义颜色"
style="width:36px;height:34px;border:2px solid #555;border-radius:6px;cursor:pointer;background:none;padding:1px;">
</div>
<div class="palette" id="palette"></div>
<div class="size-tag">画布 64×64 像素 · 8倍放大显示</div>
</div>
<div class="right">
<div class="panel">
<h3>动画预览</h3>
<canvas id="preview-canvas" width="128" height="128"></canvas>
<button class="btn btn-blue" onclick="togglePreview()" id="preview-btn">▶ 播放预览</button>
</div>
<div class="panel">
<h3>操作</h3>
<button class="btn btn-blue" onclick="copyFrame1to2()">📋 复制帧1→帧2</button>
<button class="btn btn-red" onclick="clearCurrentFrame()">🗑 清空当前帧</button>
</div>
<div class="panel">
<h3>导出</h3>
<button class="btn btn-gold" onclick="exportSpritesheet()">💾 导出 Spritesheet</button>
<button class="btn btn-blue" onclick="exportCurrentFrame()">💾 导出单帧 PNG</button>
</div>
<div class="panel">
<h3>使用说明</h3>
<div class="hint">
1. 在<b>帧1</b>画待机姿势<br>
2. 点「复制帧1→帧2」<br>
3. 切到<b>帧2</b>修改成攻击姿势<br>
4. 点▶预览动画效果<br>
5. 导出 Spritesheet 用于游戏
</div>
</div>
</div>
</div>
<script>
// ============================================================
// 配置
// ============================================================
const PX = 64; // 画布逻辑尺寸
const ZOOM = 8; // 显示倍率
const W = PX * ZOOM; // 显示尺寸 = 512
// ============================================================
// 数据
// ============================================================
const frameCanvases = [makeOffscreen(), makeOffscreen()];
let currentFrame = 0;
let currentTool = 'pen';
let currentColor = '#ffffff';
let isDrawing = false;
let previewTimer = null;
let previewIdx = 0;
function makeOffscreen() {
const c = document.createElement('canvas');
c.width = PX; c.height = PX;
return c;
}
// ============================================================
// 颜色调色板
// ============================================================
const COLORS = [
'#ffffff','#cccccc','#888888','#444444','#111111',
'#ff4444','#ff8800','#ffcc00','#aaff00','#00ff88',
'#00ffff','#0088ff','#4400ff','#aa00ff','#ff00aa',
'#ffaaaa','#ffddaa','#ffffaa','#aaffcc','#aaddff',
'#8B4513','#d2691e','#f4a460','#228B22','#006400',
'#2e86ab','#e94560','#f5a623','#7ed321','#417505',
];
function buildPalette() {
const el = document.getElementById('palette');
COLORS.forEach(c => {
const s = document.createElement('div');
s.className = 'swatch' + (c === currentColor ? ' active' : '');
s.style.background = c;
s.title = c;
s.onclick = () => {
currentColor = c;
document.getElementById('custom-color').value = c;
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('active'));
s.classList.add('active');
};
el.appendChild(s);
});
}
function setColor(hex) {
currentColor = hex;
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('active'));
}
// ============================================================
// 工具切换
// ============================================================
function setTool(t) {
currentTool = t;
['pen','eraser','fill'].forEach(n => {
document.getElementById('btn-' + n)?.classList.toggle('active', n === t);
});
}
// ============================================================
// 显示画布
// ============================================================
const display = document.getElementById('display-canvas');
display.width = W;
display.height = W;
const dCtx = display.getContext('2d');
dCtx.imageSmoothingEnabled = false;
function render() {
dCtx.clearRect(0, 0, W, W);
// 棋盘格背景(表示透明区域)
for (let y = 0; y < PX; y++) {
for (let x = 0; x < PX; x++) {
dCtx.fillStyle = (x + y) % 2 === 0 ? '#222' : '#1a1a1a';
dCtx.fillRect(x * ZOOM, y * ZOOM, ZOOM, ZOOM);
}
}
// 当前帧像素
dCtx.imageSmoothingEnabled = false;
dCtx.drawImage(frameCanvases[currentFrame], 0, 0, PX, PX, 0, 0, W, W);
// 网格线
dCtx.strokeStyle = 'rgba(255,255,255,0.04)';
dCtx.lineWidth = 0.5;
for (let i = 0; i <= PX; i++) {
dCtx.beginPath(); dCtx.moveTo(i * ZOOM, 0); dCtx.lineTo(i * ZOOM, W); dCtx.stroke();
dCtx.beginPath(); dCtx.moveTo(0, i * ZOOM); dCtx.lineTo(W, i * ZOOM); dCtx.stroke();
}
}
// ============================================================
// 画像素
// ============================================================
function getPos(e) {
const r = display.getBoundingClientRect();
return {
x: Math.max(0, Math.min(PX - 1, Math.floor((e.clientX - r.left) / ZOOM))),
y: Math.max(0, Math.min(PX - 1, Math.floor((e.clientY - r.top) / ZOOM))),
};
}
function putPixel(x, y) {
const fc = frameCanvases[currentFrame].getContext('2d');
if (currentTool === 'eraser') {
fc.clearRect(x, y, 1, 1);
} else {
fc.fillStyle = currentColor;
fc.fillRect(x, y, 1, 1);
}
render();
}
function floodFill(sx, sy) {
const fc = frameCanvases[currentFrame].getContext('2d');
const img = fc.getImageData(0, 0, PX, PX);
const d = img.data;
const ti = (sx + sy * PX) * 4;
const tR = d[ti], tG = d[ti+1], tB = d[ti+2], tA = d[ti+3];
const hex = currentColor.replace('#','');
const fR = parseInt(hex.slice(0,2),16);
const fG = parseInt(hex.slice(2,4),16);
const fB = parseInt(hex.slice(4,6),16);
if (tR===fR && tG===fG && tB===fB && tA===255) return;
const stack = [[sx, sy]];
const seen = new Set();
while (stack.length) {
const [x, y] = stack.pop();
if (x<0||x>=PX||y<0||y>=PX) continue;
const k = x + ',' + y;
if (seen.has(k)) continue;
seen.add(k);
const i = (x + y * PX) * 4;
if (d[i]!==tR||d[i+1]!==tG||d[i+2]!==tB||d[i+3]!==tA) continue;
d[i]=fR; d[i+1]=fG; d[i+2]=fB; d[i+3]=255;
stack.push([x+1,y],[x-1,y],[x,y+1],[x,y-1]);
}
fc.putImageData(img, 0, 0);
render();
}
display.addEventListener('mousedown', e => {
isDrawing = true;
const { x, y } = getPos(e);
currentTool === 'fill' ? floodFill(x, y) : putPixel(x, y);
});
display.addEventListener('mousemove', e => {
if (!isDrawing || currentTool === 'fill') return;
putPixel(getPos(e).x, getPos(e).y);
});
display.addEventListener('mouseup', () => isDrawing = false);
display.addEventListener('mouseleave', () => isDrawing = false);
// ============================================================
// 帧切换
// ============================================================
function switchFrame(idx) {
currentFrame = idx;
document.getElementById('tab0').classList.toggle('active', idx === 0);
document.getElementById('tab1').classList.toggle('active', idx === 1);
render();
}
// ============================================================
// 动画预览
// ============================================================
const previewCanvas = document.getElementById('preview-canvas');
const pCtx = previewCanvas.getContext('2d');
pCtx.imageSmoothingEnabled = false;
function togglePreview() {
const btn = document.getElementById('preview-btn');
if (previewTimer) {
clearInterval(previewTimer);
previewTimer = null;
btn.textContent = '▶ 播放预览';
pCtx.clearRect(0,0,128,128);
return;
}
btn.textContent = '⏹ 停止预览';
previewTimer = setInterval(() => {
pCtx.clearRect(0,0,128,128);
// 棋盘格
for (let y=0;y<4;y++) for (let x=0;x<4;x++) {
pCtx.fillStyle=(x+y)%2===0?'#222':'#1a1a1a';
pCtx.fillRect(x*32,y*32,32,32);
}
pCtx.imageSmoothingEnabled = false;
pCtx.drawImage(frameCanvases[previewIdx], 0, 0, PX, PX, 0, 0, 128, 128);
previewIdx = (previewIdx + 1) % 2;
}, 350);
}
// ============================================================
// 操作
// ============================================================
function copyFrame1to2() {
const src = frameCanvases[0].getContext('2d').getImageData(0,0,PX,PX);
frameCanvases[1].getContext('2d').putImageData(src, 0, 0);
if (currentFrame === 1) render();
alert('✅ 已将帧1复制到帧2\n现在切到帧2修改出攻击姿势吧');
}
function clearCurrentFrame() {
if (!confirm('确定清空当前帧吗?')) return;
frameCanvases[currentFrame].getContext('2d').clearRect(0,0,PX,PX);
render();
}
// ============================================================
// 导出
// ============================================================
function exportSpritesheet() {
const out = document.createElement('canvas');
out.width = PX * 2;
out.height = PX;
const oc = out.getContext('2d');
oc.imageSmoothingEnabled = false;
oc.drawImage(frameCanvases[0], 0, 0);
oc.drawImage(frameCanvases[1], PX, 0);
downloadCanvas(out, 'character-sheet.png');
alert('✅ Spritesheet 已导出!\n128×64 像素包含帧1和帧2');
}
function exportCurrentFrame() {
const name = currentFrame === 0 ? 'character-idle.png' : 'character-attack.png';
downloadCanvas(frameCanvases[currentFrame], name);
}
function downloadCanvas(c, name) {
const a = document.createElement('a');
a.download = name;
a.href = c.toDataURL('image/png');
a.click();
}
// ============================================================
// 初始化
// ============================================================
buildPalette();
render();
</script>
</body>
</html>

View File

@@ -0,0 +1,583 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>涂鸦PK · 对战演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: Arial, sans-serif;
color: #fff;
overflow: hidden;
}
canvas#game { display: block; }
.ui {
display: flex;
gap: 12px;
margin-top: 14px;
flex-wrap: wrap;
justify-content: center;
}
.action-btn {
padding: 12px 22px;
border: 2px solid;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
font-weight: bold;
transition: all 0.12s;
min-width: 110px;
}
.action-btn:hover:not(:disabled) { transform: translateY(-2px); }
.action-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }
.btn-attack { background: #e94560; border-color: #ff6b81; color: #fff; }
.btn-heavy { background: #c0392b; border-color: #e74c3c; color: #fff; }
.btn-block { background: #0f3460; border-color: #1a5276; color: #adf; }
.btn-skill { background: #6c3483; border-color: #9b59b6; color: #fff; }
.log-box {
width: 700px;
max-height: 80px;
overflow-y: auto;
background: rgba(0,0,0,0.5);
border: 1px solid #333;
border-radius: 8px;
padding: 8px 12px;
margin-top: 10px;
font-size: 12px;
color: #ccc;
line-height: 1.8;
}
</style>
</head>
<body>
<canvas id="game" width="700" height="380"></canvas>
<div class="ui" id="action-ui">
<button class="action-btn btn-attack" onclick="playerAction('attack')">⚔️ 普通攻击</button>
<button class="action-btn btn-heavy" onclick="playerAction('heavy')">💥 重击</button>
<button class="action-btn btn-block" onclick="playerAction('block')">🛡 格挡</button>
<button class="action-btn btn-skill" onclick="playerAction('skill')">✨ 特技</button>
</div>
<div class="log-box" id="log"></div>
<script>
// ============================================================
// Web Audio 音效系统
// ============================================================
const AC = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
const recipes = {
hit: { freqs: [300, 60], dur: 0.08, wave: 'sawtooth', vol: 0.35 },
heavyHit: { freqs: [160, 25], dur: 0.20, wave: 'sawtooth', vol: 0.60 },
block: { freqs: [600, 400], dur: 0.10, wave: 'square', vol: 0.25 },
skill: { freqs: [200, 900], dur: 0.30, wave: 'triangle', vol: 0.40 },
death: { freqs: [350, 40], dur: 0.55, wave: 'sine', vol: 0.30 },
victory: { notes: [523,659,784,1047], dur: 0.12, wave: 'sine', vol: 0.35 },
};
if (type === 'victory') {
const r = recipes.victory;
r.notes.forEach((freq, i) => {
setTimeout(() => {
const o = AC.createOscillator();
const g = AC.createGain();
o.connect(g); g.connect(AC.destination);
o.type = r.wave;
o.frequency.setValueAtTime(freq, AC.currentTime);
g.gain.setValueAtTime(r.vol, AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + r.dur);
o.start(); o.stop(AC.currentTime + r.dur);
}, i * 140);
});
return;
}
const r = recipes[type];
if (!r) return;
const o = AC.createOscillator();
const g = AC.createGain();
o.connect(g); g.connect(AC.destination);
o.type = r.wave;
o.frequency.setValueAtTime(r.freqs[0], AC.currentTime);
o.frequency.exponentialRampToValueAtTime(r.freqs[1], AC.currentTime + r.dur);
g.gain.setValueAtTime(r.vol, AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + r.dur);
o.start(); o.stop(AC.currentTime + r.dur);
}
// ============================================================
// 角色配置(学生可替换这里的属性)
// ============================================================
const PLAYER_CONFIG = {
name: '章鱼怪',
emoji: '🐙',
color: '#39ff14',
hp: 50, maxHp: 50,
atk: 15, def: 5, spd: 8,
skill: { name: '电击', type: 'burn', desc: '燃烧:每回合额外-5血持续3回合' },
skillUsed: false,
};
const ENEMY_CONFIG = {
name: '机器人',
emoji: '🤖',
color: '#e94560',
hp: 60, maxHp: 60,
atk: 12, def: 8, spd: 6,
skill: { name: '护甲', type: 'shield', desc: '护甲:下回合格挡所有伤害' },
skillUsed: false,
};
// ============================================================
// 游戏状态
// ============================================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const P_BASE_X = 160, E_BASE_X = 540, CHAR_Y = 200;
const state = {
phase: 'player-turn', // player-turn | animating | enemy-turn | gameover
player: { ...PLAYER_CONFIG, x: P_BASE_X, y: CHAR_Y, shakeX: 0, flash: 0, alpha: 1, burn: 0, shielded: false },
enemy: { ...ENEMY_CONFIG, x: E_BASE_X, y: CHAR_Y, shakeX: 0, flash: 0, alpha: 1, burn: 0, shielded: false },
winner: null,
screenShake: 0,
log: [],
};
// ============================================================
// 动画队列
// ============================================================
const animations = [];
let animating = false;
function queueAnim(fn) {
animations.push(fn);
}
async function runAnimations() {
if (animating) return;
animating = true;
setButtonsDisabled(true);
while (animations.length > 0) {
const fn = animations.shift();
await fn();
}
animating = false;
if (state.phase !== 'gameover') {
setButtonsDisabled(false);
}
}
// ============================================================
// 渲染
// ============================================================
function lerp(a, b, t) { return a + (b - a) * t; }
function drawChar(c, facingRight) {
const { x, y, shakeX, flash, alpha, burn, shielded } = c;
const size = 72;
const sx = x + shakeX;
ctx.save();
ctx.globalAlpha = alpha;
// 护盾光环
if (shielded) {
ctx.beginPath();
ctx.arc(sx, y, size / 2 + 14, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(100,180,255,${0.5 + 0.3 * Math.sin(Date.now() / 200)})`;
ctx.lineWidth = 4;
ctx.stroke();
}
// 燃烧光圈
if (burn > 0) {
ctx.beginPath();
ctx.arc(sx, y, size / 2 + 10, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255,100,0,${0.4 + 0.2 * Math.sin(Date.now() / 150)})`;
ctx.lineWidth = 3;
ctx.stroke();
}
// 主体(圆形)
ctx.beginPath();
ctx.arc(sx, y, size / 2, 0, Math.PI * 2);
if (flash > 0) {
ctx.fillStyle = `rgba(255,255,255,${flash})`;
} else {
ctx.fillStyle = c.color;
}
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = 2;
ctx.stroke();
// Emoji 角色符号
ctx.font = '36px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.emoji, sx, y + 2);
ctx.restore();
}
function drawHPBar(c, bx, by) {
const W = 180, H = 16;
const ratio = Math.max(0, c.hp / c.maxHp);
// 背景
ctx.fillStyle = '#1a1a2e';
ctx.beginPath();
ctx.roundRect(bx, by, W, H, 4);
ctx.fill();
// 血量
const barColor = ratio > 0.5 ? '#39ff14' : ratio > 0.25 ? '#ffd700' : '#e94560';
ctx.fillStyle = barColor;
ctx.beginPath();
ctx.roundRect(bx, by, W * ratio, H, 4);
ctx.fill();
// 边框
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(bx, by, W, H, 4);
ctx.stroke();
// 文字
ctx.fillStyle = '#fff';
ctx.font = 'bold 11px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${Math.max(0, c.hp)} / ${c.maxHp}`, bx + W / 2, by + H / 2);
}
function drawNamePlate(c, x, y, align) {
ctx.font = 'bold 16px Arial';
ctx.textAlign = align;
ctx.textBaseline = 'top';
ctx.fillStyle = c.color;
ctx.fillText(c.name, x, y);
ctx.font = '11px Arial';
ctx.fillStyle = '#aaa';
ctx.fillText(`ATK:${c.atk} DEF:${c.def} SPD:${c.spd}`, x, y + 20);
if (c.burn > 0) {
ctx.fillStyle = '#ff8800';
ctx.fillText(`🔥 燃烧 ×${c.burn}`, x, y + 36);
}
if (c.shielded) {
ctx.fillStyle = '#64b4ff';
ctx.fillText('🛡 护甲中', x, y + 36);
}
}
function render() {
const shake = state.screenShake;
ctx.save();
if (shake > 0) ctx.translate((Math.random() - 0.5) * shake * 8, (Math.random() - 0.5) * shake * 4);
// 背景渐变
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#1a1a2e');
grad.addColorStop(1, '#16213e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 地面线
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, 260); ctx.lineTo(canvas.width, 260);
ctx.stroke();
// VS 文字
ctx.font = 'bold 28px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillText('VS', 350, CHAR_Y);
// 角色
drawChar(state.player, true);
drawChar(state.enemy, false);
// 血条 + 名字
drawHPBar(state.player, 30, 20);
drawHPBar(state.enemy, 490, 20);
drawNamePlate(state.player, 30, 42, 'left');
drawNamePlate(state.enemy, 880, 42, 'right');
// 回合提示
if (state.phase === 'player-turn') {
ctx.font = 'bold 13px Arial';
ctx.textAlign = 'center';
ctx.fillStyle = '#ffd700';
ctx.fillText('你的回合 — 选择行动', 350, 295);
} else if (state.phase === 'enemy-turn') {
ctx.font = 'bold 13px Arial';
ctx.textAlign = 'center';
ctx.fillStyle = '#e94560';
ctx.fillText('敌人回合...', 350, 295);
}
// 特技状态
ctx.font = '11px Arial';
ctx.textAlign = 'left';
ctx.fillStyle = state.player.skillUsed ? '#555' : '#9b59b6';
ctx.fillText(`${state.player.skill.name}${state.player.skillUsed ? '已使用' : '可使用'}`, 30, 330);
// 游戏结束
if (state.phase === 'gameover') {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 48px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = state.winner === 'player' ? '#ffd700' : '#e94560';
ctx.fillText(state.winner === 'player' ? '🏆 胜利!' : '💀 失败...', 350, 160);
ctx.font = '18px Arial';
ctx.fillStyle = '#aaa';
ctx.fillText('点击页面重新开始', 350, 220);
}
ctx.restore();
requestAnimationFrame(render);
}
// ============================================================
// 战斗函数
// ============================================================
function calcDamage(atk, def, type) {
const base = Math.max(1, atk - def);
if (type === 'heavy') return Math.floor(base * 1.8);
return base;
}
function addLog(msg) {
state.log.unshift(msg);
const el = document.getElementById('log');
el.innerHTML = state.log.slice(0, 8).map(m => `<span>${m}</span>`).join('<br>');
}
function setButtonsDisabled(d) {
document.querySelectorAll('.action-btn').forEach(b => b.disabled = d);
}
// ============================================================
// 动画工厂
// ============================================================
function wait(ms) { return new Promise(r => setTimeout(r, ms)); }
async function animAttack(attacker, defender, heavy) {
const isPlayer = attacker === state.player;
const targetX = isPlayer ? E_BASE_X - 80 : P_BASE_X + 80;
const startX = attacker.x;
const steps = 12;
// 冲向对手
for (let i = 0; i <= steps; i++) {
attacker.x = lerp(startX, targetX, i / steps);
await wait(12);
}
// 受击效果
playSound(heavy ? 'heavyHit' : 'hit');
defender.flash = 1.0;
if (heavy) state.screenShake = 1;
for (let i = 0; i < 6; i++) {
defender.shakeX = (i % 2 === 0 ? 1 : -1) * (heavy ? 14 : 8);
await wait(40);
}
defender.shakeX = 0;
state.screenShake = 0;
// 弹回
for (let i = 0; i <= steps; i++) {
attacker.x = lerp(targetX, startX, i / steps);
defender.flash = Math.max(0, 1 - i / steps);
await wait(10);
}
attacker.x = startX;
defender.flash = 0;
}
async function animDeath(char) {
playSound('death');
for (let i = 0; i <= 20; i++) {
char.alpha = 1 - i / 20;
char.y = CHAR_Y + i * 3;
await wait(30);
}
}
async function animSkill(char) {
playSound('skill');
const startX = char.x;
for (let i = 0; i < 8; i++) {
char.shakeX = (i % 2 === 0 ? 1 : -1) * 6;
char.flash = 0.5;
await wait(50);
}
char.shakeX = 0;
char.flash = 0;
char.x = startX;
}
// ============================================================
// 玩家行动
// ============================================================
async function playerAction(type) {
if (state.phase !== 'player-turn' || animating) return;
if (type === 'skill' && state.player.skillUsed) {
addLog('⚠️ 特技已经使用过了');
return;
}
state.phase = 'animating';
const p = state.player, e = state.enemy;
// 玩家蓄力+出招
if (type === 'block') {
playSound('block');
p.shielded = true;
addLog(`🛡 ${p.name} 进入格挡状态!`);
await wait(400);
} else if (type === 'skill') {
p.skillUsed = true;
await animSkill(p);
if (p.skill.type === 'burn') {
e.burn = 3;
addLog(`🔥 ${p.name} 使用【${p.skill.name}】!${e.name} 进入燃烧状态持续3回合`);
}
} else {
const heavy = type === 'heavy';
const dmg = e.shielded ? 0 : calcDamage(p.atk, e.def, type);
await animAttack(p, e, heavy);
if (e.shielded) {
addLog(`🛡 ${e.name} 格挡了攻击!`);
e.shielded = false;
} else {
e.hp -= dmg;
addLog(`${heavy ? '💥' : '⚔️'} ${p.name} 攻击 ${e.name},造成 ${dmg} 点伤害${heavy ? '(重击!)' : ''}`);
}
}
// 检查敌人死亡
if (e.hp <= 0) {
e.hp = 0;
await animDeath(e);
playSound('victory');
state.phase = 'gameover';
state.winner = 'player';
addLog(`🏆 ${p.name} 获胜!`);
return;
}
// 燃烧 tick
if (e.burn > 0) {
e.hp -= 5; e.burn--;
addLog(`🔥 ${e.name} 受到燃烧伤害 -5 血(剩余 ${e.burn} 回合)`);
if (e.hp <= 0) {
e.hp = 0;
await animDeath(e);
state.phase = 'gameover';
state.winner = 'player';
addLog(`🏆 ${p.name} 获胜!`);
return;
}
}
// 玩家格挡清除
if (type !== 'block') p.shielded = false;
await wait(300);
state.phase = 'enemy-turn';
// 敌人AI回合
await wait(600);
await enemyTurn();
}
// ============================================================
// 敌人AI
// ============================================================
async function enemyTurn() {
const p = state.player, e = state.enemy;
let action = 'attack';
// 简单AIHP低于30%用技能25%概率重击,偶尔格挡
if (!e.skillUsed && e.hp < e.maxHp * 0.3) {
action = 'skill';
} else if (Math.random() < 0.25) {
action = 'heavy';
} else if (Math.random() < 0.15) {
action = 'block';
}
if (action === 'block') {
playSound('block');
e.shielded = true;
addLog(`🛡 ${e.name} 进入格挡状态!`);
await wait(400);
} else if (action === 'skill') {
e.skillUsed = true;
await animSkill(e);
if (e.skill.type === 'shield') {
e.shielded = true;
addLog(`🛡 ${e.name} 使用【${e.skill.name}】!下回合格挡所有伤害`);
}
} else {
const heavy = action === 'heavy';
const dmg = p.shielded ? 0 : calcDamage(e.atk, p.def, action);
await animAttack(e, p, heavy);
if (p.shielded) {
addLog(`🛡 ${p.name} 格挡了攻击!`);
p.shielded = false;
} else {
p.hp -= dmg;
addLog(`${heavy ? '💥' : '⚔️'} ${e.name} 攻击 ${p.name},造成 ${dmg} 点伤害${heavy ? '(重击!)' : ''}`);
}
}
// 燃烧敌人tick
if (p.burn > 0) {
p.hp -= 5; p.burn--;
}
// 检查玩家死亡
if (p.hp <= 0) {
p.hp = 0;
await animDeath(p);
state.phase = 'gameover';
state.winner = 'enemy';
addLog(`💀 ${e.name} 获胜!`);
return;
}
await wait(300);
state.phase = 'player-turn';
addLog('— 你的回合 —');
}
// 点击重开
canvas.addEventListener('click', () => {
if (state.phase === 'gameover') location.reload();
});
// ============================================================
// 启动
// ============================================================
render();
addLog('⚔️ 对战开始!你先出手。');
</script>
</body>
</html>

View File

@@ -0,0 +1,474 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>角色动画展示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
font-family: Arial, sans-serif;
color: #fff;
gap: 20px;
}
h1 { color: #ffd700; font-size: 20px; letter-spacing: 2px; }
canvas { border: 1px solid #333; border-radius: 8px; }
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.anim-btn {
padding: 10px 18px;
border: 2px solid #555;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
background: #16213e;
color: #fff;
transition: all 0.1s;
}
.anim-btn:hover { border-color: #ffd700; color: #ffd700; transform: translateY(-2px); }
.import-area {
display: flex;
align-items: center;
gap: 12px;
background: #16213e;
border: 1px solid #333;
border-radius: 8px;
padding: 12px 20px;
}
.import-area label { font-size: 13px; color: #aaa; }
.import-area input { display: none; }
.import-btn {
padding: 8px 16px;
background: #0f3460;
border: 1px solid #1a5276;
border-radius: 6px;
cursor: pointer;
color: #adf;
font-size: 13px;
}
.desc { font-size: 12px; color: #667; text-align: center; max-width: 600px; line-height: 1.7; }
</style>
</head>
<body>
<h1>🎬 角色动画展示</h1>
<div class="import-area">
<label>导入你画的角色:</label>
<button class="import-btn" onclick="document.getElementById('img-input').click()">📂 选择 PNG</button>
<input type="file" id="img-input" accept="image/png" onchange="loadCustomChar(this)">
<label id="import-hint" style="color:#ffd700;font-size:12px;">(不导入则使用默认角色)</label>
</div>
<canvas id="c" width="600" height="300"></canvas>
<div class="controls">
<button class="anim-btn" onclick="playAnim('idle')">🌀 待机</button>
<button class="anim-btn" onclick="playAnim('attack')">⚔️ 普通攻击</button>
<button class="anim-btn" onclick="playAnim('heavy')">💥 重击</button>
<button class="anim-btn" onclick="playAnim('block')">🛡 格挡</button>
<button class="anim-btn" onclick="playAnim('hit')">😵 受击</button>
<button class="anim-btn" onclick="playAnim('skill')">✨ 释放特技</button>
<button class="anim-btn" onclick="playAnim('death')">💀 死亡</button>
<button class="anim-btn" onclick="playAnim('victory')">🏆 胜利</button>
</div>
<div class="desc">
点击按钮查看不同的动画效果。<br>
所有动画都用 <strong>Phaser Tween</strong> 实现,只需要一张图片,不需要多帧素材。<br>
可以导入你在画图工具里画的角色 PNG看看它配上动画是什么效果。
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const CX = canvas.width / 2;
const CY = canvas.height / 2 - 20;
// Web Audio
const AC = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
const R = {
hit: [300, 50, 0.08, 'sawtooth', 0.35],
heavy: [160, 25, 0.20, 'sawtooth', 0.55],
block: [600, 400, 0.10, 'square', 0.25],
skill: [200, 900, 0.30, 'triangle', 0.40],
death: [350, 40, 0.55, 'sine', 0.30],
victory: null,
};
if (type === 'victory') {
[523,659,784,1047].forEach((f,i) => setTimeout(() => {
const o=AC.createOscillator(), g=AC.createGain();
o.connect(g); g.connect(AC.destination);
o.type='sine'; o.frequency.value=f;
g.gain.setValueAtTime(0.3, AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, AC.currentTime+0.12);
o.start(); o.stop(AC.currentTime+0.12);
}, i*140));
return;
}
const r = R[type]; if (!r) return;
const o=AC.createOscillator(), g=AC.createGain();
o.connect(g); g.connect(AC.destination);
o.type=r[3]; o.frequency.setValueAtTime(r[0], AC.currentTime);
o.frequency.exponentialRampToValueAtTime(r[1], AC.currentTime+r[2]);
g.gain.setValueAtTime(r[4], AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, AC.currentTime+r[2]);
o.start(); o.stop(AC.currentTime+r[2]);
}
// 角色状态
let charImg = null;
let charFrameCount = 1; // 总帧数(导入 spritesheet 时自动检测)
let charFrameW = 0; // 每帧宽度(像素)
let currentCharFrame = 0; // 当前显示第几帧
const char = {
x: CX, y: CY,
scale: 1, scaleY: 1,
rotation: 0,
alpha: 1,
shakeX: 0,
tint: null, // 'red' | 'blue' | null
glow: 0,
particles: [],
};
// 默认角色:用 canvas 画一个可爱的涂鸦怪
const defaultChar = (() => {
const c = document.createElement('canvas');
c.width = 80; c.height = 80;
const cx = c.getContext('2d');
// 身体
cx.fillStyle = '#39ff14';
cx.beginPath(); cx.ellipse(40,45,32,28,0,0,Math.PI*2); cx.fill();
// 眼睛
cx.fillStyle = '#fff';
cx.beginPath(); cx.arc(28,35,9,0,Math.PI*2); cx.fill();
cx.beginPath(); cx.arc(52,35,9,0,Math.PI*2); cx.fill();
cx.fillStyle = '#000';
cx.beginPath(); cx.arc(30,36,5,0,Math.PI*2); cx.fill();
cx.beginPath(); cx.arc(54,36,5,0,Math.PI*2); cx.fill();
// 嘴巴
cx.strokeStyle = '#000'; cx.lineWidth = 2;
cx.beginPath(); cx.arc(40,50,10,0.2,Math.PI-0.2); cx.stroke();
// 手臂
cx.strokeStyle = '#39ff14'; cx.lineWidth = 5;
cx.beginPath(); cx.moveTo(10,50); cx.lineTo(30,40); cx.stroke();
cx.beginPath(); cx.moveTo(70,50); cx.lineTo(50,40); cx.stroke();
// 轮廓
cx.strokeStyle = 'rgba(0,0,0,0.3)'; cx.lineWidth = 2;
cx.beginPath(); cx.ellipse(40,45,32,28,0,0,Math.PI*2); cx.stroke();
return c;
})();
function loadCustomChar(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
charImg = img;
// 自动检测是否是 Spritesheet宽度 >= 高度×1.5 就认为是多帧)
charFrameCount = (img.width >= img.height * 1.5)
? Math.round(img.width / img.height)
: 1;
charFrameW = img.width / charFrameCount;
currentCharFrame = 0;
document.getElementById('import-hint').textContent =
`✅ 已加载:${file.name}(检测到 ${charFrameCount} 帧)`;
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// ============================================================
// 动画系统
// ============================================================
function wait(ms) { return new Promise(r => setTimeout(r, ms)); }
function lerp(a, b, t) { return a + (b - a) * t; }
let isPlaying = false;
async function playAnim(type) {
if (isPlaying) return;
isPlaying = true;
resetChar();
await animMap[type]();
if (type !== 'death') resetChar();
isPlaying = false;
}
function resetChar() {
Object.assign(char, {
x: CX, y: CY,
scale: 1, scaleY: 1,
rotation: 0,
alpha: 1,
shakeX: 0,
tint: null,
glow: 0,
});
char.particles = [];
}
const animMap = {
async idle() {
// 呼吸:上下浮动循环
for (let cycle = 0; cycle < 4; cycle++) {
for (let i = 0; i <= 20; i++) {
char.y = CY + Math.sin(i / 20 * Math.PI) * (-8);
char.scaleY = 1 + Math.sin(i / 20 * Math.PI) * 0.04;
await wait(25);
}
}
},
async attack() {
const targetX = CX + 110;
playSound('hit');
currentCharFrame = 0; // 待机帧
// 蓄力后缩
for (let i = 0; i <= 8; i++) {
char.x = lerp(CX, CX - 25, i / 8);
char.scale = lerp(1, 0.85, i / 8);
await wait(18);
}
// 冲出时切换到攻击帧
currentCharFrame = charFrameCount > 1 ? 1 : 0;
for (let i = 0; i <= 10; i++) {
char.x = lerp(CX - 25, targetX, i / 10);
char.scale = lerp(0.85, 1.1, i / 10);
await wait(12);
}
spawnParticles(char.x + 40, char.y, '#fff', 8);
// 弹回时切回待机帧
currentCharFrame = 0;
for (let i = 0; i <= 12; i++) {
char.x = lerp(targetX, CX, i / 12);
char.scale = lerp(1.1, 1, i / 12);
await wait(14);
}
},
async heavy() {
playSound('heavy');
// 大幅蓄力
for (let i = 0; i <= 15; i++) {
char.x = lerp(CX, CX - 55, i / 15);
char.scale = lerp(1, 0.7, i / 15);
char.scaleY = lerp(1, 1.3, i / 15);
await wait(18);
}
// 爆发冲出时切到攻击帧
currentCharFrame = charFrameCount > 1 ? 1 : 0;
const targetX = CX + 140;
for (let i = 0; i <= 8; i++) {
char.x = lerp(CX - 55, targetX, i / 8);
char.scale = lerp(0.7, 1.4, i / 8);
char.scaleY = lerp(1.3, 0.8, i / 8);
await wait(10);
}
spawnParticles(char.x + 50, char.y, '#ff8800', 20);
// 弹回切回待机帧
currentCharFrame = 0;
for (let i = 0; i <= 14; i++) {
char.x = lerp(targetX, CX, i / 14);
char.scale = lerp(1.4, 1, i / 14);
char.scaleY = lerp(0.8, 1, i / 14);
await wait(14);
}
},
async block() {
playSound('block');
char.tint = 'blue';
for (let i = 0; i <= 6; i++) {
char.scale = lerp(1, 1.15, i / 6);
char.glow = lerp(0, 1, i / 6);
await wait(20);
}
await wait(600);
for (let i = 0; i <= 8; i++) {
char.scale = lerp(1.15, 1, i / 8);
char.glow = lerp(1, 0, i / 8);
await wait(20);
}
char.tint = null;
},
async hit() {
playSound('hit');
char.tint = 'red';
for (let i = 0; i < 7; i++) {
char.shakeX = (i % 2 === 0 ? 1 : -1) * 12;
char.scale = i === 0 ? 0.9 : 1;
await wait(45);
}
char.shakeX = 0;
char.tint = null;
},
async skill() {
playSound('skill');
// 旋转蓄能
for (let i = 0; i <= 20; i++) {
char.rotation = lerp(0, 360, i / 20);
char.scale = lerp(1, 1.4, i / 20);
char.glow = i / 20;
await wait(20);
}
char.rotation = 0;
spawnParticles(char.x, char.y, '#ffd700', 30);
await wait(100);
for (let i = 0; i <= 10; i++) {
char.scale = lerp(1.4, 1, i / 10);
char.glow = lerp(1, 0, i / 10);
await wait(20);
}
},
async death() {
playSound('death');
for (let i = 0; i <= 25; i++) {
char.rotation = lerp(0, 360, i / 25);
char.scale = lerp(1, 0, i / 25);
char.alpha = lerp(1, 0, i / 25);
char.y = lerp(CY, CY + 60, i / 25);
await wait(30);
}
spawnParticles(CX, CY + 30, '#e94560', 25);
},
async victory() {
playSound('victory');
// 跳跃弹跳
for (let bounce = 0; bounce < 3; bounce++) {
for (let i = 0; i <= 15; i++) {
char.y = CY - Math.sin(i / 15 * Math.PI) * (50 - bounce * 12);
char.scaleX = 1 - Math.sin(i / 15 * Math.PI) * 0.15;
char.scaleY = 1 + Math.sin(i / 15 * Math.PI) * 0.2;
await wait(25);
}
if (bounce < 2) spawnParticles(char.x, char.y + 40, '#ffd700', 12);
}
char.scaleX = 1; char.scaleY = 1;
},
};
// ============================================================
// 粒子系统
// ============================================================
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 5;
char.particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 2,
life: 1,
color,
size: 2 + Math.random() * 4,
});
}
}
function updateParticles() {
char.particles = char.particles.filter(p => p.life > 0);
for (const p of char.particles) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.18;
p.life -= 0.025;
}
}
// ============================================================
// 渲染循环
// ============================================================
function render() {
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#1a1a2e');
grad.addColorStop(1, '#16213e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 地面
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, CY + 60); ctx.lineTo(canvas.width, CY + 60);
ctx.stroke();
updateParticles();
// 粒子
for (const p of char.particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// 角色
const img = charImg || defaultChar;
const SIZE = 80;
const sx = char.x + char.shakeX;
const sy = char.y;
ctx.save();
ctx.globalAlpha = char.alpha;
ctx.translate(sx, sy);
ctx.rotate(char.rotation * Math.PI / 180);
ctx.scale(char.scale * (char.scaleX || 1), char.scaleY);
// 发光效果
if (char.glow > 0) {
ctx.shadowColor = char.tint === 'blue' ? '#64b4ff' : '#ffd700';
ctx.shadowBlur = char.glow * 30;
}
// 计算当前帧的源矩形
const fw = charImg ? charFrameW : img.width;
const fh = charImg ? img.height : img.height;
const fx = charImg ? currentCharFrame * charFrameW : 0;
// 颜色叠加
if (char.tint) {
ctx.drawImage(img, fx, 0, fw, fh, -SIZE/2, -SIZE/2, SIZE, SIZE);
ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = char.tint === 'red' ? 'rgba(255,80,80,0.55)' : 'rgba(100,150,255,0.5)';
ctx.fillRect(-SIZE/2, -SIZE/2, SIZE, SIZE);
ctx.globalCompositeOperation = 'source-over';
} else {
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, fx, 0, fw, fh, -SIZE/2, -SIZE/2, SIZE, SIZE);
}
ctx.restore();
// 说明文字
ctx.font = '13px Arial';
ctx.textAlign = 'center';
ctx.fillStyle = '#556';
ctx.fillText('← 点击上方按钮播放动画', CX, canvas.height - 14);
requestAnimationFrame(render);
}
render();
</script>
</body>
</html>