我如何教AI玩NES玩俄罗斯方块。 第1部分:游戏代码分析

在本文中,我将探索任天堂Tetris的看似简单的机制,在第二部分中,我将解释如何创建利用这些机制的AI。


自己尝试


关于项目


对于那些缺乏掌握Nintendo Tetris所需的毅力,耐心和时间的人,我创建了可以独立玩的AI。 您最终可以达到30级,甚至更高。 您将看到如何获得最大的点数,并观察行计数器,级别和统计信息的无尽变化。 您将学习在无法攀登的水平上会出现什么颜色。 看看你能走多远。

要求条件


要运行AI,您需要一个通用的NES / Famicom FCEUX仿真器 。 人工智能是为FCEUX 2.2.2开发的,是撰写本文时的最新版本的仿真器。

您还将需要Nintendo Tetris ROM文件(美国版本)。 尝试在Google上进行搜索

资料下载


从此源zip文件解压缩lua/NintendoTetrisAI.lua

发射


启动FCEUX。 从菜单中,选择文件| 打开ROM ...在“打开文件”对话框中,选择Nintendo Tetris ROM文件,然后单击“打开”。 游戏将开始。

从菜单中,选择文件| Lua | 新建Lua脚本窗口...在Lua脚本窗口中,输入NintendoTetrisAI.lua的路径,或单击“浏览”按钮进行查找。 之后,单击运行。

Lua上的脚本会将您重定向到菜单的第一个屏幕。 保留游戏类型为A-Type,然后可以选择任何音乐。 在速度较慢的计算机上,音乐会非常不平稳地播放,因此您应该将其关闭。 按开始(Enter)进入下一个菜单屏幕。 在第二个菜单中,您可以使用箭头键更改开始级别。 单击开始以开始游戏。 AI将在这里控制。

如果在第二个菜单屏幕上选择一个级别后,按住游戏板按钮A(您可以在Config | Input ...菜单中更改键盘布局)并按Start,则初始级别将比所选值大10。 最高进入级别为第十九。

构型


为了使游戏运行更快,请在文本编辑器中打开Lua脚本。 在文件的开头,找到以下行。

PLAY_FAST = false

如下所示,将false替换为true

PLAY_FAST = true

保存文件。 然后在Lua脚本窗口中单击重新启动按钮。

任天堂Tetris Mechanics


Tetrimino的描述


每个tetrimino数字对应一个与其形状相似的单字母名称。


Nintendo Tetris的设计师任意设置上面显示的Tetrimino顺序。 这些数字以它们在屏幕上出现的方向显示,电路会创建几乎对称的图像(也许这就是选择此顺序的原因)。 序列索引为每个tetrimino提供唯一的数字ID。 序列和类型标识符在编程级别很重要。 此外,它们按照统计字段中显示的数字顺序进行显示(请参见下文)。


Nintendo Tetris tetrimino中使用的19种方向编码在位于NES控制台存储器$8A9C的表中。 每个图表示为12个字节的序列,该序列可分为三倍(Y, tile, X) ,以描述图中的每个正方形。 高于$7F的坐标的上述十六进制值表示负整数( $FF= −1$FE = −2 )。

; Y0 T0 X0 Y1 T1 X1 Y2 T2 X2 Y3 T3 X3

8A9C: 00 7B FF 00 7B 00 00 7B 01 FF 7B 00 ; 00: T up
8AA8: FF 7B 00 00 7B 00 00 7B 01 01 7B 00 ; 01: T right
8AB4: 00 7B FF 00 7B 00 00 7B 01 01 7B 00 ; 02: T down (spawn)
8AC0: FF 7B 00 00 7B FF 00 7B 00 01 7B 00 ; 03: T left

8ACC: FF 7D 00 00 7D 00 01 7D FF 01 7D 00 ; 04: J left
8AD8: FF 7D FF 00 7D FF 00 7D 00 00 7D 01 ; 05: J up
8AE4: FF 7D 00 FF 7D 01 00 7D 00 01 7D 00 ; 06: J right
8AF0: 00 7D FF 00 7D 00 00 7D 01 01 7D 01 ; 07: J down (spawn)

8AFC: 00 7C FF 00 7C 00 01 7C 00 01 7C 01 ; 08: Z horizontal (spawn)
8B08: FF 7C 01 00 7C 00 00 7C 01 01 7C 00 ; 09: Z vertical

8B14: 00 7B FF 00 7B 00 01 7B FF 01 7B 00 ; 0A: O (spawn)

8B20: 00 7D 00 00 7D 01 01 7D FF 01 7D 00 ; 0B: S horizontal (spawn)
8B2C: FF 7D 00 00 7D 00 00 7D 01 01 7D 01 ; 0C: S vertical

8B38: FF 7C 00 00 7C 00 01 7C 00 01 7C 01 ; 0D: L right
8B44: 00 7C FF 00 7C 00 00 7C 01 01 7C FF ; 0E: L down (spawn)
8B50: FF 7C FF FF 7C 00 00 7C 00 01 7C 00 ; 0F: L left
8B5C: FF 7C 01 00 7C FF 00 7C 00 00 7C 01 ; 10: L up

8B68: FE 7B 00 FF 7B 00 00 7B 00 01 7B 00 ; 11: I vertical
8B74: 00 7B FE 00 7B FF 00 7B 00 00 7B 01 ; 12: I horizontal (spawn)

8B80: 00 FF 00 00 FF 00 00 FF 00 00 FF 00 ; 13: Unused


在表格的底部,有一个未使用的记录,可能使您有机会添加另一个方向。 但是,在代码的各个部分中, $13表示未向活动Tetrimino的方向标识符分配值。

为了便于阅读,下面显示了十进制正方形的坐标。

-- { { X0, Y0 }, { X1, Y1 }, { X2, Y2 }, { X3, Y3 }, },

{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, -1 }, }, -- 00: T up
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 01: T right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 02: T down (spawn)
{ { 0, -1 }, { -1, 0 }, { 0, 0 }, { 0, 1 }, }, -- 03: T left

{ { 0, -1 }, { 0, 0 }, { -1, 1 }, { 0, 1 }, }, -- 04: J left
{ { -1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 05: J up
{ { 0, -1 }, { 1, -1 }, { 0, 0 }, { 0, 1 }, }, -- 06: J right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 1, 1 }, }, -- 07: J down (spawn)

