独立开发者 Plicerin 近日将动视在 1982 年发行的雅达利 2600 射击游戏《River Raid》以纯 JavaScript 完成了指令级重写。这项工程没有调用现成的模拟器,而是把原版卡带 4,096 字节里的每一条 MOS 6502 汇编指令,逐一改写为可以在现代浏览器中执行的原生脚本,并让它们精确运行在原版规定的 128 字节 RAM 地址之上。

经由并联原版 6502 内核的逐帧比对,该移植版本创下了连续 45,000 帧无单字节差异的记录。这不仅是对传奇女性程序员 Carol Shaw 经典的数字化翻新,更像是一台高精度的显微镜,把四十多年前软件工程师如何在算力荒漠中用黑客手段压榨硬件的底层真相,完整推到了现代开发者眼前。

不是模拟器,而是把 4KB 汇编逐条直译为 JS

多数复古游戏在现代运行环境中的复活,依赖的是诸如 Stella 这类全系统模拟器。模拟器的本质是用软件构造一整套虚拟的 CPU、内存控制器与音视频接口,卡带 ROM 本身只是被动读入的二进制数据块。

原版4KB汇编指令被逐条直译为脚本,严格跑在128字节线性内存上
原版4KB汇编指令被逐条直译为脚本,严格跑在128字节线性内存上

Plicerin 选择了一条完全不同的道路:逐条转译。原版卡带 ROM 映射在地址 $F000 到 $FFFF 之间,开发者将这些底层汇编指令逐一映射为 JavaScript 函数,在内存中严格开辟出与 MOS 6507 相同的 128 字节线性数组。为了证明逻辑的绝对一致,测试管线并联了一个真正的 6502 内核作为裁判,在关卡 1 到 48 之间核对了整整 300,000 个河流生成器内部状态,游戏首帧的 31,840 个像素点与模拟器截屏完全重合。

River Raid 指令级移植的关键工程指标 4,096 字节卡带 ROM 全部代码与图素总容积 128 字节系统 RAM 主机运行期唯一可变内存 45,000 连续比对帧数 并联 6502 内核零字节差异 34 秒 满油恒定支撑时长 60Hz 下 2,048 帧倒计时

这种近乎偏执的还原,撕开了现代软件抽象层的遮羞布。雅达利 2600 的处理器主频只有 1.19 MHz,且完全没有图形显存帧缓冲区,程序员必须让程序计数器追着显像管电子枪的扫描节拍跑。当一切被原样搬进 JavaScript,原版代码为了在这 128 字节限制下求生所使用的黑魔法,也全部无所遁形。

消失的变量与锁存器:128 字节里的极限量化

在这款射击游戏里,最反直觉的事实是内存里根本不存在真正的得分变量。

游戏完全没有真正的得分变量,分数与碰撞判定全被压入底层硬件
游戏完全没有真正的得分变量,分数与碰撞判定全被压入底层硬件

常规架构会开辟整型变量累加分数,再通过算法将数字转化为屏幕上的点阵。但在仅有 128 字节 RAM 的环境下,数值转换代码太奢侈了。Carol Shaw 的解法是直接将位于 $CD 至 $D8 的 6 个内存字节定义为指向图形字模的内存指针。当玩家击落敌机需要加分时,汇编代码直接在指针数值上加上位图字模跨度 8(DIGIT_H = 8);若指针超出字模范围则向前一位进位。

游戏并没有在计算分数,它只是在内存里不断挪动屏幕位图的物理地址。

在分值区分上,原版甚至没用条件分支。击落飞机得 100 分、炸毁桥梁得 500 分,这在常数表中被存为带有最高位标记的常数。代码只要检查 bit 7($80),就能一步将百位指针向前推进,彻底省去了多字节十进制计算。

