Wie ich AI beigebracht habe, Tetris für NES zu spielen. Teil 1: Analyse des Spielcodes

In diesem Artikel werde ich die täuschend einfache Mechanik von Nintendo Tetris untersuchen und im zweiten Teil erklären, wie ich eine KI erstellt habe, die diese Mechanik ausnutzt.


Probieren Sie es selbst aus


Über das Projekt


Für diejenigen, denen die Ausdauer, Geduld und Zeit fehlt, um Nintendo Tetris zu meistern, habe ich eine KI entwickelt, die alleine spielen kann. Du kannst endlich Level 30 und noch weiter erreichen. Sie werden sehen, wie Sie die maximale Anzahl von Punkten erreichen und die endlose Veränderung von Zeilenzählern, Ebenen und Statistiken beobachten können. Sie erfahren, welche Farben auf Ebenen erscheinen, über die eine Person nicht klettern konnte. Sehen Sie, wie weit Sie gehen können.

Anforderungen


Zum Ausführen der KI benötigen Sie einen universellen NES / Famicom FCEUX-Emulator . Künstliche Intelligenz wurde für FCEUX 2.2.2 entwickelt , die neueste Version des Emulators zum Zeitpunkt des Schreibens.

Sie benötigen außerdem die Nintendo Tetris ROM-Datei (US-Version). Versuchen Sie es bei Google zu suchen.

Herunterladen


Entpacken Sie lua/NintendoTetrisAI.lua aus dieser Quell-Zip-Datei .

Starten


Starten Sie FCEUX. Wählen Sie im Menü Datei | ROM öffnen ... Wählen Sie im Dialogfeld Datei öffnen die Nintendo Tetris ROM-Datei aus und klicken Sie auf Öffnen. Das Spiel beginnt.

Wählen Sie im Menü Datei | Lua | Neues Lua-Skriptfenster ... Geben Sie im Lua-Skriptfenster den Pfad zu NintendoTetrisAI.lua oder klicken Sie auf die Schaltfläche Durchsuchen, um ihn zu finden. Klicken Sie danach auf Ausführen.

Das Skript auf Lua leitet Sie zum ersten Bildschirm des Menüs weiter. Lassen Sie die Art des Spiels A-Type, und Sie können jede Musik auswählen. Auf langsamen Computern kann Musik sehr ruckartig abgespielt werden, dann sollten Sie sie ausschalten. Drücken Sie Start (Eingabetaste), um zum nächsten Menübildschirm zu gelangen. Im zweiten Menü können Sie mit den Pfeiltasten die Startstufe ändern. Klicken Sie auf Start, um das Spiel zu starten. Und hier wird die KI die Kontrolle übernehmen.

Wenn Sie nach Auswahl einer Ebene auf dem zweiten Menübildschirm die Gamepad-Taste A gedrückt halten (Sie können das Tastaturlayout im Menü Config | Input ... ändern) und Start drücken, ist die Anfangsstufe 10 höher als der ausgewählte Wert. Das maximale Einstiegsniveau ist neunzehnte.

Konfiguration


Öffnen Sie das Lua-Skript in einem Texteditor, um das Spiel schneller laufen zu lassen. Suchen Sie am Anfang der Datei die folgende Zeile.

PLAY_FAST = false

Ersetzen Sie false durch true wie unten gezeigt.

PLAY_FAST = true

Speichern Sie die Datei. Klicken Sie dann im Lua-Skriptfenster auf die Schaltfläche Neustart.

Nintendo Tetris Mechanics


Beschreibung von Tetrimino


Jede Tetrimino-Figur entspricht einem Einzelbuchstaben, der ihrer Form ähnelt.


Nintendo Tetris-Designer legen die oben gezeigte Tetrimino-Reihenfolge willkürlich fest. Die Figuren werden in der Ausrichtung angezeigt, in der sie auf dem Bildschirm angezeigt werden, und die Schaltung erzeugt ein nahezu symmetrisches Bild (möglicherweise wird deshalb diese Reihenfolge gewählt). Der Sequenzindex gibt jedem Tetrimino eine eindeutige numerische ID. Sequenz- und Typkennungen sind auf Programmierebene wichtig. Darüber hinaus manifestieren sie sich in der Reihenfolge der im Statistikfeld angezeigten Zahlen (siehe unten).