{ { -1, 0 }, { 0, 0 }, { 0, 1 }, { 1, 1 }, }, -- 08: Z horizontal (spawn)
{ { 1, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 09: Z vertical

{ { -1, 0 }, { 0, 0 }, { -1, 1 }, { 0, 1 }, }, -- 0A: O (spawn)

{ { 0, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, }, -- 0B: S horizontal (spawn)
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 }, }, -- 0C: S vertical

{ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 1, 1 }, }, -- 0D: L right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { -1, 1 }, }, -- 0E: L down (spawn)
{ { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 }, }, -- 0F: L left
{ { 1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 10: L up

{ { 0, -2 }, { 0, -1 }, { 0, 0 }, { 0, 1 }, }, -- 11: I vertical
{ { -2, 0 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 12: I horizontal (spawn)


所有方向都放置在5×5矩阵中。


在上图中,白色正方形表示矩阵的中心,即图旋转的参考点。

方向表如下图所示。


方向标识符(表索引)以十六进制显示在每个矩阵的右上角。 左上角显示了为该项目发明的助记符。 urdlhv是“上,右,下,左,水平和垂直”的缩写。 例如,表示Jd的方向比表示$07更为容易。

在创建过程中,包含图形方向的矩阵将用白色框标记。

Tetrimino I,S和Z可以有4个不同的方向,但Nintendo Tetris的创建者决定将自己限制为两个。 此外, ZvSv并不是彼此理想的镜像。 两者都是通过逆时针旋转而产生的,这会导致不平衡。

方向表还包含每个方向图中每个正方形的图块值。 但是,通过仔细研究,可以清楚地看到一种类型的Tetrimino的值始终是相同的。

ŤĴžØ小号大号
7B7D7C7B7D7C7B

瓦片值是以下所示模式的(伪彩色)表的索引。


$7B$7C$7D贴位于单词“ STATISTICS”的“ ATIS”正下方。 这是制造四合胺的三种正方形。

出于好奇,我会说在B型模式的末尾使用鸵鸟和企鹅。 在“结尾”部分中详细讨论了该主题。

以下是用$29替换$7B后修改ROM​​的结果。 对于所有方向T,心是图案表中P符号下方的图块。


即使将修改的T锁定在适当的位置,心形瓷砖仍保留在运动场上。 如以下“创建Tetrimino”部分所述,这意味着游戏环境存储了所玩Tetrimino的图块索引的实际值。

游戏程序员可以为每个人物使用4个单独的图块,而不仅仅是一种不变的正方形。 这是一个有用的功能,可用于修改游戏的外观。 模式表有很多空白空间,可以放置新的图块,从而使每个tetrimino都拥有独特的外观。

正方形的坐标非常易于操作。 例如,下面显示了定向表中前四个三元组的修改版本。

8A9C: FE 7B FE FE 7B 02 02 7B FE 02 7B 02 ; 00: T up

此更改类似于以下内容:

{ { -2, -2 }, { 2, -2 }, { -2, 2 }, { 2, 2 }, }, -- 00: T up

结果是分裂的tritrimino。


移动分开的四聚体时,其正方形不能超出运动场的边界,也不能穿过先前锁定在适当位置的图形。 另外,如果游戏导致广场掉落在比赛场地的边界之外,或者导致广场与已经位于广场上的广场重叠,则游戏禁止朝此方向旋转。

当支持任何正方形时,已分裂的tetrimino将锁定在适当的位置。 如果人物被遮挡,则悬在空中的方块将继续悬挂。

游戏可以像处理任何正常人物一样处理分裂的四聚体。 这使我们理解没有额外的表格来存储图形的元数据。 例如,可能存在一个表,该表存储每个方向的边界框的大小,以检查是否与运动场的周边发生碰撞。 但是不使用这样的表。 取而代之的是,游戏只是在操纵形状之前检查所有四个正方形。

另外,正方形的坐标可以是任何值。 它们不限于间隔[−2, 2] 。 当然,大大超过此间隔的值将给我们提供不适合比赛场地的不适用数字。 更重要的是,如“游戏状态和渲染模式”部分所述,当人物锁定在适当位置时,清除填充线的机制仅扫描从人物中心正方形从-2到1的行的位移; y坐标超出此间隔的正方形将无法识别。

Tetrimino旋转


在方向表的图形说明中,旋转包括从矩阵移动到左侧或右侧的矩阵之一,并在必要时转移序列。 此概念编码在$88EE的表中。

; CCW CW
88EE: 03 01 ; Tl Tr
88F0: 00 02 ; Tu Td
88F2: 01 03 ; Tr Tl
88F4: 02 00 ; Td Tu
88F6: 07 05 ; Jd Ju
88F8: 04 06 ; Jl Jr
88FA: 05 07 ; Ju Jd
88FC: 06 04 ; Jr Jl
88FE: 09 09 ; Zv Zv
8900: 08 08 ; Zh Zh
8902: 0A 0A ; OO
8904: 0C 0C ; Sv Sv
8906: 0B 0B ; Sh Sh
8908: 10 0E ; Lu Ld
890A: 0D 0F ; Lr Ll
890C: 0E 10 ; Ld Lu
890E: 0F 0D ; Ll Lr
8910: 12 12 ; Ih Ih
8912: 11 11 ; Iv Iv


为了更清楚一点,我们将从该表的每一列移到下表的行。
TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
逆时针方向TlTuTrTdJdJlJuJrZvZhOSvShLuLrLdLlIhIv
顺时针方向TrTdTlTuJuJrJdJlZvZhOSvShLdLlLuLrIhIv

上面标题中的助记符可以解释为序列索引或分发键。 例如,逆时针转动Tu给我们Tl ,顺时针转动Tu给我们Tr

旋转表对方向ID的链式链接序列进行编码; 因此,我们可以修改记录,以便旋转将一种类型的四合胺转变为另一种。 该技术可以潜在地用于利用定向表中未使用的行。

旋转表的前面是访问它的代码。

88AB: LDA $0042
88AD: STA $00AE ; originalOrientationID = orientationID;

88AF: CLC
88B0: LDA $0042
88B2: ASL
88B3: TAX ; index = 2 * orientationID;

88B4: LDA $00B5
88B6: AND #$80 ; if (not just pressed button A) {
88B8: CMP #$80 ; goto aNotPressed;
88BA: BNE $88CF ; }

88BC: INX
88BD: LDA $88EE,X
88C0: STA $0042 ; orientationID = rotationTable[index + 1];

88C2: JSR $948B ; if (new orientation not valid) {
88C5: BNE $88E9 ; goto restoreOrientationID;
; }

88C7: LDA #$05
88C9: STA $06F1 ; play rotation sound effect;
88CC: JMP $88ED ; return;

aNotPressed:

88CF: LDA $00B5
88D1: AND #$40 ; if (not just pressed button B) {
88D3: CMP #$40 ; return;
88D5: BNE $88ED ; }

88D7: LDA $88EE,X
88DA: STA $0042 ; orientationID = rotationTable[index];

88DC: JSR $948B ; if (new orientation not valid) {
88DF: BNE $88E9 ; goto restoreOrientationID;
; }

88E1: LDA #$05
88E3: STA $06F1 ; play rotation sound effect;
88E6: JMP $88ED ; return;

restoreOrientationID:

88E9: LDA $00AE
88EB: STA $0042 ; orientationID = originalOrientationID;

88ED: RTS ; return;


对于逆时针旋转,通过将方向ID加倍来减去旋转台的索引。 通过将其加1,我们得到顺时针旋转索引。

当前tetrimino的xy坐标和方向ID分别存储在地址$0040$0041$0042

该代码使用一个临时变量来备份方向ID。 后来,在更改方向后,代码验证所有四个正方形都在运动场的边界内,并且没有一个与已位于正方形上的正方形重叠(验证代码位于$948B ,在上面显示的代码片段下)。 如果新方向不正确,则将恢复原始方向,不允许玩家旋转角色。

用十字计数,NES控制器具有八个按钮,其状态由地址位$00B6

76543210
请选择开始往下在左边在右边

例如,当玩家$00B6 A和Left时, $00B6将包含值$81

另一方面, $00B5报告何时按下按钮。 $00B5位仅在游戏循环的一次迭代(1个渲染帧)期间为真。 该代码使用$00B5来响应按下A和B。它们中的每一个都需要释放后才能再次使用。

$00B5$00B6$00F5$00F6 。 以下各节中的代码可交替使用这些地址。

创建Tetrimino


Nintendo Tetris比赛场地由22行10列的矩阵组成,因此播放器不显示前两行。


如下代码所示,创建Tetrimino图形时,它始终位于运动场的坐标(5, 0) 5,0 (5, 0)

98BA: LDA #$00
98BC: STA $00A4
98BE: STA $0045
98C0: STA $0041 ; Tetrimino Y = 0
98C2: LDA #$01
98C4: STA $0048
98C6: LDA #$05
98C8: STA $0040 ; Tetrimino X = 5


下面是此点上叠加的5×5矩阵。


没有一个创建矩阵在起点上方具有正方形。 也就是说,在创建四联体时,其所有四个方块立即对玩家可见。 但是,如果玩家在没有时间下落之前快速旋转该棋子,则该棋子的一部分将暂时隐藏在运动场的前两行中。

通常我们认为游戏在堆到达顶部时结束。 但是实际上,这并非完全正确。 当不再可能创建下一个游戏时,游戏结束。 即,在图形出现之前,与所创建的四聚体的正方形的位置相对应的运动场的所有四个单元应该是自由的。 可以将人物锁定在适当的位置,以使其正方形的一部分以负数编号的线出现,并且游戏不会结束。 但是,在Nintendo Tetris中,负线是仅与活动Tetrimino相关的抽象。 将该图阻止后(变为说谎),只有从零开始的行中的正方形才会被写入该字段。 从概念上讲,事实证明,阻塞后会自动清除编号为负的行。 但实际上,游戏根本不存储这些数据,从而切断了数字的上部。

运动场的可见区域20×10逐行存储在$0400 ,每个字节包含背景图块的值。 空单元格由$EF拼贴(实心黑色正方形)表示。

创建形状时,将使用三个查找表。 如果存在任意方向ID,则在创建相应类型的Tetrimino时, $9956的表将为我们提供方向ID。

9956: 02 02 02 02 ; Td
995A: 07 07 07 07 ; Jd
995E: 08 08 ; Zh
9960: 0A ; O
9961: 0B 0B ; Sh
9963: 0E 0E 0E 0E ; Ld
9967: 12 12 ; Ih


将其显示在表格中比较容易。

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TdTdTdTdJdJdJdJdZhZhOShShLdLdLdLdIhIh

例如,J的所有方向都附加到Jd

$993B处的表包含给定方向ID的Tetrimino类型。

993B: 00 00 00 00 ; T
993F: 01 01 01 01 ; J
9943: 02 02 ; Z
9945: 03 ; O
9946: 04 04 ; S
9948: 05 05 05 05 ; L
994C: 06 06 ; I


为了清楚起见,我将以表格形式显示所有内容。

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TTTTJJJJZZOSSLLLLII

我们将在下一部分中查看第三个搜索表。

Tetrimino选择


Nintendo Tetris在Fibonacci配置中使用16位线性反馈移位寄存器(LFSR)作为其伪随机数生成器(PRNG)。 16位值作为big-endian存储在地址$0017 $0018$8988的任意数量用作种子。

80BC: LDX #$89
80BE: STX $0017
80C0: DEX
80C1: STX $0018


每个随后的伪随机数生成如下:将该值视为17位数字,并通过对位1和9进行XOR获得最高有效位。然后,该值向右移,丢弃最低有效位。


此过程发生在$AB47

AB47: LDA $00,X
AB49: AND #$02
AB4B: STA $0000 ; extract bit 1

AB4D: LDA $01,X
AB4F: AND #$02 ; extract bit 9

AB51: EOR $0000
AB53: CLC
AB54: BEQ $AB57
AB56: SEC ; XOR bits 1 and 9 together

AB57: ROR $00,X
AB59: INX
AB5A: DEY ; right shift
AB5B: BNE $AB57 ; shifting in the XORed value

AB5D: RTS ; return


有趣的是,可以设置上述子例程的参数,以便调用函数可以指定移位寄存器的宽度以及可以在内存中找到它的地址。 但是,到处都使用相同的参数,因此我们可以假设开发人员在某处借用了此代码。

对于那些想进一步修改算法的人,我用Java编写了它。

 int generateNextPseudorandomNumber(int value) { int bit1 = (value >> 1) & 1; int bit9 = (value >> 9) & 1; int leftmostBit = bit1 ^ bit9; return (leftmostBit << 15) | (value >> 1); } 

而且所有这些代码都可以压缩到一行。

 int generateNextPseudorandomNumber(int value) { return ((((value >> 9) & 1) ^ ((value >> 1) & 1)) << 15) | (value >> 1); } 

从原始种子的每个循环开始,此PRNG连续确定地生成32,767个唯一值。 这是可以容纳在寄存器中的可能数字的一半以下,并且该集合中的任何值都可以用作种子。 集合外的许多值会创建一条链,最终导致集合中的数字。 但是,某些初始数字会导致无限的零序列。

为了粗略评估PRNG的性能,我用RANDOM.ORG的句子生成了它创建的值的图形表示。


创建图像时,PRNG被用作伪随机数生成器,而不是16位整数。 每个像素根据位0的值进行着色。图像的大小为128×256,即,它覆盖了整个序列。

除了上下左右几乎看不见的条纹外,它看起来是随机的。 没有明显的样式出现。

启动后,PRNG会不断移位寄存器,至少每帧工作一次。 这不仅发生在初始屏幕和菜单屏幕上,而且还发生在Tetrimino处于创建形状的操作之间时。 也就是说,接下来出现的图形取决于玩家放置图形所用的帧数。 实际上,游戏依赖于与之互动的人的行为的随机性。

在创建图形期间,代码在地址$9907执行,该地址选择新图形的类型。

9907: INC $001A ; spawnCount++;

9909: LDA $0017 ; index = high byte of randomValue;

990B: CLC
990C: ADC $001A ; index += spawnCount;

990E: AND #$07 ; index &= 7;

9910: CMP #$07 ; if (index == 7) {
9912: BEQ $991C ; goto invalidIndex;
; }

9914: TAX
9915: LDA $994E,X ; newSpawnID = spawnTable[index];

9918: CMP $0019 ; if (newSpawnID != spawnID) {
991A: BNE $9938 ; goto useNewSpawnID;
; }

invalidIndex:

991C: LDX #$17
991E: LDY #$02
9920: JSR $AB47 ; randomValue = generateNextPseudorandomNumber(randomValue);

9923: LDA $0017 ; index = high byte of randomValue;

9925: AND #$07 ; index &= 7;

9927: CLC
9928: ADC $0019 ; index += spawnID;

992A: CMP #$07
992C: BCC $9934
992E: SEC
992F: SBC #$07
9931: JMP $992A ; index %= 7;

9934: TAX
9935: LDA $994E,X ; newSpawnID = spawnTable[index];

useNewSpawnID:

9938: STA $0019 ; spawnID = newSpawnID;

993A: RTS ; return;


在地址$001A存储着上电时产生的数字的计数器。 计数器的递增由子例程的第一行执行,并且由于它是单字节计数器,因此,每256个计数器之后,它将再次返回零。 由于没有在游戏之间重置计数器,因此先前游戏的历史记录会影响人物选择过程。 这是游戏使用玩家作为随机性来源的另一种方式。

该例程将伪随机数的最高有效字节( $0017 )转换为tetrimino类型,并将其用作位于$994E的表的索引,以将类型转换为形状创建方向ID。

994E: 02 ; Td
994F: 07 ; Jd
9950: 08 ; Zh
9951: 0A ; O
9952: 0B ; Sh
9953: 0E ; Ld
9954: 12 ; Ih


在转换的第一阶段,将创建的图形的计数器添加到高字节。 然后,应用掩码以仅保存低3位。 如果结果不是7,则这是正确的tetrimino类型,如果它与先前选择的图形不同,则该数字将用作创建图形表中的索引。 否则,将生成下一个伪随机数,并应用掩码以获得高字节的低3位,然后添加前一个形状创建方向ID。 最后,执行模运算以获取正确类型的Tetrimino,并将其用作形状创建表中的索引。

由于处理器不支持用余数进行除法,因此通过重复减去7直到结果小于7来模拟此运算符。除数除法将应用于应用了掩码的高位字节的总和以及先前的形状创建方向ID。 该和的最大值为25。也就是说,将其减少到4的余数,仅需要3次迭代。

在每个游戏开始时,形状创建方向ID( $0019 )都以Tu$00 )的值初始化。 在第一个形状创建期间,可能会以$9928$9928使用此值。

当使用先前的方向ID而不是先前的类型创建图形时,Tetrimino会增加变形,因为方向ID的值分布不均匀。 如下表所示:

$ 00$02$07$08$0A$0B$0E$12
0201个3404
1个31个2451个5
24235626
35346030
464501个41个
50561个252
61个602363
7201个3404

每个单元格包含tetrimino类型,该类型是通过将创建的图形的方向ID(列)与3位值(行)相加,然后将除以7的余数应用于求和而得出的,每行都包含重复项,因为$07$0E平均除乘以7,而$0B$12具有共同的余额。 第0行和第7行相同,因为它们之间的距离为7。

有56种可能的输入组合,如果生成的tetrimino类型均匀分布,那么我们可以期望在上表中,每种类型都应该出现8次。 但是,如下所示,情况并非如此。

型式频次
Ť9
Ĵ8
ž8
Ø8
小号9
大号7
7

T和S出现的频率更高,L和I出现的频率更低。 但是,不会在每次调用子例程时执行使用方向ID的倾斜代码。

假设PRNG确实创建了一系列均匀分布的统计独立值。 考虑到游戏如何尝试从玩家的行为中获得正确的随机性,这实际上是一个合理的假设。 将创建的图形数量添加到地址$990C不会影响分配,因为调用之间的数量均匀增加。 在$990E使用位掩码类似于对除数除以8,这也不会影响分配。 因此,在所有情况的1/8中, $9910处的检查将进入invalidIndex 。 在新选择的图形与上一个图形进行比较的地址$9918处进行校验时,命中的概率为7/8,重合的概率为1/7。这意味着有更多的机会7/8 × 1/7 = 1/8进入invalidIndex通常,使用偏斜代码的概率为25%,使用均匀选择Tetrimino的代码的概率为75%。

在一组224个创建的四聚体中,每种类型的数学期望为32个实例。但是实际上,代码创建了以下分布:

型式频次
Ť33
Ĵ32
ž32
Ø32
小号33
大号31
31

也就是说,清除90条线并达到第9级,玩家将获得比统计上预期多的T和S,以及少的L和I。

选择Tetrimino具有以下概率:

型式机率
Ť14.73%
Ĵ14.29%
ž14.29%
Ø14.29%
小号14.73%
大号13.84%
13.84%

似乎在声明“长棍”在需要时我从未出现过的说法中,有一部分道理(至少对于Nintendo Tetris而言)。

Tetrimino移位


Nintendo Tetris使用延迟自动切换(DAS)。单击“左”或“右”可立即将tetrimino水平移动一个单元格。按住这些方向按钮之一可使游戏每6帧自动移动人物,初始延迟为16帧。

这种水平移动由地址处的代码控制$89AE与旋转代码一样,如果新位置不正确,此处将使用临时变量来备份坐标请注意,在播放器按下时,该支票可防止您移动棋子。

89AE: LDA $0040
89B0: STA $00AE ; originalX = tetriminoX;

89B2: LDA $00B6 ; if (pressing down) {
89B4: AND #$04 ; return;
89B6: BNE $8A09 ; }

89B8: LDA $00B5 ; if (just pressed left/right) {
89BA: AND #$03 ; goto resetAutorepeatX;
89BC: BNE $89D3 ; }

89BE: LDA $00B6 ; if (not pressing left/right) {
89C0: AND #$03 ; return;
89C2: BEQ $8A09 ; }

89C4: INC $0046 ; autorepeatX++;
89C6: LDA $0046 ; if (autorepeatX < 16) {
89C8: CMP #$10 ; return;
89CA: BMI $8A09 ; }

89CC: LDA #$0A
89CE: STA $0046 ; autorepeatX = 10;
89D0: JMP $89D7 ; goto buttonHeldDown;

resetAutorepeatX:

89D3: LDA #$00
89D5: STA $0046 ; autorepeatX = 0;

buttonHeldDown:

89D7: LDA $00B6 ; if (not pressing right) {
89D9: AND #$01 ; goto notPressingRight;
89DB: BEQ $89EC ; }

89DD: INC $0040 ; tetriminoX++;
89DF: JSR $948B ; if (new position not valid) {
89E2: BNE $8A01 ; goto restoreX;
; }

