你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。
读完之后,你应该能:
- 说出"用一张字符地图描述一个关卡"是什么意思,
# . * $ @ +各代表什么;- 明白"人能不能走 / 能不能推箱子"是先问前面是什么再决定的;
- 说清"怎么判断过关"——数一数有几个箱子已经站在目标点上。
用时:约 60 分钟。 准备:烧录
demo01_sokoban的板子 + 一根 USB 线。
一、这次不是消除,是搬运
第 31 课俄罗斯方块是"新方块掉下来,我们要消除"。这一课推箱子玩法完全相反:箱子、墙、目标点一开始就摆好了,你要做的,是把箱子搬到橙色的目标点。
规则只有四条:人只能上下左右走;撞墙不能走;推箱子时,箱子背后必须是空地/目标,否则推不动;所有箱子都推到目标点,就算过关。
可板子没有眼睛,它怎么知道哪格是墙、哪格是目标、哪格有箱子、哪格是我?答案:用一张字符地图,把整个关卡"写"进板子。
二、我观察到的现象
| 现象 | 勾选 |
|---|---|
| 屏幕出现一个四周是墙、中间是地板的房间 | |
| 有几个橙色的"目标点"散在地板上 | |
| 手指滑动,绿色小人走一格(上/下/左/右) | |
| 小人朝墙走,走不动;箱子被推向墙,推不动 | |
| 把箱子推到橙色格子上,箱子变绿 | |
| 顶部显示"已到位箱子数 / 箱子总数" |
三、关卡,就是一张字符地图
demo 里这关不是一格格画的,而是用字符写成的地图:
| 字符 | 意思 |
|---|---|
# | 墙 |
. | 地板(空地) |
| 目标点 | |
$ | 箱子 |
@ | 人 |
+ | 人站在目标点 |
这关就写在这一串字符里,一共 12 行,每行 12 个字符,四周都围一圈 #:
############
#*.........#
#..........#
#..$...$...#
#....@.....#
#..........#
#....$...*.#
#..........#
#..........#
#..........#
#..........#
############
但这里有个聪明的安排:墙 / 地板 / 目标点是"地基",永远不变;箱子是会动的"家具";人还会动。 所以程序分成两层:
| 名字 | 记什么 | 变不变 |
|---|---|---|
base[r][c] | 墙 / 地板 / 目标点 | 不变 |
grid[r][c] | 这一格有没有箱子 | 会变 |
pr,pc | 人在第几行第几列 | 会变 |
四、两个代码名,够了
1. tryMove(dc, dr)——朝这个方向挪一步,顺便决定能不能推箱
tryMove(dc, dr):人想朝"列偏移 dc、行偏移 dr"方向走一步。
第1步:算出人走过去会站到哪一格 (nr, nc)。
第2步:看那一格是什么——
是墙 / 越界 → 不动,返回。
是箱子 → 再看箱子"再往前一格":
背后是墙 / 越界 / 另一个箱子 → 推不动;
背后是空地 / 目标点 → 箱子挪过去,人跟一步。
是空地 → 人直接走过去。
dc是"往左还是往右"(列),dr是"往上还是往下"(行)。"能不能走、能不能推",全在这一句里被问清楚。
2. recomputeDone()——数一数,几个箱子到位了
recomputeDone():
把"已到位箱子数" boxDone 清零;
一格格扫:只要这格是箱子(grid==BOX) 且 地基是目标点(base==TARGET),
→ boxDone 加 1。
扫完比一比:boxDone == 箱子总数 boxCount → 过关(WIN);
不然 → 继续玩。
过关不是靠"你刚好推最后一下"的巧合,而是每次都重新数一遍:现在到底几个箱子稳稳站在目标点上?
最该记住的:能不能"走"和"推",全靠 tryMove 先问一句"前面是什么";能不能"赢",全靠 recomputeDone 数"箱子站没站在目标点上"。
五、为什么箱子要单独一张表?
| 做法 | 判断推箱不对 | 问题 |
|---|---|---|
| 一张表全记 | 想判断"箱子背后是不是空地/目标"时,得再想这格原先是墙还是箱子 | 墙和箱子搅在一起,容易算重、把墙当箱子 |
| 分两张表 | base 永远回答"是不是墙/目标",grid 永远回答"有没有箱子" | 地基不动,箱子单独管,清爽 |
数据怎么存,决定规则好不好写。 分开记,tryMove 就能写得特别干净。
六、改一个条件:箱子比目标点多一个
数一数 demo 这关:$(箱子)有 3 个,*(目标点)有 2 个。
| 我改了什么 | 我预测 | 实际 |
|---|---|---|
| 箱子 3 个、目标点 2 个 | 目标点只有 2 个 → boxDone 最多 2;boxDone==boxCount(2==3)永远不成立 → 这一关永远到不了过关 | |
再补一个目标点(把某个 $ 改 *),箱=目标 | boxDone 最多能到 3,boxCount 也是 3,就能触发 WIN |
过关这条规则本身没错,是关卡数据"箱子≠目标点"让它失效了。 写游戏时,规则和关卡是两回事:规则对了,还得保证关卡数据能让规则成立。
七、一条完整的"推一次箱子"链路
手指滑动 → 算出方向(上/下/左/右)→ tryMove(方向)
人走过去会到哪一格:
墙 / 越界 → 不动
空地 → 人走过去
箱子 → 看箱子背后:
墙 / 越界 / 另一个箱子 → 推不动
空地 / 目标点 → 箱子挪一步,人跟上
每推/走一步 → recomputeDone() 数"箱子站目标点上"的个数
已到位数 == 箱子总数 → 过关(WIN)
未到位 → 继续玩
想重开 → 轻触屏幕:IDLE 开始这关,WIN 进(或循环回)下一关
真正要记住的只有一句:推箱子 = 一张字符地图记下墙/地/目标/箱/人 + tryMove 先问前面是什么 + recomputeDone 数到位箱子判过关。
八、推不动 / 没反应,这样排查
| 现象 | 是观察还是推测? | 下一步查什么 |
|---|---|---|
| 手指滑动,人不动 | 观察 | 推测没识别成滑动 → 查 SWIPE_THRESH 和触摸坐标 |
| 冲着空地划却没走 | 观察 | 推测方向算错 → 查主导轴判定 abs(dx)>abs(dy) |
| 推箱子"穿墙"或乱跑 | 观察 | 推测箱子背后没认真查 → 查 tryMove 有没有漏掉越界/墙/另一个箱子 |
| 箱子推到目标点却推不动 | 观察 | 推测把箱子背后当成墙了 → 查"允许推"的条件 |
| 顶部"已到位/总数"不变 | 观察 | 推测 boxDone 数得不对 → 查 recomputeDone 的判断条件 |
| 过关后"下一关"又回开头 | 观察 | 推测只内置了一关 → totalLevels() 返回 1 |
老规矩:"滑了人不动"是观察,"没滑动超过门槛"是推测。 先看是不是滑动没触发,再查是方向错还是规则漏判。
九、过关检查
- 一个关卡怎么装进板子?
# . * $ @ +各代表什么? - "人往前挪一格"要先检查什么?"撞墙不能走"怎么判断?
- 为什么推箱子要先看箱子背后那格?背后是什么就推不动?
- 怎么判断过关?
recomputeDone()数的是什么? - 为什么箱子要单独一张表,和墙/地/目标分开?
- 手指"滑动"怎么变成"上/下/左/右"?
留下三样东西:你说说每个字符代表什么、你亲眼把箱子推到橙色目标点看到它变绿的那一步、"推箱子在板子里是什么"的一句话。
十、还能玩什么
| 作品方向 | 描述给 AI 的效果 | 再想想 |
|---|---|---|
| 多关卡推箱子 | 过一关进下一关,难度递进 | totalLevels() 只返回 1,怎么改多关?关卡数据放哪? |
| 步数挑战 | 顶部记录并显示你走了多少步 | 每走一步在哪加 1?怎么清零重开? |
| 限时通关 | 倒计时内推完才算赢 | 时钟怎么和画面共用 loop()?会不会拖慢滑块? |
| 换主题皮肤 | 把墙/箱子/目标点画成不同图案 | 改颜色动哪几行?画成"礼物盒"改 drawTile 哪里? |
| 推箱子 AI | 让板子自己找到解法自动通关 | 它怎么试一条路?走错要"回头"(回溯)该怎么记? |
下节课预告:学会"关卡=一张字符地图 + 规则先问前面是什么 + 数到位箱子判过关"后,第 33 课让飞机在天上飞、躲子弹、开炮打敌机——碰撞、计分、状态切换正是飞行大战的核心。
