写在开始
Vive Coding 之旅继续,这一次使用AI编程开发一个浏览器小游戏:俄罗斯方块。这是一个经典的开发项目,在“古法编程”时代,是由基础到进阶的敲门砖,虽然游戏规则很简单,但是涉及到的细节还是不少的,比如旋转、下降,碰撞检测,性能开销等等。那么我们就一起看下这个“古法编程”时代经典的项目,在“AI编程”时代需要多久能完成,质量几何吧。
效果图
源码地址奉上 github.com/tianqinisna…
往期回顾
阶段 A:准备工作
技术方案
- 构建与框架:Vite + React
- 游戏渲染:Canvas(游戏画面在 Canvas 上绘制)
项目搭建
- 形态:单页面应用(SPA)
- 路由:使用 React Router;当前仅需配置根路径
/(后续路由可再扩展)
阶段 B:页面布局
页面分为 上下两层;上层为功能区(左:按钮,中:当前关卡,右:得分与掉落块);下层为 三栏:左「下一块」预览 Canvas、中主游戏 Canvas、右七种方块图例 Canvas。整体约束在一屏内展示,不出现整页纵向滚动。
一、上部分:功能区
- 按钮:开始、结束、暂停 / 继续游戏(不可用时置灰)
- 中间:显示当前 关卡(如「第 1 关」)
- 游戏信息
- 得分:显示本局游戏的总得分
- 掉落块:显示本局已锁定的方块总数
二、下层左侧:下一块预览
- 独立 Canvas(逻辑尺寸 5×5 grid,与主棋盘共用
GRID),用于绘制 下一个 即将入场的方块(旋转 0、与生成朝向一致)。
三、下层中间:游戏区
- 游戏主画面区域;挂载 一个 Canvas(逻辑分辨率示例
320×640),作为游戏逻辑与绘制的载体。 - 在可用空间内按比例缩放,保持画布宽高比。
四、下层右侧:方块展示
- 与主游戏区 并列,单独挂载 一个 Canvas,用于图例 / 预览。
- 画布形态为 竖长、横窄;宽度为 5 个 grid,高度按竖向七种方块所需设定(实现中逻辑高度为 1120,对应
5×35个 grid)。 - 侧栏宽度随布局限制在合理区间(如
clamp),画布在栏内等比缩放。
阶段 C:基本功能实现(绘制与网格)
1. 格子(grid)与高清绘制
- grid:游戏区与方块区的绘制均以 1 grid 为基本单位;两 Canvas 共用同一套像素步长常量(如
GRID = 32逻辑像素,即一格在屏幕上的边长)。 - 主游戏区:逻辑尺寸 10×20 grid →
320×640像素。 - 右侧展示区:逻辑尺寸 5×35 grid →
160×1120像素(便于以「每槽 5 行 grid」竖向排布 7 种形状且网格线均为整格)。 - 清晰度:先按不低于 2 倍 物理像素分配 backing store(
canvas.width/height),再结合devicePixelRatio取更大者;显示尺寸仍为一倍逻辑大小(style宽高 +ctx.setTransform),实现「高分缓冲、一倍呈现」,避免模糊。
2. 公共绘制方法(单位:1 grid)
drawGrid:在给定CanvasRenderingContext2D上,按 1 grid 间距绘制横竖网格线(列数、行数、每格像素宽度可参数化,默认等于共享的GRID)。- 七种方块的绘制:形状与命名对照 俄罗斯方块-玩法说明.md 第 2 节;每种方块 单独一个绘制函数(如
drawTetrominoI…drawTetrominoL),并可在上层用drawTetromino(kind, …)统一分发。- 颜色约定(可与 Guideline 接近,便于辨认):
- I:
#00ddee - O:
#f0c000 - T:
#a020f0 - S:
#00c853 - Z:
#e53935 - J:
#1e88e5 - L:
#fb8c00
- I:
- 颜色约定(可与 Guideline 接近,便于辨认):
3. 首屏绘制内容
- 两个 Canvas 均绘制背景色 + 完整网格。
- 右侧 Canvas:在竖向预留的槽位内,将 7 种形状各绘制一例,自上而下排列。
阶段 D:游戏设计
以下约定为可玩逻辑与交互的验收口径;其中 7-Bag、锁定、SRS 踢墙、碰撞判定、DAS/ARR 与输入缓冲等概念,均按常见 Guideline 系做法理解并写死到可实现的参数级。
一、积分规则
- 落块计分:每 成功锁定 一块(新块已生成且上一块已固定到场地)计 +1 分,并与界面「掉落块」计数同步;不计 仅生成尚未锁定的块。
- 消行计分:单次同时消除 n 行(
n ≥ 1)时,得分10 * n + n * (n - 1)(例:n=1→10,n=2→22,n=3→36,n=4→52)。若同一次锁定后既消行又满足上一条,两笔分数都加(不做「消行时不再给落块 +1」的互斥)。 - 加速下落加分:
玩家按住下键软降每格 +1 分(阶段 E 已取消软降加分;自然下落仍不计分)。
二、游戏规则
-
随机序列(7-Bag):全局维护一只「袋子」,内装 I、O、T、S、Z、J、L 各一枚;开局与每取空一袋时,将七枚 洗牌 后依次作为出块顺序;取完再装一袋重洗。这样任意时刻不会出现长期缺某一形状,也不会无限连出同一种。新局开始时重新装袋。
-
场地与可见区:棋盘 10 列;可见 20 行用于渲染与判定玩家视野。为减少 SRS 在顶部旋转顶死、以及生成位过浅的问题,逻辑上在可见区上方再保留 4 行缓冲带(即整块逻辑棋盘为 10 列 × 24 行,最底下 20 行与当前画布对应;缓冲行不画进 UI 亦可,但碰撞与锁定必须在完整逻辑矩阵上计算)。若实现选择「无缓冲」则须自行承担顶行旋转失败率升高。
-
重力(基础值,关卡修正见阶段 E):自然下落基准为 每 1000ms 下落 1 grid(第 1 关);暂停 时重力计时器冻结。
-
生成高度:新块生成时竖直位置取 可见区顶行(逻辑行
BUFFER_ROWS)对齐:使旋转 0 下整块的最小行坐标落在可见顶行,不再长时间停留在顶部缓冲带内,进入画面即可见。 -
左右移动:左 / 右方向键单次平移 1 grid。长按使用 DAS / ARR:按住满 170ms 后进入连发,之后每隔 50ms 再步进 1 grid(左右共用同一组参数)。
-
软降(下键):下键每次令活动块 主动下落 1 grid(不计分);长按连发与左右一致:首段间隔 170ms,之后每 50ms 一步(与左右共用同一套 DAS/ARR 状态机)。
-
旋转:上键为 顺时针旋转(本阶段仅这一种旋转输入)。每次旋转先在当前格坐标下生成旋转后的占用格,若与墙或已锁定格 碰撞,则按 SRS 顺序尝试有限次 踢墙偏移(含
(0,0));全部失败则本次旋转作废,保持旋转前姿态。实现可先做「完整 SRS 表」或先做「简化踢墙(如仅左右各移一格)」再迭代,但对外行为须符合「能踢则踢、不能则放弃」。O 块旋转不改变形状,可不调用踢墙表。 -
碰撞检测:任意时刻对活动块所占的每一格判断:是否越过 左右下边界(上边界由逻辑高度与缓冲带约束)、是否与 已锁定 的格子重叠。移动、下落、旋转及每一次踢墙尝试都须通过同一套判定。
-
锁定延迟与 move reset:当块因重力或操作 触底 且 无法再因重力下移 时,进入锁定流程:在 500ms 的锁定窗口内,玩家仍可左右移动、旋转或软降;每次 成功的移动或旋转 可将该窗口 重新计时(move reset),但单块累计 最多重置 15 次,超过后不再重置,窗口耗尽即 锁定。若在窗口内块再次变为可因重力下落(如移出承托),则退出锁定态按正常重力处理。
-
消行与重力:任意一行 10 格均被占满 则该行消除;多行可同时消除。消除后 上方所有行整体下移填补空洞,再结算消行得分。
-
游戏结束(Dead):满足其一即本局结束:① 下一块在 生成位 已无法合法摆放(与已锁定格重叠或越界);或② 锁定完成后,已有方块占据 可见区上沿以上(即溢出到玩家视野顶线之上,具体判定与缓冲行坐标一致)。玩家在 游戏中 / 暂停 下点击 结束,视为立即触发 Dead,与上两条等价。
-
状态机:全局处于且仅处于:初始、游戏中、暂停、游戏结束。
- 暂停:不推进重力、不消耗锁定计时、不响应游戏操作键(或等价冻结游戏时钟);继续 后从中断点恢复。
- 开始:仅在 初始 或 游戏结束 可点;清空场地、分数、袋、当前/下一块等,进入新一局 游戏中。
- 结束:在 游戏中 或 暂停 可点;立即转 游戏结束。
- 暂停 / 继续:仅在 游戏中 与 暂停 之间切换,互斥;初始 / 游戏结束 下该按钮置灰或无效。
-
输入缓冲:对 左、右、下、上(旋转) 各维护短窗:在 120ms 内若按下时尚不可执行,则 最多缓存 1 次 该操作;一旦下一帧变为可执行,立即消耗这 1 次(避免「刚好差一帧按不到」的挫败感)。
-
本阶段明确不做(留待扩展 / 部分由阶段 E 承接):硬降、Hold、T-Spin / Combo 等;关卡与动态重力由 阶段 E 描述。硬降与 Hold 仍可不暴露键位。进一步扩展路线见 阶段 F。
阶段 E:细节优化(消行动画、软降计分、下一块预览、关卡)
一、消行动画
- 锁定后出现满行时,先不立即压缩矩阵:在约 320ms 内播放动画——满行格块 淡出,其上方未消行格块按 ease-out 曲线 竖直下移至落位后的逻辑行(视觉上为「整段下移」)。
- 动画结束后再执行矩阵压缩并生成下一块;暂停 时消行动画的累计时间不增加(进度冻结),恢复后继续;活动块重力与输入仍按阶段 D 在暂停下不推进。
二、软降计分
- 取消下键软降每下移 1 grid 的加分;落块 +1、消行公式仍按阶段 D。
三、下一块预览
- 主棋盘 左侧 增加独立 Canvas(5×5 grid),绘制 下一个 即将入场的方块(与 7-Bag 的
peek一致,旋转状态 0)。
四、关卡与重力
- 升级依据:以 本关内已锁定块数 累计(与界面「掉落块」总数同步增长);达到本关阈值后 关卡 +1,本关计数清零。
- 阈值递增:第 1→2 关需 10 块;之后每升一级所需块数 +3(10 → 13 → 16 → 19…,实现公式:
7 + 当前关卡 × 3)。越往后升级 需要的掉落块越多。 - 重力:第 1 关 1000ms / 1 grid;每升一级间隔乘以 0.92,并设 下限 180ms/格(速度上限,再快不再缩短)。
- 界面:顶部栏 正中 显示「第 n 关」。
五、其它
- 顶部按钮在不可用状态下 置灰(降低对比、
cursor: not-allowed)。
阶段 F:建议路线(玩法扩展、体验、无障碍与工程)
以下基于当前代码与文档整理,不要求一次做完;可按优先级拆成多个小迭代。
一、玩法与规则(承接阶段 D 第 14 条)
- 硬降(Hard Drop):单次瞬间落底并立即锁定;常见计分可单独约定(如每格 +2 或仅加速感不计分,需与阶段 D 文档对齐)。
- Hold:单槽暂存当前块,与下一块交换;每落一块仅允许 使用一次 Hold(经典 Guideline 限制)。
- 幽灵块(Ghost):在主棋盘用半透明轮廓显示落底位置,降低误操作。
- 进阶计分(可选):Back-to-Back、Combo、T-Spin 检测与加分;需与现有「消行公式 + 落块 +1」是否叠加写清楚。
二、视听与反馈
- 音效:锁定、单消~四消、游戏结束、升级等分层音效;提供 静音 开关并持久化到
localStorage。 - 轻量画面反馈:多行消除时短暂 屏震 或边框高亮(注意与现有消行动画时长协调)。
- 升级提示:关卡变化时顶部或 Toast 简短提示(可与音效同步)。
三、无障碍与输入
- 键盘:
keydown对方向键preventDefault,避免焦点在页内时 滚动页面;游戏区容器tabIndex={0},开局或点击后 聚焦,保证键盘始终生效。 - 说明文案:功能区或页脚增加「操作说明」折叠区(方向键、暂停等),便于新玩家与验收。
- 可选键位:左右手友好的备用键(如
A/D、W旋转)可作为后续配置项。
四、界面信息(在不大改布局前提下)
- 关卡进度:例如「本关已锁定 a / 还需 b 块升级」或进度条,与
dropsNeededToAdvance一致。 - 当前重力:显示「约 n ms / 格」或与阶段 E 公式对应的档位说明,减少「突然变快」的困惑。
- 高分榜:本机
localStorage存 Top N(姓名可省略,仅存分数 + 日期)。
五、生命周期与多环境
- 切后台:监听
document.visibilityState,隐藏标签页时自动暂停,避免后台仍消耗计时与误触。 - 移动端(若需要):触控 虚拟方向键 或滑动手势;需单独调 DAS/ARR 手感与误触。
六、工程与质量
- 单元测试:对 无 UI 模块优先写测例——
boardUtils(消行、溢出判定)、bag7(洗牌与取块顺序)、srs(若干已知踢墙用例)、levelSystem(阈值与重力公式)。可选用 Vitest。 - CI:提交/PR 时跑
npm run build+ 测试脚本,保证重构不破坏规则。 - 性能:主循环已用 rAF;若将来 UI 状态增多,继续保持「高频数据不进 React state、仅 Canvas 重绘」的策略。
七、可选长期项
- 本地存档 / 继续游戏(单局中途退出恢复)。
- 主题:高对比 / 色弱友好调色板。
- 联网排行(需后端,超出当前 SPA 范围)。
写在最后
以上阶段A - 阶段F,就是通过和AI对话,生成的开发文档,然后按照生成好的文档,按步骤进行代码实现,总计耗时2 - 3小时。那么问题来了,如果不使用AI,纯手写实现,你需要多久呢?
好了,那这次的Vibe Coding就是这样了,下次见。