Comment j'ai appris à l'IA à jouer à Tetris pour NES. Partie 1: analyse du code du jeu

Dans cet article, j'explorerai les mécanismes d'une simplicité trompeuse de Nintendo Tetris, et dans la deuxième partie, j'expliquerai comment j'ai créé une IA qui exploite ces mécanismes.


Essayez-le vous-même


À propos du projet


Pour ceux qui manquent de persévérance, de patience et de temps pour maîtriser Nintendo Tetris, j'ai créé une IA qui peut jouer seule. Vous pouvez enfin atteindre le niveau 30 et même plus loin. Vous verrez comment obtenir le nombre maximum de points et observer le changement sans fin des compteurs de lignes, des niveaux et des statistiques. Vous apprendrez quelles couleurs apparaissent à des niveaux au-dessus desquels une personne ne peut pas grimper. Voyez jusqu'où vous pouvez aller.

Prérequis


Pour exécuter l'IA, vous avez besoin d'un émulateur universel NES / Famicom FCEUX . L'intelligence artificielle a été développée pour FCEUX 2.2.2 , la dernière version de l'émulateur au moment de la rédaction.

Vous aurez également besoin du fichier ROM Nintendo Tetris (version américaine). Essayez de le rechercher sur Google .

Téléchargez


Décompressez lua/NintendoTetrisAI.lua de ce fichier zip source .

Lancement


Lancez FCEUX. Dans le menu, sélectionnez Fichier | Ouvrir la ROM ... Dans la boîte de dialogue Ouvrir un fichier, sélectionnez le fichier ROM Nintendo Tetris et cliquez sur Ouvrir. Le jeu va commencer.

Dans le menu, sélectionnez Fichier | Lua | Nouvelle fenêtre de script Lua ... Dans la fenêtre de script Lua, entrez le chemin d'accès à NintendoTetrisAI.lua ou cliquez sur le bouton Parcourir pour le trouver. Après cela, cliquez sur Exécuter.

Le script sur Lua vous redirigera vers le premier écran du menu. Laissez le type de jeu A-Type et vous pouvez choisir n'importe quelle musique. Sur les ordinateurs lents, la musique peut jouer très saccadée, alors vous devez la désactiver. Appuyez sur Démarrer (Entrée) pour passer à l'écran de menu suivant. Dans le deuxième menu, vous pouvez utiliser les touches fléchées pour modifier le niveau de démarrage. Cliquez sur Démarrer pour démarrer le jeu. Et ici, l'IA prendra le contrôle.

Si après avoir sélectionné un niveau sur le deuxième écran de menu, maintenez enfoncé le bouton A de la manette de jeu (vous pouvez changer la disposition du clavier dans le menu Config | Input ...) et appuyez sur Démarrer, le niveau initial sera 10 de plus que la valeur sélectionnée. Le niveau d'entrée maximal est le dix-neuvième.

La configuration


Pour accélérer le jeu, ouvrez le script Lua dans un éditeur de texte. Au début du fichier, recherchez la ligne suivante.

PLAY_FAST = false

Remplacez false par true comme indiqué ci-dessous.

PLAY_FAST = true

Enregistrez le fichier. Cliquez ensuite sur le bouton Redémarrer dans la fenêtre Script Lua.

Nintendo Tetris Mechanics


Description de Tetrimino


Chaque figure tetrimino correspond à un nom à une seule lettre qui ressemble à sa forme.


Les concepteurs de Nintendo Tetris ont arbitrairement défini l'ordre du tetrimino indiqué ci-dessus. Les figures sont représentées dans l'orientation dans laquelle elles apparaissent à l'écran, et le circuit crée une image presque symétrique (c'est peut-être pour cela que cet ordre est choisi). L'index de séquence donne à chaque tétrimino un ID numérique unique. Les identificateurs de séquence et de type sont importants au niveau de la programmation; en outre, ils se manifestent dans l'ordre des chiffres affichés dans le champ statistique (voir ci-dessous).