人机交互错觉与底层代码真相 玩家层面的交互直觉 · 油门推到底高速飞行,燃油消耗更快 · 击落敌机后,系统数值变量执行加法 · 战机与敌人的机身边缘发生几何重叠碰撞 6502 汇编的底层现实 · 每帧固定扣减 $20,全速倒计时 2,048 帧 · 没有数字变量,直接对字模内存指针累加 8 · 无任何坐标包围盒算术,全靠 TIA 硬件锁存器

碰撞检测同样没有消耗任何 CPU 几何算力。整整 4KB 代码里找不到一行包围盒比对,所有判定全部交给雅达利图形芯片 TIA 的底层硬件碰撞锁存器。这种极端省力带来了一连串奇异的副作用:战机撞击敌机坠毁的同一时刻,系统依然判定碰撞得分;战机触碰房屋甚至直接复用触碰油箱的代码;当一个新河流图块刚刚滚入视口顶部时,硬件碰撞标志会被错记在不存在的第 7 个图块名下,导致脏数据直接覆盖第一个屏幕物体的坐标字节。

种子 $A814:算尽 5.7 万截面的数学确定性

许多老玩家坚信《River Raid》是一条真正随机生成的无尽河流,但代码揭示的世界是严格数学确定性的闭环。

燃油消耗与飞行摇杆推拉无关,满油续航由底层时钟严格锁死在34秒
燃油消耗与飞行摇杆推拉无关,满油续航由底层时钟严格锁死在34秒

在 4KB 空间内不可能存储上百关的地形地图。Carol Shaw 设计了一套 16 位线性反馈移位寄存器(LFSR)作为伪随机数发生器,初始种子恒定为 $A814(SEED_LO = $14,SEED_HI = $A8)。高位 randomHi 决定敌机类型与生成阈值,低位 randomLo 控制坐标分布与岛屿走向。整条河流的 57,337 种截面组合完全由此派生。玩家坠机后之所以能原地精准重现关卡,只是因为游戏把坠机那一刻的随机种子存了快照。

代码里还埋着一套动态调整的数值公式:

LDA #64; SBC level; ASL

关卡上限设为 48 级(MAX_LEVEL = 48)。随着关卡数递增,油箱的出现概率被数学公式逐步压缩,腾出的空间被无分值的房屋填满。前 4 关代码将河谷宽度锁死在 valleyWidth = 6,防止初学者遭遇极窄河道;直到第 5 关该变量归零,系统才完全放开全截面地形。

另一个被代码彻底戳破的常识是燃油机制。绝大多数玩家下意识认为加速飞行会加剧耗油,但 6502 汇编证实耗油完全由固定的每帧减 32(fuelLo -= $20)决定。满油容量在 60Hz NTSC 制式下恒定只能支撑 34 秒。摇杆的推拉仅仅改变了战机的屏幕位移,丝毫不会改变燃油倒计时的物理节拍。玩家感受到的耗油加剧,纯粹是单位位移与恒定时间差交织带来的人机交互错觉。


这次重写还意外澄清了一桩文献公案。移植作者在开发文档中记录“32 个图块构成一个关卡截面(Section)”,而逆向反汇编社区的源码公认定义是“1 个截面包含 16 个图块,每个图块由 32 条扫描线构成”。两代工程师在图形滚屏窗口切分上的术语错位,恰好见证了复古计算技术在代际传递中的流变。

  • 建议.对于从事嵌入式开发或极端受限环境的系统架构师,这套代码是极其罕见的无显存图形编程标本,它用极限量化证明了数据结构与渲染管线之间可以完全抹平边界。
  • 风险.尽管 Plicerin 的目标是代码保真与逆向验证,但直接将动视拥有数字产权的卡带二进制、字模及音频数据硬编码转译至 Web 端公开发布,依然游走在版权法与软件遗产保护的灰色地带。

脱离了硬件模拟外壳的《River Raid》,不再是一盘老卡带的模糊镜像。它把上世纪八十年代工程师面对硬件绝壁时的狡黠与严谨,原原本本地编译成了每一行明晰的现代脚本。