第1部分。密码
NES游戏Mike Tyson的Punch-Out使用密码系统,允许玩家从特定位置继续游戏。 每个密码由10位数字组成,范围从0到9。游戏可以接受两种类型的密码,我称之为“普通”和“特殊”密码。 特殊密码是由10位数字组成的特定组合,游戏以独特的方式响应输入。 特殊密码的完整列表如下所示:
- 075 541 6113-忙信号1
- 800422 2602-忙信号2
- 206882 2040-忙音3
- 135 792 4680-一场隐藏锦标赛中的游戏:“另一个世界巡回赛”(要接受密码,您必须按住“选择”按钮并按A + B)
- 106113 0120-显示标题(要接受密码,必须按住“选择”按钮并按A + B)
- 007 373 5963-将玩家转入与Mike Tyson的战斗
游戏接受的第二种密码是常规密码。 使用常规密码对玩家在游戏中取得的进度进行编码。 以下游戏数据使用常规密码编码:
密码编码
例如,为了研究密码生成,我们使用了一场24胜1负19淘汰赛的游戏,并在世界锦标赛中与超级猛男进行了战斗。
用密码对游戏状态进行编码的过程始于将赢,输和淘汰赛的数量收集到缓冲区中。 游戏以
二进制十进制代码的形式显示每个数字,该
二进制十进制代码由每位8位和每个值2位组成。 也就是说,对于24场胜利,您需要一个值为2的字节和另一个值为4的字节。成对的丢失和击倒字节对也是如此,即总共有6个字节的数据。 在下图中,这6个字节用十进制和二进制值表示。
下一步是为这6个字节生成校验和。 校验和字节是通过将6个独立的字节相加并从255中减去结果而得出的。在我们的示例中,2 + 4 + 0 + 1 + 1 + 9 = 17,即255-17 = 238。
然后,我们将6字节的一些位写入新缓冲区。 该缓冲区可以解释为一个28位中间值,我们将逐步填充它。 来自第一个缓冲区的位被分成两组,并被移动到第二个缓冲区的不同硬编码位置。 这是几个步骤中的第一步,其唯一任务是简单地混淆数据,以使为玩家生成密码的过程复杂化。
注意,并非原始缓冲区中的所有位都传送到新的中间缓冲区中。 这些位将被忽略,因为已知它们始终为0。由于游戏规则的原因,仅用2位信息就足以传输密码中的丢失次数。 如果损失总数达到3,则游戏结束并且玩家未收到密码。 因此,用数字0、1和2描述损耗的数目就足够了,为此,仅2位就足够了。
然后,我们将其他位对写入中间缓冲区。 前四对取自先前计算的校验和值。 另一对来自敌人的价值。 对手的值是一个数字,指示玩家在输入密码后将与哪个对手战斗。 可以使用三种可能的敌人值:
0-唐·弗拉门科(主要比赛的第一场比赛)
1-PISTON HONDA(世界锦标赛首战)
2-SUPER MACHO MAN(世界锦标赛的最后一战)
由于我们想生成超级强壮的男人面对我们的密码,因此我们使用2作为对手的值,然后将校验和位和对手值写入中间位,如下所示:
下一步是对左侧的中间位执行几个循环置换。 左侧的一个循环排列意味着所有位都向左移动一个位置,而最左边的一位移动并变为最右边的一位。 要计算左边的排列数,我们将对手的值与损失数之和,加1,然后将其余的结果除以3。 在我们的例子中,结果为2 +1 + 1 =4。那么4/3的余数为1,因此我们将中间位循环左移一次。
至此,中间位已经完全混合在一起了,是时候开始破解它们了,以获取组成密码的数字。 密码应该由10位数字组成,因此我们将28个中间位分解为10个独立的数字,我们将其称为密码值P0,P1,P2等。 前九个密码值中的每个都接收3位数据,而最后一个仅获取中间位之一。 为了完成完成的密码值,我们还将包括一些位,这些位指示在上一步中执行的排列数量。
最后,我们向每个密码值添加一个唯一的硬编码偏移量。 完成的密码位将是该总和除以10的余数。例如,在第七位,我们使用偏移量1,即得到5 + 1 = 6,最后一位是从6/10的余数,即6。在第四位,我们使用偏移量7,即得到5 + 7 = 12,最终数字等于余数12/10,即2。
因此,我们获得了可以在游戏中检查的现成密码数字。
密码解码
按照上述所有步骤的相反顺序,将密码解码回赢/输/击倒次数以及对手的值的过程是一个简单的实现。 我会将其作为读者的任务。 但是,游戏在解码和检查玩家输入的密码时会出现两个明显的错误。
第一个错误发生在解码密码的第一步,即减去偏移量以返回到密码值时。 初始密码值每个包含3位数据,也就是说,应用偏移之前的初始密码值应在0-7范围内。 但是,玩家可以输入密码,在减去偏移量后,密码值将为8或9(用10除以余数)。 游戏没有立即拒绝这种密码,而是错误地不检查这种情况,并允许您向密码值添加一个额外的数据位,该数据位会污染中间位的集合,从而使密码不再唯一。 由于可以用密码的相应数字或相邻密码值的附加位来设置某些中间位,因此有许多密码可以转换为同一组中间位。 这就是为什么您可以找到提供相同游戏结果的不同密码的原因,尽管它们应该是唯一的。
第二个错误是游戏在解码密码后用来验证数据的逻辑错误。 游戏正在尝试应用以下条件:
- 密码中存储的校验和对应于应考虑到密码中存储的赢/输/淘汰次数而获得的校验和
- 损失值为0、1或2
- 敌人值为0、1或2
- 考虑到损失值和存储在密码中的对手的值,密码中存储的循环排列数是正确的数字
- 密码中存储的所有赢/输/淘汰号码都在0-9范围内
- 胜数> = 3
- 胜> =淘汰赛
如果不满足以上任何条件,则游戏必须拒绝密码。 但是,在最终检查的实现中存在一个错误(即,当检查由BCD编码的数字时)。游戏不检查胜利> =淘汰赛,而是允许较高的获胜次数为0,较低的获胜次数> = 3,并且较高的淘汰赛次数小于较低的次数的情况。获胜。 例如,游戏将接受具有3胜0负和23个淘汰赛的记录(证明密码099 837 5823),尽管该记录应该被拒绝(因为如果您仅在3场比赛中获胜,就不可能通过淘汰赛赢得23场战斗)。
结论
这种编码方案的特定细节是Punch-Out特有的,但是获得游戏状态重要位,将其转换为可能的状态以转换成混淆初始状态,然后使用它们生成一定数量的字符以作为密码向玩家展示的一般想法是相当普遍的方法。 您可以使用校验和,以便意外更改密码(例如,如果玩家犯了错误)通常会导致拒绝密码,而不是创建具有随机游戏状态的另一个密码。
第2部分。
迈克·泰森(Mike Tyson)的《 Punch-Out》中的每个战士! 由一个或多个解释的字节码脚本控制。 玩家的角色Little Mac运行一个简单的脚本,其中包含玩家可用的每个动作的逻辑(躲避,阻挡,重击等)。对手的角色由3个独立的脚本级别控制,这些脚本共同创建了角色的行为。
比赛脚本
最高级别的敌人脚本会在所有三轮战斗中执行,并控制对手行为中最雄心勃勃的变化。 我将此脚本称为“匹配脚本”。 他的主要任务是选择敌人在战斗中对各种事件的反应行为。 例如,在击倒对手后上升或玩家精疲力尽后,将立即触发某种行为。 这些行为将写入表中,并由游戏引擎响应相应的事件进行调用。 比赛脚本还会为与战斗的复杂性有关的配置选项设置初始值(例如,错过击中对手后仍保持脆弱状态的时间。)最后,比赛脚本会在战斗中开始等待某些临时标记来更改先前设置的值。
行为脚本
较低级别的对手脚本是“行为脚本”。 该级别负责对手在当前行为框架内(由比赛脚本指定)必须执行的某些击打和进攻的顺序。行为脚本执行以下命令:“应用右戳戳,暂停28帧,随机应用左或右上勾,重复全部是5次。” 该脚本还具有从游戏引擎读取和写入内存中任何地址的命令,因此行为可能非常动态。
动画脚本
最低级别的对手脚本是“动画脚本”。 这样的脚本会作为行为的一部分执行每次击中,阻挡或特殊攻击的详细信息(由行为脚本定义。)在此级别,例如“将精灵23分配给敌人动画的当前帧,然后在第二帧中将其上下左右移动1个像素,在接下来的10帧中,将动画帧更改为精灵24,播放7“的音效。 除了动画命令之外,动画脚本还执行与敌方动作密切相关的游戏状态的各种变化序列。 例如,在特殊攻击的长动画中,动画脚本可以插入命令,使敌人很容易在很短的时间内被击倒。 与行为脚本一样,动画脚本可以在游戏引擎中读取和写入任意内存地址,以实现更多的动态效果。
剧本Little Mac
Little Mac播放器角色执行的脚本与敌人的动画脚本最相似。 它将更改反射动画的当前帧,并使播放器在屏幕上移动。 与动画脚本一样,Little Mac脚本执行某些游戏事件的序列,例如,Mac应该在哪个特定时间点击中敌人,或者何时执行阻止或躲避。 Little Mac脚本控制玩家的输入,类似于行为脚本控制敌人动画脚本的方式。
这四个脚本中的每个脚本均由其自己的解释器处理。 尽管它们中的许多具有相同的功能,例如基本控制控制和对内存的直接访问,但是每个系统都实现自己的版本,并且不与其他系统共享公共代码(或操作码空间)。 这允许每种类型的脚本非常具体,并有效地使用少量目标命令。 脚本数据约占游戏卡带非图形数据的22%(游戏引擎本身的机器代码仅占17%),因此,脚本外观必须紧凑。
第3部分。淘汰比赛脚本
比赛的脚本在最高级别控制对手的行为。 他反复执行的主要操作是等待回合中的某个时间,然后在那一刻更改对手的配置数据。 该视频显示了与秃头斗牛的第一次战斗的第一轮,以及控制其整体行为的比赛脚本。
匹配脚本可以执行三个主要操作。 首先是等待,直到循环计时器达到某个值。 第二个问题是询问对手的当前行为是否已更改。 行为记录在内存中的战斗配置表中,然后在不同时间由比赛脚本和游戏引擎本身调用。 该表具有匹配脚本使用的两个行为段。 我称它们为“基本”行为和“特殊”行为。 例如,特殊行为是秃头公牛的对手的公牛冲锋或活塞本田的对手的本田拉什,主要行为是对手在其余时间中通常发出的打击。 在比赛期间,比赛脚本可以直接更改用于实现这些类型行为的特定行为脚本,因此,战斗机能够从一种主要行为开始,然后切换到另一种主要行为(如视频中所注意到的,当计时器为0时,秃头公牛会这样做) :20.)
比赛脚本执行的更改行为的功能是,可以将其替换为游戏引擎请求的行为更改。 当Mac失去全部精力并变得疲倦时,以及当对手在击倒后站起来时,游戏引擎使用四个行为部分来请求新的行为。 如果比赛脚本满足了更改行为的请求,但是游戏引擎的这四个事件之一发生在请求被处理之前(直到对手进入等待状态才可以处理请求),然后游戏引擎设置所需的行为并且对比赛脚本的请求被拒绝。 一些战斗机,例如秃头斗牛犬,在短时间内多次要求特殊行为。 看来这仅是为了减少这些请求中的任何一个被意外丢弃的可能性。
匹配脚本的第三个主要操作是内存修补。 大多数内存补丁会影响战斗配置表,并记录行为脚本。 除了行为集以外,该表还包含与战斗复杂性有关的数据。 例如,当视频中的计时器达到0:30时,Bald Bull更改其安全设置。 这导致玩家无法再通过向上按压然后对身体进行打击来欺骗他。 此外,比赛脚本具有修补任意内存地址的功能,但是此功能仅使用一次-在第二轮比赛开始时与Mike Tyson一起使用,以便玩家在击中星标时第一次获得星标,处于待机模式。
第4部分。冲出行为脚本
现在,我们看一下与执行行为直接相关的行为脚本。
该视频说明了活塞本田1号竞争对手行为脚本在英语球队中的样子。
动画指令
行为脚本负责按顺序启动动画,就像匹配脚本负责启动动画一样。
anim
命令播放特定的动画,
anim_rnd
命令执行从8个选项的列表中随机选择的动画。 在上面的视频中,从选项列表中随机选择时,所选的选项会暂时以红色突出显示。 当本田活塞应用前两个刺刺时,每个
anim
使用
anim
。 之后,他使用
anim_rnd
从包含6个钩子动画和2个空动画的集合中随机选择。 结果,他在75%的时间上钩,而在25%的时间上什么也不做。
从脚本的角度来看,动画的行为是同步播放的,因为当动画系统不处于空闲模式时,脚本解释器会暂停。
执行控制命令
有几个命令可以修改行为脚本本身的执行。
pause
命令可以暂停脚本执行一定数量的帧,或者暂停从2个选项的列表中随机选择的帧数。
有各种分支命令,在某些情况下,可以有选择地切换到行为脚本的不同部分。
branch_rnd
具有给定的概率,即每次执行分支时都会发生分支。 概率分支的一种特殊情况是
branch_always
命令,该命令具有100%的分支概率。
行为脚本解释器内置了一个简单的循环机制。
set_loop_count
命令设置循环计数器的当前值。 每次
branch_while_loop
命令时
branch_while_loop
它都会将循环计数器的值减一,并且仅在计数器值大于零时才执行分支。
分支的最后一种类型检查内存的内容,以决定是否分支。 本田活塞使用此
branch_mem_test
命令检查他在特定行为中的最后击中是否成功。 如果命中命中目标,那么它将分支到下一个命中。 如果命中不成功,则仅当累积5次失败命中时,才使用
branch_while_loop
命令继续命中。
行为命令
行为脚本可以通过两个命令来控制行为系统本身。
begin_behavior_main
命令用于结束当前的可执行行为并启动主要行为。 这与行为脚本中的分支不同,因为在匹配过程中,匹配脚本可以更改被视为当前“主要”行为的脚本部分(请参阅关于匹配脚本的上一部分)。
另一个与行为相关的命令是
enable_behavior_change
。 启动新行为时,它将以锁定状态开始,而所有其他更改行为的请求均被阻止。 使用
enable_behavior_change
命令
enable_behavior_change
脚本会发出信号,表明已准备好允许其他行为。 例如,在“本田活塞”特殊行为中,永远不会执行
enable_behavior_change
命令,因此,如果Mac在这段时间内累了,则特殊行为会继续。 但是,击倒事件会绕过该系统,因此,如果在本田活塞的特殊行为中,主要角色被击倒,则在任何情况下,行为都会发生变化。