Les 19 orientations utilisées dans le tetrimino de Nintendo Tetris sont encodées dans un tableau situé à $8A9C de la mémoire de la console NES. Chaque figure est représentée comme une séquence de 12 octets qui peut être divisée en triplets (Y, tile, X) qui décrivent chaque carré de la figure. Les valeurs hexadécimales ci-dessus des coordonnées supérieures à $7F désignent des entiers négatifs ( $FF= −1 et $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


Au bas du tableau, il y a un enregistrement inutilisé, donnant potentiellement la possibilité d'ajouter une autre orientation. Cependant, dans diverses parties du code, $13 indique que l'identificateur d'orientation du tétrimino actif n'a pas de valeur.

Pour faciliter la lecture, les coordonnées des carrés en décimal sont indiquées ci-dessous.

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


Toutes les orientations sont placées dans une matrice 5 × 5.


Dans la figure ci-dessus, le carré blanc indique le centre de la matrice, le point de référence pour la rotation de la figure.

Le tableau d'orientation est présenté graphiquement ci-dessous.


L'identifiant d'orientation (index du tableau) est affiché en hexadécimal dans le coin supérieur droit de chaque matrice. Et les mnémoniques inventées pour ce projet sont affichées dans le coin supérieur gauche. u , r , d , l , h et v sont des abréviations de «haut, droite, bas, gauche, horizontal et vertical». Par exemple, il est plus facile de désigner l'orientation de Jd plutôt que $07 .

Les matrices contenant les orientations des figures lors de la création sont marquées d'un cadre blanc.

Tetrimino I, S et Z pourrait recevoir 4 orientations distinctes, mais les créateurs de Nintendo Tetris ont décidé de se limiter à deux. De plus, Zv et Sv ne sont pas des images miroir idéales l'une de l'autre. Les deux sont créés en tournant dans le sens antihoraire, ce qui conduit à un déséquilibre.

Le tableau d'orientation contient également des valeurs de tuile pour chaque carré de chaque figure orientée. Cependant, avec une étude minutieuse, il devient clair que les valeurs pour un type de tétrimino sont toujours les mêmes.

TJZOSLJe
7B7D7C7B7D7C7B

Les valeurs de tuile sont les indices de la table (pseudo-couleur) du motif illustré ci-dessous.


Les tuiles $7B , $7C $7D et $7D sont situées directement sous «ATIS» du mot «STATISTIQUES». Ce sont les trois types de carrés à partir desquels le tétrimino est fabriqué.

Pour les curieux, je dirai que les autruches et les pingouins sont utilisés à la fin du mode B-Type. Ce sujet est traité en détail dans la section "Fin".

Vous trouverez ci-dessous le résultat de la modification de la ROM après avoir remplacé $7B de $29 . Le cœur est la tuile sous le symbole P dans le tableau des motifs pour toutes les orientations T.


Les tuiles Cœur restent sur le terrain de jeu même après que les T modifiés soient verrouillés en place Comme indiqué ci-dessous dans la section «Création de Tetrimino», cela signifie que le terrain de jeu stocke les valeurs réelles des indices de tuile du Tetrimino joué.

Les programmeurs de jeu ont permis d'utiliser 4 tuiles distinctes pour chaque figure, et pas seulement un type de carrés invariable. Il s'agit d'une fonctionnalité utile qui peut être utilisée pour modifier l'apparence du jeu. La table de modèle a beaucoup d'espace vide pour de nouvelles tuiles qui peuvent donner à chaque tetrimino un look unique.

Les coordonnées des carrés sont très faciles à manipuler. Par exemple, une version modifiée des quatre premiers triplets de la table d'orientation est présentée ci-dessous.

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

Cette modification est similaire à la suivante:

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

Le résultat est un tetrimino divisé.


Lorsque vous déplacez un tétrimino divisé, ses carrés ne peuvent pas dépasser les limites du terrain de jeu et ne peuvent pas traverser des figures précédemment verrouillées. De plus, le jeu interdit la rotation dans cette orientation s'il conduit à un carré tombant en dehors des limites du terrain de jeu ou au fait que le carré chevauche un carré déjà couché.

Le tétrimino fendu est verrouillé en place lorsqu'il y a un support pour l'un de ses carrés. Si la figure est bloquée, les carrés suspendus en l'air continuent de pendre.

Le jeu gère les tetriminos divisés comme n'importe quelle figure normale. Cela nous fait comprendre qu'il n'y a pas de tableau supplémentaire stockant les métadonnées des figures. Par exemple, il pourrait y avoir un tableau stockant la taille du cadre de délimitation de chaque orientation pour vérifier les collisions avec le périmètre du terrain de jeu. Mais une telle table n'est pas utilisée. Au lieu de cela, le jeu vérifie simplement les quatre carrés juste avant de manipuler la forme.

De plus, les coordonnées des carrés peuvent être n'importe quelles valeurs; ils ne sont pas limités à l'intervalle [−2, 2] . Bien sûr, des valeurs qui dépassent largement cet intervalle nous donneront des chiffres inapplicables qui ne peuvent pas tenir sur le terrain. Plus important encore, comme indiqué dans la section «États du jeu et modes de rendu», lorsqu'une figure est verrouillée en place, le mécanisme de nettoyage des lignes pleines ne scanne que les déplacements des rangées de -2 à 1 par rapport au carré central de la figure; un carré avec une coordonnée y dehors de cet intervalle ne sera pas reconnu.

Rotation du tétrimino


Dans une illustration graphique du tableau d'orientation, la rotation consiste à passer d'une matrice à l'une des matrices à gauche ou à droite avec le transfert de la série si nécessaire. Ce concept est encodé dans un tableau à $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


Pour le rendre plus clair, nous déplacerons chaque colonne de ce tableau vers la ligne du tableau ci-dessous.
TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
Dans le sens antihoraireTlTuTrTdJdJlJuJrZvZhOSvShLuLrLdLlIhIv
Dans le sens des aiguilles d'une montreTrTdTlTuJuJrJdJlZvZhOSvShLdLlLuLrIhIv

Les mnémoniques des en-têtes ci-dessus peuvent être interprétés comme un index de séquence ou une clé de distribution. Par exemple, tourner Tu dans le sens antihoraire nous donne Tl , et tourner Tu dans le sens horaire donne Tr .

La table de rotation code les séquences chaînées d'ID d'orientation; par conséquent, nous pouvons modifier les enregistrements afin que la rotation transforme un type de tétrimino en un autre. Cette technique peut potentiellement être utilisée pour tirer parti d'une ligne inutilisée dans la table d'orientation.

Devant la table de rotation se trouve un code pour y accéder.

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;


Pour une rotation dans le sens antihoraire, l'indice de la table de rotation est soustrait en doublant l'ID d'orientation. En y ajoutant 1, nous obtenons l'indice de rotation dans le sens horaire.

Les coordonnées x , y et l'ID d'orientation du tétrimino actuel sont stockés aux adresses $0040 , $0041 et $0042 respectivement.

Le code utilise une variable temporaire pour sauvegarder l'ID d'orientation. Plus tard, après avoir changé l'orientation, le code vérifie que les quatre carrés sont dans les limites du terrain de jeu et aucun d'eux ne chevauche les carrés déjà couchés (le code de vérification est situé à $948B , sous le fragment de code illustré ci-dessus). Si la nouvelle orientation est incorrecte, celle d'origine est restaurée, ne permettant pas au joueur de faire pivoter la figure.

En comptant avec une croix, le contrôleur NES a huit boutons, dont l'état est représenté par le bit d'adresse $00B6 .

76543210
UnBSélectionnezCommencerEn hautVers le basVers la gaucheÀ droite

Par exemple, $00B6 contiendra la valeur $81 tandis que le joueur détient A et Left.

D'un autre côté, $00B5 signale lorsque les boutons ont été enfoncés; les bits $00B5 sont vrais que pendant une itération de la boucle de jeu (1 image rendue). Le code utilise $00B5 pour répondre aux pressions sur A et B. Chacun d'eux doit être libéré avant d'être réutilisé.

$00B5 et $00B6 sont des miroirs de $00F5 $00F6 et $00F6 . Le code des sections suivantes utilise ces adresses de manière interchangeable.

Créer Tetrimino


Le terrain de jeu de Nintendo Tetris se compose d'une matrice de 22 lignes et 10 colonnes afin que les deux premières lignes soient cachées au joueur.


Comme indiqué dans le code ci-dessous, lors de la création d'une figure Tetrimino, elle est toujours située dans les coordonnées (5, 0) terrain de jeu.

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


Ci-dessous, une matrice 5 × 5 superposée à ce point.


Aucune des matrices de création n'a de carrés au-dessus du point de départ. Autrement dit, lors de la création d'un tétrimino, ses quatre carrés deviennent immédiatement visibles pour le joueur. Cependant, si le joueur tourne rapidement la pièce avant qu'il n'ait le temps de tomber, une partie de la pièce sera temporairement cachée dans les deux premières lignes du terrain de jeu.

Habituellement, nous pensons que le jeu se termine lorsque le tas atteint le sommet. Mais en fait, ce n'est pas entièrement vrai. Le jeu se termine lorsqu'il n'est plus possible de créer la pièce suivante. Autrement dit, avant l'apparition de la figure, les quatre cellules du terrain de jeu correspondant aux positions des carrés du tétrimino créé doivent être libres. La figurine peut être verrouillée en place de telle manière qu'une partie de ses carrés apparaît en lignes numérotées négativement, et le jeu ne se termine pas; cependant, dans Nintendo Tetris, les lignes négatives sont une abstraction liée uniquement au tétrimino actif. Une fois que la figure est bloquée (devient couchée), seuls les carrés dans les lignes de zéro et plus sont écrits dans le champ. Conceptuellement, il s'avère que les lignes numérotées négativement sont automatiquement effacées après le blocage. Mais en réalité, le jeu ne stocke tout simplement pas ces données, coupant les parties supérieures des chiffres.

La zone visible du terrain de jeu 20 × 10 est stockée à $0400 ligne par ligne, chaque octet contenant la valeur de la tuile d'arrière-plan. Les cellules vides sont désignées par la tuile $EF , un carré noir uni.

Lors de la création d'une forme, trois tables de recherche sont utilisées. S'il y a un ID d'orientation arbitraire, le tableau à $9956 nous donne l'ID d'orientation lors de la création du type correspondant de tetrimino.

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


Il est plus facile de le montrer dans le tableau.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TdTdTdTdJdJdJdJdZhZhOShShLdLdLdLdIhIh

Par exemple, toutes les orientations de J sont attachées à Jd .

Le tableau à $993B contient le type Tetrimino pour l'ID d'orientation donné.

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


Pour plus de clarté, je vais tout montrer sous forme de tableau.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TTTTJJJJZZOSSLLLLII

Nous examinerons le troisième tableau de recherche dans la section suivante.

Sélection Tetrimino


Nintendo Tetris utilise un registre à décalage à rétroaction linéaire 16 bits (LFSR) comme générateur de nombres pseudo-aléatoires (PRNG) dans sa configuration Fibonacci. La valeur 16 bits est stockée en tant que big-endian aux adresses $0017 $0018 - $0018 . Un nombre arbitraire de $8988 utilisé comme graine.

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


Chaque nombre pseudo-aléatoire suivant est généré comme suit: la valeur est perçue comme un nombre de 17 bits, et le bit le plus significatif est obtenu en effectuant XOR pour les bits 1 et 9. Ensuite, la valeur est décalée vers la droite, en éliminant le bit le moins significatif.


Ce processus se produit à $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


Il est intéressant de noter que les paramètres du sous-programme ci-dessus peuvent être définis de sorte que la fonction appelante puisse spécifier la largeur du registre à décalage et l'adresse à laquelle il peut être trouvé en mémoire. Cependant, les mêmes paramètres sont utilisés partout, nous pouvons donc supposer que les développeurs ont emprunté ce code quelque part.

Pour ceux qui souhaitent modifier davantage l'algorithme, je l'ai écrit en Java.

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

Et tout ce code peut être compressé sur une seule ligne.

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

Ce PRNG génère de façon continue et déterministe 32 767 valeurs uniques, en commençant chaque cycle à partir de la graine d'origine. C'est un moins de la moitié des nombres possibles qui peuvent tenir dans le registre, et toute valeur de cet ensemble peut être utilisée comme graine. De nombreuses valeurs en dehors de l'ensemble créent une chaîne qui mène finalement à un nombre de l'ensemble. Cependant, certains nombres initiaux entraînent une séquence infinie de zéros.

Pour évaluer grossièrement les performances de ce PRNG, j'ai généré une représentation graphique des valeurs qu'il crée en fonction d'une phrase avec RANDOM.ORG .


Lors de la création de l'image, PRNG a été utilisé comme générateur de nombres pseudo-aléatoires, plutôt que comme entiers 16 bits. Chaque pixel est coloré en fonction de la valeur du bit 0. L'image a une taille de 128 × 256, c'est-à-dire qu'elle couvre toute la séquence.

Mis à part les rayures à peine perceptibles sur les côtés supérieur et gauche, il semble aléatoire. Aucun motif évident n'apparaît.

Après le démarrage, le PRNG décale constamment le registre, travaillant au moins une fois par trame. Cela ne se produit pas seulement sur l'écran de démarrage et les écrans de menu, mais aussi lorsque le tetrimino se situe entre les opérations de création de formes. Autrement dit, la figure qui apparaît ensuite dépend du nombre d'images que le joueur prend pour placer la figure. En fait, le jeu repose sur le caractère aléatoire des actions de la personne qui interagit avec lui.

Lors de la création de la figure, le code est exécuté à l'adresse $9907 , qui sélectionne le type de la nouvelle figure.

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;


À l'adresse $001A stocke un compteur du nombre de chiffres créés lors de la mise sous tension. L'incrémentation du compteur est effectuée par la première ligne du sous-programme, et puisqu'il s'agit d'un compteur à un octet, après chaque 256 pièces, il revient à zéro. Étant donné que le compteur n'est pas réinitialisé entre les jeux, l'historique des jeux précédents affecte le processus de sélection des figures. C'est une autre façon dont le jeu utilise le joueur comme source de hasard.

La routine convertit l'octet le plus significatif du nombre pseudo-aléatoire ( $0017 ) en type tetrimino et l'utilise comme index de la table située à $994E pour convertir le type en ID d'orientation de création de forme.

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


Au premier stade de la conversion, le compteur des chiffres créés est ajouté à l'octet supérieur. Ensuite, un masque est appliqué pour enregistrer uniquement les 3 bits inférieurs. Si le résultat n'est pas 7, alors c'est le bon type de tetrimino, et s'il n'est pas le même que la figure sélectionnée précédente, alors le nombre est utilisé comme index dans le tableau pour créer des figures. Sinon, le nombre pseudo-aléatoire suivant est généré et le masque est appliqué pour obtenir les 3 bits inférieurs de l'octet supérieur, puis l'ID d'orientation de création de forme précédente est ajouté. Enfin, une opération modulo est effectuée pour obtenir le type correct de tetrimino, qui est utilisé comme index dans la table de création de forme.

Étant donné que le processeur ne prend pas en charge la division avec le reste, cet opérateur est émulé en soustrayant à plusieurs reprises 7 jusqu'à ce que le résultat devienne inférieur à 7. La division avec le reste est appliquée à la somme de l'octet supérieur avec le masque appliqué et à l'ID de création d'orientation précédente. La valeur maximale de cette somme est de 25. Autrement dit, pour la réduire au reste de 4, seulement 3 itérations sont nécessaires.

Au début de chaque jeu, l'ID d'orientation de création de forme ( $0019 ) est initialisé avec une valeur de Tu ( $00 ). Cette valeur pourrait potentiellement être utilisée à $9928 lors de la première création de forme.

Lorsque vous utilisez l'ID d'orientation précédent pour créer une figure, plutôt que le type précédent, Tetrimino ajoute une distorsion, car les valeurs de l'ID d'orientation ne sont pas réparties uniformément. Ceci est indiqué dans le tableau:

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

Chaque cellule contient un type tetrimino, calculé en ajoutant l'ID d'orientation de la figure (colonne) créée à une valeur de 3 bits (ligne), puis en appliquant le reste de la division par 7 à la somme. Chaque ligne contient des doublons, car $07 et $0E également répartis par 7, tandis que 0 $0B et $12 ont un équilibre commun. Les lignes 0 et 7 sont identiques car elles sont à une distance de 7.

Il existe 56 combinaisons d'entrée possibles, et si les types de tétrimino résultants sont uniformément répartis, alors nous pouvons nous attendre à ce que dans le tableau ci-dessus, chaque type apparaisse exactement 8 fois. Mais comme indiqué ci-dessous, ce n'est pas le cas.

TapezLa fréquence
T9
J8
Z8
O8
S9
L7
Je7

T et S apparaissent plus souvent, et L et I - moins souvent. Mais le code biaisé utilisant l'ID d'orientation ne s'exécute pas chaque fois que le sous-programme est appelé.

Supposons que PRNG crée une séquence de valeurs statistiques indépendantes uniformément réparties. C'est en fait une hypothèse juste, étant donné la façon dont le jeu essaie d'obtenir le bon caractère aléatoire des actions du joueur. L'ajout du nombre de chiffres créés à l'adresse $990C pas la distribution, car le nombre augmente régulièrement entre les appels. L'utilisation du masque de bits à $990E similaire à l'application de la division par 8 avec le reste, ce qui n'affecte pas non plus la distribution. Par conséquent, la vérification à $9910 va à invalidIndex dans 1/8 de tous les cas. Et la probabilité de frapper lors de la vérification à l'adresse $9918 , où le chiffre nouvellement sélectionné est comparé au chiffre précédent, est de 7/8, avec une probabilité de coïncidence de 1/7.Cela signifie qu'il y a une chance supplémentaire d' 7/8 × 1/7 = 1/8être présent invalidIndex. En général, il existe une probabilité de 25% d'utiliser un code asymétrique et une probabilité de 75% d'utiliser un code qui sélectionne Tetrimino de manière uniforme.

Dans un ensemble de 224 tetriminos créés, l'attente mathématique est de 32 instances pour chaque type. Mais en fait, le code crée la distribution suivante:

TapezLa fréquence
T33
J32
Z32
O32
S33
L31
Je31

Autrement dit, en nettoyant 90 lignes et en atteignant le niveau 9, le joueur recevra un T et S supplémentaire et un L et I de moins que ce qui est prévu statistiquement.

Le tétrimino est choisi avec les probabilités suivantes:

TapezProbabilité
T14,73%
J14,29%
Z14,29%
O14,29%
S14,73%
L13,84%
Je13,84%

Il semble que dans l'affirmation selon laquelle le «long stick» je n'apparaît jamais quand il est nécessaire, il y a une partie de la vérité (du moins pour Nintendo Tetris).

Tetrimino Shift


Nintendo Tetris utilise le décalage automatique différé (DAS). Cliquer sur "Gauche" ou "Droite" déplace instantanément le tetrimino d'une cellule horizontalement. Tout en maintenant l'un de ces boutons de direction, le jeu décale automatiquement le chiffre toutes les 6 images avec un retard initial de 16 images.

Ce type de mouvement horizontal est contrôlé par le code à l'adresse $89AE. Comme dans le code de rotation, une variable temporaire est utilisée ici pour sauvegarder les coordonnées au cas où la nouvelle position serait incorrecte. Notez que la vérification vous empêche de déplacer la pièce pendant que le joueur appuie vers le bas.

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



Lancer de Tetrimino


La vitesse de descente automatique du Tetrimino est fonction du nombre de niveaux. Les vitesses sont codées comme le nombre d'images rendues pour la descente dans le tableau situé à $898E. Étant donné que NES fonctionne à 60,0988 images / s, vous pouvez calculer la période entre les descentes et la vitesse.

NiveauCadres de descentePériode (s / descente)Vitesse (cellules / 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

Le tableau comprend un total de 30 entrées. Après le niveau 29, la valeur des trames de descente est toujours 1.

Un nombre entier de trames de descente n'est pas une manière très détaillée de décrire la vitesse. Comme le montre le graphique ci-dessous, la vitesse augmente de façon exponentielle avec chaque niveau. En fait, le niveau 29 est deux fois plus rapide que le niveau 28.


Avec 1 image / descente, le joueur n'a pas plus d'un tiers de seconde pour positionner la figure, après quoi elle commencera à bouger. À cette vitesse de descente, DAS ne permet pas à la figurine d'atteindre les bords du terrain de jeu jusqu'à ce qu'elle se verrouille en place, ce qui signifie pour la plupart des gens une fin rapide du jeu. Cependant, certains joueurs, notamment Thor Akerlund , ont réussi à vaincre DAS avec la vibration rapide des boutons croisés ( D-pad). Dans le code de décalage illustré ci-dessus, on peut voir que tandis que le bouton de direction horizontale est relâché à travers le cadre, il est possible de décaler le tetrimino aux niveaux 29 et supérieurs avec une demi-fréquence. Il s'agit d'un maximum théorique, mais toute vibration du pouce supérieure à 3,75 taps / s peut annuler le retard d'origine de 16 images.

Si la descente automatique et contrôlée par le joueur (en appuyant sur "Bas") coïncide et se produit dans une seule image, l'effet ne s'additionne pas. L'un de ces événements, ou les deux, font descendre la forme exactement d'une cellule dans ce cadre.

La logique de commande de déclenchement se trouve à $8914. La table du cadre de descente est sous l'étiquette . Comme mentionné ci-dessus, au niveau 29 et au-dessus, la vitesse est constamment égale à 1 obturateur / image. (adresse ) commence la descente lorsqu'elle atteint ( ). L'incrémentation est effectuée à une adresse en dehors de ce fragment de code. Lors d'une descente automatique ou contrôlée, elle est remise à 0. La variable ( ) est initialisée avec la valeur (à l'adresse

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), qui est interprété comme −96. Une condition au tout début entraîne un retard initial. Le tout premier Tetrimino reste suspendu dans l'air au point de création jusqu'à ce autorepeatYqu'il augmente à 0, ce qui prend 1,6 seconde. Cependant, lorsque vous appuyez vers le bas dans cette phase, le autorepeatY0 est instantanément attribué. Il est intéressant de pouvoir déplacer et faire pivoter la figure dans cette phase du délai initial sans l'annuler.

L'incrémentation autorepeatYest effectuée tout en maintenant enfoncée. Quand il atteint 3, une descente contrôlée par l'homme (descente «douce») se produit et est autorepeatYaffectée à 1. Par conséquent, la descente douce initiale nécessite 3 images, mais elle se répète ensuite dans chaque image.

De plus, il autorepeatYpasse de 0 à 1 uniquement lorsque le jeu reconnaît que le joueur vient de cliquer vers le bas (à$00B5), mais ne reconnaît pas le maintien. Ceci est important car il est autorepeatYréinitialisé à 0 lors de la création d'un tetrimino (à l'adresse $98E8), ce qui crée une fonctionnalité importante: si le joueur lui-même abaisse la figure et qu'elle est bloquée, et qu'il continue d'appuyer sur «Bas» lors de la création de la figure suivante, ce qui se produit souvent à des niveaux élevés, puis cela ne conduira pas à une descente en douceur de la nouvelle figure. Pour cela, le joueur doit relâcher «Down», puis appuyer à nouveau sur le bouton.

Une descente potentiellement douce peut augmenter les points. holdDownPoints( $004F) augmente à chaque descente, mais une fois relâché, «Down» est remis à 0. Par conséquent, pour marquer des points, il est nécessaire d'abaisser le tetrimino dans la serrure avec une descente en douceur. Une descente douce à court terme, qui peut se produire sur le chemin de la figure, n'affecte pas les points. Le compte est mis à jour à$9BFE, mais est holdDownPointsremis à 0 peu de temps après, à l'adresse $9C2F.

Le contrôle qui empêche le joueur d'effectuer une descente douce avec un décalage horizontal de la figure complique l'ensemble des points. Cela signifie que le dernier mouvement avant de verrouiller la pièce en place doit être «Bas».

Lorsque la descente se produit, tetriminoY( $0041) est copié dans originalY( $00AE). Si la nouvelle position créée par l'incrément tetriminoYs'avère incorrecte (c'est-à-dire que la figure pousse à travers le sol du terrain de jeu ou est superposée à des carrés déjà couchés), le tétrimino reste à la position précédente. Dans ce cas, il est restaurétetriminoYet le chiffre est considéré comme bloqué. Cela signifie que le délai avant le verrouillage (le nombre maximal d'images qu'un tétrimino attend, retenant dans l'air avant le verrouillage) est égal au délai de descente.

La descente rigide (tombant instantanément) n'est pas prise en charge dans Nintendo Tetris.

Glissement et défilement


Le livret du manuel Nintendo Tetris contient un exemple de fiche illustrée:


Le glissement consiste à se déplacer le long de la surface d'autres personnages ou le long du terrain de jeu. Il est généralement utilisé pour pousser une figure sous un carré en surplomb. Le glissement peut être effectué jusqu'à ce que la minuterie de chute atteigne la vitesse de descente, après quoi la figure sera verrouillée en place. Un exemple animé est illustré ci-dessous.


D'un autre côté, le défilement vous permet de pousser des personnages dans des espaces inaccessibles d'une autre manière (voir ci-dessous).


Comme le glissement, le défilement n'est pas possible sans délai de verrouillage. Mais au-delà, le défilement exploite la façon dont le jeu manipule les formes. Avant de déplacer ou de faire pivoter la figurine, le jeu vérifie qu'après avoir changé la position, tous les carrés tetrimino seront dans des cellules vides à l'intérieur des limites du terrain de jeu. Une telle vérification, comme illustré ci-dessous, n'empêche pas la rotation à travers les blocs remplis à proximité. Comme indiqué dans la section Description de Tetrimino, chaque ligne du tableau d'orientation contient 12 octets; par conséquent, l'indice dans ce tableau est calculé en multipliant l'ID d'orientation du tétrimino actif par 12. Comme indiqué ci-dessous, toutes les multiplications dans la routine sont effectuées en utilisant des décalages et des ajouts.

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


Chaque itération du cycle décale la position du tétrimino par les coordonnées relatives de l'un des carrés de la table d'orientation afin d'obtenir l'emplacement de cellule correspondant sur le terrain de jeu. Elle vérifie ensuite que les coordonnées de la cellule sont dans les limites du terrain de jeu et que la cellule elle-même est vide.

Les commentaires décrivent plus clairement comment vérifier l'espacement des lignes. En plus des cellules dans les lignes visibles, le code considère les deux lignes cachées au-dessus du terrain de jeu comme les positions légales des carrés sans utiliser de condition composée. Cela fonctionne car dans le code supplémentaire, les nombres négatifs représentés par des variables à un octet sont équivalents à des valeurs supérieures à 127. Dans ce cas, la valeur minimale est -2, qui est stockée sous la forme

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 en notation décimale).