Die 19 im Nintendo Tetris Tetrimino verwendeten Ausrichtungen sind in einer Tabelle codiert, die sich bei $8A9C des NES-Konsolenspeichers befindet. Jede Figur wird als eine Folge von 12 Bytes dargestellt, die in Tripel (Y, tile, X) , die jedes Quadrat in der Figur beschreiben. Die obigen Hex-Werte von Koordinaten über $7F bezeichnen negative ganze Zahlen ( $FF= −1 und $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


Am Ende der Tabelle befindet sich ein nicht verwendeter Datensatz, der möglicherweise die Möglichkeit bietet, eine weitere Ausrichtung hinzuzufügen. In verschiedenen Teilen des Codes gibt $13 an, dass der Orientierungskennung des aktiven Tetriminos kein Wert zugewiesen wurde.

Zur Erleichterung des Lesens sind die Koordinaten der Quadrate in Dezimalzahl unten gezeigt.

-- { { 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)


Alle Orientierungen werden in eine 5 × 5-Matrix gelegt.


In der obigen Abbildung gibt das weiße Quadrat die Mitte der Matrix an, den Bezugspunkt für die Drehung der Abbildung.

Die Orientierungstabelle ist unten grafisch dargestellt.


Die Orientierungskennung (Tabellenindex) wird in der oberen rechten Ecke jeder Matrix hexadezimal angezeigt. Die für dieses Projekt erfundene Mnemonik wird in der oberen linken Ecke angezeigt. u , r , d , l , h und v sind Abkürzungen für „oben, rechts, unten, links, horizontal und vertikal“. Zum Beispiel ist es einfacher, die Ausrichtung von Jd als $07 .

Matrizen, die die Ausrichtung der Figuren während der Erstellung enthalten, sind mit einem weißen Rahmen markiert.

Tetrimino I, S und Z konnten 4 verschiedene Ausrichtungen erhalten, aber die Schöpfer von Nintendo Tetris beschlossen, sich auf zwei zu beschränken. Außerdem sind Zv und Sv keine idealen Spiegelbilder voneinander. Beide entstehen durch Drehen gegen den Uhrzeigersinn, was zu einem Ungleichgewicht führt.

Die Orientierungstabelle enthält auch Kachelwerte für jedes Quadrat in jeder orientierten Figur. Bei sorgfältiger Untersuchung wird jedoch deutlich, dass die Werte für eine Tetrimino-Art immer gleich sind.

T.J.Z.O.S.L.Ich
7B7D7C7B7D7C7B

Die Kachelwerte sind die Indizes der (Pseudofarbtabelle) des unten gezeigten Musters.


Die Kacheln $7B , $7C und $7D befinden sich direkt unter "ATIS" aus dem Wort "STATISTICS". Dies sind die drei Arten von Quadraten, aus denen Tetrimino hergestellt wird.

Für Neugierige werde ich sagen, dass Strauße und Pinguine am Ende des B-Typ-Modus verwendet werden. Dieses Thema wird im Abschnitt "Beenden" ausführlich behandelt.

Unten sehen Sie das Ergebnis der Änderung des ROM nach dem Ersetzen von $7B durch $29 . Das Herz ist die Kachel unter dem P-Symbol in der Mustertabelle für alle Ausrichtungen T.


Herzplättchen bleiben auch nach dem Einrasten der modifizierten Ts auf dem Spielfeld. Wie unten im Abschnitt „Erstellen von Tetrimino“ angegeben, bedeutet dies, dass das Spielfeld die tatsächlichen Werte der Kachelindizes des gespielten Tetrimino speichert.

Spielprogrammierer ermöglichten die Verwendung von 4 separaten Kacheln für jede Figur und nicht nur eines unveränderlichen Quadrattyps. Dies ist eine nützliche Funktion, mit der Sie das Erscheinungsbild des Spiels ändern können. Die Mustertabelle bietet viel Platz für neue Kacheln, die jedem Tetrimino ein einzigartiges Aussehen verleihen können.

Die Koordinaten der Quadrate sind sehr einfach zu manipulieren. Beispielsweise wird unten eine modifizierte Version der ersten vier Tripel in der Orientierungstabelle gezeigt.

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

Diese Änderung ähnelt der folgenden:

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

Das Ergebnis ist ein gespaltenes Tetrimino.


Wenn Sie ein geteiltes Tetrimino bewegen, dürfen seine Quadrate nicht über die Grenzen des Spielfelds hinausgehen und nicht durch zuvor festgelegte Figuren hindurchgehen. Außerdem verbietet das Spiel eine Drehung in dieser Ausrichtung, wenn ein Quadrat außerhalb der Grenzen des Spielfelds liegt oder wenn das Quadrat ein bereits liegendes Quadrat überlappt.

Das geteilte Tetrimino ist an Ort und Stelle verriegelt, wenn eines seiner Quadrate unterstützt wird. Wenn die Figur blockiert ist, hängen die in der Luft hängenden Quadrate weiter.

Das Spiel behandelt geteilte Tetriminos wie jede normale Figur. Dies macht uns verständlich, dass es keine zusätzliche Tabelle gibt, in der die Metadaten der Figuren gespeichert sind. Zum Beispiel könnte es einen Tisch geben, der die Größe des Begrenzungsrahmens jeder Ausrichtung speichert, um auf Kollisionen mit dem Umfang des Spielfelds zu prüfen. Eine solche Tabelle wird jedoch nicht verwendet. Stattdessen überprüft das Spiel einfach alle vier Felder, bevor die Form bearbeitet wird.

Außerdem können die Koordinaten der Quadrate beliebige Werte sein. Sie sind nicht auf das Intervall [−2, 2] . Werte, die dieses Intervall stark überschreiten, führen natürlich zu nicht zutreffenden Zahlen, die nicht auf das Spielfeld passen. Noch wichtiger ist, dass, wie im Abschnitt „Spielzustände und Rendering-Modi“ angegeben, der Mechanismus zum Reinigen gefüllter Linien beim Verriegeln einer Figur nur die Verschiebungen von Zeilen von –2 bis 1 vom zentralen Quadrat der Figur abtastet. Ein Quadrat mit einer y Koordinate außerhalb dieses Intervalls wird nicht erkannt.

Tetrimino-Rotation


In einer grafischen Darstellung der Orientierungstabelle besteht die Drehung darin, von einer Matrix zu einer der Matrizen links oder rechts zu wechseln und gegebenenfalls die Reihe zu übertragen. Dieses Konzept ist in einer Tabelle bei $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


Zur Verdeutlichung verschieben wir jede Spalte aus dieser Tabelle in die Zeile der folgenden Tabelle.
TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
Gegen den UhrzeigersinnTlTuTrTdJdJlJuJrZvZhOSvShLuLrLdLlIhIv
Im UhrzeigersinnTrTdTlTuJuJrJdJlZvZhOSvShLdLlLuLrIhIv

Die Mnemonik in den Überschriften oben kann als Sequenzindex oder Verteilungsschlüssel interpretiert werden. Wenn Sie beispielsweise Tu gegen den Uhrzeigersinn drehen, erhalten Sie Tl , und wenn Sie Tu im Uhrzeigersinn drehen, erhalten Sie Tr .

Die Rotationstabelle codiert kettengebundene Sequenzen von Orientierungs-IDs; Daher können wir die Aufzeichnungen so ändern, dass die Rotation einen Tetrimino-Typ in einen anderen umwandelt. Diese Technik kann möglicherweise verwendet werden, um eine nicht verwendete Zeile in der Ausrichtungstabelle zu nutzen.

Vor der Rotationstabelle befindet sich ein Code für den Zugriff darauf.

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;


Bei Drehung gegen den Uhrzeigersinn wird der Index der Drehtabelle durch Verdoppeln der Orientierungs-ID subtrahiert. Durch Hinzufügen von 1 erhalten wir den Rotationsindex im Uhrzeigersinn.

Die x , y Koordinaten und die Orientierungs-ID des aktuellen Tetriminos werden unter den Adressen $0040 , $0041 bzw. $0042 gespeichert.

Der Code verwendet eine temporäre Variable, um die Orientierungs-ID zu sichern. Später, nach dem Ändern der Ausrichtung, überprüft der Code, ob sich alle vier Quadrate innerhalb der Grenzen des Spielfelds befinden und dass keines von ihnen die bereits liegenden Quadrate überlappt (der Bestätigungscode befindet sich bei $948B unter dem oben gezeigten Codefragment). Wenn die neue Ausrichtung nicht korrekt ist, wird die ursprüngliche wiederhergestellt, sodass der Spieler die Figur nicht drehen kann.

Der NES-Controller zählt mit einem Kreuz und verfügt über acht Tasten, deren Status durch das Adressbit $00B6 .

76543210
A.B.Wählen SieStarten SieAufRunterLinksRechts

Zum Beispiel enthält $00B6 den Wert $81 während der Spieler A und Links hält.

Auf der anderen Seite meldet $00B5 , wann die Tasten gedrückt wurden. Die Bits $00B5 sind nur während einer Iteration der Spielschleife (1 gerenderter Frame) wahr. Der Code verwendet $00B5 um auf das Drücken von A und B zu reagieren. Jeder von ihnen muss freigegeben werden, bevor er erneut verwendet werden kann.

$00B5 und $00B6 sind Spiegel von $00F5 und $00F6 . Der Code in den folgenden Abschnitten verwendet diese Adressen austauschbar.

Erstellen Sie Tetrimino


Das Nintendo Tetris-Spielfeld besteht aus einer Matrix mit 22 Zeilen und 10 Spalten, sodass die beiden obersten Zeilen vor dem Player verborgen sind.


Wie im folgenden Code gezeigt, befindet sich eine Tetrimino-Figur beim Erstellen immer in den Koordinaten (5, 0) Spielfelds.

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


Unten ist eine 5 × 5-Matrix, die diesem Punkt überlagert ist.


Keine der Erstellungsmatrizen hat Quadrate über dem Startpunkt. Das heißt, wenn ein Tetrimino erstellt wird, werden alle vier Felder sofort für den Spieler sichtbar. Wenn der Spieler das Stück jedoch schnell dreht, bevor es Zeit zum Ablegen hat, wird ein Teil des Stücks vorübergehend in den ersten beiden Zeilen des Spielfelds ausgeblendet.

Normalerweise denken wir, dass das Spiel endet, wenn der Haufen die Spitze erreicht. Tatsächlich ist dies jedoch nicht ganz richtig. Das Spiel endet, wenn es nicht mehr möglich ist, das nächste Stück zu erstellen. Das heißt, vor dem Erscheinen der Figur sollten alle vier Zellen des Spielfelds, die den Positionen der Quadrate des erzeugten Tetriminos entsprechen, frei sein. Die Figur kann so fixiert sein, dass ein Teil ihrer Quadrate in negativ nummerierten Linien erscheint und das Spiel nicht endet. In Nintendo Tetris sind negative Linien jedoch eine Abstraktion, die sich nur auf aktives Tetrimino bezieht. Nachdem die Figur blockiert ist (lügt), werden nur Quadrate in Zeilen von Null und mehr in das Feld geschrieben. Konzeptionell stellt sich heraus, dass negativ nummerierte Zeilen nach dem Blockieren automatisch gelöscht werden. In Wirklichkeit speichert das Spiel diese Daten einfach nicht und schneidet die oberen Teile der Figuren ab.

Der sichtbare Bereich des Spielfelds 20 × 10 wird zeilenweise bei $0400 gespeichert, jedes Byte enthält den Wert der Hintergrundkachel. Leere Zellen werden durch die $EF Kachel gekennzeichnet, ein durchgehendes schwarzes Quadrat.

Beim Erstellen einer Form werden drei Nachschlagetabellen verwendet. Wenn es eine beliebige Orientierungs-ID gibt, gibt uns die Tabelle bei $9956 die Orientierungs-ID beim Erstellen des entsprechenden Tetrimino-Typs.

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


Es ist einfacher, dies in der Tabelle anzuzeigen.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TdTdTdTdJdJdJdJdZhZhOShShLdLdLdLdIhIh

Zum Beispiel sind alle Orientierungen von J an Jd .

Die Tabelle bei $993B Mrd. $993B enthält den Tetrimino-Typ für die angegebene Orientierungs-ID.

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


Der Klarheit halber werde ich alles in tabellarischer Form zeigen.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TTTTJJJJZZOSSLLLLII

Wir werden uns die dritte Suchtabelle im nächsten Abschnitt ansehen.

Tetrimino Auswahl


Nintendo Tetris verwendet in seiner Fibonacci-Konfiguration ein 16-Bit-Linear-Feedback-Schieberegister (LFSR) als Pseudozufallszahlengenerator (PRNG). Der 16-Bit-Wert wird als Big-Endian unter den Adressen $0017 - $0018 gespeichert. Eine beliebige Zahl von $8988 als $8988 verwendet.

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


Jede nachfolgende Pseudozufallszahl wird wie folgt erzeugt: Der Wert wird als 17-Bit-Zahl wahrgenommen, und das höchstwertige Bit wird durch Ausführen von XOR für die Bits 1 und 9 erhalten. Dann wird der Wert nach rechts verschoben, wobei das niedrigstwertige Bit verworfen wird.


Dieser Prozess findet bei $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


Interessanterweise können die Parameter des obigen Unterprogramms so eingestellt werden, dass die aufrufende Funktion die Breite des Schieberegisters und die Adresse angeben kann, an der es sich im Speicher befindet. Es werden jedoch überall dieselben Parameter verwendet, sodass wir davon ausgehen können, dass die Entwickler diesen Code irgendwo ausgeliehen haben.

Für diejenigen, die den Algorithmus weiter modifizieren möchten, habe ich ihn in Java geschrieben.

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

Und all dieser Code kann in einer Zeile zusammengefasst werden.

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

Dieses PRNG erzeugt kontinuierlich und deterministisch 32.767 eindeutige Werte, wobei jeder Zyklus vom ursprünglichen Samen ausgeht. Dies ist weniger als die Hälfte der möglichen Zahlen, die in das Register passen, und jeder Wert in diesem Satz kann als Startwert verwendet werden. Viele der Werte außerhalb der Menge bilden eine Kette, die schließlich zu einer Zahl aus der Menge führt. Einige Anfangszahlen führen jedoch zu einer unendlichen Folge von Nullen.

Um die Leistung dieses PRNG grob zu bewerten, habe ich eine grafische Darstellung der Werte erstellt, die basierend auf einem Satz mit RANDOM.ORG erstellt wurden .


Bei der Erstellung des Bildes wurde PRNG als Pseudozufallszahlengenerator anstelle von 16-Bit-Ganzzahlen verwendet. Jedes Pixel wird basierend auf dem Wert von Bit 0 gefärbt. Das Bild hat eine Größe von 128 × 256, dh es deckt die gesamte Sequenz ab.

Abgesehen von den kaum wahrnehmbaren Streifen auf der oberen und linken Seite sieht es zufällig aus. Es erscheinen keine offensichtlichen Muster.

Nach dem Start verschiebt der PRNG ständig das Register und arbeitet mindestens einmal pro Frame. Dies geschieht nicht nur auf dem Begrüßungs- und Menübildschirm, sondern auch, wenn das Tetrimino zwischen den Vorgängen zum Erstellen von Formen liegt. Das heißt, die folgende Abbildung hängt von der Anzahl der Frames ab, die der Spieler benötigt, um die Figur zu platzieren. Tatsächlich beruht das Spiel auf der Zufälligkeit der Handlungen der Person, die mit ihm interagiert.

Während der Erstellung der Figur wird der Code unter der Adresse $9907 , wodurch der Typ der neuen Figur ausgewählt wird.

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;


Unter der Adresse $001A ein Zähler für die Anzahl der beim Einschalten erstellten Zahlen $001A . Das Inkrementieren des Zählers wird von der ersten Zeile des Unterprogramms ausgeführt, und da es sich um einen Einzelbytezähler handelt, kehrt er nach jeweils 256 Teilen wieder auf Null zurück. Da der Zähler zwischen den Spielen nicht zurückgesetzt wird, wirkt sich der Verlauf der vorherigen Spiele auf den Figurenauswahlprozess aus. Dies ist eine andere Art und Weise, wie das Spiel den Spieler als Zufallsquelle verwendet.

Die Routine konvertiert das höchstwertige Byte der Pseudozufallszahl ( $0017 ) in einen Tetrimino-Typ und verwendet es als Index der Tabelle bei $994E , um den Typ in die $994E Orientierungs-ID zu konvertieren.

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


In der ersten Konvertierungsphase wird der Zähler der erstellten Zahlen zum oberen Byte hinzugefügt. Dann wird eine Maske angewendet, um nur die unteren 3 Bits zu speichern. Wenn das Ergebnis nicht 7 ist, ist dies der richtige Tetrimino-Typ. Wenn es nicht mit der zuvor ausgewählten Zahl übereinstimmt, wird die Zahl als Index in der Tabelle zum Erstellen von Zahlen verwendet. Andernfalls wird die nächste Pseudozufallszahl generiert und die Maske wird angewendet, um die unteren 3 Bits des oberen Bytes zu erhalten, und dann wird die vorherige ID zur Formerstellungsorientierung hinzugefügt. Schließlich wird eine Modulo-Operation ausgeführt, um den richtigen Tetrimino-Typ zu erhalten, der als Index in der Formerstellungstabelle verwendet wird.

Da der Prozessor die Division mit dem Rest nicht unterstützt, wird dieser Operator durch wiederholtes Subtrahieren von 7 emuliert, bis das Ergebnis kleiner als 7 wird. Die Division mit dem Rest wird auf die Summe des oberen Bytes mit der angewendeten Maske und auf die ID der vorherigen Formerstellungsorientierung angewendet. Der Maximalwert dieser Summe ist 25. Das heißt, um sie auf den Rest von 4 zu reduzieren, sind nur 3 Iterationen erforderlich.

Zu Beginn jedes Spiels wird die ID $0019 ( $0019 ) mit dem Wert Tu ( $00 ) initialisiert. Dieser Wert kann möglicherweise bei der ersten $9928 verwendet werden.

Wenn Tetrimino die vorherige Orientierungs-ID zum Erstellen einer Figur anstelle des vorherigen Typs verwendet, fügt er eine Verzerrung hinzu, da die Werte der Orientierungs-ID nicht gleichmäßig verteilt sind. Dies ist in der Tabelle gezeigt:

$ 00$02$07$08$0A$0B$0E$12
02013404
13124515
24235626
35346030
46450141
50561252
61602363
72013404

Jede Zelle enthält einen Tetrimino-Typ, der berechnet wird, indem die Orientierungs-ID der erstellten Figur (Spalte) zu einem 3-Bit-Wert (Zeile) addiert und dann der Rest der Division durch 7 auf die Summe $0E . Jede Zeile enthält Duplikate, da $07 und $0E gleichmäßig geteilt werden um 7, während $0B und $12 ein gemeinsames Guthaben haben. Die Zeilen 0 und 7 sind identisch, da sie einen Abstand von 7 haben.

Es gibt 56 mögliche Eingabekombinationen, und wenn die resultierenden Tetrimino-Typen gleichmäßig verteilt sind, können wir erwarten, dass in der obigen Tabelle jeder Typ genau achtmal vorkommt. Wie unten gezeigt, ist dies jedoch nicht der Fall.

TypFrequenz
T.9
J.8
Z.8
O.8
S.9
L.7
Ich7

T und S erscheinen häufiger und L und I - seltener. Aber verzerrter Code, der die Orientierungs-ID verwendet, wird nicht jedes Mal ausgeführt, wenn das Unterprogramm aufgerufen wird.

Angenommen, PRNG erstellt eine Folge gleichmäßig verteilter statistischer unabhängiger Werte. Dies ist eigentlich eine faire Annahme, wenn man bedenkt, wie das Spiel versucht, die richtige Zufälligkeit aus den Aktionen des Spielers zu ziehen. Das Hinzufügen der Anzahl der erstellten Zahlen zur Adresse $990C sich nicht auf die Verteilung aus, da sich die Anzahl zwischen den Anrufen gleichmäßig erhöht. Die Verwendung der Bitmaske bei $990E ähnelt der Anwendung der Division durch 8 mit dem Rest, was sich auch nicht auf die Verteilung auswirkt. Daher geht die Überprüfung bei $9910 invalidIndex in 1/8 aller Fälle an invalidIndex . Und die Wahrscheinlichkeit, beim Prüfen an der Adresse $9918 , wo die neu ausgewählte Zahl mit der vorherigen Zahl verglichen wird, beträgt 7/8, mit einer Wahrscheinlichkeit des Zusammentreffens von 1/7.Dies bedeutet, dass es eine zusätzliche Chance gibt, 7/8 × 1/7 = 1/8dabei zu sein invalidIndex. Im Allgemeinen besteht eine Wahrscheinlichkeit von 25%, einen verzerrten Code zu verwenden, und eine Wahrscheinlichkeit von 75%, einen Code zu verwenden, der Tetrimino gleichmäßig auswählt.

In einem Satz von 224 erstellten Tetriminos beträgt die mathematische Erwartung 32 Instanzen für jeden Typ. Tatsächlich erstellt der Code jedoch die folgende Verteilung:

TypFrequenz
T.33
J.32
Z.32
O.32
S.33
L.31
Ich31

Das heißt, wenn Sie 90 Zeilen löschen und Level 9 erreichen, erhält der Spieler ein zusätzliches T und S und ein L und I weniger als statistisch erwartet.

Tetrimino werden mit folgenden Wahrscheinlichkeiten ausgewählt:

TypWahrscheinlichkeit
T.14,73%
J.14,29%
Z.14,29%
O.14,29%
S.14,73%
L.13,84%
Ich13,84%

Es scheint, dass in der Aussage, dass der „lange Stock“, den ich nie sehe, wenn er gebraucht wird, ein Teil der Wahrheit enthalten ist (zumindest für Nintendo Tetris).

Tetrimino-Schicht


Nintendo Tetris verwendet Delayed Auto Shift (DAS). Durch Klicken auf "Links" oder "Rechts" wird das Tetrimino sofort um eine Zelle horizontal verschoben. Wenn Sie eine dieser Richtungstasten gedrückt halten, verschiebt das Spiel die Figur automatisch alle 6 Frames mit einer anfänglichen Verzögerung von 16 Frames.

Diese Art der horizontalen Bewegung wird durch den Code an der Adresse gesteuert $89AE. Wie im Rotationscode wird hier eine temporäre Variable verwendet, um die Koordinaten zu sichern, falls die neue Position falsch ist. Beachten Sie, dass die Prüfung verhindert, dass Sie das Stück bewegen, während der Spieler nach unten drückt.

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 werfen


Die Geschwindigkeit des automatischen Abstiegs von Tetrimino ist eine Funktion der Levelnummer. Die Geschwindigkeiten werden als Anzahl der gerenderten Frames für den Abstieg in der Tabelle bei codiert $898E. Da NES mit 60,0988 Bildern / s arbeitet, können Sie den Zeitraum zwischen Abfahrten und die Geschwindigkeit berechnen.

LevelAbstiegsrahmenZeitraum (s / Abstieg)Geschwindigkeit (Zellen / s)
048.7991,25
143.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.05020.03
19–282.03330.05
29+1.01760.10

Die Tabelle enthält insgesamt 30 Einträge. Nach Stufe 29 ist der Wert der Frames für den Abstieg immer 1.

Eine ganzzahlige Anzahl von Frames für den Abstieg ist keine sehr detaillierte Methode zur Beschreibung der Geschwindigkeit. Wie in der folgenden Grafik gezeigt, steigt die Geschwindigkeit mit jedem Level exponentiell an. Tatsächlich ist Level 29 doppelt so schnell wie Level 28.


Bei 1 Frame / Abstieg hat der Spieler nicht mehr als 1/3 Sekunde Zeit, um die Figur zu positionieren. Danach beginnt sie sich zu bewegen. Bei dieser Abstiegsgeschwindigkeit erlaubt DAS der Figur nicht, die Ränder des Spielfelds zu erreichen, bis sie einrastet, was für die meisten Menschen ein schnelles Ende des Spiels bedeutet. Einige Spieler, insbesondere Thor Akerlund , haben es jedoch geschafft, DAS mit der schnellen Vibration der Kreuztasten ( D-pad) zu besiegen . In dem oben gezeigten Verschiebungscode ist zu sehen, dass es möglich ist, das Tetrimino bei Stufen 29 und höher mit halber Frequenz zu verschieben, während die horizontale Richtungstaste durch den Rahmen losgelassen wird. Dies ist ein theoretisches Maximum, aber jede Vibration des Daumens über 3,75 Taps / s kann die ursprüngliche Verzögerung von 16 Bildern zunichte machen.

Wenn der automatische und vom Spieler gesteuerte Abstieg (durch Drücken von "Down") zusammenfällt und in einem Frame erfolgt, summiert sich der Effekt nicht. Durch eines oder beide dieser Ereignisse wird die Form in diesem Frame um genau eine Zelle verringert.

Die Triggersteuerlogik befindet sich bei $8914. Die Abstiegsrahmentabelle befindet sich unter dem Etikett . Wie oben erwähnt, beträgt die Geschwindigkeit ab Stufe 29 konstant 1 Verschluss / Bild. (Adresse ) startet den Abstieg, wenn er ( ) erreicht. Das Inkrement wird an einer Adresse außerhalb dieses Codefragments ausgeführt. Während des automatischen oder kontrollierten Abstiegs wird es auf 0 zurückgesetzt. Die Variable ( ) wird mit dem Wert (an der Adresse ) initialisiert

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), was als −96 interpretiert wird. Ein Zustand ganz am Anfang verursacht eine anfängliche Verzögerung. Der allererste Tetrimino bleibt zum Zeitpunkt der Erstellung in der Luft schwebend, bis autorepeatYer auf 0 ansteigt, was 1,6 Sekunden dauert. Wenn Sie in dieser Phase jedoch auf Nach unten drücken, wird autorepeatYsofort 0 zugewiesen. Es ist interessant, dass Sie die Figur in dieser Phase der anfänglichen Verzögerung verschieben und drehen können, ohne sie abzubrechen.

Das Inkrement autorepeatYwird ausgeführt, während Sie gedrückt halten. Wenn es 3 erreicht, tritt ein vom Menschen kontrollierter Abstieg ("weicher" Abstieg) auf und wird 1 autorepeatYzugewiesen. Daher erfordert der anfängliche weiche Abstieg 3 Rahmen, der sich dann jedoch in jedem Rahmen wiederholt.

Außerdem autorepeatYerhöht es sich nur dann von 0 auf 1, wenn das Spiel erkennt, dass der Spieler gerade auf Nach unten geklickt hat (um$00B5), erkennt aber nicht das Halten. Dies ist wichtig, da es autorepeatYbeim Erstellen eines Tetrimino (an der Adresse $98E8) auf 0 zurückgesetzt wird , was eine wichtige Funktion erzeugt: Wenn der Spieler selbst die Figur senkt und sie blockiert ist und er beim Erstellen der nächsten Figur weiter auf „Nach unten“ drückt, was dann häufig auf hohen Ebenen geschieht Dies führt nicht zu einem sanften Abstieg der neuen Figur. Dazu muss der Player „Down“ loslassen und dann die Taste erneut drücken.

Potenziell sanfter Abstieg kann Punkte erhöhen. holdDownPoints( $004F) nimmt mit jedem Abstieg zu, aber wenn losgelassen, wird „Down“ auf 0 zurückgesetzt. Um Punkte zu erzielen, muss das Tetrimino daher mit einem sanften Abstieg in das Schloss abgesenkt werden. Ein kurzfristiger sanfter Abstieg, der in der Abbildung auftreten kann, wirkt sich nicht auf die Punkte aus. Das Konto wird um aktualisiert$9BFE, wird aber holdDownPointsbald darauf an der Adresse auf 0 zurückgesetzt $9C2F.

Die Prüfung, die den Spieler daran hindert, einen sanften Abstieg mit einer horizontalen Verschiebung der Figur durchzuführen, erschwert das Setzen von Punkten. Dies bedeutet, dass der letzte Zug vor dem Verriegeln des Teils "Ab" sein sollte.

Wenn der Abstieg erfolgt, wird tetriminoY( $0041) nach originalY( $00AE) kopiert . Wenn tetriminoYsich herausstellt, dass die durch das Inkrement erzeugte neue Position falsch ist (dh die Figur schiebt sich entweder durch den Boden des Spielfelds oder überlagert bereits liegende Felder), bleibt das Tetrimino in der vorherigen Position. In diesem Fall wird es wiederhergestellttetriminoYund die Figur gilt als blockiert. Dies bedeutet, dass die Verzögerung vor dem Verriegeln (die maximale Anzahl von Frames, die ein Tetrimino erwartet und die vor dem Verriegeln in der Luft hält) gleich der Verzögerung beim Abstieg ist.

Starrer Abstieg (sofortiger Abstieg) wird in Nintendo Tetris nicht unterstützt.

Schieben und Scrollen


Das Nintendo Tetris-Handbuch enthält ein illustriertes Belegbeispiel:


Das Gleiten besteht darin, sich entlang der Oberfläche anderer Figuren oder entlang des Bodens des Spielfelds zu verschieben. Es wird normalerweise verwendet, um eine Figur unter ein überhängendes Quadrat zu schieben. Das Gleiten kann durchgeführt werden, bis der Fall-Timer die Sinkgeschwindigkeit erreicht hat. Danach wird die Figur fixiert. Ein animiertes Beispiel ist unten dargestellt.


Auf der anderen Seite können Sie durch Scrollen Figuren in Räume verschieben, die auf andere Weise nicht erreichbar sind (siehe unten).


Wie beim Schieben ist das Scrollen ohne Sperrverzögerung nicht möglich. Darüber hinaus nutzt das Scrollen die Art und Weise, wie das Spiel Formen manipuliert. Vor dem Bewegen oder Drehen der Figur prüft das Spiel, ob sich nach dem Ändern der Position alle Tetrimino-Quadrate in leeren Zellen innerhalb der Grenzen des Spielfelds befinden. Eine solche Prüfung, wie unten gezeigt, verhindert nicht die Drehung durch nahegelegene gefüllte Blöcke. Wie im Abschnitt Tetrimino-Beschreibung angegeben, enthält jede Zeile der Orientierungstabelle 12 Bytes. Daher wird der Index in dieser Tabelle berechnet, indem die Orientierungs-ID des aktiven Tetriminos mit 12 multipliziert wird. Wie unten gezeigt, werden alle Multiplikationen in der Routine unter Verwendung von Verschiebungen und Additionen durchgeführt.

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


Jede Iteration des Zyklus verschiebt die Tetrimino-Position um die relativen Koordinaten eines der Quadrate aus der Orientierungstabelle, um den entsprechenden Zellenort auf dem Spielfeld zu erhalten. Anschließend überprüft sie, ob die Koordinaten der Zelle innerhalb der Grenzen des Spielfelds liegen und ob die Zelle selbst leer ist.

In den Kommentaren wird klarer beschrieben, wie der Zeilenabstand überprüft wird. Zusätzlich zu den Zellen in den sichtbaren Linien betrachtet der Code die beiden ausgeblendeten Linien über dem Spielfeld als die rechtlichen Positionen der Quadrate, ohne eine zusammengesetzte Bedingung zu verwenden. Dies funktioniert, weil im zusätzlichen Code die negativen Zahlen, die durch Einzelbyte-Variablen dargestellt werden, Werten größer als 127 entsprechen. In diesem Fall ist der Mindestwert –2, der als gespeichert wird

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 in Dezimalschreibweise).

