会犯错的AI-探秘雅达利Video Olympics的早期AI对手

 

Pong的黄金年代

Pong并不是第一个商业电子游戏,但我们绝对的可以自信的说他是第一个大获成功的商业电子游戏。

Pong街机游戏画面

游戏非常简单,左右两个玩家各控制一个旋钮,击打来回反弹的一个小球,每次一个玩家没有接住,对手就加一分。

Pong街机

用一句话给Pong成功的原因下论断是武断的,但我们可以得出的结论是,它相较第一个商业游戏Computer Space,不仅操作更加简单直接,也直接来自于现实生活中熟悉的体育运动,这对其受众群体有极大扩宽,特别是在那时街机并没有单独区域,而是被安置在弹球桌等设备旁。

Computer Space街机游戏画面

Pong很难说是一个独创游戏,游戏的开发过程中,游戏创始人Nolan Bushnell向新招的程序员Alcorn描述了一个与他先前在Magnovox Odyssey上看到的乒乓球游戏,还向Alcorn谎称接到了通用电器的一笔订单,来作为一个小训练项目,就这样Alcorn在完全不知情的情况下完成了开发。

雅达利后还被Magnovox起诉,虽Bushnell不承认见过Magnovox的乒乓球游戏,他在Magnovox展会上的入场签名却被扒了出来。

Bushnell的入场签名

但无论如何,Pong获得了前所未有的成功。Alcorn先把它安置在一个小酒吧试验,结果一周之后Alcorn就接到了店主的电话,说机器坏了。Alcorn过去一看,一开机器至少有100美金的硬币,把机子塞到无法正常运作了。

Alcorn与雅达利核心的照片

雅达利售出了8000多台Pong机器,这还不包括来自于Midway,Nutting  Associates等多个公司大量售出的仿品。

雅达利由SEARS发售的家用Pong

1974,雅达利员工Harold Lee提出了推出家用Pong以扩大市场的想法,在近一年的开发后,终于有了一个试验机。但各大经销商因为Magnovox Odyssey的销量不佳对家用游戏并不热忱。雅达利必须联系更有财力的公司。在SEARS的名下,第一个家用Pong主机发售了。在SEARS家用Pong大获成功后,雅达利发售了自己品牌下的主机。家用Pong的影响力甚至波及到了全世界。

任天堂的第一个主机Color TV-Game6就是一个Pong的仿品

Color TV-GAME 6

甚至苏联电子工业部还在1978年发售了自己的家用Pong仿品—Turnir。

Turnir主机

在家用Pong机器取得成功的同年,雅达利组建了代号Stella项目的核心团队,也就是未来的雅达利VCS(2600)。随着VCS开发的开始,几个游戏也作为首发游戏同步开始开发,其中包括首发游戏Video Olympics—一个类Pong游戏

雅达利2600(VCS)

将一个类Pong游戏作为首发游戏是一个很自然的决定。Pong已经主宰电子游戏市场数年,而且在替换卡带游戏机(首个是仙童Channel F)仍是新奇东西的时候,给游戏机一个类似家用Pong,但是更强大的版本既能提高消费者的接受度,又能展现机器的强大能力。

Video Olympics盒子封面

很明显地,雅达利在平台开发上早就考虑了类Pong游戏。雅达利2600采用MOS 6507(丐版6502)作为处理器,只有128字节的RAM,因此游戏中出现的画面元素被严格限制成两个精灵(Sprite,可理解为图案),两个导弹,和一个球。两个导弹明显与坦克游戏Combat的开发有关,而球明显与Video Olympics相关。Decuir在采访中也曾确认过这一点

游戏开发者Decuir给游戏创造了很多的不同版本。不仅有两人改版,四人版本,还有篮球,排球,手球等改版。

每人两个球拍的Super Pong

为四人游玩设计的Pong Four

需要将球弹入篮筐的Basketball

最为有趣的是,在Pong街机的反馈下,Decuir决定开发”Robot Pong”模式,玩家可以与一个虚拟的AI对手进行比试,这在商业电子游戏中是极其新奇和开创性的。在谈论Robot Pong之前,我们需要先回头看看