L'indice du terrain de jeu est la somme cellYmultipliée par 10 et cellX. Cependant, lorsque cellY−1 ( $FF= 255) ou −2 ( $FE= 254), le produit donne −10 ( $F6= 246) et −20 ( $EC= 236). Étant dans l'intervalle, il cellXne peut pas être plus de 9, ce qui donne un indice maximum de 246 + 9 = 255, et c'est bien plus loin que la fin du terrain de jeu. Cependant, le jeu s'initialise $0400- $04FFavec une valeur $EF(d'une tuile vide), créant encore 56 octets supplémentaires d'espace vide.

Étrange cette vérification d'intervallecellXeffectué après avoir examiné la cellule du terrain de jeu. Mais cela fonctionne correctement dans n'importe quel ordre. De plus, la vérification de l'intervalle évite la condition composite, comme indiqué dans le commentaire ci-dessous. Les exemples de défilement ci-dessous sont possibles en raison de la façon dont ce code vérifie les positions.

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






Comme illustré ci-dessous, vous pouvez même effectuer un glissement avec défilement.


L'IA tire pleinement parti des capacités de déplacement de la Nintendo Tetris, notamment le glissement et le défilement.

Niveau 30 et supérieur


Après avoir atteint le niveau 30, il semble que le niveau soit remis à zéro.


Mais le niveau 31 montre qu'il se passe autre chose:


Les valeurs de niveau affichées sont situées dans le tableau à l'adresse $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

Comme indiqué ci - dessous, le tableau de configuration est ordonnée de telle sorte que les carreaux avec $00sur $0Fdes symboles pour Glyphes 0sur F. Cela signifie que lors de l'affichage d'un chiffre décimal ou hexadécimal, la valeur du chiffre lui-même est utilisée comme index de la table de modèle. Dans notre cas, les valeurs de niveau sont stockées sous forme décimale codée binaire (BCD); chaque quartet de chaque octet de la séquence est une valeur de tuile.


Malheureusement, il semble que les concepteurs de jeux aient supposé que personne ne passerait le niveau 29 et ont donc décidé d'insérer seulement 30 entrées dans le tableau. Les valeurs affichées étranges sont des octets différents après le tableau. Un seul octet (à l'adresse $0044) est utilisé pour indiquer le numéro de niveau , c'est pourquoi le jeu tourne lentement autour des 256 valeurs indiquées ci-dessous.

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