Der Index des Spielfeldes ist die Summe cellYmultipliziert mit 10 und cellX. Wenn jedoch cellY–1 ( $FF= 255) oder –2 ( $FE= 254), ergibt das Produkt –10 ( $F6= 246) und –20 ( $EC= 236). Im Intervall cellXkann es nicht mehr als 9 sein, was einen maximalen Index von 246 + 9 = 255 ergibt, und dies ist viel weiter als das Ende des Spielfelds. Das Spiel wird jedoch initialisiert $0400- $04FFmit einem Wert $EF(eines leeren Plättchens), wodurch weitere 56 zusätzliche Bytes leeren Raums erstellt werden.

Seltsam diese IntervallprüfungcellXdurchgeführt nach der Untersuchung der Zelle des Spielfeldes. Aber es funktioniert in jeder Reihenfolge richtig. Durch Überprüfen des Intervalls wird außerdem die zusammengesetzte Bedingung vermieden, wie im folgenden Kommentar angegeben. Die unten gezeigten Bildlaufbeispiele sind aufgrund der Art und Weise möglich, wie dieser Code Positionen überprüft.

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






Wie unten gezeigt, können Sie sogar durch Scrollen gleiten.


AI nutzt die Bewegungsfunktionen des Nintendo Tetris voll aus, einschließlich Schieben und Scrollen.

