
字节笔记本
2026年10月7日 · 约 7 分钟读完
AI 一次生成的俄罗斯方块,为什么一帧都画不出来
如果只能挑一个项目来理解「游戏到底是怎么跑起来的」,俄罗斯方块大概仍是标准答案:规则人尽皆知、逻辑密度高,又小到可以塞进一个 HTML 文件。最近我让 AI 一次性生成一个单文件版的俄罗斯方块,骨架给得相当完整,但把代码原样保存打开,页面连一帧都画不出来。这篇文章先拆解 Canvas 俄罗斯方块的最小骨架,再完整复盘这份生成代码里真实存在的四处缺陷——它们恰好对应审阅 AI 生成代码时最常见的几类断裂。
一、为什么仍是俄罗斯方块
前端的渲染框架换了一茬又一茬,但游戏的底层模型四十年来几乎没变:一个循环,反复做三件事——读输入、更新状态、画出来。俄罗斯方块把这个模型压缩到了极致:场景是一个固定网格,实体只有下落的方块一种,输入只有平移、旋转、软降三个动作,规则核心只有两条,碰撞即合并、满行即消除。它也因此成为检验是否真正理解游戏循环的试金石:写明白一个俄罗斯方块,就能看懂几乎所有二维小游戏的骨架,顺带把状态机、定时更新、输入响应这些通用概念一次性过一遍。单文件、零依赖还有一层价值:反馈闭环极短,保存、刷新、立刻见效果,特别适合验证某条机制是否与你想的一致。
二、最小骨架:把游戏拆成五件事
第一,场景与方块统一用二维数组。 场地是 20 行 12 列的二维数组,Canvas 设为 240×400、格子 20 像素刚好铺满;每个方块也是一个小矩阵,数字 0 代表空、1 代表实体。方块落地时把值拷进场地矩阵,渲染时分别绘制场地和当前方块两个矩阵即可,不需要任何绘图库。
第二,主循环用 requestAnimationFrame 加时间累加器。 不要用 setInterval 当心跳:rAF 与屏幕刷新对齐,后台标签页会自动暂停。重力和帧率解耦的关键是 delta time——每帧累加「这一帧距上一帧过了多久」,累计超过 1000 毫秒就执行一次下落。以后要做加速,只需把间隔改小,逻辑一行不动。
第三,碰撞检测只写一个函数。 把方块矩阵与场地矩阵按偏移量叠加扫描,凡是方块实体格对应的位置越界或已被占用,即判定碰撞。一个函数同时覆盖左墙、右墙、地板和堆叠四种情况;移动和下落都变成「先试后撤」:坐标改一格,撞了就改回去。
第四,旋转用转置加反转。 矩阵转置后把每行反转,就是顺时针旋转 90 度;反转行序则是逆时针。旋转撞墙时做简单的踢墙尝试:依次试探左右偏移一至两格,都不行就把整次旋转回退复原。
第五,消行与计分。 从底行向上倒序扫描,一行全满就 splice 掉,再把缓存的原数组填零后 unshift 回去,数组复用不新建;连消得分翻倍,单消 10 分、两消 20 分、四连消 80 分。用带标签的循环在发现空格时直接跳到下一行,代码短得几乎透明。
键盘映射顺势而出:左右键平移、下键软降、Q 和 W 分别逆时针顺时针旋转,各自对应一个只改坐标或矩阵的小函数。整体结构如下:

三、踩坑实录:这份 AI 代码为什么一帧都画不出来
上面五件事,生成代码里居然一件不缺:delta time 累加器、转置旋转、踢墙偏移、带标签的消行循环写得都有模有样。但页面黑屏,控制台第一秒就抛异常。我把脚本剥出来放进 Node 里逐句执行,定位出四处问题,全部实机复现:
坑一:字母被当成数组下标。 方块选取用字符串 ILJOTSZ 随机取一个字母,再拿字母去索引方块定义数组——用七个字母逐个实测,返回值全部是 undefined,于是初始化第一步就抛出 cannot read properties of undefined,游戏死于加载。七个字母恰好对应下标 0 到 6,差的只是一张映射表。
坑二:数据形状自相矛盾。 七个方块被定义成一维数组,I 块是 [1,1,1,1],而绘制和旋转函数全按二维矩阵书写。实测用一维矩阵跑碰撞,内层循环读到的是数字的 length 属性,循环体永远不执行,碰撞恒为假,方块会直接穿透地板;跑到绘制,第一帧就抛 row.forEach is not a function。同一个概念在一份代码里存在两种形状,这是 AI 生成代码里最典型的断裂。
坑三:转置法只对方阵成立。 就算把方块改成二维,原地交换的转置写法默认矩阵是 N×N 方阵;实测 J 块的 2×3 矩形转出来形状直接被破坏,得到的是一滩既非旋转也非镜像的残渣。经典解法是所有方块统一补零成方阵——后来的 SRS 旋转系统同样建立在方阵之上——或者用 map 逐列重建转置。
坑四:引用了不存在的 DOM。 计分函数给 getElementById 的返回值直接赋值,而 HTML 里根本没有 id 为 score 的元素。实测即便前三处都修好,第一次得分仍会抛 cannot set properties of null。初始化路径上任何一个对不存在节点的引用,都足以让整个应用哑火。
顺带一提,原代码对游戏结束的处理相当粗暴:新方块一出生就与堆叠碰撞时,直接清空整个场地并把分数归零重来,作为最小实现可以接受,正式作品则应有明确的结束画面。四处修完(字母映射、方阵化、补上计分节点),我在 Node 里把完整流程跑通:方块受重力落底、合并进场地、填满底行后正确消行、计分归位。结论是骨架本身是好的,断的全在细节的接缝处。另有一处小提醒:keyCode 已被 MDN 标记为废弃,新代码应改用 event.key 判断按键。

四、修好之后,值得继续加的东西
这个最小骨架离「像样的游戏」还有几步,每一步也都是通用练法。给七种方块配七种颜色,用下标映射色调,而不是全画成红色;把随机改成七袋随机器,每种方块整袋洗牌发完再洗下一袋,杜绝 I 块十几拍不来的挫败感;加落点预览的幽灵块,只需把当前方块直接投影到底部,画一层半透明;再加等级系统,让重力间隔随消行数递减,难度曲线就有了。移动端则要处理触控手势与按键长按连发,那是输入层的另一门功课。
收尾
AI 已经能把一个游戏的架构一次给全,但数据形状的一致性、对不存在 DOM 节点的引用、废弃 API 这些「最后一公里」,仍然要靠人来审。小而完整的项目之所以是最好的试金石,恰恰因为它们完整——每一行都会被真实执行到,任何断裂都无处藏身。审 AI 代码和审同事的代码其实是同一件事:先让它跑起来,再谈架构。