Les 20 premières valeurs ordinales sont en fait une autre table qui stocke les décalages sur le terrain de jeu pour chacune des 20 lignes. Étant donné que le terrain de jeu commence par et que chaque ligne contient 10 cellules, l'adresse d'une cellule arbitraire est: Étant donné que le processeur ne prend pas directement en charge la multiplication, cette table de recherche fournit un moyen extrêmement rapide d'obtenir le produit. Le tableau correspondant occupe les 40 octets suivants. Il contient 20 adresses en petit format endian pour la table de noms 0 (une zone de mémoire VRAM contenant les valeurs des tuiles d'arrière-plan). Ce sont des pointeurs sur les lignes de décalage du terrain de jeu . Les octets restants à partir desquels les valeurs de niveau affichées sont composées sont des instructions.

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


$0400

$0400 + 10 * y + x



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

$06



Lignes et statistiques


Le nombre de lignes terminées et les statistiques tetrimino occupent chacun 2 octets aux adresses suivantes.

AdressesLa quantité
0050 - 0051Rangs
03F0 - 03F1T
03F2 - 03F3J
03F4 - 03F5Z
03F6 - 03F7O
03F8 - 03F9S
03FA - 03FBL
03FC - 03FDJe

En fait, ces valeurs sont stockées sous forme de petits BCD endian compactés sur 16 bits. Par exemple, le nombre de lignes est indiqué ci-dessous, qui est 123. Les octets sont comptés de droite à gauche afin que les chiffres décimaux se suivent dans l'ordre.


Cependant, les concepteurs de jeux ont supposé qu'aucune des valeurs ne serait supérieure à 999. Par conséquent, la logique d'affichage traite correctement le premier octet comme un BCD compressé, où chaque quartet est utilisé comme valeur de tuile. Mais le deuxième octet entier est en fait utilisé comme premier chiffre décimal. Lorsque les chiffres inférieurs vont de 99à 00, l'incrément normal du deuxième octet se produit. Par conséquent, le deuxième octet parcourt les 256 tuiles. Un exemple de ceci est illustré ci-dessous.


Après avoir effacé la ligne, le code suivant est exécuté pour incrémenter le nombre de lignes. Des vérifications sont effectuées pour les chiffres du milieu et du bas afin qu'ils restent entre 0 et 9. Mais le chiffre du haut peut être augmenté à l'infini. Si après l'incrément du nombre de lignes le chiffre inférieur est 0, cela signifie que le joueur vient de terminer un ensemble de 10 lignes et que vous devez augmenter le nombre de niveaux. Comme vous pouvez le voir dans le code ci-dessous, une vérification supplémentaire est effectuée avant l'incrément de niveau. Le deuxième contrôle est lié au niveau d'entrée sélectionné. Pour accéder à un certain niveau , quel que soit le niveau initial, le joueur doit effacer

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


X10Xlignes. Par exemple, si un joueur commence au niveau 5, il y restera jusqu'à ce qu'il ait effacé 60 lignes, après quoi il ira au niveau 6. Après cela, toutes les 10 lignes supplémentaires entraîneront une augmentation du nombre de niveaux.

Pour effectuer cette vérification, la valeur des lignes remplies est copiée de $0050- $0051vers $00A8- $00A9. Ensuite, la copie est décalée de 4 fois vers la droite, ce qui pour un BCD compressé est similaire à la division par 10. Le plus petit chiffre décimal est rejeté et les chiffres le plus élevé et le milieu sont décalés d'une position, ce qui entraîne des grignotages $00A8.


Cependant, à l'adresse, $9BEAle numéro de niveau est directement comparé à la valeur compressée du BCD $00A8. Il n'y a aucune recherche dans le tableau pour convertir la valeur BCD en décimal, et c'est une erreur claire. Par exemple, dans l'image ci-dessus, le numéro de niveau doit être comparé à $12(18 en décimal) et non à 12. Par conséquent, si un joueur décide de commencer au niveau 17, le niveau ira en fait à 120 lignes, car 18 est plus que 17.

Le tableau indique le nombre attendu de lignes requises pour la transition à chaque niveau initial. Il est comparé à ce qui se passe réellement en raison d'un bug.

012345678910111213141516171819
102030405060708090100110120130140150160170180190200
102030405060708090100100100100100100100110120130140

Le montant attendu est identique à vrai pour les niveaux initiaux 0–9. En fait, la coïncidence pour le niveau d'entrée 9 est aléatoire; 10-15 passe également au niveau suivant avec 100 lignes, car $10- c'est 16 sous forme décimale. La plus grande différence entre les prévisions et les réelles est de 60 lignes.

Je soupçonne que le bogue est dû à des modifications de conception dans les derniers stades de développement. Regardez l'écran du menu, permettant au joueur de choisir un niveau d'entrée.


Il n'y a aucune explication sur la façon de commencer à partir de niveaux supérieurs à 9. Mais dans le livret Nintendo Tetris, ce secret est révélé:


Il semble que cette fonctionnalité cachée ait été inventée au dernier moment. Peut-être qu'il a été ajouté très près de la date de sortie, ce qui n'a pas permis de le tester complètement.

En fait, la vérification de la série initiale contient une deuxième erreur liée à la sortie des valeurs de l'intervalle. Vous trouverez ci-dessous des commentaires dans le code qui expliquent mieux ce qui se passe à un bas niveau. La comparaison est effectuée en soustrayant et en vérifiant le signe du résultat. Mais un nombre signé sur un octet est limité de −128 à 127. Si la différence est inférieure à −128, le nombre est reporté et le résultat devient un nombre positif. Ce principe est expliqué dans les commentaires sur le code. Lors de la vérification que la différence se situe dans cet intervalle, il faut tenir compte du fait que le numéro de niveau, lors de l'incrémentation à des valeurs supérieures à 255, effectue le transfert à 0, et

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


$00A8potentiellement, il peut contenir n'importe quelle valeur, car son quartet supérieur est pris $0051, dont l'incrément peut se produire à l'infini.