Level 30 und höher


Nach Erreichen von Level 30 scheint das Level auf Null zurückgesetzt worden zu sein.


Aber Level 31 zeigt, dass etwas anderes passiert:


$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 0 F . , . - (binary-coded decimal, BCD); .


Leider scheinen die Spieleentwickler davon ausgegangen zu sein, dass niemand Level 29 bestehen würde, und haben daher beschlossen, nur 30 Einträge in die Tabelle einzufügen. Seltsame angezeigte Werte sind verschiedene Bytes nach der Tabelle. Nur ein Byte (an der Adresse $0044) wird verwendet, um die Levelnummer anzugeben , weshalb das Spiel langsam die unten gezeigten 256 Werte umläuft .

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

20 — , 20 .

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 10 , :

$0400 + 10 * y + x

, , .

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

40 . 20 little endian nametable 0 ( VRAM, ). $06 .

, , .


Die Anzahl der abgeschlossenen Zeilen und Tetrimino-Statistiken belegen an den folgenden Adressen jeweils 2 Byte.

AdressenMenge
0050 - - 0051Ränge
03F0 - - 03F1T.
03F2 - - 03F3J.
03F4 - - 03F5Z.
03F6 - - 03F7O.
03F8 - - 03F9S.
03FA - - 03FBL.
03FC - - 03FDIch

Tatsächlich werden diese Werte als 16-Bit-gepackte Little-Endian-BCDs gespeichert. Beispielsweise wird unten die Anzahl der Zeilen angezeigt, die 123 beträgt. Die Bytes werden von rechts nach links gezählt, sodass die Dezimalstellen der Reihe nach angeordnet sind.