89E4: LDA #$03
89E6: STA $06F1 ; play shift sound effect;
89E9: JMP $8A09 ; return;

notPressingRight:

89EC: LDA $00B6 ; if (not pressing left) {
89EE: AND #$02 ; return;
89F0: BEQ $8A09 ; }

89F2: DEC $0040 ; tetriminoX--;
89F4: JSR $948B ; if (new position not valid) {
89F7: BNE $8A01 ; goto restoreX;
; }

89F9: LDA #$03
89FB: STA $06F1 ; play shift sound effect;
89FE: JMP $8A09 ; return;

restoreX:

8A01: LDA $00AE
8A03: STA $0040 ; tetriminoX = originalX;

8A05: LDA #$10
8A07: STA $0046 ; autorepeatX = 16;

8A09: RTS ; return;


x



投掷Tetrimino


Tetrimino自动下降的速度是等级数的函数。速度编码为位于的表格中用于下降的渲染帧数$898E由于NES的运行速度为60.0988帧/秒,因此您可以计算下降之间的时间间隔和速度。

等级下降架周期(秒/下降)速度(格/秒)
048.7991.25
1个43.7151.40
238.6321.58
333.5491.82
428.4662.15
523.3832.61
618岁.3003.34
713.2164.62
88.1337.51
96.10010.02
10-125.08312.02
13-154.06715.05
16-183.05003/20
19–282.03330.05
29岁以上1个.01760.10

该表共有30个条目。在级别29之后,下降帧的值始终为1。下降帧

的整数不是描述速度的非常详细的方法。如下图所示,每个级别的速度都呈指数增长。实际上,第29级的速度是第28级的两倍。


每次下降1帧时,播放器将人物定位的时间不超过1/3秒,此后它将开始移动。在这种下降速度下,DAS不允许角色在锁定到位之前到达游戏场地的边缘,这对大多数人来说意味着游戏的快速结束。但是,一些玩家,尤其是Thor Akerlund,通过快速按下十字键(D-pad击败了DAS 。在上面显示的移位代码中,可以看出,在水平方向按钮通过框架释放的同时,可以将Tetrimino以一半的频率移动到29级及以上的水平。这是理论上的最大值,但是拇指在3.75拍/秒以上的任何振动都可以抵消16帧的原始延迟。

如果自动下降和由玩家控制的下降(通过按“向下”键)重合并且发生在一帧中,则效果不累加。这些事件中的任何一个或两个都导致形状恰好在此帧中沿一个单元下降。

触发控制逻辑位于$8914下降架表在标签下方。如上所述,在级别29和更高级别上,速度始终等于1个快门/帧。(地址)到达时开始下降。递增是在此代码段之外的地址执行的。在自动或受控下降过程中,它将重置为0。变量)初始化为值(在地址

8914: LDA $004E ; if (autorepeatY > 0) {
8916: BPL $8922 ; goto autorepeating;
; } else if (autorepeatY == 0) {
; goto playing;
; }

; game just started
; initial Tetrimino hanging at spawn point

8918: LDA $00B5 ; if (not just pressed down) {
891A: AND #$04 ; goto incrementAutorepeatY;
891C: BEQ $8989 ; }

; player just pressed down ending startup delay

891E: LDA #$00
8920: STA $004E ; autorepeatY = 0;
8922: BNE $8939

playing:

8924: LDA $00B6 ; if (left or right pressed) {
8926: AND #$03 ; goto lookupDropSpeed;
8928: BNE $8973 ; }

; left/right not pressed

892A: LDA $00B5
892C: AND #$0F ; if (not just pressed only down) {
892E: CMP #$04 ; goto lookupDropSpeed;
8930: BNE $8973 ; }

; player exclusively just presssed down

8932: LDA #$01
8934: STA $004E ; autorepeatY = 1;

8936: JMP $8973 ; goto lookupDropSpeed;

autorepeating:

8939: LDA $00B6
893B: AND #$0F ; if (down pressed and not left/right) {
893D: CMP #$04 ; goto downPressed;
893F: BEQ $894A ; }

; down released

8941: LDA #$00
8943: STA $004E ; autorepeatY = 0
8945: STA $004F ; holdDownPoints = 0
8947: JMP $8973 ; goto lookupDropSpeed;

downPressed:

894A: INC $004E ; autorepeatY++;
894C: LDA $004E
894E: CMP #$03 ; if (autorepeatY < 3) {
8950: BCC $8973 ; goto lookupDropSpeed;
; }

8952: LDA #$01
8954: STA $004E ; autorepeatY = 1;

8956: INC $004F ; holdDownPoints++;

drop:

8958: LDA #$00
895A: STA $0045 ; fallTimer = 0;

895C: LDA $0041
895E: STA $00AE ; originalY = tetriminoY;

8960: INC $0041 ; tetriminoY++;
8962: JSR $948B ; if (new position valid) {
8965: BEQ $8972 ; return;
; }

; the piece is locked

8967: LDA $00AE
8969: STA $0041 ; tetriminoY = originalY;

896B: LDA #$02
896D: STA $0048 ; playState = UPDATE_PLAYFIELD;
896F: JSR $9CAF ; updatePlayfield();

8972: RTS ; return;

lookupDropSpeed:

8973: LDA #$01 ; tempSpeed = 1;

8975: LDX $0044 ; if (level >= 29) {
8977: CPX #$1D ; goto noTableLookup;
8979: BCS $897E ; }

897B: LDA $898E,X ; tempSpeed = framesPerDropTable[level];

noTableLookup:

897E: STA $00AF ; dropSpeed = tempSpeed;

8980: LDA $0045 ; if (fallTimer >= dropSpeed) {
8982: CMP $00AF ; goto drop;
8984: BPL $8958 ; }

8986: JMP $8972 ; return;

incrementAutorepeatY:

8989: INC $004E ; autorepeatY++;
898B: JMP $8972 ; return;


lookupDropSpeed

fallTimer$0045dropSpeed$00AFfallTimer$8892

autorepeatY$004E$0A$8739),其解释为-96。一开始的情况会导致初始延迟。在创建时,第一个Tetrimino仍然悬浮在空中,直到autorepeatY增加到0,这需要1.6秒。但是,在此阶段中按下Down时,它会autorepeatY立即分配为0。有趣的是,您可以在初始延迟的此阶段中移动和旋转图形而不取消它。按住时执行

增量autorepeatY。当它达到3时,将发生人为控制的下降(“软”下降)并autorepeatY分配为1。因此,初始的软下降需要3帧,但随后在每帧中重复一次。

此外,autorepeatY只有当游戏识别出玩家刚刚单击了Down(在$00B5),但不识别按住。这很重要,因为autorepeatY在创建Tetrimino(位于地址$98E8时会将重置为0 ,这会创建一个重要功能:如果玩家本人降低了身材并被阻止,并且在创建下一个身材时继续按“向下”键(通常发生在较高级别),则这不会导致新数字的下降。为此,播放器必须释放“ Down”,然后再次按下按钮。

潜在的轻微下降会增加得分。holdDownPoints$004F)随每次下降而增加,但释放时,“下降”将重置为0。因此,要得分,必须以柔和下降的方式将tetrimino降低到锁中。可能以图形方式出现的短期软下降不影响点。帐户更新时间为$9BFE,但holdDownPoints不久之后在地址处将其重置为0 $9C2F

此项检查会阻止玩家执行随着图形水平移动而进行的软下降操作,这会使积分复杂化。这意味着在将棋子锁定到位之前的最后一步应该是“向下”。

发生下降时,tetriminoY$0041)被复制到originalY$00AE)。如果通过增量创建的新位置被tetriminoY证明是不正确的(也就是说,人物要么穿过运动场的地板,要么叠加在已经躺下的正方形上),则四聚体保持在先前的位置。在这种情况下,将其还原tetriminoY该数字被认为是受阻的。这意味着锁定之前的延迟(锁定之前,tetrimino期望保留在空中的最大帧数)等于下降延迟。

Nintendo Tetris不支持刚性下降(立即下降)。

滑动和滚动


Nintendo Tetris手册中有插图说明示例:


滑动包括沿着其他人物的表面或运动场的地板移动。通常用于将人物推到悬垂的正方形下。可以执行滑动,直到下降计时器达到下降速度为止,此后数字将被锁定到位。动画示例如下所示。


另一方面,滚动允许您将图形推入其他任何方式都无法达到的空间(请参见下文)。


像滑动一样,没有锁定延迟也无法滚动。但是除此之外,滚动还可以利用游戏操纵形状的方式。在移动或旋转图形之前,游戏会检查以确保在更改位置之后,所有的tetrimino正方形都将位于运动场边界内的空白单元格中。如下所示,这种检查不会阻止旋转通过附近的填充块。如“ Tetrimino描述”部分所述,方向表的每一行包含12个字节;因此,该表中的索引是通过将活动Tetrimino的方向ID乘以12来计算的。如下所示,例程中的所有乘法都是使用移位和加法执行的。

948B: LDA $0041
948D: ASL
948E: STA $00A8
9490: ASL
9491: ASL
9492: CLC
9493: ADC $00A8
9495: ADC $0040
9497: STA $00A8

9499: LDA $0042
949B: ASL
949C: ASL
949D: STA $00A9
949F: ASL
94A0: CLC
94A1: ADC $00A9
94A3: TAX ; index = 12 * orientationID;
94A4: LDY #$00

94A6: LDA #$04
94A8: STA $00AA ; for(i = 0; i < 4; i++) {

94AA: LDA $8A9C,X ; squareY = orientationTable[index];
94AD: CLC
94AE: ADC $0041 ; cellY = squareY + tetriminoY;
94B0: ADC #$02 ; if (cellY < -2 || cellY >= 20) {
94B2: CMP #$16 ; return false;
94B4: BCS $94E9 ; }

94B6: LDA $8A9C,X
94B9: ASL
94BA: STA $00AB
94BC: ASL
94BD: ASL
94BE: CLC
94BF: ADC $00AB
94C1: CLC
94C2: ADC $00A8
94C4: STA $00AD

94C6: INX
94C7: INX ; index += 2;

94C8: LDA $8A9C,X ; squareX = orientationTable[index];
94CB: CLC
94CC: ADC $00AD
94CE: TAY ; cellX = squareX + tetriminoX;
94CF: LDA ($B8),Y ; if (playfield[10 * cellY + cellX] != EMPTY_TILE) {
94D1: CMP #$EF ; return false;
94D3: BCC $94E9 ; }

94D5: LDA $8A9C,X
94D8: CLC
94D9: ADC $0040 ; if (cellX < 0 || cellX >= 10) {
94DB: CMP #$0A ; return false;
94DD: BCS $94E9 ; }

94DF: INX ; index++;
94E0: DEC $00AA
94E2: BNE $94AA ; }

94E4: LDA #$00
94E6: STA $00A8
94E8: RTS ; return true;

94E9: LDA #$FF
94EB: STA $00A8
94ED: RTS




index = (orientationID << 3) + (orientationID << 2); // index = 8 * orientationID + 4 * orientationID;

(cellY << 3) + (cellY << 1) // 8 * cellY + 2 * cellY


循环的每次迭代都使四聚体位置偏离定向表中一个正方形的相对坐标,以便在运动场上获得相应的单元位置。然后,她检查单元格的坐标是否在运动场的边界内,以及单元格本身是否为空。

这些注释更清楚地描述了如何检查行距。除了可见行中的单元格外,该代码还将运动场上方的两条隐藏线视为正方形的合法位置,而无需使用复合条件。之所以起作用,是因为在附加代码中,单字节变量表示负数等于大于127的值。在这种情况下,最小值为-2,存储为

94AA: LDA $8A9C,X ; squareY = orientationTable[index];
94AD: CLC
94AE: ADC $0041 ; cellY = squareY + tetriminoY;
94B0: ADC #$02 ; if (cellY + 2 >= 22) {
94B2: CMP #$16 ; return false;
94B4: BCS $94E9 ; }


cellY$FE(254个十进制表示法)。

比赛场地的索引是总和cellY乘以10和cellX。但是,当cellY-1($FF= 255)或-2($FE= 254)时,乘积得出-10($F6= 246)和-20($EC= 236)。在此间隔中,它cellX不能大于9,这给出的最大索引为246 + 9 = 255,这比比赛场地的结尾要远得多。但是,游戏会初始化$0400- $04FF使用$EF(空图块的值)创建另外56个字节的空白空间。

奇怪的是间隔检查cellX在检查了比赛场地的电池后进行的。但是它可以按任何顺序正确工作。此外,检查时间间隔可避免出现复合情况,如下面的注释所示。由于此代码检查位置的方式,因此可能显示以下滚动示例。

94D5: LDA $8A9C,X
94D8: CLC
94D9: ADC $0040 ; if (cellX >= 10) {
94DB: CMP #$0A ; return false;
94DD: BCS $94E9 ; }






如下所示,您甚至可以通过滚动执行滑动。


AI充分利用了Nintendo Tetris的移动功能,包括滑动和滚动。

30级以上


达到30级后,似乎该级别已重置为零。


但是第31级表明发生了其他事情:


显示的液位值位于表格中的地址处$96B8

96B8: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

如下所示,该图案表排序,使得与所述瓦片$00$0F是字形的符号0F。这意味着当显示十进制或十六进制数字时,该数字本身的值将用作模式表的索引。在我们的例子中,级别值存储为二进制编码的十进制(BCD)。序列中每个字节的每个半字节都是一个平铺值。