点击体验Video Olympics

拨动右下角的“Game Reset”开始游戏,拨动“Game Select”选择游戏类型。游戏1为默认对阵AI对手的游戏。

电脑端控制:使用FH键控制球拍,TG键控制球拍速度。

游戏中的早期AI

第一个出现在电子游戏中的”AI”出现在1952年。在同时期,两个早期计算机上的井字棋游戏被开发出来,游戏分别由A S Douglas和Christopher Strachey完成。

Electronic Delay Storage Automatic Computer

 

Manchester Mark 1

他们的共同点在于,均可以有不败的AI对手。因为井字棋的规则极为简单,游戏可有的变式也较少,计算机可以轻易不犯任何错误地把把不败。

直到1956,计算机科学家EDMUND C. BERKELEY开发出一个基于继电器的井字棋计算机,名叫Relay Moe。

刊物Radio Electronics中的Relay Moe结构图

在Relay Moe上安装有偏心凸轮,其工作原理很简单,一个脱离中心,异型的塑料碟在旋转中会时时关闭和打开其控制的电路。

偏心凸轮示意图

Berkeley利用了此原理来使部分控制AI逻辑的电路时不时被关闭,因此AI会有时在游戏中犯错。会犯错的AI将电子游戏的发展中被证明至关重要。在与AI的对抗中找到规律和战略才能让玩家获得乐趣。失误与逻辑在一个AI的有趣性和真实性上同样重要

二十年后,电子游戏开始走出实验室,最早商业游戏中的“AI”也随着单人街机游戏出现。最早的之一可以说是Taito在1974年的Speed Race。

Speed Race游戏画面

Speed Race中,玩家驾驶赛车,躲避路上左右移动由AI操纵的车。此处的AI只用遵循一个固定的左右规律,虽然会有不同移动频率的车,但他们本质上并没有对玩家的操作有任何反映,也远不是与玩家地位相似的对手,更像是增加玩家挑战的一个小要素。

同年,雅达利发售了Qwak! 在游戏中,玩家拿着枪打屏幕上的鸭子,每只鸭子玩家只有三次的开枪机会,鸭子还会向玩家射空的反方向进行躲避。我们很难说AI被和玩家放到同等的地位上,因为有明显的主被动关系,但这里的AI已经会因人类的输入有对应其的行为。

Quak!游戏画面

随着单人街机的普及,这样的例子也多起来。最为出名的莫过于1978年的Space Invaders。在那里,无数的外星人根据玩家的位置选择发不发射子弹。

Space Invaders游戏画面

这样,我们得以看到Video Olympics的重要性。AI坐在了本应由玩家控制的对手席,这不仅代表和玩家位置相当,也代表它需要在游戏中模仿人类的智能和反应。明显地,AI的举动也不能局限于简单的规律,它需要时刻与玩家的输入互动。

深究Video Olympics的AI

对于AI开发的一大难点在于,Pong的挑战不仅是策略的考验,也是反应速度的考验。在当今视角下极其微小的128字节RAM和4K游戏大小下,塞入一个有挑战性的AI是难度很高的。例如,在雅达利1979发售的Video Chess中,电脑在玩家下出棋子的几秒后才会反应,这给了处理器充足的运算时间

Video Chess游戏画面

但在一个类似Pong游戏中,每帧中,电脑玩家的球拍的位置都必须被更新。这本身并不难,甚至可以说很简单,只要电脑玩家的球拍时时与球的y坐标保持一致,球就每一次都会被球拍击中。但是,一个无敌的AI并不好玩,也显得不够智能

在Bogost和Montfort所著的Racing the Beam中,我们可以读到如下的记载:球拍在每帧会根据球的相对位置上下调整最多两行(我们可以将行粗略理解成竖向像素),但是,每八帧,球拍的调整会被跳过一次,这样,累积的错误足以让球拍偏移,而且球的角度越大,这个偏移就越明显,玩家就越容易得分。