Die Spieleentwickler gingen jedoch davon aus, dass keiner der Werte größer als 999 sein würde. Daher verarbeitet die Anzeigelogik das erste Byte korrekt als gepacktes BCD, wobei jedes Halbbyte als Kachelwert verwendet wird. Das gesamte zweite Byte wird jedoch tatsächlich als oberste Dezimalstelle verwendet. Wenn die unteren Ziffern von 99bis gehen , 00tritt das normale Inkrement des zweiten Bytes auf. Infolgedessen durchläuft das zweite Byte alle 256 Kacheln. Ein Beispiel hierfür ist unten dargestellt.


Nach dem Löschen der Zeile wird der folgende Code ausgeführt, um die Anzahl der Zeilen zu erhöhen. Überprüfungen werden für die mittlere und untere Ziffer durchgeführt, so dass sie zwischen 0 und 9 bleiben. Die obere Ziffer kann jedoch unendlich erhöht werden. Wenn nach dem Erhöhen der Anzahl der Zeilen die untere Ziffer 0 ist, bedeutet dies, dass der Spieler gerade einen Satz von 10 Zeilen abgeschlossen hat und Sie die Levelnummer erhöhen müssen. Wie Sie dem folgenden Code entnehmen können, wird vor dem Pegelinkrement eine zusätzliche Überprüfung durchgeführt. Die zweite Prüfung bezieht sich auf das ausgewählte Einstiegsniveau. Um zu einem bestimmten Level zu gelangen , muss der Spieler unabhängig vom Anfangslevel klären

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
; }
; }


X10XLinien. Wenn ein Spieler beispielsweise mit Level 5 beginnt, bleibt er darauf, bis er 60 Zeilen geklärt hat. Danach geht er zu Level 6. Danach führen alle weiteren 10 Zeilen zu einer Erhöhung der Levelnummer.

Um diese Prüfung durchzuführen, wird der Wert der gefüllten Zeilen von $0050- $0051nach $00A8- kopiert $00A9. Dann wird die Kopie viermal nach rechts verschoben, was für eine gepackte BCD der Division durch 10 ähnelt. Die kleinste Dezimalstelle wird verworfen, und die höchste und die mittlere Stelle werden um eine Position verschoben, was zu Knabbereien führt $00A8.


An der Adresse wird $9BEAdie Ebenennummer jedoch direkt mit dem gepackten Wert des BCD verglichen $00A8. In der Tabelle gibt es keine Suche, um den BCD-Wert in eine Dezimalzahl umzuwandeln, und dies ist ein eindeutiger Fehler. Im obigen Bild sollte beispielsweise die Levelnummer mit $12(18 in Dezimalzahl) und nicht mit 12 verglichen werden. Wenn sich ein Spieler entscheidet, mit Level 17 zu beginnen, geht das Level tatsächlich auf 120 Zeilen, da 18 mehr als ist 17.

Die Tabelle zeigt die erwartete Anzahl von Zeilen, die für den Übergang auf jeder Anfangsebene erforderlich sind. Es wird mit dem verglichen, was aufgrund eines Fehlers tatsächlich passiert.

012345678910111213141516171819
102030405060708090100110120130140150160170180190200
102030405060708090100100100100100100100110120130140

Der erwartete Betrag entspricht dem für die Anfangsstufen 0–9. Tatsächlich ist der Zufall für Einstiegsniveau 9 zufällig; 10-15 geht auch mit 100 Zeilen zur nächsten Ebene, weil $10- dies ist 16 in Dezimalform. Der größte Unterschied zwischen erwartet und tatsächlich beträgt 60 Zeilen.

Ich vermute, dass der Fehler auf Designänderungen in späteren Entwicklungsstadien zurückzuführen ist. Schauen Sie sich den Menübildschirm an, damit der Spieler eine Einstiegsstufe auswählen kann.


Es gibt keine Erklärung, wie man von Levels über 9 ausgeht. In der Nintendo Tetris-Broschüre wird dieses Geheimnis jedoch enthüllt:


Es scheint, dass dieses versteckte Merkmal im letzten Moment erfunden wurde. Vielleicht wurde es sehr kurz vor dem Veröffentlichungsdatum hinzugefügt, was es nicht ermöglichte, es vollständig zu testen.

Tatsächlich enthält das Überprüfen der anfänglichen Reihe einen zweiten Fehler, der sich auf die Ausgabe von Werten für das Intervall bezieht. Unten finden Sie Kommentare im Code, die besser erklären, was auf niedriger Ebene passiert. Der Vergleich erfolgt durch Subtrahieren und Überprüfen des Vorzeichens des Ergebnisses. Eine vorzeichenbehaftete Einzelbyte-Zahl ist jedoch auf –128 bis 127 begrenzt. Wenn die Differenz kleiner als –128 ist, wird die Zahl übertragen und das Ergebnis wird zu einer positiven Zahl. Dieses Prinzip wird in den Kommentaren zum Code erläutert. Bei der Überprüfung, ob die Differenz in diesem Intervall liegt, muss berücksichtigt werden, dass die Ebenennummer beim Inkrementieren auf Werte größer als 255 die Übertragung auf 0 und ausführt

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
; }


$00A8möglicherweise kann es einen beliebigen Wert enthalten, da sein oberes Halbbyte entnommen wird $0051, dessen Inkrement unendlich auftreten kann.

Diese Effekte überschneiden sich und erzeugen Perioden, in denen die Levelnummer fälschlicherweise unverändert bleibt. Perioden treten in regelmäßigen Abständen von 2.900 Zeilen auf, beginnend bei 2.190 Zeilen und dauern 800 Zeilen. Beispielsweise bleibt der Pegel von 2190 ( L90) bis 2990 ( T90) gleich $DB( 96), wie unten gezeigt.


Der nächste Zeitraum liegt zwischen 5090 und 5890, der Pegel ist konstant gleich $AD( 06). Darüber hinaus ändert sich während dieser Zeiträume auch die Farbpalette nicht.

Tetrimino Malvorlagen


Auf jeder Ebene werden Tetrimino-Kacheln 4 einzigartige Farben zugewiesen. Die Farben stammen aus der Tabelle unter $984C. Ihre Aufzeichnungen werden alle 10 Ebenen wiederverwendet. Von links nach rechts: Die Spalten der Tabelle entsprechen den schwarzen, weißen, blauen und roten Bereichen des Bildes unten.

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





Die Werte entsprechen der NES-Farbpalette.


Die ersten beiden Farben jedes Eintrags sind immer schwarz und weiß. Die erste Farbe wird jedoch tatsächlich ignoriert. Unabhängig vom Wert wird es als transparente Farbe betrachtet, durch die ein fester schwarzer Hintergrund späht.

Der Zugriff auf die Farbtabelle erfolgt in der Routine unter $9808. Der Farbtabellenindex basiert auf der Ebenennummer geteilt durch einen Rest von 10. Der Zyklus kopiert den Eintrag in die Palettentabellen in VRAM. Die Division mit dem Rest wird durch eine konstante Subtraktion von 10 emuliert, bis das Ergebnis kleiner als 10 ist. Der Beginn der Subroutine mit Kommentaren ist unten gezeigt.

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 ; }


Wie im vorherigen Abschnitt angegeben, werden jedoch im Vergleich Subtraktion und Verzweigung basierend auf dem Differenzzeichen verwendet. Eine vorzeichenbehaftete Einzelbyte-Nummer ist auf –128 bis 127 begrenzt. Die aktualisierten Kommentare unten spiegeln dieses Prinzip wider. Die folgenden Kommentare werden weiter vereinfacht. Dieser Wortlaut zeigt einen Fehler im Code. Der Restdivisionsvorgang wird für Stufen ab 138 vollständig übersprungen. Stattdessen wird der Index direkt der Ebenennummer zugewiesen, die den Zugriff auf Bytes weit über das Ende der Farbtabelle hinaus ermöglicht. Wie unten gezeigt, kann dies sogar zu einem fast unsichtbaren Tetrimino führen.

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 ; }





Unten sind die Farben aller 256 Ebenen. Die Kacheln sind in 10 Spalten angeordnet, um die zyklische Verwendung der Farbtabelle hervorzuheben, gegen die auf Stufe 138 verstoßen wurde. Zeilen und Spalten in den Überschriften werden dezimal angegeben.


Nach 255 kehrt die Ebenennummer auf 0 zurück

. Wie im vorherigen Abschnitt erwähnt, ändern sich einige Ebenen erst, wenn 800 Zeilen entfernt wurden. Während dieser langen Pegel bleiben die Farben unverändert.

Spielmodus


Der unter der Adresse gespeicherte Spielemodus $00C0bestimmt, welcher der verschiedenen Bildschirme und Menüs dem Benutzer derzeit angezeigt wird.

WertBeschreibung
00Bildschirm mit rechtlichen Informationen
01Begrüßungsbildschirm
02Spieltyp-Menü
03Menü der Ebenen und Höhen
04Spiel / Highscore / Ende / Pause
05Demo

Wie oben gezeigt, verfügt das Spiel über eine clever geschriebene Routine, die als Switch-Anweisung unter Verwendung der kleinen Endian-Navigationstabelle fungiert, die sich unmittelbar nach dem Aufruf befindet. Die obige Liste zeigt die Adressen aller Spielmodi. Beachten Sie, dass die Modi "Spiel" und "Demo" denselben Code verwenden. Diese Routine kehrt niemals zurück. Stattdessen verwendet der Code die Absenderadresse. Normalerweise zeigt es auf die Anweisung unmittelbar nach dem Aufruf des Sprunges zum Unterprogramm (minus 1 Byte), aber in diesem Fall zeigt es auf die Sprungtabelle. Die Absenderadresse wird vom Stapel genommen und in - gespeichert . Nach dem Speichern der Adresse der Sprungtabelle verwendet der Code den Wert in Register A als Index und führt den entsprechenden Übergang durch.

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]


Der Code kann diese Schaltroutine verwenden, solange die Indizes nahe bei 0 liegen und zwischen den möglichen Fällen keine oder nur wenige Leerzeichen stehen.

Bildschirm mit rechtlichen Informationen