不幸的是,游戏设计师似乎认为没有人会超过29级,因此决定只在表中插入30个条目。表格后面显示的奇怪值是不同的字节。仅一个字节(位于address $0044用于指示级别号,这就是为什么游戏缓慢地循环显示以下256个​​值的原因。

000123456789ABCDEF
000010203040506070809101112131415
11617181920212223242526272829000A
2141E28323C46505A646E78828C96A0AA
3B4BEC620E62006212621462166218621
4A621C621E62106222622462266228622
5A622C622E6220623262385A829F04A4A
64A4A8D0720A5A8290F8D072060A649E0
7151053BDD696A88A0AAAE8BDEA968D06
820CAA5BEC901F01EA5B9C905F00CBDEA
99638E9028D06204C6797BDEA9618690C
A8D06204C6797BDEA961869068D0620A2
B0AB1B88D0720C8CAD0F7E649A549C914
C3004A920854960A5B12903D078A90085
DAAA6AAB54AF05C0AA8B9EA9685A8A5BE
EC901D00AA5A818690685A84CBD97A5B9
FC904D00AA5A838E90285A84CBD97A5A8

实际上,前20个序数值是另一个表,该表存储了20行中每行在比赛场地上的偏移量。由于竞争环境始于并且每行包含10个单元,因此任意单元的地址为:鉴于处理器不直接支持乘法,因此此查找表提供了一种非常快速的获取乘积的方法。相应的表占用接下来的40个字节。它包含20个小字节序格式的地址,用于命名表0(VRAM存储区包含背景图块的值)。它们是指向比赛场地偏移线的指针构成显示的电平值的其余字节为指令。

96D6: 00 ; 0
96D7: 0A ; 10
96D8: 14 ; 20
96D9: 1E ; 30
96DA: 28 ; 40
96DB: 32 ; 50
96DC: 3C ; 60
96DD: 46 ; 70
96DE: 50 ; 80
96DF: 5A ; 90
96E0: 64 ; 100
96E1: 6E ; 110
96E2: 78 ; 120
96E3: 82 ; 130
96E4: 8C ; 140
96E5: 96 ; 150
96E6: A0 ; 160
96E7: AA ; 170
96E8: B4 ; 180
96E9: BE ; 190


$0400

$0400 + 10 * y + x



$0400 + [$96D6 + y] + x

$06



行和统计


在以下地址,已完成的行数和tetrimino统计信息各自占用2个字节。

地址数量
0050 -- 0051等级
03F0 -- 03F1Ť
03F2 -- 03F3Ĵ
03F4 -- 03F5ž
03F6 -- 03F7Ø
03F8 -- 03F9小号
03FA -- 03FB大号
03FC -- 03FD

实际上,这些值存储为16位打包的小字节序BCD。例如,下面显示的行数是123。字节从右到左计数,以便十进制数字按顺序排列。


但是,游戏设计人员假定这些值都不大于999。因此,显示逻辑将第一个字节正确地处理为打包的BCD,其中每个半字节都用作图块值。但是整个第二个字节实际上被用作前十进制数字。当低位数字从99到时00,第二个字节的正常增量发生。结果,第二个字节循环遍历所有256个图块。下面是一个示例。


清除该行之后,执行以下代码以增加行数。检查中位数和下位数,使它们保持在0到9之间。但是高位数可以无限地增加。如果在增加行数之后,下一位数字为0,则表示玩家刚刚完成了10行的设置,则需要增加级别数。从下面的代码中可以看到,在级别增加之前会执行其他检查。第二项检查与所选的入门级别有关。要达到某个级别,无论初始级别如何,玩家都必须清除

9BA8: INC $0050 ; increment middle-lowest digit pair
9BAA: LDA $0050
9BAC: AND #$0F
9BAE: CMP #$0A ; if (lowest digit > 9) {
9BB0: BMI $9BC7
9BB2: LDA $0050
9BB4: CLC
9BB5: ADC #$06 ; set lowest digit to 0, increment middle digit
9BB7: STA $0050
9BB9: AND #$F0
9BBB: CMP #$A0 ; if (middle digit > 9) {
9BBD: BCC $9BC7
9BBF: LDA $0050
9BC1: AND #$0F
9BC3: STA $0050 ; set middle digit to 0
9BC5: INC $0051 ; increment highest digit
; }
; }






9BC7: LDA $0050
9BC9: AND #$0F
9BCB: BNE $9BFB ; if (lowest digit == 0) {
9BCD: JMP $9BD0

9BD0: LDA $0051
9BD2: STA $00A9
9BD4: LDA $0050
9BD6: STA $00A8 ; copy digits from $0050-$0051 to $00A8-$00A9

9BD8: LSR $00A9
9BDA: ROR $00A8
9BDC: LSR $00A9
9BDE: ROR $00A8
9BE0: LSR $00A9
9BE2: ROR $00A8 ; treat $00A8-$00A9 as a 16-bit packed BCD value
9BE4: LSR $00A9 ; and right-shift it 4 times
9BE6: ROR $00A8 ; this leaves the highest and middle digits in $00A8

9BE8: LDA $0044
9BEA: CMP $00A8 ; if (level < [$00A8]) {
9BEC: BPL $9BFB

9BEE: INC $0044 ; increment level
; }
; }


X10X线。例如,如果玩家从第5级开始,则他将一直停留在该级别上,直到清除了60行为止,之后他将进入第6级。此后,每增加10行将导致级别数增加。

要执行此检查,将填充行的值从$0050- 复制$0051$00A8- $00A9。然后将副本向右移4次,对于打包的BCD而言,该乘积类似于除以10。最小的十进制数字将被舍弃,最高和中间的数字将被移位一个位置,从而导致半字节$00A8


但是,在该地址处,$9BEA级别号直接与BCD的打包值进行比较$00A8在表中没有搜索将BCD值转换为十进制,这是一个明显的错误。例如,在上图中,应该将级别号与$12(十进制的18)进行比较,而不是12。因此,如果玩家决定从级别17开始,那么该级别实际上将达到120行,因为18大于17.

该表显示了每个初始级别上过渡所需的预期行数。将其与由于错误实际发生的情况进行比较。

01个23456789101112131415161718岁19
102030405060708090100110120130140150160170180190200
102030405060708090100100100100100100100110120130140

预期数量与初始级别0–9的真实数量相同。实际上,入门级9的重合是随机的。10-15也会进入100行的下一个级别,因为$10-这是十进制形式的16。预期行与实际行之间的最大差异是60行。

我怀疑该错误是由于开发后期的设计变更所致。查看菜单屏幕,允许玩家选择入门级别。


没有关于如何从9以上的级别开始的解释。但是在Nintendo Tetris手册中,这个秘密被揭示:


似乎此隐藏功能是在最后一刻发明的。也许它是在发布日期前不久添加的,因此无法对其进行全面测试。

实际上,检查初始序列包含与间隔值输出有关的第二个错误。下面是代码中的注释,可以更好地解释在低级别时会发生什么。通过减去并检查结果的符号来执行比较。但是,单字节有符号数限制为-128到127。如果差值小于-128,则该数将被保留,结果变为正数。在代码注释中解释了此原理。当检查差值是否在此间隔内时,必须考虑到级别编号在增加到大于255的值时执行到0的转移,并且

9BE8: LDA $0044
9BEA: CMP $00A8 ; if (level - [$00A8] < 0) {
9BEC: BPL $9BFB

9BEE: INC $0044 ; increment level
; }




9BE8: LDA $0044 ; difference = level - [$00A8];
9BEA: CMP $00A8 ; if (difference < 0 && difference >= -128) {
9BEC: BPL $9BFB

9BEE: INC $0044 ; increment level
; }


$00A8可能它可以包含任何值,因为其上半字节取自$0051,其增量可以无限地发生。

这些效果重叠,从而形成了错误地保持级别编号不变的时间段。周期以2900行的固定间隔发生,从2190行开始,持续800行。例如,从2190(L90)到2990(T90),电平保持等于$DB96),如下所示。


下一个周期是从5090到5890,该水平始终等于$AD06)。此外,在这些时间段内,调色板也不会改变。

Tetrimino着色页


在每个级别,俄罗斯方块瓷砖都被分配4种独特的颜色。颜色取自位于的表$984C她的记录每10级重用一次。从左至右:表格的各栏对应于下图的黑色,白色,蓝色和红色区域。

984C: 0F 30 21 12 ; level 0
9850: 0F 30 29 1A ; level 1
9854: 0F 30 24 14 ; level 2
9858: 0F 30 2A 12 ; level 3
985C: 0F 30 2B 15 ; level 4
9860: 0F 30 22 2B ; level 5
9864: 0F 30 00 16 ; level 6
9868: 0F 30 05 13 ; level 7
986C: 0F 30 16 12 ; level 8
9870: 0F 30 27 16 ; level 9





这些值对应于NES调色板。


每个条目的前2种颜色始终为黑白。但是,第一种颜色实际上被忽略了。不管值如何,它都被视为透明的颜色,黑色实心背景可通过该颜色窥视。

在的例程中可以访问颜色表$9808颜色表索引基于级别数除以10的余数。循环将条目复制到VRAM中的调色板表中。通过乘以10进行连续减法来模拟与余数的除法,直到结果小于10。下面显示带注释的子例程的开头。

9808: LDA $0064
980A: CMP #$0A
980C: BMI $9814
980E: SEC
980F: SBC #$0A
9811: JMP $980A ; index = levelNumber % 10;

9814: ASL
9815: ASL
9816: TAX ; index *= 4;

9817: LDA #$00
9819: STA $00A8 ; for(i = 0; i < 32; i += 16) {

981B: LDA #$3F
981D: STA $2006
9820: LDA #$08
9822: CLC
9823: ADC $00A8
9825: STA $2006 ; palette = $3F00 + i + 8;

9828: LDA $984C,X
982B: STA $2007 ; palette[0] = colorTable[index + 0];

982E: LDA $984D,X
9831: STA $2007 ; palette[1] = colorTable[index + 1];

9834: LDA $984E,X
9837: STA $2007 ; palette[2] = colorTable[index + 2];

983A: LDA $984F,X
983D: STA $2007 ; palette[3] = colorTable[index + 3];

9840: LDA $00A8
9842: CLC
9843: ADC #$10
9845: STA $00A8
9847: CMP #$20
9849: BNE $981B ; }

984B: RTS ; return;






9808: LDA $0064 ; index = levelNumber;
980A: CMP #$0A ; while(index >= 10) {
980C: BMI $9814
980E: SEC
980F: SBC #$0A ; index -= 10;
9811: JMP $980A ; }


但是,如前一节所述,在比较中使用了基于差号的减法和分支。单字节有符号数限制为-128到127。下面更新的注释反映了这一原理。下面的评论将进一步简化。该措辞揭示了代码中的错误。对于138及更高级别,其余的除法运算将完全跳过。而是直接将索引分配给级别编号,该级别编号提供对远远超出颜色表末尾的字节的访问。如下所示,这甚至可能导致几乎看不见的四聚胺。

9808: LDA $0064 ; index = levelNumber;
; difference = index - 10;
980A: CMP #$0A ; while(difference >= 0 && difference <= 127) {
980C: BMI $9814
980E: SEC ; index -= 10;
980F: SBC #$0A ; difference = index - 10;
9811: JMP $980A ; }




9808: LDA $0064 ; index = levelNumber;
980A: CMP #$0A ; while(index >= 10 && index <= 137) {
980C: BMI $9814
980E: SEC
980F: SBC #$0A ; index -= 10;
9811: JMP $980A ; }





以下是所有256级的颜色。磁贴排列在10列中,以强调颜色表的周期性使用,这在级别138上是违反​​的。标头中的行和列以十进制表示。


255之后,级别号返回0。

此外,如上一节所述,某些级别直到更改800行才保持不变。在这些长时间内,颜色保持不变。

游戏模式


存储在该地址的游戏模式$00C0确定当前向用户显示各种屏幕和菜单中的哪一个。

价值内容描述
00法律信息屏幕
01开机画面
02游戏类型菜单
03水平和高度菜单
04游戏/高分/结局/暂停
05演示版

如上所示,游戏有一个巧妙编写的例程,该例程使用位于调用之后的小尾数导航表充当切换语句。上面的列表显示了所有游戏模式的地址。请注意,“游戏”和“演示”模式使用相同的代码。该例程永远不会返回。相反,代码使用返回地址。通常,它指向在跳转到子例程(减去1个字节)之后立即执行的指令,但是在这种情况下,它指向跳转表。返回地址从堆栈中弹出并存储在-中。保存跳转表的地址后,代码将寄存器A中的值用作索引并执行相应的转换。

8161: LDA $00C0
8163: JSR $AC82 ; switch(gameMode) {
8166: 00 82 ; case 0: goto 8200; //
8168: 4F 82 ; case 1: goto 824F; //
816A: D1 82 ; case 2: goto 82D1; //
816C: D7 83 ; case 3: goto 83D7; //
816E: 5D 81 ; case 4: goto 815D; // / / /
8170: 5D 81 ; case 5: goto 815D; //
; }




$0000$0001

AC82: ASL
AC83: TAY
AC84: INY

AC85: PLA
AC86: STA $0000
AC88: PLA ; pop return address off of stack
AC89: STA $0001 ; and store it at $0000-$0001

AC8B: LDA ($00),Y
AC8D: TAX
AC8E: INY
AC8F: LDA ($00),Y
AC91: STA $0001
AC93: STX $0000
AC95: JMP ($0000) ; goto Ath 16-bit address
; in table at [$0000-$0001]