Ces effets se chevauchent, créant des périodes pendant lesquelles le numéro de niveau reste par erreur inchangé. Les périodes se produisent à intervalles réguliers de 2 900 lignes, à partir de 2 190 lignes et durent 800 lignes. Par exemple, de 2190 ( L90) à 2990 ( T90), le niveau reste égal à $DB( 96), comme illustré ci-dessous.


La période suivante se passe de 5090 à 5890, le niveau est constamment égal à $AD( 06). De plus, pendant ces périodes, la palette de couleurs ne change pas non plus.

Coloriage - Tetrimino


À chaque niveau, les tuiles tetrimino se voient attribuer 4 couleurs uniques. Les couleurs sont tirées du tableau situé à $984C. Ses enregistrements sont réutilisés tous les 10 niveaux. De gauche à droite: les colonnes du tableau correspondant aux zones noires, blanches, bleues et rouges de l'image ci-dessous.

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





Les valeurs correspondent à la palette de couleurs NES.


Les 2 premières couleurs de chaque entrée sont toujours en noir et blanc. Cependant, la première couleur est en fait ignorée; quelle que soit la valeur, elle est considérée comme une couleur transparente à travers laquelle un fond noir uni apparaît.

L'accès à la table des couleurs est effectué dans la routine à $9808. L'index de la table des couleurs est basé sur le nombre de niveaux divisé par un reste de 10. Le cycle copie l'entrée dans les tables de palette dans VRAM. La division avec le reste est émulée par une soustraction constante de 10 jusqu'à ce que le résultat soit inférieur à 10. Le début du sous-programme avec commentaires est illustré ci-dessous.

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


Cependant, comme indiqué dans la section précédente, la soustraction et la ramification basées sur le signe de différence sont utilisées en comparaison. Un nombre signé sur un octet est limité de −128 à 127. Les commentaires mis à jour ci-dessous reflètent ce principe. Les commentaires ci-dessous sont encore simplifiés. Cette formulation révèle une erreur dans le code. L'opération de division restante est complètement ignorée pour les niveaux à partir de 138 et plus. Au lieu de cela, l'index est attribué directement au numéro de niveau, ce qui permet d'accéder aux octets bien au-delà de la fin de la table des couleurs. Comme indiqué ci-dessous, cela peut même conduire à un tétrimino presque invisible.

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





Vous trouverez ci-dessous les couleurs des 256 niveaux. Les vignettes sont disposées en 10 colonnes pour souligner l'utilisation cyclique de la table des couleurs, violée au niveau 138. Les lignes et les colonnes dans les en-têtes sont indiquées en décimal.


Après 255, le numéro de niveau revient à 0.

En outre, comme mentionné dans la section précédente, certains niveaux ne changent pas jusqu'à ce que 800 lignes soient supprimées. Pendant ces longs niveaux, les couleurs restent inchangées.

Mode de jeu


Le mode de jeu enregistré à l'adresse $00C0détermine lequel des divers écrans et menus est actuellement affiché pour l'utilisateur.

ValeurLa description
00Écran d'informations légales
01Écran d'accueil
02Menu Type de jeu
03Menu des niveaux et hauteurs
04Jeu / meilleur score / fin / pause
05Démo

Comme indiqué ci-dessus, le jeu a une routine intelligemment écrite qui agit comme une instruction de commutation en utilisant la petite table de navigation endian située immédiatement après l'appel. La liste ci-dessus montre les adresses de tous les modes de jeu. Notez que les modes «Jeu» et «Démo» utilisent le même code. Cette routine ne revient jamais. Au lieu de cela, le code utilise l'adresse de retour; généralement, il pointe vers l'instruction immédiatement après l'appel au saut vers le sous-programme (moins 1 octet), mais dans ce cas, il pointe vers la table de saut. L'adresse de retour est extraite de la pile et stockée dans - . Après avoir enregistré l'adresse de la table de sauts, le code utilise la valeur du registre A comme index et effectue la transition correspondante.

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]


Le code peut utiliser cette routine de commutation tant que les indices sont proches de 0 et qu'il n'y a pas d'espaces ou peu entre les cas possibles.

Écran d'informations légales


Le jeu démarre avec un écran affichant un avis légal.