Das Spiel beginnt mit einem Bildschirm mit einem rechtlichen Hinweis.


Am unteren Rand des Bildschirms wird Aleksey Pazhitnov als Erfinder, Designer und Programmierer des ersten Tetris erwähnt. 1984 entwickelte er als Computerentwickler am Dorodnitsyn Computing Center (einem führenden Forschungsinstitut der Russischen Akademie der Wissenschaften in Moskau) einen Prototyp des Spiels auf Electronics-60 (sowjetischer Klon DEC LSI-11 ). Für einen grünen monochromen Textmodus wurde ein Prototyp entwickelt, bei dem Quadrate durch Paare von eckigen Klammern angezeigt werden []. Mit Hilfe des 16-jährigen Schülers Vadim Gerasimov und des Computeringenieurs Dmitry Pavlovsky wurde der Prototyp wenige Tage nach der Erfindung des Spiels auf einen IBM-PC mit MS DOS und Turbo Pascal portiert. Im Laufe von zwei Jahren perfektionierten sie das Spiel gemeinsam und fügten Funktionen wie Tetrimino-Farben, Statistiken und vor allem einen Timing- und Grafikcode hinzu, mit dem das Spiel auf einer Vielzahl von PC-Modellen und Klonen arbeiten konnte.

Leider waren ihre Versuche, das Spiel zu monetarisieren, aufgrund der Besonderheiten der Sowjetunion zu dieser Zeit erfolglos und am Ende beschlossen sie, die PC-Version kostenlos mit ihren Freunden zu teilen. Von diesem Moment an verbreitete sich „Tetris“ viral im ganzen Land und darüber hinaus und wurde von Festplatte zu Festplatte kopiert. Da das Spiel jedoch von Mitarbeitern einer Regierungsbehörde entwickelt wurde, gehörte es dem Staat, und 1987 übernahm die für den internationalen Handel mit elektronischen Technologien zuständige Organisation die Lizenzierung des Spiels ( Electronorgtekhnika (ELORG)). ). V/O «Version Originale».

- Andromeda Tetris , , Mirrorsoft . Mirrorsoft, , Tengen , Atari Games. Tengen Bullet-Proof Software , Tetris Nintendo Famicom . .


Interessanterweise wird in dieser Version der Schüler Vadim Gerasimov der ursprüngliche Designer und Programmierer genannt.

Nintendo versuchte, die tragbare Version der kommenden Game Boy-Konsole zu sichern, und verwendete die Bullet-Proof-Software, um einen erfolgreichen Deal direkt mit ELORG abzuschließen. Während des Abschlusses des Vertrags überarbeitete ELORG seinen Vertrag mit Andromeda und fügte hinzu, dass Andromeda nur Rechte an Spielen für Computer und Arcade-Automaten erhielt. Aus diesem Grund musste Bullet-Proof Software ELORG-Lizenzgebühren für alle für Famicom verkauften Patronen zahlen, da sich die von Tengen erhaltenen Rechte als Fälschung herausstellten. Durch die Abstimmung mit ELORG gelang es Bullet-Proof Software schließlich, weltweite Konsolenspielrechte für Nintendo zu erlangen.

Bullet-Proof Software hat Nintendos Rechte an tragbaren Spielen unterlizenziert und gemeinsam Game Boy Tetris entwickelt, was sich im folgenden Bildschirm mit rechtlichen Informationen widerspiegelt.


Mit globalen Konsolenspielrechten hat Nintendo die Tetris-Version für NES entwickelt, die wir in diesem Artikel untersuchen. Dann hat Bullet-Proof Software die Rechte von Nintendo unterlizenziert, wodurch sie weiterhin Patronen für Famicom in Japan verkaufen konnte.

Es folgte ein komplexer Rechtsstreit. Sowohl Nintendo als auch Tengen forderten die gegnerische Partei auf, ihre Version des Spiels nicht mehr zu produzieren und zu verkaufen. Infolgedessen gewann Nintendo und Hunderttausende von Tengen Tetris- Patronen wurden zerstört. Das Gerichtsurteil verbot auch mehreren anderen Unternehmen wie Mirrorsoft, Konsolenversionen zu erstellen.

Pajitnov erhielt niemals Abzüge von ELORG oder dem Sowjetstaat. 1991 zog er jedoch in die USA und 1996 mit Unterstützung des Inhabers von Bullet-Proof SoftwareHenka Rogers war Mitbegründer von The Tetris Company , wodurch er von Versionen für mobile Geräte und moderne Konsolen profitieren konnte.

Es ist interessant, den Bildschirm mit rechtlichen Informationen als ein Fenster zu betrachten, das eine Vorstellung von der bescheidenen Herkunft des Spiels und den darauf folgenden Kämpfen um Rechte an geistigem Eigentum gibt, da dieser Bildschirm für die meisten Spieler nur ein ärgerliches Hindernis darstellt, dessen Verschwinden scheinbar ewig warten muss. Die Verzögerung wird durch zwei Zähler eingestellt, die nacheinander von 255 bis 0 zählen. Die erste Phase kann nicht übersprungen werden, und die zweite Phase wird durch Drücken der Starttaste übersprungen. Daher wird der Bildschirm mit den rechtlichen Informationen mindestens 4,25 Sekunden und höchstens 8,5 Sekunden lang angezeigt. Ich denke jedoch, dass die meisten Spieler aufgeben und während des ersten Intervalls aufhören, Start zu drücken, und aus diesem Grund auf den vollständigen Abschluss warten.

Das Timing der Phasen sowie der Rest des Spiels wird von einem nicht maskierten Interrupt-Handler gesteuert, der zu Beginn jedes vertikalen Austastintervalls aufgerufen wird, eine kurze Zeitspanne zwischen dem Rendern von Fernsehbildern. Das heißt, alle 16.6393 Millisekunden wird die normale Programmausführung durch den folgenden Code unterbrochen. Der Handler beginnt damit, die Werte der Hauptregister an den Stapel zu übergeben und sie nach Abschluss abzurufen, um die unterbrochene Aufgabe nicht zu stören. Der Aufruf aktualisiert VRAM und konvertiert die Beschreibung des Speichermodells in das, was auf dem Bildschirm angezeigt wird. Ferner reduziert der Handler den Wert des Zählers des Bildschirms für rechtliche Informationen, wenn er größer als Null ist. Herausforderung

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- little endian, $00B1$00B2 , . ; , . $8040 vertical blanking interval, , . , ; , «».

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;


Diese Blockierungsroutine wird von zwei Zeitstufen des Rechtsinformationsbildschirms verwendet, die nacheinander ausgeführt werden. Das Lua AI-Skript umgeht diese Verzögerung, indem beide Zähler auf 0 gesetzt werden.

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;




Demo


80 . , , . . , $DF00 , :

TJTSZJTSZJSZLZJTTSITO JSZLZLIOLZLIOJTSITOJ

, , . $98EB .

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();
; }


6, 5 4 . $07 — . ( $994E ), ID , :

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 ) $872B .

Die zweite Tabelle der Demo enthält eine Aufzeichnung der Gamepad-Schaltflächen, die in Bytepaaren codiert sind. Die Bits des ersten Bytes entsprechen den Tasten.

76543210
A.B.Wählen SieStarten SieAufRunterLinksRechts

Das zweite Byte speichert die Anzahl der Frames, während der eine Tastenkombination gedrückt wird.

Die Tabelle nimmt Adressen auf $DD00- $DEFFund besteht aus 256 Paaren. Der Zugriff darauf erfolgt durch das Unterprogramm an der Adresse $9D5B. Da die Demo-Schaltflächentabelle 512 Byte lang ist, ist ein Zwei-Byte-Index erforderlich, um darauf zuzugreifen. Der Index wird als Little Endian bei - gespeichert . Es wird mit dem Wert der Adresse der Tabelle initialisiert und sein Inkrement wird durch den folgenden Code ausgeführt. Die Programmierer haben die Eingabeverarbeitung des Players im Code belassen, sodass wir den Entwicklungsprozess betrachten und die Demo durch einen anderen Datensatz ersetzen können. Der Demo-Aufnahmemodus wird aktiviert, wenn ein Wert zugewiesen wird.

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. In diesem Fall wird der folgende Code gestartet, der zum Schreiben in die Schaltflächentabelle für die Demo vorgesehen ist. Die Tabelle ist jedoch im PRG-ROM gespeichert. Der Versuch, darauf zu schreiben, wirkt sich nicht auf die gespeicherten Daten aus. Stattdessen löst jede Schreiboperation einen Bankwechsel aus, was zu dem unten gezeigten Glitch-Effekt führt.

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;





Dies deutet darauf hin, dass Entwickler das Programm teilweise oder vollständig im RAM ausführen könnten.

Um dieses Hindernis zu umgehen, habe ich lua/RecordDemo.luaeine in einer Zip mit Quellcode erstellt . Nach dem Umschalten in den Demo-Aufnahmemodus werden die Schreibvorgänge in die Tabelle in der Lua-Konsole umgeleitet. Von dort können Bytes kopiert und in das ROM eingefügt werden.

Um Ihre eigene Demo aufzunehmen, führen Sie FCEUX aus und laden Sie die Nintendo Tetris ROM-Datei herunter (Datei | ROM öffnen ...). Öffnen Sie dann das Lua-Skriptfenster (Datei | Lua | Neues Lua-Skriptfenster ...), navigieren Sie zur Datei oder geben Sie den Pfad ein. Drücken Sie die Run-Taste, um den Demo-Aufnahmemodus zu starten, und klicken Sie dann auf das FCEUX-Fenster, um den Fokus darauf zu schalten. Sie können die Formen steuern, bis die Schaltflächentabelle voll ist. Danach kehrt das Spiel automatisch zum Bildschirmschoner zurück. Klicken Sie im Lua-Skriptfenster auf Stopp, um das Skript zu stoppen. Die aufgezeichneten Daten werden in der Ausgabekonsole angezeigt (siehe Abbildung unten).