只要索引接近0并且在可能的情况之间没有空格或很少,代码就可以使用此切换例程。

法律信息屏幕


游戏从显示法律声明的屏幕开始。


在屏幕底部,Aleksey Pazhitnov称为第一位俄罗斯方块的发明者,设计师和程序员。 1984年,他在Dorodnitsyn计算中心(位于莫斯科的俄罗斯科学院的领先研究所)担任计算机开发人员,电子60(苏联克隆DEC LSI-11上开发了该游戏的原型。开发了一种绿色单色文本模式的原型,其中用方括号对表示正方形[]。在游戏发明几天后,在16岁的男生Vadim Gerasimov和计算机工程师Dmitry Pavlovsky 的帮助下,将原型移植到具有MS DOS和Turbo Pascal的IBM PC。在过去的两年中,他们共同完善了游戏,添加了诸如Tetrimino颜色,统计数据等功能,更重要的是,添加了计时和图形代码,使游戏可以在各种PC型号和克隆上运行。

不幸的是,由于当时苏联的特殊性,他们尝试通过游戏获利的尝试失败,最终他们决定免费与朋友共享PC版本。从那时起,“俄罗斯方块”开始在全国范围内病毒传播,从磁盘复制到磁盘。但是由于该游戏是由政府机构的员工开发的,因此由国家所有。1987年,负责电子技术国际贸易的组织接管了该游戏的许可(Electronorgtekhnika(ELORG))法律信息屏幕上的缩写V / O可能是Version Originale的缩写。

英国软件公司Andromeda试图获得俄罗斯方块的权利,并在交易完成前将游戏再授权给其他供应商,例如,英国计算机游戏Mirrorsoft发行商。反之,Mirrorsoft将其转给Atari Games的子公司Tengen。 Tengen授予Bullet-Proof Software在日本开发用于计算机和游戏机的游戏的权利,从而为Nintendo Famicom带来了Tetris 。以下是他的法律信息屏幕。


有趣的是,在这个版本中,男生Vadim Gerasimov被称为原始设计师和程序员。

为了保护即将到来的Game Boy控制台的便携式版本,任天堂使用Bullet-Proof软件直接与ELORG达成了成功的交易。在达成交易的过程中,ELORG修改了与Andromeda的合同,并补充说Andromeda仅获得计算机和街机的游戏权利。因此,Bullet-Proof Software必须为出售给Famicom的所有墨盒支付ELORG特许权使用费,因为它从Tengen获得的权利被证明是伪造的。但是,通过与ELORG的和解,防弹软件最终成功为Nintendo赢得了全球主机游戏的版权。

防弹软件再授权任天堂的便携式游戏权利,并且他们共同开发了Game Boy Tetris,这反映在下面的法律信息屏幕中。


拥有全球游戏机游戏权,任天堂已经开发了适用于NES的Tetris版本,我们将在本文中进行探讨。然后,防弹软件再授权了任天堂的权利,这使她得以继续在日本为Famicom销售墨盒。

随后进行了复杂的法律斗争。任天堂和Tengen都要求对方停止生产和销售其游戏版本。结果,任天堂获胜,数十万个Tengen Tetris弹药筒被摧毁。法院的判决还禁止其他类似Mirrorsoft的公司创建控制台版本。

帕吉诺夫从未从ELORG或苏联政府获得任何扣除。但是,在1991年他移居美国,并在Bullet-Proof Software所有者的支持下于1996年移居美国。亨卡·罗杰斯(Henka Rogers)共同创立了《俄罗斯方块公司The Tetris Company)》,这使他从移动设备和现代游戏机的版本中获利。

将法律信息屏幕视为一个窗口,可以了解游戏的起源以及随后的知识产权斗争,这很有趣,因为对于大多数玩家而言,该屏幕只是一个令人讨厌的障碍,其消失似乎必须永远等待。延迟由两个计数器设置,依次从255到0计数。不能跳过第一阶段,而通过按“开始”按钮可以跳过第二阶段。因此,法律信息屏幕至少显示为4.25秒且不超过8.5秒。但是,我认为大多数玩家都放弃了,在第一个时间间隔内停止按开始键,因此他们在等待完整的完成。

阶段的时间以及游戏的其余部分由一个未屏蔽的中断处理程序控制,该中断处理程序在每个垂直消隐间隔(电视帧渲染之间的一小段时间)开始时调用。即,每隔16.6393毫秒,以下代码会中断正常的程序执行。处理程序首先将主寄存器的值传递到堆栈,并在完成后检索它们,以免干扰被中断的任务。该调用将更新VRAM,将内存模型的描述转换为屏幕上显示的内容。此外,如果处理程序的法律信息屏幕的计数器的值大于零,则该值会减小。挑战赛

8005: PHA
8006: TXA
8007: PHA
8008: TYA
8009: PHA ; save A, X, Y

800A: LDA #$00
800C: STA $00B3
800E: JSR $804B ; render();

8011: DEC $00C3 ; legalScreenCounter1--;

8013: LDA $00C3
8015: CMP #$FF ; if (legalScreenCounter1 < 0) {
8017: BNE $801B ; legalScreenCounter1 = 0;
8019: INC $00C3 ; }

801B: JSR $AB5E ; initializeOAM();

801E: LDA $00B1
8020: CLC
8021: ADC #$01
8023: STA $00B1
8025: LDA #$00
8027: ADC $00B2
8029: STA $00B2 ; frameCounter++;

802B: LDX #$17
802D: LDY #$02
802F: JSR $AB47 ; randomValue = generateNextPseudorandomNumber(randomValue);

8032: LDA #$00
8034: STA $00FD
8036: STA $2005 ; scrollX = 0;
8039: STA $00FC
803B: STA $2005 ; scrollY = 0;

803E: LDA #$01
8040: STA $0033 ; verticalBlankingInterval = true;

8042: JSR $9D51 ; pollControllerButtons();

8045: PLA
8046: TAY
8047: PLA
8048: TAX
8049: PLA ; restore A, X, Y

804A: RTI ; resume interrupted task


render()initializeOAM()执行帧生成设备所需的步骤。处理器继续执行该柜台工作人员的增量- 16位值是小端,存放在$00B1- $00B2这是他在不同的地方进行控制的定时使用。之后,生成以下伪随机数;如上所述,与模式无关,每帧至少发生一次。$8040垂直消隐间隔标志设置在address ,这意味着处理程序刚刚被执行。最后,对控制器按钮进行轮询。该例程的行为将在下面的“演示”部分中描述。

该标志verticalBlankingInterval由上述例程使用。它一直持续到中断处理程序开始执行为止。

AA2F: JSR $E000 ; updateAudio();

AA32: LDA #$00
AA34: STA $0033 ; verticalBlankingInterval = false;

AA36: NOP

AA37: LDA $0033
AA39: BEQ $AA37 ; while(!verticalBlankingInterval) { }

AA3B: LDA #$FF
AA3D: LDX #$02
AA3F: LDY #$02
AA41: JSR $AC6A ; fill memory page 2 with all $FF's

AA44: RTS ; return;


此阻止例程由法律信息屏幕的两个计时阶段使用,这两个阶段相继执行。Lua AI脚本通过将两个计数器都设置为0来绕过此延迟。

8236: LDA #$FF
8238: JSR $A459

...

A459: STA $00C3 ; legalScreenCounter1 = 255;

A45B: JSR $AA2F ; do {
A45E: LDA $00C3 ; waitForVerticalBlankingInterval();
A460: BNE $A45B ; } while(legalScreenCounter1 > 0);

A462: RTS ; return;


823B: LDA #$FF
823D: STA $00A8 ; legalScreenCounter2 = 255;

; do {

823F: LDA $00F5 ; if (just pressed Start) {
8241: CMP #$10 ; break;
8243: BEQ $824C ; }

8245: JSR $AA2F ; waitForVerticalBlankingInterval();

8248: DEC $00A8 ; legalScreenCounter2--;
824A: BNE $823F ; } while(legalScreenCounter2 > 0);

824C: INC $00C0 ; gameMode = TITLE_SCREEN;




演示版


该演示显示了大约80秒的预先录制的游戏时间。它不仅显示视频文件,而且使用与游戏中相同的引擎。在播放期间,使用两个表。第一个位于地址处$DF00,包含创建tetrimino的以下顺序:

TJTSZJTSZJSZLZJTTSITO JSZLZLIOLZLIOJTSITOJ

创建图形时,根据模式的不同,它是随机选择的,还是从表中读取的。切换发生在该地址$98EB从每个字节的位6、5和4中提取Tetrimino类型。有时,此操作会为我们提供一个值-错误的类型。然而,表创建形状()中的俄罗斯ID取向用于类型转换实际上是位于两个链接的表之间:含义

98EB: LDA $00C0
98ED: CMP #$05
98EF: BNE $9903 ; if (gameMode == DEMO) {

98F1: LDX $00D3
98F3: INC $00D3
98F5: LDA $DF00,X ; value = demoTetriminoTypeTable[++demoIndex];

98F8: LSR
98F9: LSR
98FA: LSR
98FB: LSR
98FC: AND #$07
98FE: TAX ; tetriminoType = bits 6,5,4 of value;

98FF: LDA $994E,X
9902: RTS ; return spawnTable[tetriminoType];
; } else {
; pickRandomTetrimino();
; }


$07$994E

993B: 00 00 00 00 ; T
993F: 01 01 01 01 ; J
9943: 02 02 ; Z
9945: 03 ; O
9946: 04 04 ; S
9948: 05 05 05 05 ; L
994C: 06 06 ; I


994E: 02 ; Td
994F: 07 ; Jd
9950: 08 ; Zh
9951: 0A ; O
9952: 0B ; Sh
9953: 0E ; Ld
9954: 12 ; Ih


9956: 02 02 02 02 ; Td
995A: 07 07 07 07 ; Jd
995E: 08 08 ; Zh
9960: 0A ; O
9961: 0B 0B ; Sh
9963: 0E 0E 0E 0E ; Ld
9967: 12 12 ; Ih


$07强制它在表的末尾读取,在下一个给出Td$02)。

由于这种影响,该方案可以为我们提供创建的图形方向的无限但可重复的伪随机ID序列。该代码将起作用,因为按字节顺序更改的任何任意地址都不允许我们确定表的结束位置。实际上,该地址处的序列$DF00可能是与此完全无关的某些部分,尤其是考虑到其余5个非零位的分配不明确,并且生成的序列证明了可重复性。

在演示模式初始化期间,表索引($00D3)重置为address $872B

演示的第二个表包含以字节对编码的游戏手柄按钮的记录。第一个字节的位与按钮相对应。

76543210
请选择开始往下在左边在右边

第二个字节存储在按下组合键期间的帧数。

该表占用地址$DD00- $DEFF并由256对组成。对它的访问由地址处的子例程执行$9D5B由于演示按钮表的长度为512字节,因此需要两个字节的索引才能访问它。该指数被存储为小端的地址- 用表的地址值初始化它,其增量由以下代码执行。程序员将播放器的输入处理留在了代码中,这使我们可以查看开发过程并将演示替换为另一条记录。分配值后,演示记录模式被激活

9D5B: LDA $00D0 ; if (recording mode) {
9D5D: CMP #$FF ; goto recording;
9D5F: BEQ $9DB0 ; }

9D61: JSR $AB9D ; pollController();
9D64: LDA $00F5 ; if (start button pressed) {
9D66: CMP #$10 ; goto startButtonPressed;
9D68: BEQ $9DA3 ; }

9D6A: LDA $00CF ; if (repeats == 0) {
9D6C: BEQ $9D73 ; goto finishedMove;
; } else {
9D6E: DEC $00CF ; repeats--;
9D70: JMP $9D9A ; goto moveInProgress;
; }

finishedMove:

9D73: LDX #$00
9D75: LDA ($D1,X)
9D77: STA $00A8 ; buttons = demoButtonsTable[index];

9D79: JSR $9DE8 ; index++;

9D7C: LDA $00CE
9D7E: EOR $00A8
9D80: AND $00A8
9D82: STA $00F5 ; setNewlyPressedButtons(difference between heldButtons and buttons);

9D84: LDA $00A8
9D86: STA $00CE ; heldButtons = buttons;

9D88: LDX #$00
9D8A: LDA ($D1,X)
9D8C: STA $00CF ; repeats = demoButtonsTable[index];

9D8E: JSR $9DE8 ; index++;

9D91: LDA $00D2 ; if (reached end of demo table) {
9D93: CMP #$DF ; return;
9D95: BEQ $9DA2 ; }

9D97: JMP $9D9E ; goto holdButtons;

moveInProgress:

9D9A: LDA #$00
9D9C: STA $00F5 ; clearNewlyPressedButtons();

holdButtons:

9D9E: LDA $00CE
9DA0: STA $00F7 ; setHeldButtons(heldButtons);

9DA2: RTS ; return;

startButtonPressed:

9DA3: LDA #$DD
9DA5: STA $00D2 ; reset index;

9DA7: LDA #$00
9DA9: STA $00B2 ; counter = 0;

9DAB: LDA #$01
9DAD: STA $00C0 ; gameMode = TITLE_SCREEN;

9DAF: RTS ; return;


$00D1$00D2$872D

9DE8: LDA $00D1
9DEA: CLC ; increment [$00D1]
9DEB: ADC #$01 ; possibly causing wrap around to 0
9DED: STA $00D1 ; which produces a carry

9DEF: LDA #$00
9DF1: ADC $00D2
9DF3: STA $00D2 ; add carry to [$00D2]

9DF5: RTS ; return