Au bas de l'écran, Aleksey Pazhitnov est mentionné comme l'inventeur, le concepteur et le programmeur du premier Tetris. En 1984, en tant que développeur informatique au Dorodnitsyn Computing Center (un des principaux instituts de recherche de l'Académie russe des sciences de Moscou), il a développé un prototype du jeu sur l' électronique-60 (clone soviétique DEC LSI-11 ). Un prototype a été développé pour un mode de texte monochrome vert dans lequel les carrés sont indiqués par des paires de crochets []. Avec l'aide de l'écolier de 16 ans Vadim Gerasimov et de l'ingénieur informatique Dmitry Pavlovsky quelques jours après l'invention du jeu, le prototype a été porté sur un PC IBM avec MS DOS et Turbo Pascal. Au cours de deux ans, ils ont amélioré le jeu ensemble, en ajoutant des fonctionnalités telles que les couleurs tetrimino, des statistiques et, plus important encore, un code temporel et graphique qui a permis au jeu de fonctionner sur une variété de modèles de PC et de clones.

Malheureusement, en raison des particularités de l'Union soviétique à l'époque, leurs tentatives de monétiser le jeu ont échoué et, à la fin, ils ont décidé de partager la version PC avec leurs amis gratuitement. À partir de ce moment, «Tetris» a commencé à se propager viralement dans tout le pays et au-delà, copié d'un disque à l'autre. Mais depuis que le jeu a été développé par des employés d'une agence gouvernementale, l'État en était propriétaire et, en 1987, l'organisation responsable du commerce international des technologies électroniques a repris la licence du jeu (ELORG)) L'abréviation V / O sur l'écran d'informations légales peut être abrégée pour Version Originale.

La société britannique de logiciels Andromeda a tenté d'obtenir des droits sur Tetris et, avant la conclusion de la transaction, a sous-licencié le jeu à d'autres fournisseurs, par exemple l'éditeur britannique de jeux informatiques Mirrorsoft . Mirrorsoft, à son tour, l'a sous-licencié à Tengen , une filiale d'Atari Games. Tengen a accordé à Bullet-Proof Software les droits de développer un jeu pour ordinateurs et consoles au Japon, ce qui a donné Tetris pour Nintendo Famicom . Voici son écran d'informations légales.


Fait intéressant, dans cette version, l'écolier Vadim Gerasimov est appelé le concepteur et programmeur d'origine.

En essayant de sécuriser la version portable de la prochaine console Game Boy, Nintendo a utilisé le logiciel Bullet-Proof pour conclure une affaire réussie directement avec ELORG. Dans le processus de conclusion de l'accord, ELORG a révisé son contrat avec Andromeda, ajoutant qu'Andromeda n'avait obtenu que les droits sur les jeux pour ordinateurs et machines d'arcade. Pour cette raison, Bullet-Proof Software a dû payer des redevances à ELORG pour toutes les cartouches vendues pour Famicom, car les droits reçus de Tengen se sont révélés être faux. Mais grâce à la réconciliation avec ELORG, Bullet-Proof Software a finalement réussi à obtenir des droits de jeu de console mondiaux pour Nintendo.

Bullet-Proof Software a sous-licencié les droits des jeux portables de Nintendo et ensemble, ils ont développé Game Boy Tetris, qui se reflète dans l'écran d'informations légales ci-dessous.


Avec les droits mondiaux de jeu sur console, Nintendo a développé la version Tetris pour NES que nous explorons dans cet article. Ensuite, Bullet-Proof Software a sous-licencié les droits de Nintendo, ce qui lui a permis de continuer à vendre des cartouches pour Famicom au Japon.

Cela a été suivi d'une bataille juridique complexe. Nintendo et Tengen ont tous deux exigé que la partie adverse cesse de produire et de vendre leur version du jeu. En conséquence, Nintendo a gagné et des centaines de milliers de cartouches Tengen Tetris ont été détruites. Le verdict du tribunal a également interdit à plusieurs autres sociétés comme Mirrorsoft de créer des versions de console.

Pajitnov n'a jamais reçu de déductions d'ELORG ou de l'État soviétique. Cependant, en 1991, il a déménagé aux États-Unis et en 1996 avec le soutien du propriétaire du logiciel Bullet-ProofHenka Rogers a cofondé The Tetris Company , ce qui lui a permis de profiter de versions pour appareils mobiles et consoles modernes.

Il est intéressant de regarder l'écran d'informations légales comme une fenêtre donnant une idée de l'origine modeste du jeu et des batailles qui s'ensuivent pour les droits de propriété intellectuelle, car pour la plupart des joueurs, cet écran n'est qu'un ennui ennuyeux, dont la disparition semble devoir attendre indéfiniment. Le retard est défini par deux compteurs, comptant séquentiellement de 255 à 0. La première phase ne peut pas être ignorée et la seconde est ignorée en appuyant sur le bouton Démarrer. Par conséquent, l'écran d'informations légales s'affiche au moins 4,25 secondes et pas plus de 8,5 secondes. Cependant, je pense que la plupart des joueurs abandonnent, arrêtant d'appuyer sur Start pendant le premier intervalle, et à cause de cela, ils attendent la fin complète.

Le timing des phases, ainsi que le reste du jeu, est régi par un gestionnaire d'interruption non masqué appelé au début de chaque intervalle de suppression vertical (intervalle de suppression court) - une courte période de temps entre le rendu des images de télévision. Autrement dit, toutes les 16,6393 millisecondes, l'exécution normale du programme est interrompue par le code suivant. Le gestionnaire commence par passer les valeurs des registres principaux à la pile et les récupérer après l'achèvement afin de ne pas interférer avec la tâche interrompue. L'appel met à jour la VRAM, convertissant la description du modèle de mémoire en ce qui est affiché à l'écran. En outre, le gestionnaire réduit la valeur du compteur de l'écran d'informations légales s'il est supérieur à zéro. Défi

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()effectue l'étape requise par l'équipement de génération de trame. Le gestionnaire continue de travailler en incrémentant le compteur de trames - la petite valeur endian 16 bits stockée à l'adresse $00B1- $00B2qu'il utilise à différents endroits pour un timing contrôlé. Après cela, le nombre pseudo-aléatoire suivant est généré; comme mentionné ci-dessus, cela se produit quel que soit le mode au moins une fois par image. L' $8040indicateur d'intervalle de suppression verticale est défini à l'adresse , ce qui signifie que le gestionnaire vient d'être exécuté. Enfin, les boutons du contrôleur sont interrogés; le comportement de cette routine est décrit ci-dessous dans la section Démo.

Le drapeau est verticalBlankingIntervalutilisé par la routine décrite ci-dessus. Il continue jusqu'à ce que l'exécution du gestionnaire d'interruption commence.

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;


Cette routine de blocage est utilisée par deux étapes de synchronisation de l'écran d'informations légales, qui sont exécutées l'une après l'autre. Le script Lua AI contourne ce délai en mettant les deux compteurs à 0.

8236: LDA #$FF
8238: JSR $A459

...

A459: STA $00C3 ; legalScreenCounter1 = 255;

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

A462: RTS ; return;


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

; do {

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

8245: JSR $AA2F ; waitForVerticalBlankingInterval();

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

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




Démo


La démo montre environ 80 secondes de gameplay préenregistré. Il n'affiche pas seulement le fichier vidéo, mais utilise le même moteur que dans le jeu. Pendant la lecture, deux tables sont utilisées. Le premier, situé à l'adresse $DF00, contient la séquence suivante de création de tetrimino:

TJTSZJTSZJSZLZJTTSITO JSZLZLIOLZLIOJTSITOJ

Lors de la création d'une figure, elle est soit sélectionnée au hasard, soit lue dans le tableau, selon le mode. La commutation a lieu à l'adresse $98EB. Le type tetrimino est extrait des bits 6, 5 et 4 de chaque octet. De temps en temps, cette opération nous donne une valeur - le mauvais type. Cependant, la table en créant des formes ( ) utilisée pour une conversion de type en Tetrimino orientation ID est en fait situé entre les deux tables liées: Signification

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

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

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

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


$07$994E

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


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


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


$07l'oblige à lire au-delà de la fin du tableau, dans le suivant, ce qui donne Td( $02).

En raison de cet effet, ce schéma peut nous donner une séquence illimitée mais reproductible d'ID pseudo-aléatoires de l'orientation des figures créées. Le code fonctionnera car toute adresse arbitraire dans une séquence d'octets changeante ne nous permet pas de déterminer où se termine la table. En fait, la séquence à l'adresse $DF00peut faire partie de quelque chose de complètement étranger à cela, surtout si l'on considère que le but des 5 bits non nuls restants n'est pas clair et que la séquence générée démontre une répétabilité.

Lors de l'initialisation du mode démo, l'index de table ( $00D3) est réinitialisé à l'adresse $872B.

Le deuxième tableau de la démo contient un enregistrement des boutons de la manette de jeu encodés en paires d'octets. Les bits du premier octet correspondent aux boutons.

76543210
UnBSélectionnezCommencerEn hautVers le basVers la gaucheÀ droite

Le deuxième octet stocke le nombre de trames pendant lesquelles une combinaison de boutons est enfoncée.

Le tableau prend des adresses $DD00- $DEFFet se compose de 256 paires. L'accès est effectué par le sous-programme à l'adresse $9D5B. Étant donné que la table des boutons de démonstration fait 512 octets, un index de deux octets est nécessaire pour y accéder. L'index est stocké sous forme de petit endian à - . Il est initialisé avec la valeur de l'adresse de la table , et son incrémentation est effectuée par le code suivant. Les programmeurs ont laissé le traitement d'entrée du lecteur dans le code, ce qui nous permet de regarder le processus de développement et de remplacer la démo par un autre enregistrement. Le mode d'enregistrement de démonstration est activé lorsqu'une valeur est attribuée.

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. Dans ce cas, le code suivant est lancé, destiné à l'écriture dans le tableau des boutons de la démo. Cependant, la table est stockée dans le PRG-ROM. Tenter d'y écrire n'affectera pas les données enregistrées. Au lieu de cela, chaque opération d'écriture déclenche un changement de banque, ce qui entraîne l'effet glitch illustré ci-dessous.

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;





Cela suggère que les développeurs pourraient exécuter le programme partiellement ou entièrement en RAM.

Pour contourner cet obstacle, j'en ai créé lua/RecordDemo.luaun situé dans un zip avec le code source . Après être passé en mode d'enregistrement de démonstration, il redirige les opérations d'écriture vers la table dans la console Lua. De là, les octets peuvent être copiés et collés dans la ROM.

Pour enregistrer votre propre démo, lancez FCEUX et téléchargez le fichier ROM Nintendo Tetris (Fichier | Ouvrir ROM ...). Ouvrez ensuite la fenêtre Lua Script (Fichier | Lua | Nouvelle fenêtre Lua Script ...), recherchez le fichier ou entrez le chemin. Appuyez sur le bouton Exécuter pour démarrer le mode d'enregistrement de démonstration, puis cliquez sur la fenêtre FCEUX pour y basculer le focus. Vous pouvez contrôler les formes jusqu'à ce que la table des boutons soit pleine. Après cela, le jeu reviendra automatiquement à l'économiseur d'écran. Cliquez sur Arrêter dans la fenêtre Lua Script pour arrêter le script. Les données enregistrées apparaîtront dans la console de sortie, comme illustré dans la figure ci-dessous.


Sélectionnez tout le contenu et copiez-le dans le presse-papiers (Ctrl + C). Exécutez ensuite l'éditeur Hex (Debug | Hex Editor ...). Dans le menu Hex Editor, sélectionnez Afficher | Fichier ROM puis Fichier | Aller à l'adresse. Dans la boîte de dialogue Goto, entrez 5D10 (adresse du tableau des boutons de démonstration dans le fichier ROM) et cliquez sur OK. Collez ensuite le contenu du presse-papiers (Ctrl + V).


Enfin, dans le menu FCEUX, sélectionnez NES | Réinitialiser Si vous avez réussi à répéter toutes ces étapes, la démo devrait être remplacée par votre propre version.

Si vous souhaitez enregistrer les modifications, sélectionnez Fichier | Enregistrez Rom sous ... et entrez le nom du fichier ROM modifié, puis cliquez sur Enregistrer.

De la même manière, vous pouvez ajuster la séquence des tetriminos créés.

Écran de décès


Comme mentionné ci-dessus, la plupart des joueurs ne peuvent pas faire face à la vitesse de descente des chiffres au niveau 29, ce qui conduit rapidement à la fin du jeu. Par conséquent, les joueurs auxquels il est devenu associé avec le nom «écran de la mort». Mais d'un point de vue technique, l'écran de mort ne permet pas au joueur d'aller plus loin en raison d'un bug dans lequel une descente rapide n'est en fait pas un bug, mais une fonctionnalité. Les concepteurs étaient si gentils qu'ils ont permis au jeu de continuer tandis que le joueur était capable de résister à une vitesse surhumaine.

Un véritable écran de mort apparaît sur environ 1550 rangées rétractées. Il se manifeste de différentes manières. Parfois, le jeu redémarre. Dans d'autres cas, l'écran devient noir. Généralement, un jeu se bloque («se bloque») immédiatement après la suppression d'une ligne, comme illustré ci-dessous. Ces effets sont souvent précédés d'artefacts graphiques aléatoires.


L'écran de mort est le résultat d'un bogue dans le code qui ajoute des points lors de la suppression de lignes. Le compte à six caractères est stocké sous la forme d'un petit BCD endian compressé de 24 bits et se trouve à $0053- $0055. Pour effectuer des conversions entre le nombre de lignes effacées et les points obtenus, un tableau est utilisé; chaque entrée est une petite valeur endian BCD compactée de 16 bits. Après avoir incrémenté le nombre total de lignes et éventuellement le niveau, la valeur de cette liste est multipliée par le numéro de niveau plus un et le résultat est ajouté aux points. Cela est clairement démontré dans le tableau du livret du manuel Nintendo Tetris:

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





Comme illustré ci-dessous, la multiplication est simulée par un cycle qui ajoute des points au score. Il est exécuté après le verrouillage de la forme, même si aucune ligne n'est effacée. Malheureusement, le Ricoh 2A03 n'a pas de mode décimal binaire 6502; il pourrait grandement simplifier le corps du cycle. Au lieu de cela, l'addition est effectuée par étapes en utilisant le mode binaire. Tout chiffre dépassant 9 après addition est essentiellement obtenu en soustrayant 10 et en incrémentant les chiffres à gauche. Par exemple, cela est converti en . Mais un tel schéma n'est pas entièrement protégé. Prendre : un chèque ne peut pas convertir le résultat en

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. Pour compenser cela, aucun des chiffres décimaux dans les entrées du tableau de bord ne dépasse 6. De plus, pour pouvoir l'utiliser, le dernier chiffre de tous les enregistrements est toujours 0.

Il faut du temps pour terminer ce cycle long et compliqué. À des niveaux élevés, un grand nombre d'itérations affecte le timing du jeu, car il faut plus de 1/60 seconde pour générer chaque image. Tout cela conduit à diverses manifestations de «l'écran de la mort».

Le script Lua AI limite le nombre d'itérations dans une boucle à 30 - la valeur maximale que les concepteurs pourraient atteindre telle que conçue par les concepteurs, ce qui élimine l'écran de mort.

Fin


Dans le livret Nintendo Tetris, le jeu A-Type est décrit comme suit:


Le jeu récompense les joueurs qui ont marqué un nombre suffisamment important de points dans l'une des cinq animations des fins. Le choix de la fin est entièrement basé sur les deux chiffres les plus à gauche du score à six chiffres. Comme indiqué ci-dessous, pour obtenir l'une des fins, le joueur doit marquer au moins 30 000 points. Il convient de noter que - est un miroir des adresses - . Le compte est dupliqué aux adresses - . Après avoir réussi le premier test, l'animation de fin est sélectionnée par l'instruction switch suivante.

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


Au final, des fusées de taille croissante sont lancées depuis la rampe de lancement à côté de la cathédrale Saint-Basile. Dans la quatrième fin, le vaisseau spatial Bourane est montré - la version soviétique de la navette spatiale américaine. Dans la meilleure fin, la cathédrale elle-même s'élève dans les airs et un OVNI est suspendu au-dessus de la rampe de lancement. Ci-dessous, une image de chaque fin et le score qui lui est associé.
30000–49999
50000–69999
70000–99999
100000–119999
120000+

En mode de jeu de type B, un autre test est implémenté, décrit dans le livret Nintendo Tetris comme suit:


Si le joueur réussit à effacer 25 lignes, le jeu montre la fin, selon le niveau initial. Les terminaisons des niveaux 0 à 8 se composent d'animaux et d'objets volant ou courant dans le cadre, passant mystérieusement derrière la cathédrale Saint-Basile. L'ovni de la meilleure fin du mode A-Type apparaît à la fin 3. À la fin 4, des ptérosaures volants disparus apparaissent, et à la fin 7 les dragons volants mythiques sont montrés. Dans les terminaisons 2 et 6, des oiseaux sans ailes sont représentés: pingouins en cours d'exécution et autruches. À la fin de 5, le ciel est rempli de BONS dirigeables (à ne pas confondre avec les dirigeables Goodyear). Et à la fin du 8, beaucoup de «Buranas» balayent l'écran, bien qu'en réalité il n'y en ait qu'un.

La hauteur de départ (plus 1) est utilisée comme multiplicateur, récompensant le joueur avec un grand nombre d'animaux / objets pour une complexité accrue.

Dans la meilleure fin du B-Type, un château rempli de personnages de l'univers Nintendo est montré: la princesse Peach frappe des mains, Kid Icarus joue du violon, Donkey Kong frappe au gros tambour, Mario et Luigi dansent, Bowser joue de l'accordéon, Samus joue du violoncelle, Link - sur une flûte, tandis que les coupoles de la cathédrale Saint-Basile s'élancent dans les airs. La quantité de ces éléments indiquée à la fin dépend de la hauteur initiale. Vous trouverez ci-dessous des images des 10 fins.


L'IA peut rapidement effacer les 25 lignes requises en mode de type B à n'importe quel niveau et hauteur initiaux, ce qui vous permet de voir n'importe quelle fin. Il vaut également la peine d'évaluer à quel point il gère de grands tas de blocs aléatoires.

Aux extrémités 0 à 8, jusqu'à 6 objets peuvent se déplacer dans le cadre. Les coordonnées y des objets sont stockées dans une table située à en $A7B7. Les distances horizontales entre les objets sont stockées dans un tableau à l'adresse . Une séquence de valeurs avec un signe à l'adresse détermine la vitesse et la direction des objets. Les indices de sprite sont stockés à . En fait, chaque objet se compose de deux sprites avec des indices adjacents. Pour obtenir le deuxième index, vous devez ajouter 1. Par exemple, un dragon se compose de

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


$38et $39. Les tuiles de ces sprites sont contenues dans les tableaux de modèles ci-dessous.


Nous avons examiné le tableau central du motif ci-dessus, il est utilisé pour afficher le tetrimino et le terrain de jeu. Fait intéressant, il contient tout l'alphabet, tandis que d'autres n'en contiennent qu'une partie pour économiser de l'espace. Mais encore plus intéressants sont les sprites d'avion et d'hélicoptère dans le tableau des motifs à gauche; ils n'apparaissent pas dans les fins ou dans d'autres parties du jeu. Il est avéré que l'avion et l'hélicoptère ont des indices sprites $30et $16vous pouvez modifier le tableau ci - dessus, pour les voir en action.



Malheureusement, les supports d'hélicoptère ne sont pas affichés, mais les rotors principal et arrière sont magnifiquement animés.

2 joueurs contre


Nintendo Tetris contient un mode à deux joueurs incomplet, qui peut être activé en changeant le nombre de joueurs ( $00BE) à 2. Comme indiqué ci-dessous, deux champs de jeu apparaissent à l'arrière-plan du mode solo.


Il n'y a pas de frontière entre les champs car la région centrale de l'arrière-plan est d'un noir uni. Les valeurs 003affichées au-dessus des terrains de jeu indiquent le nombre de lignes effacées par chaque joueur. La seule figure commune pour deux joueurs apparaît au même endroit qu'en mode solo. Malheureusement, il est situé sur le bon terrain de jeu. Les carrés et autres tuiles sont mal colorés. Et lorsque le joueur perd, le jeu redémarre.

Mais si vous ignorez ces problèmes, le mode est tout à fait jouable. Chaque joueur peut contrôler indépendamment les pièces dans le terrain de jeu correspondant. Et lorsqu'un joueur tape Double, Triple ou Tetris (c'est-à-dire qu'il efface deux, trois ou quatre rangées), des rangées d'ordures avec un carré manquant apparaissent dans la partie inférieure du terrain de jeu de l'adversaire.

Un champ supplémentaire est situé à $0500. A $0060- $007F, étant généralement un miroir $0040- $005F, sont utilisés pour le deuxième joueur.

Probablement, ce mode intéressant a été abandonné en raison d'un calendrier de développement chargé. Ou peut-être qu'il a été intentionnellement laissé inachevé. L'une des raisons pour lesquelles Tetris a été choisi comme jeu fourni avec le Nintendo Game Boy était parce qu'il encourageait l'achat de Game Link Cable- un accessoire qui relie deux Game Boys ensemble pour lancer le mode 2 joueurs contre. Ce câble a ajouté un élément de «socialité» au système - il a encouragé des amis à acheter un Game Boy pour se joindre au plaisir. Peut-être que Nintendo avait peur que si la version console du jeu avait 2 joueurs contre le mode, alors le pouvoir "publicitaire" de Tetris, qui a stimulé l'achat de Game Boy, pourrait être affaibli.

Musique et effets sonores


La musique de fond est activée lorsque l' $06F5une des valeurs répertoriées dans le tableau est affectée.

ValeurLa description
01Musique d'écran de démarrage inutilisée
02Cible du mode B atteinte
03Musique-1
04Musique-2
05Musique-3
06Musique-1 allegro
07Musique-2 allegro
08Musique-3 allegro
09Écran Félicitations
0AFin
0BCible du mode B atteinte
Vous pouvez écouter la musique inutilisée de l'économiseur d'écran ici . Dans le jeu lui-même, rien ne retentit pendant l'écran d'économiseur d'écran.

Music-1 est une version de " Dance of the Dragee Fairy ", musique pour la ballerine du troisième acte de la valse pas de deux "The Nutcracker" de Tchaikovsky. La musique finale est une variation des « Vers torero », un air de l'opéra Carmen Georges Bizet. Ces compositions sont arrangées par le compositeur du reste de la musique d' Hirokazu Tanaka .

Music-2 s'inspire des chansons traditionnelles du folklore russe. Music-3 est mystérieux, futuriste et tendre; Pendant un certain temps, c'était la sonnerie du téléphone du service client de Nintendo of America.

Pour aider le joueur à paniquer lorsque la hauteur du tas s'approche du plafond du terrain de jeu, une version de la musique de fond commence à jouer à un rythme rapide ( $06- $08).

Fait intéressant, parmi les compositions musicales, il n'y a pas de « Chapman », un thème célèbre qui sonne dans Game Boy Tetris.

Les effets sonores sont déclenchés par l'enregistrement dans $06F0et $06F1, selon le tableau suivant.

L'adresseValeurLa description
06F002Le rideau de fin de partie
06F003Fusée à la fin
06F101Sélection des options de menu
06F102Sélection de l'écran de menu
06F103Tetrimino Shift
06F104Reçu Tetris
06F105Rotation du tétrimino
06F106Nouveau niveau
06F107Serrure Tetrimino
06F108Gazouillis
06F109Nettoyage des rangées
06F10ALigne remplie

États de jeu et modes de rendu


Pendant le jeu, l'état actuel du jeu est représenté par un entier à l'adresse $0048. La plupart du temps, il a une signification $01qui indique que le joueur contrôle le tetrimino actif. Cependant, lorsque la pièce est verrouillée en place, le jeu passe progressivement d'un état $02à l'autre $08, comme le montre le tableau.

ConditionLa description
00ID d'orientation non attribué
01Le joueur contrôle le tetrimino actif
02Serrure Tetrimino sur le terrain de jeu
03Vérification des lignes remplies
04Afficher l'animation de nettoyage des lignes
05Mise à jour des lignes et des statistiques
06Vérification de la cible du mode B-Type
07Non utilisé
08Créer le prochain Tetrimino
09Non utilisé
0AMise à jour du rideau de jeu
0BIncrément d'état de jeu

La ramification du code, selon l'état du jeu, se produit à l'adresse suivante $81B2: Dans l'état de commutation, il passe au code qui attribue une valeur indiquant que l'orientation n'est pas spécifiée. Le gestionnaire n'est jamais appelé; cependant, l'état du jeu sert de signal à d'autres parties du code. L'état permet au joueur de déplacer, tourner et abaisser le tetrimino actif: Comme indiqué dans les sections précédentes, les routines de shift, rotation et d'abaissement de la figure avant d'exécuter le code vérifient les nouvelles positions du tetrimino. La seule façon de bloquer une forme dans la mauvaise position est de la créer au-dessus d'une forme existante. Dans ce cas, le jeu se termine. Comme indiqué ci-dessous, le code d'état effectue cette vérification.

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


$00orientationID$13

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

9E33: RTS ; return;


$00

$01

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

81D8: RTS ; return;


$02. Si la position verrouillée est correcte, elle marque les 4 cellules associées du terrain de jeu comme occupées. Sinon, elle fait la transition vers un état - le rideau inquiétant de la fin du jeu.

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


Le rideau est tiré du haut du terrain de jeu vers le bas, en descendant d'une ligne toutes les 4 images. curtainRow( $0058) s'initialise avec une valeur de -16, créant un délai supplémentaire de 0,27 seconde entre le verrouillage final et le début de l'animation. À l'adresse $9A21dans l'état du $0Acode indiqué ci-dessous, on accède à la table de multiplication, qui est affichée par erreur sous forme de numéros de niveau. Cela se fait à l'échelle curtainRowde 10. De plus, comme indiqué ci-dessus, le code à l'adresse $9A51démarre l'animation de fin si le score du joueur n'est pas inférieur à 30 000 points; sinon, il s'attend à cliquer sur Démarrer. Le code se termine en affectant une valeur à l'état du jeu , mais le gestionnaire correspondant n'est pas appelé car le jeu est terminé.

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

Les lignes du terrain de jeu sont copiées de façon incrémentielle dans la VRAM pour les afficher. L'index de la ligne courante à copier est contenu dans vramRow( $0049). Une $9A3C vramRowvaleur est affectée à l'adresse curtainRow, ce qui rend finalement cette ligne visible lors du rendu.

Les manipulations avec VRAM se produisent pendant un intervalle de suppression vertical, qui est reconnu par le gestionnaire d'interruption décrit dans la section «Écran d'informations légales». Il appelle le sous-programme indiqué ci-dessous (marqué dans les commentaires du gestionnaire d'interruption comme render()). Le mode de rendu est similaire au mode de jeu. Il est stocké à l'adresse et peut avoir l'une des valeurs suivantes:

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

ValeurLa description
00Écran avec légal informations et économiseur d'écran
01Écrans de menu
02Écran Félicitations
03Jeu et démo
04Fin de l'animation

Une partie du mode de rendu est $03illustrée ci-dessous. Comme vous pouvez le voir ci-dessous, il passe en VRAM une ligne du terrain de jeu avec un index . Si elle est supérieure à 20, la routine ne fait rien. Le tableau ( ) contient les adresses VRAM en petit format endian correspondant aux lignes affichées du terrain de jeu décalées de 6 en mode normal et de -2 et 12 pour le terrain de jeu en mode inachevé 2 Player Versus. Les octets de ce tableau font partie de la liste de valeurs qui sont affichées par erreur sous forme de numéros de niveau après le niveau 29. Les octets inférieurs et supérieurs adjacents de chaque adresse sont obtenus séparément et sont essentiellement combinés en une adresse 16 bits qui est utilisée dans le cycle de copie. Un incrément est exécuté à la fin du sous-programme.

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


copyPlayfieldRowToVRAM()vramRowvramRow

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

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

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

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

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

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

974F: JMP $9767 ; } else {

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

975B: JMP $9767 ; } else {

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

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

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

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

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

977E: RTS ; return;


vramPlayfieldRows$96EA

vramRow. Si la valeur atteint 20, une valeur de 32 lui est attribuée, ce qui signifie que la copie est entièrement terminée. Comme indiqué ci-dessus, seules 4 lignes sont copiées par image.

Le gestionnaire d'état $03est responsable de reconnaître les lignes terminées et de les retirer du terrain de jeu. Pendant 4 appels séparés, il scanne les décalages de ligne [−2, 1]près du centre du tetrimino (les deux coordonnées de tous les carrés du tetrimino sont dans cet intervalle). Les index des lignes terminées sont stockés dans $004A- $004D; l'index enregistré 0 est utilisé pour indiquer qu'aucune ligne terminée n'a été trouvée dans cette passe. Le gestionnaire est illustré ci-dessous. La vérification au début ne permet pas au gestionnaire de s'exécuter lors du transfert des lignes du terrain de jeu vers la VRAM (gestionnaire d'état

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$03appelé dans chaque trame). Si des lignes remplies sont détectées, elle est vramRowréinitialisée à 0, ce qui force un transfert complet.

lineIndex( $00A9) est initialisé avec une valeur de 0 et son incrémentation est effectuée à chaque passage.

Contrairement à l'état du jeu $0Aet à la routine de copie de champ de jeu, qui utilisent la table de multiplication des adresses $96D6, un bloc commençant par $9A82multiplie rowYpar 10 en utilisant des décalages et des ajouts:

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

cela se fait uniquement parce qu'il est rowYlimité par l'intervalle [0, 20], et la table de multiplication ne couvre que [0, 19]. Le balayage des lignes peut s'étendre au-delà de la fin du terrain de jeu. Cependant, comme dit précédemment, le jeu s'initialise $0400- $04FFavec une valeur$EF(tuile vide), créant plus de 5 lignes cachées vides supplémentaires sous le sol du terrain de jeu.

Un bloc commençant par $9ADAfait partie du mode incomplet de 2 joueurs contre. Comme mentionné ci-dessus, le nettoyage des rangées ajoute des débris au terrain de jeu de l'adversaire. Le nombre de lignes de déchets est déterminé par le tableau à l'adresse $9B53: le cycle à l'adresse déplace le matériau au-dessus de la ligne remplie d'une ligne vers le bas. Il profite du fait que chaque ligne d'une séquence continue est séparée de l'autre de 10 octets. La boucle suivante efface la ligne supérieure. L'animation d'effacement de ligne est effectuée pendant l'état de jeu , mais comme illustré ci-dessous, elle ne se produit pas dans le gestionnaire d'état de jeu, qui est complètement vide.

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


$9AA6

$04

9E39: RTS ; return;

Au lieu de cela, pendant l'état du jeu $04, la prochaine branche du mode de rendu est effectuée $03. et des valeurs en miroir sont nécessaires pour le mode 2 Player Versus inachevé. Le sous-programme est illustré ci-dessous . Il est appelé dans chaque trame, mais la condition au début ne permet de l'exécuter que dans toutes les quatre trames. À chaque passage, il parcourt la liste des index des lignes terminées et efface 2 colonnes de ces lignes, se déplaçant de la colonne centrale vers l'extérieur. Une adresse VRAM 16 bits est construite de la même manière que celle illustrée dans la routine de champ de copie. Cependant, dans ce cas, il effectue un décalage par l'index de colonne obtenu à partir du tableau ci-dessous.

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


Pour l'animation de nettoyage, 5 passes sont nécessaires. Ensuite, le code passe à l'état de jeu suivant.

Le gestionnaire d'état du jeu $05contient le code décrit dans la section «Lignes et statistiques». Le gestionnaire se termine avec ce code: La variable n'est pas réinitialisée jusqu'à la fin de l'état du jeu , après quoi elle est utilisée pour mettre à jour le nombre total de lignes et le score. Cette séquence permet d'exécuter un bug intéressant. En mode démo, vous devez attendre que le jeu recueille la ligne complète, puis appuyer rapidement sur Démarrer jusqu'à la fin de l'animation pour effacer la série. Le jeu reviendra à l'économiseur d'écran, mais si vous choisissez le bon moment, la valeur sera enregistrée. Vous pouvez maintenant démarrer le jeu en mode A-Type. Lorsqu'il est verrouillé à la place de la première figure, le gestionnaire d'état du jeu

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

9CA2: INC $0048 ; playState = B_TYPE_GOAL_CHECK;

9CA4: RTS ; return;


completedLines$05completedLines$03commence à analyser les lignes terminées. Il ne les trouvera pas, mais les laissera completedLinesinchangés. Enfin, lorsque l'état du jeu est atteint, le $05nombre total de lignes et le score augmenteront, comme si vous les aviez marqués.

La façon la plus simple de le faire est d'obtenir le plus gros montant, en attendant que la démo récupère Tetris (il y en aura 2 dans la démo). Dès que vous voyez le scintillement de l'écran, cliquez sur Démarrer.


Après avoir commencé une nouvelle partie, l'écran continuera de clignoter. Tout cela grâce au code suivant appelé par le gestionnaire d'interruption. En fait, si vous laissez le premier chiffre descendre automatiquement sur le terrain de jeu, le score augmentera d'une valeur encore plus grande, car ( ) enregistrera également sa valeur dans la démo. Cela est vrai même pour les cas où la démo n'a pas rempli une seule ligne. Il n'est pas réinitialisé tant que le bouton «Down» n'est pas enfoncé. De plus, si vous appuyez sur Démarrer pendant l'animation d'effacement de la série de combinaisons Tetris en mode démo, puis attendez que la démo recommence, non seulement les points pour Tetris seront comptés dans la démo, mais tout le timing sera mélangé. En conséquence, la démo perdra le jeu. Après avoir bouclé la fin du jeu, vous pouvez revenir à l'économiseur d'écran en cliquant sur Démarrer.

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

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

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

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

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

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

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

; }
; }
; }

9698: STX $2007 ; update background tile color;


holdDownPoints$004FholdDownPoints



L'état du jeu $06effectue une vérification de cible pour les jeux de type B. En mode A, il s'agit essentiellement d'une trame inutilisée.

L'état du jeu $07contient exclusivement la logique incomplète à 2 joueurs. En mode solo, il se comporte comme un cadre inutilisé.

L'état du jeu est $08abordé dans les sections «Création de Tetrimino» et «Choix de Tetrimino».

L'état du jeu n'est $09pas utilisé. $0Baugmente l'état du jeu, mais semble également inutilisé.

Et enfin, le cycle principal du jeu:

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


All Articles