Wählen Sie den gesamten Inhalt aus und kopieren Sie ihn in die Zwischenablage (Strg + C). Führen Sie dann den Hex-Editor aus (Debug | Hex-Editor ...). Wählen Sie im Menü Hex Editor die Option Ansicht | ROM-Datei und dann Datei | Gehe zu Adresse. Geben Sie im Dialogfeld Springen 5D10 (Adresse der Tabelle der Demo-Schaltflächen in der ROM-Datei) ein und klicken Sie auf OK. Fügen Sie dann den Inhalt der Zwischenablage ein (Strg + V).


Wählen Sie schließlich im FCEUX-Menü NES | Zurücksetzen Wenn Sie alle diese Schritte wiederholt haben, sollte die Demo durch Ihre eigene Version ersetzt werden.

Wenn Sie Änderungen speichern möchten, wählen Sie Datei | Speichern Sie Rom unter ..., geben Sie den Namen der geänderten ROM-Datei ein und klicken Sie dann auf Speichern.

Auf ähnliche Weise können Sie die Reihenfolge der erstellten Tetriminos anpassen.

Bildschirm des Todes


Wie oben erwähnt, können die meisten Spieler die Geschwindigkeit des Abstiegs von Figuren auf Stufe 29 nicht bewältigen, was schnell zum Abschluss des Spiels führt. Daher wurden die Spieler, die er mit dem Namen "Bildschirm des Todes" verbunden. Aus technischer Sicht erlaubt der Todesbildschirm dem Spieler jedoch nicht, aufgrund eines Fehlers, bei dem ein schneller Abstieg eigentlich kein Fehler, sondern eine Funktion ist, weiter zu gehen. Die Designer waren so nett, dass sie das Spiel fortsetzen ließen, während der Spieler der übermenschlichen Geschwindigkeit standhalten konnte.

Ein wahrer Todesbildschirm erscheint in ungefähr 1550 zurückgezogenen Reihen. Es manifestiert sich auf unterschiedliche Weise. Manchmal wird das Spiel neu gestartet. In anderen Fällen wird der Bildschirm nur schwarz. Normalerweise friert ein Spiel sofort nach dem Löschen einer Zeile ein („friert ein“), wie unten gezeigt. Diesen Effekten gehen häufig zufällige grafische Artefakte voraus.


Der Todesbildschirm ist das Ergebnis eines Fehlers im Code, der beim Löschen von Zeilen Punkte hinzufügt. Das aus sechs Zeichen bestehende Konto wird als 24-Bit-gepacktes Little-Endian-BCD gespeichert und befindet sich unter $0053- $0055. Um Konvertierungen zwischen der Anzahl der gelöschten Zeilen und den erhaltenen Punkten durchzuführen, wird eine Tabelle verwendet. Jeder Eintrag darin ist ein 16-Bit-gepackter BCD-Little-Endian-Wert. Nach dem Erhöhen der Gesamtzahl der Zeilen und möglicherweise der Ebene wird der Wert in dieser Liste mit der Ebenennummer plus eins multipliziert und das Ergebnis zu den Punkten addiert. Dies wird in der Tabelle aus dem Nintendo Tetris-Handbuch deutlich:

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





Wie unten gezeigt, wird die Multiplikation durch einen Zyklus simuliert, der der Punktzahl Punkte hinzufügt. Es wird ausgeführt, nachdem die Form gesperrt wurde, auch wenn keine Zeilen gelöscht wurden. Leider verfügt der Ricoh 2A03 nicht über einen binären Dezimalmodus 6502; er konnte den Körper des Zyklus stark vereinfachen. Stattdessen erfolgt die Addition schrittweise im Binärmodus. Jede Ziffer, die nach der Addition 9 überschreitet, wird im Wesentlichen durch Subtrahieren von 10 und Inkrementieren der Ziffern links erhalten. Zum Beispiel wird das in konvertiert . Ein solches System ist jedoch nicht vollständig geschützt. Nehmen Sie : Ein Scheck kann das Ergebnis nicht in konvertieren

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 30 — , , .


Nintendo Tetris A-Type :


Das Spiel belohnt die Spieler, die in einer der fünf Animationen der Endungen eine ausreichend große Anzahl von Punkten erzielt haben. Die Wahl der Endung basiert vollständig auf den beiden Ziffern ganz links der sechsstelligen Punktzahl. Wie unten gezeigt, muss der Spieler mindestens 30.000 Punkte erzielen, um eines der Endungen zu erhalten. Es ist erwähnenswert, dass - ein Spiegel der Adressen ist - . Das Konto wird an den Adressen dupliziert - . Nach dem Bestehen des ersten Tests wird die Endanimation durch die folgende switch-Anweisung ausgewählt.

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;
; }


Am Ende werden Raketen von zunehmender Größe von der Startrampe neben der Basilius-Kathedrale abgefeuert. Am vierten Ende wird das Raumschiff Buran gezeigt - die sowjetische Version des amerikanischen Space Shuttles. Am besten Ende erhebt sich die Kathedrale selbst in die Luft und ein UFO hängt über der Startrampe. Unten sehen Sie ein Bild von jedem Ende und die damit verbundene Punktzahl.
30000–49999
50000–69999
70000–99999
100000–119999
120000+

Im B-Type-Spielemodus wird ein weiterer Test implementiert, der im Nintendo Tetris-Booklet wie folgt beschrieben wird:


Wenn der Spieler 25 Zeilen erfolgreich beendet hat, zeigt das Spiel das Ende, abhängig vom Anfangslevel. Die Enden der Stufen 0 bis 8 bestehen aus Tieren und Gegenständen, die im Rahmen fliegen oder rennen und auf mysteriöse Weise hinter der Basilius-Kathedrale vorbeiziehen. Das UFO vom besten Ende des A-Typ-Modus erscheint am Ende 3. Am Ende 4 erscheinen ausgestorbene fliegende Flugsaurier, und am Ende 7 werden die mythischen fliegenden Drachen gezeigt. In den Endungen 2 und 6 sind flügellose Vögel dargestellt: laufende Pinguine und Strauße. Am Ende von 5 ist der Himmel mit GUTEN Luftschiffen gefüllt (nicht zu verwechseln mit Goodyear-Luftschiffen). Und am Ende von 8 fegen viele „Buranas“ über den Bildschirm, obwohl es in Wirklichkeit nur einen gab.

Die Starthöhe (plus 1) wird als Multiplikator verwendet, wodurch der Spieler mit einer großen Anzahl von Tieren / Objekten für eine erhöhte Komplexität belohnt wird.

Am besten Ende des B-Typs wird ein Schloss mit Charakteren aus dem Nintendo-Universum gezeigt: Prinzessin Peach klatscht in die Hände, Kid Icarus spielt Geige, Donkey Kong klopft auf die große Trommel, Mario und Luigi tanzen, Bowser spielt Akkordeon, Samus spielt Cello, Link - auf einer Flöte, während die Kuppeln der Basilius-Kathedrale in die Luft fliegen. Die Anzahl dieser Elemente am Ende hängt von der Anfangshöhe ab. Unten finden Sie Bilder aller 10 Endungen.


AI kann schnell alle 25 Zeilen löschen, die im B-Type-Modus auf jeder Anfangsebene und Höhe erforderlich sind, sodass Sie alle Endungen sehen können. Es lohnt sich auch zu bewerten, wie cool er mit großen Haufen zufälliger Blöcke umgeht.

In den Endungen 0–8 können sich bis zu 6 Objekte im Rahmen bewegen. Die y-Koordinaten der Objekte werden in einer Tabelle unter at gespeichert $A7B7. Horizontale Abstände zwischen Objekten werden in einer Tabelle unter der Adresse gespeichert . Eine Folge von Werten mit einem Vorzeichen an der Adresse bestimmt die Geschwindigkeit und Richtung der Objekte. Sprite-Indizes werden bei gespeichert . Tatsächlich besteht jedes Objekt aus zwei Sprites mit benachbarten Indizes. Um den zweiten Index zu erhalten, müssen Sie 1 hinzufügen. Zum Beispiel besteht ein Drache aus

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 . .


, . , , . ; , . , $30 $16 , .



, , .

2 Player Versus


Nintendo Tetris enthält einen unvollständigen Zwei-Spieler-Modus, den Sie aktivieren können, indem Sie die Anzahl der Spieler ( $00BE) auf 2 ändern . Wie unten gezeigt, werden im Hintergrund des Einzelspieler-Modus zwei Spielfelder angezeigt.


, . 003 , , . , . , . . .

, . . Double, Triple Tetris ( , ), .

Ein zusätzliches Feld befindet sich unter $0500. A $0060- $007Fnormalerweise ein Spiegel $0040- $005Fwird für den zweiten Spieler verwendet.

Wahrscheinlich wurde dieser interessante Modus aufgrund eines vollen Entwicklungsplans aufgegeben. Oder vielleicht wurde er absichtlich unvollendet gelassen. Einer der Gründe, warum Tetris als mit dem Nintendo Game Boy gebündeltes Spiel ausgewählt wurde, war, dass es den Kauf von Game Link Cable förderte- Ein Zubehör, das zwei Game Boys miteinander verbindet, um den 2-Spieler-Versus-Modus zu starten. Dieses Kabel fügte dem System ein Element der „Sozialität“ hinzu - es ermutigte Freunde, einen Game Boy zu kaufen, um sich dem Spaß anzuschließen. Vielleicht befürchtete Nintendo, dass die Werbekraft von Tetris, die den Kauf von Game Boy stimulierte, geschwächt werden könnte, wenn die Konsolenversion des Spiels einen 2-Spieler-Versus-Modus hätte.

Musik und Soundeffekte


Hintergrundmusik wird eingeschaltet, wenn $06F5einer der in der Tabelle aufgeführten Werte zugewiesen wird.