$00D0$FF在这种情况下,将启动以下代码,旨在将其写入演示的按钮表。但是,该表存储在PRG-ROM中。尝试对其进行写入不会影响已保存的数据。取而代之的是,每个写操作都会触发一个存储体开关,从而导致如下所示的故障效应。

recording:

9DB0: JSR $AB9D ; pollController();

9DB3: LDA $00C0 ; if (gameMode != DEMO) {
9DB5: CMP #$05 ; return;
9DB7: BNE $9DE7 ; }

9DB9: LDA $00D0 ; if (not recording mode) {
9DBB: CMP #$FF ; return;
9DBD: BNE $9DE7 ; }

9DBF: LDA $00F7 ; if (getHeldButtons() == heldButtons) {
9DC1: CMP $00CE ; goto buttonsNotChanged;
9DC3: BEQ $9DE4 ; }

9DC5: LDX #$00
9DC7: LDA $00CE
9DC9: STA ($D1,X) ; demoButtonsTable[index] = heldButtons;

9DCB: JSR $9DE8 ; index++;

9DCE: LDA $00CF
9DD0: STA ($D1,X) ; demoButtonsTable[index] = repeats;

9DD2: JSR $9DE8 ; index++;

9DD5: LDA $00D2 ; if (reached end of demo table) {
9DD7: CMP #$DF ; return;
9DD9: BEQ $9DE7 ; }

9DDB: LDA $00F7
9DDD: STA $00CE ; heldButtons = getHeldButtons();

9DDF: LDA #$00
9DE1: STA $00CF ; repeats = 0;

9DE3: RTS ; return;

buttonsNotChanged:

9DE4: INC $00CF ; repeats++;

9DE6: RTS
9DE7: RTS ; return;





这表明开发人员可以在RAM中部分或全部运行程序。

为了解决这一障碍,我在带有源代码lua/RecordDemo.luazip中创建了一个切换到演示记录模式后,它将写操作重定向到Lua控制台中的表。从中可以将字节复制并粘贴到ROM中。

要录制自己的演示,请运行FCEUX并下载Nintendo Tetris ROM文件(“文件” |“打开ROM ...”)。然后打开Lua脚本窗口(文件| Lua |新建Lua脚本窗口...),浏览到文件或输入路径。按下运行按钮以启动演示录制模式,然后在FCEUX窗口上单击以将焦点切换到该模式。您可以控制形状,直到按钮表已满。之后,游戏将自动返回到屏幕保护程序。单击“ Lua脚本”窗口中的“停止”以停止脚本。记录的数据将显示在“输出控制台”中,如下图所示。


选择所有内容并复制到剪贴板(Ctrl + C)。然后运行十六进制编辑器(调试|十六进制编辑器...)。从十六进制编辑器菜单中,选择查看| ROM文件,然后选择文件| 转到地址。在“转到”对话框中,输入5D10(ROM文件中演示按钮表的地址),然后单击“确定”。然后粘贴剪贴板的内容(Ctrl + V)。


最后,在FCEUX菜单中,选择NES | 重设 如果您设法重复了所有这些步骤,则应将演示替换为您自己的版本。

如果要保存更改,请选择“文件” |“更改”。将Rom另存为...,然后输入修改后的ROM文件的名称,然后单击“保存”。

以类似的方式,您可以调整创建的四联蛋白的顺序。

死亡之屏


如上所述,大多数玩家无法应付29级人物的下降速度,这很快导致游戏完成。因此,他与“死亡之屏”这个名字联系在一起。但是从技术角度来看,死亡屏幕不允许玩家走得更远,这是由于存在漏洞,而快速下降实际上并不是漏洞,而是功能。设计师是如此善良,以至于他们允许游戏继续进行,而玩家却能够承受超人的速度。

撤回约1550行时,出现真正的死亡画面。它以不同的方式表现出来。有时游戏会重新启动。在其他情况下,屏幕只会变黑。通常,游戏在删除一行后立即冻结(“冻结”),如下所示。这种效果通常在随机图形伪像之前。


死亡屏幕是代码中错误的结果,该错误会在删除行时添加点。六个字符的帐户存储为24位压缩的小字节序BCD,位于$0053- $0055。为了在清除的行数和获得的点数之间进行转换,使用了一个表。其中的每个条目都是一个16位压缩的BCD Little Endian值。在增加总行数以及可能的级别后,将此列表中的值乘以级别数加一,然后将结果添加到这些点。任天堂Tetris手册的表格清楚地表明了这一点:

9CA5: 00 00 ; 0: 0
9CA7: 40 00 ; 1: 40
9CA9: 00 01 ; 2: 100
9CAB: 00 03 ; 3: 300
9CAD: 00 12 ; 4: 1200





如下所示,乘法是通过将分数添加到分数的循环来模拟的。即使没有清除任何行,它也会在形状锁定后执行。不幸的是,理光2A03没有 6502二进制十进制模式。他可以大大简化周期的过程。而是使用二进制模式分步执行加法。加法后超过9的任何数字本质上是通过减去10并增加左侧的数字来获得的。例如,将其转换为。但是这样的方案没有得到充分的保护。采取:支票无法将结果转换为

9C31: LDA $0044
9C33: STA $00A8
9C35: INC $00A8 ; for(i = 0; i <= level; i++) {

9C37: LDA $0056
9C39: ASL
9C3A: TAX
9C3B: LDA $9CA5,X ; points[0] = pointsTable[2 * completedLines];

9C3E: CLC
9C3F: ADC $0053
9C41: STA $0053 ; score[0] += points[0];

9C43: CMP #$A0
9C45: BCC $9C4E ; if (upper digit of score[0] > 9) {

9C47: CLC
9C48: ADC #$60
9C4A: STA $0053 ; upper digit of score[0] -= 10;
9C4C: INC $0054 ; score[1]++;
; }

9C4E: INX
9C4F: LDA $9CA5,X ; points[1] = pointsTable[2 * completedLines + 1];

9C52: CLC
9C53: ADC $0054
9C55: STA $0054 ; score[1] += points[1];

9C57: AND #$0F
9C59: CMP #$0A
9C5B: BCC $9C64 ; if (lower digit of score[1] > 9) {

9C5D: LDA $0054
9C5F: CLC ; lower digit of score[1] -= 10;
9C60: ADC #$06 ; increment upper digit of score[1];
9C62: STA $0054 ; }

9C64: LDA $0054
9C66: AND #$F0
9C68: CMP #$A0
9C6A: BCC $9C75 ; if (upper digit of score[1] > 9) {

9C6C: LDA $0054
9C6E: CLC
9C6F: ADC #$60
9C71: STA $0054 ; upper digit of score[1] -= 10;
9C73: INC $0055 ; score[2]++;
; }

9C75: LDA $0055
9C77: AND #$0F
9C79: CMP #$0A
9C7B: BCC $9C84 ; if (lower digit of score[2] > 9) {

9C7D: LDA $0055
9C7F: CLC ; lower digit of score[2] -= 10;
9C80: ADC #$06 ; increment upper digit of score[2];
9C82: STA $0055 ; }

9C84: LDA $0055
9C86: AND #$F0
9C88: CMP #$A0
9C8A: BCC $9C94 ; if (upper digit of score[2] > 9) {

9C8C: LDA #$99
9C8E: STA $0053
9C90: STA $0054
9C92: STA $0055 ; max out score to 999999;
; }

9C94: DEC $00A8
9C96: BNE $9C37 ; }


$07 + $07 = $0E$14$09 + $09 = $12$18为了弥补这一点,记分卡中条目的十进制数字中没有一个超过6。此外,为了能够使用它,所有条目的最后数字始终为0。

完成这一漫长而复杂的循环需要花费时间。在高级别上,大量的迭代会影响游戏的计时,因为生成每个帧需要花费超过1/60秒的时间。结果,所有这些导致“死亡之屏”的各种表现。

Lua AI脚本将循环中的迭代次数限制为30,这是设计人员可以按照设计人员的设计实现的最大值,从而消除了死亡屏幕。

结局


在Nintendo Tetris手册中,A型游戏的描述如下:


游戏奖励在结局的五个动画之一中得分足够多的玩家。结束词的选择完全基于六位数分数的最左两位。如下所示,要获得其中一个结局,玩家必须获得至少30,000分。值得注意的是- 是地址的镜像- 。该帐户在- 处重复通过第一个测试后,通过以下switch语句选择结束动画。

9A4D: LDA $0075
9A4F: CMP #$03
9A51: BCC $9A5E ; if (score[2] >= $03) {

9A53: LDA #$80
9A55: JSR $A459
9A58: JSR $9E3A
9A5B: JMP $9A64 ; select ending;
; }


$0060$007F$0040$005F$0073$0075



A96E: LDA #$00
A970: STA $00C4
A972: LDA $0075 ; if (score[2] < $05) {
A974: CMP #$05 ; ending = 0;
A976: BCC $A9A5 ; }

A978: LDA #$01
A97A: STA $00C4
A97C: LDA $0075 ; else if (score[2] < $07) {
A97E: CMP #$07 ; ending = 1;
A980: BCC $A9A5 ; }

A982: LDA #$02
A984: STA $00C4
A986: LDA $0075 ; else if (score[2] < $10) {
A988: CMP #$10 ; ending = 2;
A98A: BCC $A9A5 ; }

A98C: LDA #$03
A98E: STA $00C4
A990: LDA $0075 ; else if (score[2] < $12) {
A992: CMP #$12 ; ending = 3;
A994: BCC $A9A5 ; }

A996: LDA #$04 ; else {
A998: STA $00C4 ; ending = 4;
; }


最后,越来越大的火箭从圣巴西尔大教堂旁边的发射台发射出去。在第四个结尾中,显示了Buran航天器-美国航天飞机的苏联版。最棒的结局是,大教堂本身升空,不明飞行物悬在发射台上方。下面是每个结尾和与之相关的分数的图像。
30000–49999
50000–69999
70000–99999
100000–119999
120000+

在B型游戏模式下,将实施另一项测试,Nintendo Tetris手册对此进行了说明,如下所示:


如果玩家成功清除了25行,则游戏将根据初始级别显示结局。 0-8级的结尾包括在框架中飞行或奔跑的动物和物体,它们神秘地经过了圣巴西尔大教堂。从A型模式最佳结尾开始的UFO出现在结尾3中。最后,出现了灭绝的翼龙,最后出现了7条,显示了神话中的飞龙。在结尾2和6中,显示了没有翅膀的鸟:奔跑的企鹅和鸵鸟。在5月底,天空充满了GOOD飞艇(请勿与Goodyear飞艇相混淆)。在8月底,尽管屏幕上只有一个“ Buranas”横扫屏幕,但很多。

起始高度(加1)用作乘数,奖励玩家增加许多动物/物体的复杂性。

在B-Type的最佳结局中,展示了一座充满任天堂角色的城堡:桃子公主拍手,小子伊卡洛斯(Kid Icarus)弹小提琴,大金刚敲大鼓,马里奥(Mario)和路易吉(Luigi)舞蹈,鲍瑟(Bowser)弹奏手风琴,萨姆斯(Samus)弹奏大提琴,林克(Link) -长笛演奏,而圣巴西尔大教堂的穹顶则高高耸入。结尾中显示的这些元素的数量取决于初始高度。以下是所有10个结尾的图片。


AI可以在任何初始水平和高度快速清除B型模式所需的全部25行,这使您可以看到任何结尾。还值得评估他处理大量随机块的能力如何。

在结尾0-8中,框架中最多可以移动6个对象。对象的y坐标存储在位于的表中$A7B7对象之间的水平距离存储在地址表中地址处带有符号的一系列值确定对象的速度和方向。精灵索引存储在实际上,每个对象都由两个具有相邻索引的精灵组成。要获得第二个索引,您需要添加1。例如,一条龙由

A7B7: 98 A8 C0 A8 90 B0 ; 0
A7BD: B0 B8 A0 B8 A8 A0 ; 1
A7C3: C8 C8 C8 C8 C8 C8 ; 2
A7C9: 30 20 40 28 A0 80 ; 3
A7CF: A8 88 68 A8 48 78 ; 4
A7D5: 58 68 18 48 78 38 ; 5
A7DB: C8 C8 C8 C8 C8 C8 ; 6
A7E1: 90 58 70 A8 40 38 ; 7
A7E7: 68 88 78 18 48 A8 ; 8


$A77B

A77B: 3A 24 0A 4A 3A FF ; 0
A781: 22 44 12 32 4A FF ; 1
A787: AE 6E 8E 6E 1E 02 ; 2
A78D: 42 42 42 42 42 02 ; 3
A793: 22 0A 1A 04 0A FF ; 4
A799: EE DE FC FC F6 02 ; 5
A79F: 80 80 80 80 80 FF ; 6
A7A5: E8 E8 E8 E8 48 FF ; 7
A7AB: 80 AE 9E 90 80 02 ; 8


$A771

A771: 01 ; 0: 1
A772: 01 ; 1: 1
A773: FF ; 2: -1
A774: FC ; 3: -4
A775: 01 ; 4: 1
A776: FF ; 5: -1
A777: 02 ; 6: 2
A778: 02 ; 7: 2
A779: FE ; 8: -1


$A7F3

A7F3: 2C ; 0: dragonfly
A7F4: 2E ; 1: dove
A7F5: 54 ; 2: penguin
A7F6: 32 ; 3: UFO
A7F7: 34 ; 4: pterosaur
A7F8: 36 ; 5: blimp
A7F9: 4B ; 6: ostrich
A7FA: 38 ; 7: dragon
A7FB: 3A ; 8: Buran