雅达利2600使用的处理器是MOS Technology的6507处理器,这个处理器是一个经过修改的6502,并且使用和6502一样的指令集。因此,所有雅达利游戏都是由汇编语言写成,可以说是在对人类可读的条件下,最为底层的语言。

作为举例,这是6502处理器接受的一条命令和它对应的机器语言,它的意思为向处理器的A寄存器中存入$02的值:

LDA #$02 ;汇编命令
A9 02 ;16进制(hex)下表示的机器语言
1010 1001 0000 0010 ;二进制下的机器语言

任何的汇编代码需要经过汇编器(Assembler),以转为机器语言。以此同理,经过反汇编器(Disassembler),我们可以将任何雅达利游戏的ROM(只读存储,即卡带上的数据)通过反汇编过程转为汇编代码。但是如果单单使用反汇编程序,而没有人为的Label标注和注释下,代码的可读性依然很低。

;Setup a room for print.                                                                                           
SetupRoomPrint:
       LDA    $8A                 ;Get current room number. ;3    
       JSR    RoomNumToAddress    ;Convert it to an address.;6    
       LDY    #$00                                          ;2    
       LDA    ($93),Y             ;Get low pointer to room  ;5    
       STA    $80                 ;      Graphics           ;3    
       LDY    #$01                                          ;2    
       LDA    ($93),Y             ;Get high pointer to room ;5    
       STA    $81                 ;      Graphics           ;3

举例来说,上面的代码是经过人为标注的Adventure代码片段,而下方是我用反汇编过程得到的代码。

LF10F: LDA    $8A     ;3
       JSR    LF271   ;6
       LDY    #$00    ;2
       LDA    ($93),Y ;5
       STA    $80     ;3
       LDY    #$01    ;2
       LDA    ($93),Y ;5
       STA    $81     ;3

在Bogost和Montfort所著的Racing the Beam中,我们可以读到如下的记载:球拍在每帧会根据球的相对位置上下调整最多两行(我们可以将行粗略理解成竖向像素),但是,每八帧,球拍的调整会被跳过一次,这样,累积的错误足以让球拍偏移,而且球的角度越大,这个偏移就越明显,玩家就越容易得分。

而Video Olympics则恰恰没有被人为标注过,因此我们只能使用调试器从内存的变动开始分析。下图展现的表格是雅达利2600 128个字节的RAM(我们常说的“内存”),每一个小方格展现的是一个字节,竖编号+横编号则是每个内存的编号。如果要有每八帧一跳过的机制,代码中内存中的帧计数是一定会被读取的。

雅达利的128字节RAM

我们可以清晰的看到内存地址$88是游戏中的帧计数RAM,从a0计数到9f,再循环往复。

因此,我们要在代码中寻找的是LD* RAM_88,即是将内存地址88中的数据加载到寄存器*中,我们在地址f1e3找到了如下的代码

f1e3	LDA	RAM_88   ;将88号内存(帧计数)的数据加载进累加器(Accumulator,一种暂存器)
f1e5	AND	#$77   ;将累加器中的数据使用$77进行遮罩
f1e7	TAY   ;将累加器的数据转移到Y暂存器

这段代码所干的事情如下:首先,读取在RAM_88存储的帧计数数据到累加器(Accumulator,寄存器A,6502中最为全能的寄存器)中,再将$77作为寄存器A中数据的遮罩,覆盖寄存器A中的数据,最后TAY命令将Y寄存器的值设为A的

这听起来有一些晦涩,但只要我们将hex值都转换为二进制,事情就会清晰很多 例如,当帧计数的hex值是b6时,它写入累加器的值转换为二进制就是:

10110110

而遮罩$77的二进制值则是:

00000111

只有遮罩和原值都为1的地方,才会是1,所以b6经过遮罩后是:

00000110

那这个遮罩是怎么完成每8帧的追踪暂停的呢?

帧计数RAM_88的变动

我们可以看到,每8帧,RAM_88数据的末三位会成为000,这就代表经过$77的遮罩,每8帧,会有一帧计算后得到的值会是00000000,即$00