WertBeschreibung
01Nicht verwendete Begrüßungsbildschirmmusik
02Ziel im B-Typ-Modus erreicht
03Musik-1
04Musik-2
05Musik-3
06Musik-1 Allegro
07Musik-2 Allegro
08Musik-3 Allegro
09Glückwunsch Bildschirm
0AEndungen
0BZiel im B-Typ-Modus erreicht
. .

Music-1 — " ", -- «» . — " ", . .

Music-2 . Music-3 , ; - Nintendo of America.

, , ( $06$08 ).

, " ", , Game Boy Tetris.

$06F0 $06F1 , .

Die AdresseBeschreibung
06F002
06F003
06F101
06F102
06F103
06F104Tetris
06F105
06F106
06F107
06F108
06F109
06F10A


Während des Spiels wird der aktuelle Status des Spiels durch eine Ganzzahl an der Adresse dargestellt $0048. Meistens hat es eine Bedeutung $01, die angibt, dass der Spieler das aktive Tetrimino kontrolliert. Wenn das Stück jedoch verriegelt ist, wechselt das Spiel allmählich von Zustand $02zu Zustand $08, wie in der Tabelle gezeigt.

ZustandBeschreibung
00Nicht zugewiesene Orientierungs-ID
01Der Spieler steuert das aktive Tetrimino
02Tetrimino-Sperre auf dem Spielfeld
03Gefüllte Zeilen prüfen
04Zeilenbereinigungsanimation anzeigen
05Zeilen und Statistiken aktualisieren
06Überprüfen des Ziels des B-Typ-Modus
07Wird nicht benutzt
08Erstellen Sie das nächste Tetrimino
09Wird nicht benutzt
0ASpielvorhang Update
0BInkrement des Spielstatus

$81B2 :

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
; }


$00 switch , orientationID $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. Wenn die gesperrte Position korrekt ist, werden die 4 zugeordneten Zellen des Spielfelds als belegt markiert. Ansonsten macht sie den Übergang zu einem Staat - dem bedrohlichen Vorhang zum Ende des Spiels.

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


Der Vorhang wird von der Oberseite des Spielfelds nach unten gezogen und alle 4 Bilder um eine Linie nach unten verschoben. curtainRow( $0058) wird mit einem Wert von –16 initialisiert, wodurch eine zusätzliche Verzögerung von 0,27 Sekunden zwischen der endgültigen Sperre und dem Start der Animation entsteht. An der Adresse $9A21im Status des $0Aunten gezeigten Codes wird auf die Multiplikationstabelle zugegriffen, die fälschlicherweise als Ebenennummern angezeigt wird. Dies erfolgt auf curtainRow10 skaliert . Wie oben gezeigt, $9A51startet der Code an der Adresse die Endanimation, wenn die Punktzahl des Spielers nicht weniger als 30.000 Punkte beträgt. Andernfalls wird erwartet, dass auf Start geklickt wird. Der Code wird vervollständigt, indem dem Spielstatus ein Wert zugewiesen wird. Der entsprechende Handler wird jedoch nicht aufgerufen, da das Spiel abgeschlossen ist.

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

Linien des Spielfelds werden schrittweise in den VRAM kopiert, um sie anzuzeigen. Der Index der aktuell zu kopierenden Zeile ist in vramRow( $0049) enthalten. An der Adresse wird ein $9A3C vramRowWert zugewiesen curtainRow, der diese Zeile beim Rendern letztendlich sichtbar macht.

Manipulationen mit VRAM erfolgen während eines vertikalen Austastintervalls, das vom Interrupt-Handler erkannt wird, der im Abschnitt „Bildschirm mit rechtlichen Informationen“ beschrieben ist. Es ruft die unten gezeigte Unterroutine auf (markiert in den Kommentaren des Interrupt-Handlers als render()). Der Rendering-Modus ähnelt dem Spielemodus. Es wird unter der Adresse gespeichert und kann einen der folgenden Werte haben:

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

WertBeschreibung
00Bildschirm mit legal Informationen und Bildschirmschoner
01Menübildschirme
02Glückwunsch Bildschirm
03Spiel und Demo
04Animation beenden

$03 .

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


, copyPlayfieldRowToVRAM() VRAM , vramRow . vramRow 20, .

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 ) VRAM little endian, , 6 −2 12 2 Player Versus. , 29. 16- , .

vramRow. Wenn der Wert 20 erreicht, wird ihm ein Wert von 32 zugewiesen, was bedeutet, dass die Kopie vollständig abgeschlossen ist. Wie oben gezeigt, werden nur 4 Zeilen pro Frame kopiert.

Der State Handler $03ist dafür verantwortlich, fertige Zeilen zu erkennen und vom Spielfeld zu entfernen. Während 4 separater Anrufe scannt er die Linienversätze [−2, 1]nahe der Mitte des Tetriminos (beide Koordinaten aller Quadrate des Tetriminos liegen in diesem Intervall). Indizes abgeschlossener Zeilen werden bei $004A- gespeichert $004D; Der aufgezeichnete Index 0 wird verwendet, um anzuzeigen, dass in diesem Durchgang keine abgeschlossenen Zeilen gefunden wurden. Der Handler wird unten gezeigt. Die Prüfung am Anfang erlaubt es dem Handler nicht, auszuführen, wenn Zeilen des Spielfelds an VRAM (State Handler) übertragen werden

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 , , $9A82 rowY 10 :

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

, rowY [0, 20] , [0, 19] . . , , $0400$04FF $EF(leeres Plättchen), wodurch mehr als 5 zusätzliche leere versteckte Linien unter dem Boden des Spielfelds entstehen.

Ein Block, der mit beginnt, $9ADAist Teil des unvollständigen Modus von 2 Player Versus. Wie oben erwähnt, fügt das Löschen der Reihen dem Spielfeld des Gegners Schmutz hinzu. Die Anzahl der Müllzeilen wird durch die Tabelle an der Adresse bestimmt $9B53: Der Zyklus an der Adresse verschiebt das Material über der gefüllten Zeile um eine Zeile nach unten. Er nutzt die Tatsache, dass jede Zeile in einer fortlaufenden Sequenz um 10 Bytes voneinander getrennt ist. Die nächste Schleife löscht die oberste Zeile. Die Animation zum Löschen von Zeilen wird während des Spielstatus ausgeführt , aber wie unten gezeigt, tritt sie nicht im Spielstatus-Handler auf, der vollständig leer ist.

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


$9AA6

$04

9E39: RTS ; return;

Stattdessen wird während des Spielzustands $04die nächste Verzweigung des Rendering-Modus durchgeführt $03. und gespiegelte Werte werden für den unfertigen Modus 2 Player Versus benötigt. Das Unterprogramm ist unten dargestellt . Es wird in jedem Frame aufgerufen, aber die Bedingung am Anfang erlaubt, dass es nur in jedem vierten Frame ausgeführt wird. In jedem Durchgang durchläuft es die Liste der Indizes der abgeschlossenen Zeilen und löscht 2 Spalten in diesen Zeilen, wobei es sich von der mittleren Spalte nach außen bewegt. Eine 16-Bit-VRAM-Adresse wird auf die gleiche Weise wie in der Kopierfeldroutine gezeigt aufgebaut. In diesem Fall wird jedoch ein Versatz um den Spaltenindex ausgeführt, der aus der folgenden Tabelle erhalten wird.

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


Für die Reinigung der Animation sind 5 Durchgänge erforderlich. Dann geht der Code zum nächsten Spielstatus über.

Der Handler für $05den Spielstatus enthält den Code, der im Abschnitt „Zeilen und Statistiken“ beschrieben ist. Der Handler endet mit folgendem Code: Die Variable wird erst am Ende des Spielstatus zurückgesetzt. Anschließend wird die Gesamtzahl der Zeilen und die Punktzahl aktualisiert. Diese Sequenz ermöglicht die Ausführung eines interessanten Fehlers. Im Demo-Modus müssen Sie warten, bis das Spiel die gesamte Zeile gesammelt hat, und dann schnell Start drücken, bis die Animation zum Löschen der Serie beendet ist. Das Spiel kehrt zum Bildschirmschoner zurück. Wenn Sie jedoch den richtigen Zeitpunkt auswählen, wird der Wert gespeichert. Jetzt können Sie das Spiel im A-Type-Modus starten. Wenn anstelle der ersten Figur gesperrt, der Spielstatus-Handler

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

9CA2: INC $0048 ; playState = B_TYPE_GOAL_CHECK;

9CA4: RTS ; return;


completedLines$05completedLines$03Startet das Scannen abgeschlossener Zeilen. Er wird sie nicht finden, sondern completedLinesunverändert lassen. Wenn der Spielstatus erfüllt ist, $05erhöhen sich die Gesamtzahl der Zeilen und die Punktzahl, als hätten Sie sie erzielt.

Der einfachste Weg, dies zu tun, besteht darin, den größten Betrag zu erhalten und darauf zu warten, dass die Demo Tetris sammelt (es werden 2 davon in der Demo sein). Sobald der Bildschirm flackert, klicken Sie auf Start.


. , .

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 ( $004F ) . , . holdDownPoints , «».

, Start Tetris , , Tetris, . . Start.

Der Spielstatus $06führt eine Zielprüfung für Spiele vom Typ B durch. Im A-Type-Modus handelt es sich im Wesentlichen um einen nicht verwendeten Frame.

Der Spielstatus $07enthält ausschließlich die unvollständige 2-Spieler-Versus-Logik. Im Einzelspielermodus verhält es sich wie ein nicht verwendeter Frame.

Der Spielstatus wird $08in den Abschnitten „Tetrimino erstellen“ und „Tetrimino auswählen“ erläutert.

Der Spielstatus wird $09nicht verwendet. $0Berhöht den Spielstatus, sieht aber auch unbenutzt aus.

Und schließlich der Hauptzyklus des Spiels:

; 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/de420725/


All Articles