$38$39这些精灵的图块包含在下面的模式表中。


我们检查了上述模式的中央表,它用于显示tetrimino和运动场。有趣的是,它包含整个字母,而其他字母仅包含一部分以节省空间。但是更有趣的是左侧模式表中的飞机和直升机精灵。它们不会出现在游戏的结局或其他部分。原来,这架飞机和直升机有索引,精灵$30$16,你可以改变上面显示的表,看到他们在行动。



不幸的是,没有显示直升机的安装座,但是主旋翼和尾旋翼都经过了精美的动画处理。

2人对战


Nintendo Tetris包含一个不完整的两人模式,可以通过将玩家数($00BE更改为2 来启用。如下所示,两个游戏字段出现在单人模式的背景上。


域之间没有边界,因为背景的中心区域是纯黑色。003比赛区域上方显示的值表示每个玩家清除的行数。两个玩家的唯一共同点出现在与单个玩家模式相同的位置。不幸的是,它位于正确的运动场上。正方形和其他瓷砖的颜色不正确。当玩家输掉游戏时,游戏会重新开始。

但是,如果您忽略这些问题,那么该模式是可以玩的。每个玩家可以在相应的比赛场地中独立控制棋子。当玩家输入Double,Triple或Tetris(即清除两行,三行或四行)时,对手所在比赛场的底部会出现一个缺少正方形的垃圾行。

另一个字段位于$0500第二个玩家使用A- $0060$007F通常是镜像$0040-)$005F

可能是由于繁忙的开发计划而放弃了这种有趣的模式。或者,也许是故意让他未完成。选择《俄罗斯方块》作为Nintendo Game Boy捆绑游戏的原因之一是因为它鼓励购买Game Link Cable-将两个Game Boy连接在一起以启动2个玩家对战模式的配件。这条电缆为系统增加了“社交性”元素-鼓励朋友购买游戏男孩加入其中。也许任天堂担心,如果游戏的游戏机版本具有2个玩家对战模式,那​​么刺激“游戏男孩”购买的俄罗斯方块的“广告”能力可能会减弱。

音乐和声音效果


$06F5分配表中列出的值之一后,背景音乐将打开

价值内容描述
01未使用的启动画面音乐
02实现B型模式目标
03音乐1
04音乐2
05音乐3
06音乐1快板
07音乐2快板
08音乐3快板
09祝贺画面
0A结局
0B实现B型模式目标
您可以在此处从屏幕保护程序收听未使用的音乐。在游戏本身中,屏幕保护程序屏幕没有声音。

音乐- 1 -是“一个版本的糖梅仙子之舞 ”,音乐的第三幕芭蕾舞蹈家双人舞华尔兹的“胡桃夹子”柴可夫斯基。结尾音乐是歌剧《卡门 ·乔治·比才》中的咏叹调斗牛士诗歌的变体。这些作品由田中弘和其余音乐的作曲家安排音乐2的灵感来自传统的民间传说俄罗斯歌曲。 Music-3具有神秘感,未来感和柔和感;有一段时间,这是美国任天堂客户支持的手机铃声。



为了帮助玩家在堆高接近运动场的上限时陷入恐慌状态,背景音乐的版本开始以快节奏($06- $08播放

有趣的是,在音乐作品中没有“ 查普曼 ”,这是在游戏男孩俄罗斯方块中听起来很著名的主题。根据下表,

$06F0和中录制会触发声音效果$06F1

地址价值内容描述
06F002游戏结束的帷幕
06F003火箭到底
06F101菜单选项选择
06F102菜单画面选择
06F103Tetrimino移位
06F104收到俄罗斯方块
06F105Tetrimino旋转
06F106新水平
06F107Tetrimino锁
06F108rp
06F109行清洗
06F10A行填充

游戏状态和渲染模式


在游戏过程中,游戏的当前状态由address处的整数表示$0048在大多数情况下,它的含义$01是表示玩家控制着活跃的Tetrimino。但是,将棋子锁定在适当的位置后,游戏逐渐从一个状态过渡到另一个$02状态$08,如表所示。

条件内容描述
00未分配的方向ID
01播放器控制活动的Tetrimino
02Tetrimino锁定在运动场上
03检查填充的行
04显示行清除动画
05更新行和统计信息
06检查B型模式的目标
07未使用
08创建下一个Tetrimino
09未使用
0A游戏幕更新
0B游戏状态增量

根据游戏状态,代码的分支发生在以下地址$81B2切换状态下,它跳转到分配一个的代码,该指示未指定方向。永远不会调用处理程序;但是,游戏状态可作为代码其他部分的信号。该状态允许玩家移动,旋转和降低活动的Tetrimino:如前所述,执行代码之前,人物的移动,旋转和降低例程会检查Tetrimino的新位置。将形状阻止在错误位置的唯一方法是在现有形状的顶部创建它。在这种情况下,游戏结束。如下所示,状态代码执行此检查。

81B2: LDA $0048
81B4: JSR $AC82 ; switch(playState) {
81B7: 2F 9E ; case 00: goto 9E2F; // Unassign orientationID
81B9: CF 81 ; case 01: goto 81CF; // Player controls active Tetrimino
81BB: A2 99 ; case 02: goto 99A2; // Lock Tetrimino into playfield
81BD: 6B 9A ; case 03: goto 9A6B; // Check for completed rows
81BF: 39 9E ; case 04: goto 9E39; // Display line clearing animation
81C1: 58 9B ; case 05: goto 9B58; // Update lines and statistics
81C3: F2 A3 ; case 06: goto A3F2; // B-Type goal check; Unused frame for A-Type
81C5: 03 9B ; case 07: goto 9B03; // Unused frame; Execute unfinished 2 player mode logic
81C7: 8E 98 ; case 08: goto 988E; // Spawn next Tetrimino
81C9: 39 9E ; case 09: goto 9E39; // Unused
81CB: 11 9A ; case 0A: goto 9A11; // Update game over curtain
81CD: 37 9E ; case 0B: goto 9E37; // Increment play state
; }


$00orientationID$13

9E2F: LDA #$13
9E31: STA $0042 ; orientationID = UNASSIGNED;

9E33: RTS ; return;


$00

$01

81CF: JSR $89AE ; shift Tetrimino;
81D2: JSR $88AB ; rotate Tetrimino;
81D5: JSR $8914 ; drop Tetrimino;

81D8: RTS ; return;


$02如果锁定位置正确,则将运动场的四个相关单元标记为已占用。否则,她将过渡到状态-游戏结束时的不祥之幕。

99A2: JSR $948B ; if (new position valid) {
99A5: BEQ $99B8 ; goto updatePlayfield;
; }

99A7: LDA #$02
99A9: STA $06F0 ; play curtain sound effect;

99AC: LDA #$0A
99AE: STA $0048 ; playState = UPDATE_GAME_OVER_CURTAIN;

99B0: LDA #$F0
99B2: STA $0058 ; curtainRow = -16;

99B4: JSR $E003 ; updateAudio();

99B7: RTS ; return;


$0A


窗帘是从运动场的顶部向下绘制的,每4帧下降一行。curtainRow$0058)的初始值为-16,从而在最终锁定和动画开始之间增加了0.27秒的延迟。在下面所示代码$9A21状态下的地址$0A处,访问乘法表,该表错误地显示为级别号。缩放比例curtainRow为10。此外,如上所示,$9A51如果玩家的得分不低于30,000分,则地址处的代码将开始结束动画;否则,它希望单击开始。通过为游戏状态分配值来完成代码,但是由于游戏完成,因此未调用相应的处理程序。

9A11: LDA $0058 ; if (curtainRow == 20) {
9A13: CMP #$14 ; goto endGame;
9A15: BEQ $9A47 ; }

9A17: LDA $00B1 ; if (frameCounter not divisible by 4) {
9A19: AND #$03 ; return;
9A1B: BNE $9A46 ; }

9A1D: LDX $0058 ; if (curtainRow < 0) {
9A1F: BMI $9A3E ; goto incrementCurtainRow;
; }

9A21: LDA $96D6,X
9A24: TAY ; rowIndex = 10 * curtainRow;

9A25: LDA #$00
9A27: STA $00AA ; i = 0;

9A29: LDA #$13
9A2B: STA $0042 ; orientationID = NONE;

drawCurtainRow:

9A2D: LDA #$4F
9A2F: STA ($B8),Y ; playfield[rowIndex + i] = CURTAIN_TILE;
9A31: INY
9A32: INC $00AA ; i++;
9A34: LDA $00AA
9A36: CMP #$0A ; if (i != 10) {
9A38: BNE $9A2D ; goto drawCurtainRow;
; }

9A3A: LDA $0058
9A3C: STA $0049 ; vramRow = curtainRow;

incrementCurtainRow:

9A3E: INC $0058 ; curtainRow++;

9A40: LDA $0058 ; if (curtainRow != 20) {
9A42: CMP #$14 ; return;
9A44: BNE $9A46 ; }

9A46: RTS ; return;

endGame:

9A47: LDA $00BE
9A49: CMP #$02
9A4B: BEQ $9A64 ; if (numberOfPlayers == 1) {

9A4D: LDA $0075
9A4F: CMP #$03
9A51: BCC $9A5E ; if (score[2] >= $03) {

9A53: LDA #$80
9A55: JSR $A459
9A58: JSR $9E3A
9A5B: JMP $9A64 ; select ending;
; }

9A5E: LDA $00F5 ; if (not just pressed Start) {
9A60: CMP #$10 ; return;
9A62: BNE $9A6A ; }
; }

9A64: LDA #$00
9A66: STA $0048 ; playState = INITIALIZE_ORIENTATION_ID;
9A68: STA $00F5 ; clear newly pressed buttons;

9A6A: RTS ; return;


$00

比赛场地的线会逐渐复制到VRAM中以显示它们。要复制的当前行的索引包含在vramRow$0049)中。在address $9A3C vramRow处分配了一个curtainRow,最终在渲染时使该行可见。

对VRAM的操作发生在垂直消隐间隔内,该间隔由“法律信息屏幕”部分中所述的中断处理程序识别。它调用下面显示的子例程(在中断处理程序的注释中标记为render())。渲染模式类似于游戏模式。它存储在该地址,并且可以具有以下值之一:

804B: LDA $00BD
804D: JSR $AC82 ; switch(renderMode) {
8050: B1 82 ; case 0: goto 82B1; // Legal and title screens
8052: DA 85 ; case 1: goto 85DA; // Menu screens
8054: 44 A3 ; case 2: goto A344; // Congratulations screen
8056: EE 94 ; case 3: goto 94EE; // Play and demo
8058: 95 9F ; case 4: goto 9F95; // Ending animation
; }


$00BD

价值内容描述
00屏幕合法 信息和屏幕保护程序
01菜单画面
02祝贺画面
03游戏和演示
04结束动画

渲染模式的一部分$03如下所示。如下所示,它在VRAM中传递了一个带有index的运动场行。如果大于20,则例程不执行任何操作。)包含小字节序格式的VRAM地址,对应于正常模式下运动场的显示行偏移了6,而未完成模式2运动员对则偏移了运动场的-2和12。该表的字节是值列表的一部分,这些值错误地显示为29级之后的级编号。每个地址的相邻低字节和高字节分别获得,并实质上组合为一个16位地址,该地址在复制周期中使用。在子例程的末尾执行增量。

952A: JSR $9725 ; copyPlayfieldRowToVRAM();
952D: JSR $9725 ; copyPlayfieldRowToVRAM();
9530: JSR $9725 ; copyPlayfieldRowToVRAM();
9533: JSR $9725 ; copyPlayfieldRowToVRAM();


copyPlayfieldRowToVRAM()vramRowvramRow

9725: LDX $0049 ; if (vramRow > 20) {
9727: CPX #$15 ; return;
9729: BPL $977E ; }

972B: LDA $96D6,X
972E: TAY ; playfieldAddress = 10 * vramRow;

972F: TXA
9730: ASL
9731: TAX
9732: INX ; high = vramPlayfieldRows[vramRow * 2 + 1];
9733: LDA $96EA,X
9736: STA $2006
9739: DEX

973A: LDA $00BE
973C: CMP #$01
973E: BEQ $975E ; if (numberOfPlayers == 2) {

9740: LDA $00B9
9742: CMP #$05
9744: BEQ $9752 ; if (leftPlayfield) {

9746: LDA $96EA,X
9749: SEC
974A: SBC #$02
974C: STA $2006 ; low = vramPlayfieldRows[vramRow * 2] - 2;

974F: JMP $9767 ; } else {

9752: LDA $96EA,X
9755: CLC
9756: ADC #$0C
9758: STA $2006 ; low = vramPlayfieldRows[vramRow * 2] + 12;

975B: JMP $9767 ; } else {

975E: LDA $96EA,X
9761: CLC
9762: ADC #$06 ; low = vramPlayfieldRows[vramRow * 2] + 6;
9764: STA $2006 ; }

; vramAddress = (high << 8) | low;

9767: LDX #$0A
9769: LDA ($B8),Y
976B: STA $2007
976E: INY ; for(i = 0; i < 10; i++) {
976F: DEX ; vram[vramAddress + i] = playfield[playfieldAddress + i];
9770: BNE $9769 ; }

9772: INC $0049 ; vramRow++;
9774: LDA $0049 ; if (vramRow < 20) {
9776: CMP #$14 ; return;
9778: BMI $977E ; }

977A: LDA #$20
977C: STA $0049 ; vramRow = 32;

977E: RTS ; return;


vramPlayfieldRows$96EA

vramRow。如果该值达到20,则将其分配为32,这意味着副本已完全完成。如上所示,每帧仅复制4行。

状态处理程序$03负责识别已完成的台词并将其从比赛场地中删除。在4次单独的通话中,他扫描了[−2, 1]Tetrimino中心附近的线偏移(tetrimino的所有正方形的坐标都在此间隔内)。已完成行的索引存储在$004A-处$004D;记录的索引0用于指示在此过程中未找到完整的行。该处理程序如下所示。开头的检查不允许在将比赛场地的线路转移到VRAM时执行处理程序(状态处理程序)

9A6B: LDA $0049
9A6D: CMP #$20 ; if (vramRow < 32) {
9A6F: BPL $9A74 ; return;
9A71: JMP $9B02 ; }