现在,我们知道了在此代码段,每8帧,Y寄存器中的数值会是$00,于是,我们只需要找到代码中下一个读取Y寄存器的指令即可。我们很快在地址f1f4找到了相应代码:

f1f2	LDA	RAM_95   ;将95号内存的数据加载进累加器
f1f4	CPY	#$00   ;将Y暂存器中的值与$00比较
f1f6	BEQ	Lf202   ;如果上一条满足,分支到Lf202(跳过球拍位置更新)

f1f2我们暂且不谈,f1f4中CPY指令会将Y暂存器中的数值与$00进行比较,而BEQ是Branch if Equal的意思,如果Y暂存器的数值等于$00(每八帧),分支到Lf202(也就是跳过球拍位置的同步,跳过下面的f1f8-Lf200)

接下来的代码如下:

f1f8	CMP	RAM_B6   ;将累加器里的值与B6地址的RAM比较
f1fa	BEQ	Lf202   ;如果累加器里的值与B6地址的RAM相等,分支到Lf202
f1fc	BCS	Lf200   ;如果累加器的值大于B6地址RAM,分支到Lf200
f1fe	ADC	#$05   ;将累加器里的值加$05
Lf200	SBC	#$02   ;将累加器里的值减$02
Lf202	STA	RAM_95   ;将累加器里的值存入RAM95

这段比较复杂,咱们慢下来分析。f1f8中CMP命令将暂存器A里的值与B6地址的RAM比较。问题来了,暂存器A中的值是什么,B6中存储的又是什么?我们需要追踪到上一个写入寄存器A的命令,这就是f1f2地址下的LDA RAM_95,也就是说程序正在将内存地址95和B6的两个字节进行比较。既然我们知道这是根据球的位置调整球拍位置的代码片段,我们可以合理猜测内存地址95和B6的值正是二者所在的行数。

Debugger录制

在图片右侧的RAM表格中,此帧被修改的字节会亮成红色。我们可以很快确定RAM_B6就是球所在的行数(纵坐标),因为在球在持续往上走的时候,RAM_B6的HEX值不断下降,而当球触顶反弹,RAM_95的HEX值又从$36开始增长。而RAM_95一直紧随RAM_B6其后,明显是球拍所在的位置(行数)。(这个位置记录的其实是球拍上沿的行数,这是因为雅达利在CRT电视上运行,且缺少帧缓冲,也就是说,对开发者而言实际上没有帧的概念,只有电子枪打出的每行的概念,因为屏幕从上至下绘制,使用球拍上沿作为行数(纵坐标)参考是更经济的。)

CRT电视的显示简图

f1f8	CMP	RAM_B6   ;将累加器里的值与B6地址的RAM比较
f1fa	BEQ	Lf202   ;如果累加器里的值与B6地址的RAM相等,分支到Lf202
f1fc	BCS	Lf200   ;如果累加器的值大于B6地址RAM,分支到Lf200
f1fe	ADC	#$05   ;将累加器里的值加$05
Lf200	SBC	#$02   ;将累加器里的值减$02
Lf202	STA	RAM_95   ;将累加器里的值存入RAM95

让我们继续分析代码。首先f1f8将球的坐标和拍子上沿的坐标比较,如果已经匹配,f1f2中BEQ命令会直接跳到Lf202,用RAM_95本身的值复写自己,即拍子不用移动。如果两个坐标不匹配,即会在f1fc比较球和拍子的坐标哪个更大,如果球拍的坐标大于球的,会分支到Lf200,将球拍的坐标减去$02,如果反之,则不跳过,继续运行f1fe和Lf200,将球拍的坐标加上$05再减去$02。这里其实困扰了我很久:为什么加5再减2得到的是2而不是3?(此处与计算机二进制的进位和借位有关(Carry和Borrow),与我们更直觉的整数加减法有所不同,其细节我们在这里不作讨论,但通过分步运行我们能看到实际的结果是先加5再减3,与我们的结论相符。如果对此感兴趣,可以在搜索引擎搜索Carry Flag和Assembly的关键词。)