9A74: LDA $0041 ; rowY = tetriminoY - 2;
9A76: SEC
9A77: SBC #$02 ; if (rowY < 0) {
9A79: BPL $9A7D ; rowY = 0;
9A7B: LDA #$00 ; }

9A7D: CLC
9A7E: ADC $0057
9A80: STA $00A9 ; rowY += lineIndex;

9A82: ASL
9A83: STA $00A8
9A85: ASL
9A86: ASL
9A87: CLC
9A88: ADC $00A8
9A8A: STA $00A8 ; rowIndex = 10 * rowY;

9A8C: TAY
9A8D: LDX #$0A
9A8F: LDA ($B8),Y
9A91: CMP #$EF ; for(i = 0; i < 10; i++) {
9A93: BEQ $9ACC ; if (playfield[rowIndex + i] == EMPTY_TILE) {
9A95: INY ; goto rowNotComplete;
9A96: DEX ; }
9A97: BNE $9A8F ; }

9A99: LDA #$0A
9A9B: STA $06F1 ; play row completed sound effect;

9A9E: INC $0056 ; completedLines++;

9AA0: LDX $0057
9AA2: LDA $00A9
9AA4: STA $4A,X ; lines[lineIndex] = rowY;

9AA6: LDY $00A8
9AA8: DEY
9AA9: LDA ($B8),Y
9AAB: LDX #$0A
9AAD: STX $00B8
9AAF: STA ($B8),Y
9AB1: LDA #$00
9AB3: STA $00B8
9AB5: DEY ; for(i = rowIndex - 1; i >= 0; i--) {
9AB6: CPY #$FF ; playfield[i + 10] = playfield[i];
9AB8: BNE $9AA9 ; }

9ABA: LDA #$EF
9ABC: LDY #$00
9ABE: STA ($B8),Y
9AC0: INY ; for(i = 0; i < 10; i++) {
9AC1: CPY #$0A ; playfield[i] = EMPTY_TILE;
9AC3: BNE $9ABE ; }

9AC5: LDA #$13
9AC7: STA $0042 ; orientationID = UNASSIGNED;

9AC9: JMP $9AD2 ; goto incrementLineIndex;

rowNotComplete:

9ACC: LDX $0057
9ACE: LDA #$00
9AD0: STA $4A,X ; lines[lineIndex] = 0;

incrementLineIndex:

9AD2: INC $0057 ; lineIndex++;

9AD4: LDA $0057 ; if (lineIndex < 4) {
9AD6: CMP #$04 ; return;
9AD8: BMI $9B02 ; }

9ADA: LDY $0056
9ADC: LDA $9B53,Y
9ADF: CLC
9AE0: ADC $00BC
9AE2: STA $00BC ; totalGarbage += garbageLines[completedLines];

9AE4: LDA #$00
9AE6: STA $0049 ; vramRow = 0;
9AE8: STA $0052 ; clearColumnIndex = 0;

9AEA: LDA $0056
9AEC: CMP #$04
9AEE: BNE $9AF5 ; if (completedLines == 4) {
9AF0: LDA #$04 ; play Tetris sound effect;
9AF2: STA $06F1 ; }

9AF5: INC $0048 ; if (completedLines > 0) {
9AF7: LDA $0056 ; playState = DISPLAY_LINE_CLEARING_ANIMATION;
9AF9: BNE $9B02 ; return;
; }

9AFB: INC $0048 ; playState = UPDATE_LINES_AND_STATISTICS;

9AFD: LDA #$07
9AFF: STA $06F1 ; play piece locked sound effect;

9B02: RTS ; return;


vramRow$03在每一帧中调用)。如果检测到已填充的行,则将其vramRow重置为0,这将强制完成传输。

lineIndex$00A9)初始化为0值,并且其增量在每次通过中执行。

$0A使用地址乘法表的游戏状态和游戏场复制例程不同,该程序$96D6使用移位和加法以10 $9A82乘以一个开始的块rowY

rowIndex = (rowY << 1) + (rowY << 3); // rowIndex = 2 * rowY + 8 * rowY;

这样做是因为它rowY受到间隔的限制[0, 20],并且乘法表仅覆盖[0, 19]。行扫描可能会超出比赛场地的范围。但是,如前所述,游戏会初始化$0400- $04FF使用一个值$EF(空白图块),在运动场的地板下方创建5条以上的其他隐藏线。

以2开头的方块$9ADA是不完整模式2 Player Versus的一部分。如上所述,清除行会给对手的比赛场地增加碎屑。垃圾行的数量由地址处的表确定$9B53地址处的循环将填充行上方的材料向下移动一行。他利用了这样一个事实,即连续序列中的每一行都彼此隔开10个字节。下一个循环清除第一行。行清除动画是在游戏状态期间执行的,但如下所示,它在完全为空的游戏状态处理程序中不会发生。

9B53: 00 ; no cleared lines
9B54: 00 ; Single
9B55: 01 ; Double
9B56: 02 ; Triple
9B57: 04 ; Tetris


$9AA6

$04

9E39: RTS ; return;

相反,在游戏状态期间$04,将执行渲染模式的下一个分支$03对于未完成的模式2 Player Versus,需要镜像值。该子例程如下所示。它在每个帧中都被调用,但是开始时的条件允许它仅在每四个帧中执行一次。在每遍中,它循环浏览已完成行的索引列表,并清除这些行中的2列,从中心列向外移动。以与复制字段例程中所示的相同方式构造16位VRAM地址。但是,在这种情况下,它将执行从下表中获得的列索引的偏移量。

94EE: LDA $0068
94F0: CMP #$04
94F2: BNE $9522 ; if (playState == DISPLAY_LINE_CLEARING_ANIMATION) {

94F4: LDA #$04
94F6: STA $00B9 ; leftPlayfield = true;

94F8: LDA $0072
94FA: STA $0052
94FC: LDA $006A
94FE: STA $004A
9500: LDA $006B
9502: STA $004B
9504: LDA $006C
9506: STA $004C
9508: LDA $006D
950A: STA $004D
950C: LDA $0068
950E: STA $0048 ; mirror values;

9510: JSR $977F ; updateLineClearingAnimation();

; ...
; }


leftPlayfield

updateLineClearingAnimation()

977F: LDA $00B1 ; if (frameCounter not divisible by 4) {
9781: AND #$03 ; return;
9783: BNE $97FD ; }

9785: LDA #$00 ; for(i = 0; i < 4; i++) {
9787: STA $00AA ; rowY = lines[i];
9789: LDX $00AA ; if (rowY == 0) {
978B: LDA $4A,X ; continue;
978D: BEQ $97EB ; }

978F: ASL
9790: TAY
9791: LDA $96EA,Y
9794: STA $00A8 ; low = vramPlayfieldRows[2 * rowY];

9796: LDA $00BE ; if (numberOfPlayers == 2) {
9798: CMP #$01 ; goto twoPlayers;
979A: BNE $97A6 ; }

979C: LDA $00A8
979E: CLC
979F: ADC #$06
97A1: STA $00A8 ; low += 6;

97A3: JMP $97BD ; goto updateVRAM;

twoPlayers:

97A6: LDA $00B9
97A8: CMP #$04
97AA: BNE $97B6 ; if (leftPlayfield) {

97AC: LDA $00A8
97AE: SEC
97AF: SBC #$02
97B1: STA $00A8 ; low -= 2;

97B3: JMP $97BD ; } else {

97B6: LDA $00A8
97B8: CLC
97B9: ADC #$0C ; low += 12;
97BB: STA $00A8 ; }

updateVRAM:

97BD: INY
97BE: LDA $96EA,Y
97C1: STA $00A9
97C3: STA $2006
97C6: LDX $0052 ; high = vramPlayfieldRows[2 * rowY + 1];
97C8: LDA $97FE,X
97CB: CLC ; rowAddress = (high << 8) | low;
97CC: ADC $00A8
97CE: STA $2006 ; vramAddress = rowAddress + leftColumns[clearColumnIndex];
97D1: LDA #$FF
97D3: STA $2007 ; vram[vramAddress] = 255;

97D6: LDA $00A9
97D8: STA $2006
97DB: LDX $0052 ; high = vramPlayfieldRows[2 * rowY + 1];
97DD: LDA $9803,X
97E0: CLC ; rowAddress = (high << 8) | low;
97E1: ADC $00A8
97E3: STA $2006 ; vramAddress = rowAddress + rightColumns[clearColumnIndex];
97E6: LDA #$FF
97E8: STA $2007 ; vram[vramAddress] = 255;

97EB: INC $00AA
97ED: LDA $00AA
97EF: CMP #$04
97F1: BNE $9789 ; }

97F3: INC $0052 ; clearColumnIndex++;
97F5: LDA $0052 ; if (clearColumnIndex < 5) {
97F7: CMP #$05 ; return;
97F9: BMI $97FD ; }

97FB: INC $0048 ; playState = UPDATE_LINES_AND_STATISTICS;

97FD: RTS ; return;




97FE: 04 03 02 01 00 ; left columns
9803: 05 06 07 08 09 ; right columns


对于清洁动画,需要5次通过。然后代码进入下一个游戏状态。

游戏状态处理程序$05包含“行和统计”部分中描述的代码。处理程序以以下代码结尾:该变量直到游戏状态结束才重置,此后用于更新行数和得分的总数。此序列允许执行一个有趣的错误。在演示模式下,您需要等待直到游戏收集了整行,然后快速按开始,直到清除系列的动画结束。游戏将返回到屏幕保护程序,但是如果您选择正确的时间,则将保存该值。现在您可以在A-Type模式下开始游戏。当锁定第一个图形时,游戏状态处理程序

9C9E: LDA #$00
9CA0: STA $0056 ; completedLines = 0;

9CA2: INC $0048 ; playState = B_TYPE_GOAL_CHECK;

9CA4: RTS ; return;


completedLines$05completedLines$03开始扫描完成的行。他不会找到它们,而是completedLines保持不变。最后,当满足游戏状态时,$05总行数和得分将增加,就像您已经对它们进行了得分一样。

最简单的方法是获取最大金额,等待演示收集俄罗斯方块(演示中有2个)。一旦看到屏幕闪烁,请单击“开始”。


开始新游戏后,屏幕将继续闪烁。这一切都要归功于中断处理程序调用的以下代码。实际上,如果您让第一个数字自动下降到比赛场地的底部,分数将增加更大的值,因为)还将从演示中保存其值。即使对于演示没有填充一行的情况,也是如此。直到按下“向下”按钮,它才会重置。此外,如果在演示模式下清除Tetris组合行的动画期间单击“开始”,然后等待演示再次开始,则演示中不仅会计算Tetris的点数,还会混淆整个时间。结果,演示将失去游戏。在游戏结束后,您可以单击“开始”返回到屏幕保护程序。

9673: LDA #$3F
9675: STA $2006
9678: LDA #$0E
967A: STA $2006 ; prepare to modify background tile color;

967D: LDX #$00 ; color = DARK_GRAY;

967F: LDA $0056
9681: CMP #$04
9683: BNE $9698 ; if (completedLines == 4) {

9685: LDA $00B1
9687: AND #$03
9689: BNE $9698 ; if (frameCounter divisible by 4) {

968B: LDX #$30 ; color = WHITE;

968D: LDA $00B1
968F: AND #$07
9691: BNE $9698 ; if (frameCounter divisible by 8) {

9693: LDA #$09
9695: STA $06F1 ; play clear sound effect;

; }
; }
; }

9698: STX $2007 ; update background tile color;


holdDownPoints$004FholdDownPoints



游戏状态$06对B型游戏执行目标检查。在A型模式下,它实质上是未使用的帧。

游戏状态仅$07包含不完整的2人对战逻辑。在单人游戏模式下,其行为类似于未使用的帧。

游戏状态$08在“创建Tetrimino”和“选择Tetrimino”部分中进行了讨论。不使用

游戏状态$09$0B增加游戏状态,但看起来也没用。

最后,游戏的主要周期:

; while(true) {

8138: JSR $8161 ; branchOnGameMode();

813B: CMP $00A7 ; if (vertical blanking interval wait requested) {
813D: BNE $8142 ; waitForVerticalBlankingInterval();
813F: JSR $AA2F ; }

8142: LDA $00C0
8144: CMP #$05
8146: BNE $815A ; if (gameMode == DEMO) {

8148: LDA $00D2
814A: CMP #$DF
814C: BNE $815A ; if (reached end of demo table) {

814E: LDA #$DD
8150: STA $00D2 ; reset demo table index;

8152: LDA #$00
8154: STA $00B2 ; clear upper byte of frame counter;

8156: LDA #$01
8158: STA $00C0 ; gameMode = TITLE_SCREEN;
; }
; }
815A: JMP $8138 ; }

Source: https://habr.com/ru/post/zh-CN420725/


All Articles