非常有趣地,我们可以看到此处的AI遵循了与20多年前Relay Moe相似的原理,即使让AI时不时犯错,在这里,帧计数与偏心凸轮并无本质区别。一个看起来智能和有趣的AI,需要既有人类的智能,也有人类的马虎和不擅长的地方,这与当今几乎所有游戏Boss有迹可循的失误和Pattern是符合的。

为了验证我们的发现,我们可以尝试修改Video Olympics游戏的代码。

f1f3	LDA	RAM_95   ;将95号内存的数据加载进累加器
f1f5	CPY	#$00   ;将Y暂存器中的值与$00比较
f1f7	BEQ	Lf202   ;如果上一条满足,分支到Lf202(跳过球拍位置更新)

游戏的位置调整每过八帧失灵一次的功能全部位于f1f4-f1f7,如果我们将这些地址的指令全部改为NOP(什么都不干,消耗两个machine cycles)的话,我们应该就可以造出一个无敌的ai。修改后的代码如下:

f1f4	NOP   ;什么也不干,消耗两个Machine Cycles
f1f5	NOP   ;同上
f1f6	NOP
f1f7	NOP

当我们再次运行游戏,AI已经基本无敌,它在每一帧都会按照大小关系调整自己的位置,而不会犯错。玩家得分的唯一可能性是侧击球以使其速度超过2行每帧(球拍的移动速度最多是两行每帧),例如下面的情景。

Video Olympics画面

但我们并没有解决游戏AI机制的全部,在Racing the Beam中还有另外一段记载:但是每八帧停止追踪的球没多久就会完全失去同步,为了解决这点,当球从一个墙壁反弹,如果球拍也与其对得上,球拍的位置会被重新调整,先前积累的误差都会被清零。不幸地,此处的逻辑没有球拍调整的逻辑简单直接,我也暂未在代码中找到对应。由于STA RAM_95的代码(即设置球拍的位置)只能在游戏中找到一处(除了开始的初始归零),我们可以几乎确定地说这个逻辑处于f1f7之前。我将附着一份经过解编的ASM汇编文件在网站上,另外还有一个可以使用Stella模拟器运行的ROM文件,其中内置的Debugger的教程可以在此找到。如果找到了这部分的逻辑,欢迎在contact@rgmuseum.org或微信联系我,如果未来可以补全这部分的文字,我会更新这个文章。(一点可能线索:球与墙壁的碰撞似乎会切换RAM_82和RAM_94的值;另外如果书中关于球拍位置比较的记载属实,可能需要寻找RAM_B2与RAM_95某种直接或间接的compare逻辑,一份较为全面的6502开发者指南在此。)

现代游戏AI

当今游戏已经大大发展,我们也不可能在本文章的篇幅下全面地介绍它的发展,从光环2开始广为应用,较有代表性的是行为树。游戏会时时根据不同条件完成预先设置好的树状选择,以决定应该干什么。举个例子,行为树可能判定是否发现了敌人(玩家),如果发现了,即根据距离选择近战或远战;如果进展,则根据弹药余量决定拿出喷子还是拿出剑

虚幻引擎中的行为树

但不论技术如何发展,游戏AI背后的一些原则是恒久不变的。正如在Video Olympics和更早的Relay Moe中体现的,一个好的游戏AI需要有可预测的缺陷的。

Halo(光环)的技术经理曾在采访中说:“我们的目标不是做出一个不可预测的智能,我们想得到的是:玩家可以做特定事而期待从AI得到特定反应。当玩家偷偷靠近一个小怪并吓它一下,小怪会吓得开始乱跑,虽然它会往哪里跑不确定,但它一定会跑”

光环游戏画面

玩家像这样在Video Olympics中在正确的时机侧击球,就像玩家在Minecraft里把自己封到两格高的空间打三格高的末影人一样,他们本质上并无区别。当我们以正确的时机直击游戏AI的弱点时,这就像我们与最熟悉的朋友下棋时让他再次落入熟悉的陷阱一样。

1人评论了“会犯错的AI-探秘雅达利Video Olympics的早期AI对手”

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注