En este artículo, exploraré la mecánica aparentemente simple de Nintendo Tetris, y en la segunda parte explicaré cómo creé una IA que explota estas mecánicas.
Pruébalo tú mismo
Sobre el proyecto
Para aquellos que carecen de la perseverancia, la paciencia y el tiempo necesarios para dominar Nintendo Tetris, creé una IA que puede jugar por sí sola. Finalmente puedes llegar al nivel 30 e incluso más. Verá cómo obtener el número máximo de puntos y observará el cambio sin fin de contadores de filas, niveles y estadísticas. Aprenderá qué colores aparecen en niveles por encima de los cuales una persona no podría escalar. Mira hasta dónde puedes llegar.
Requisitos
Para ejecutar la IA, necesita un
emulador universal NES / Famicom
FCEUX . La inteligencia artificial fue desarrollada para
FCEUX 2.2.2 , la versión más nueva del emulador en el momento de la escritura.
También necesitarás el archivo ROM de Nintendo Tetris (versión estadounidense). Intenta buscarlo en
Google .
Descargar
Descomprima
lua/NintendoTetrisAI.lua
de este
archivo zip de origen .
Lanzamiento
Lanzamiento de FCEUX. Desde el menú, seleccione Archivo | Abrir ROM ... En el cuadro de diálogo Abrir archivo, selecciona el archivo ROM de Nintendo Tetris y haz clic en Abrir. El juego comenzará.
Desde el menú, seleccione Archivo | Lua Nueva ventana de Lua Script ... En la ventana de Lua Script, ingrese la ruta a
NintendoTetrisAI.lua
o haga clic en el botón Examinar para encontrarla. Después de eso, haga clic en Ejecutar.
El script en Lua lo redireccionará a la primera pantalla del menú. Deje el tipo de juego A-Type, y puede elegir cualquier música. En computadoras lentas, la música puede reproducirse muy bruscamente, entonces debes apagarla. Presione Inicio (Entrar) para ir a la siguiente pantalla de menú. En el segundo menú, puede usar las teclas de flecha para cambiar el nivel de inicio. Haz clic en Iniciar para comenzar el juego. Y aquí la IA tomará el control.
Si después de seleccionar un nivel en la segunda pantalla del menú, mantenga presionado el botón A del gamepad (puede cambiar la distribución del teclado en el menú Config | Entrada ...) y presione Inicio, entonces el nivel inicial será 10 más que el valor seleccionado. El nivel de entrada máximo es el decimonoveno.
Configuracion
Para que el juego se ejecute más rápido, abra el script Lua en un editor de texto. Al comienzo del archivo, busque la siguiente línea.
PLAY_FAST = false
Reemplace
false
con
true
como se muestra a continuación.
PLAY_FAST = true
Guarda el archivo. Luego haga clic en el botón Reiniciar en la ventana Lua Script.
Mecánica de Nintendo Tetris
Descripción del Tetrimino
Cada figura tetrimino corresponde a un nombre de una letra que se asemeja a su forma.
Los diseñadores de Nintendo Tetris establecen arbitrariamente el orden de tetrimino que se muestra arriba. Las figuras se muestran en la orientación en la que aparecen en la pantalla, y el circuito crea una imagen casi simétrica (quizás por eso se elige este orden). El índice de secuencia le da a cada tetrimino una identificación numérica única. Los identificadores de secuencia y tipo son importantes a nivel de programación; Además, se manifiestan en el orden de las cifras que se muestran en el campo de estadísticas (ver más abajo).
Las 19 orientaciones utilizadas en el Nintendo Tetris tetrimino están codificadas en una tabla ubicada en
$8A9C
de la memoria de la consola NES. Cada figura se representa como una secuencia de 12 bytes que se puede dividir en triples
(Y, tile, X)
que describen cada cuadrado de la figura. Los valores hexadecimales anteriores de las coordenadas superiores a
$7F
denotan enteros negativos (
$FF= −1
y
$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
En la parte inferior de la tabla hay un registro no utilizado, que potencialmente brinda la oportunidad de agregar otra orientación. Sin embargo, en varias partes del código,
$13
indica que el identificador de orientación del tetrimino activo no tiene asignado un valor.
Para facilitar la lectura, las coordenadas de los cuadrados en decimal se muestran a continuación.
-- { { 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)
Todas las orientaciones se colocan en una matriz de 5 × 5.
En la figura anterior, el cuadrado blanco indica el centro de la matriz, el punto de referencia para la rotación de la figura.
La tabla de orientación se presenta gráficamente a continuación.
El identificador de orientación (índice de la tabla) se muestra en hexadecimal en la esquina superior derecha de cada matriz. Y la mnemotecnia inventada para este proyecto se muestra en la esquina superior izquierda.
u
,
r
,
d
,
l
,
h
y
v
son abreviaturas de "arriba, derecha, abajo, izquierda, horizontal y vertical". Por ejemplo, es más fácil denotar la orientación de
Jd
lugar de
$07
.
Las matrices que contienen las orientaciones de las figuras durante la creación están marcadas con un marco blanco.
Tetrimino I, S y Z podrían recibir 4 orientaciones separadas, pero los creadores de Nintendo Tetris decidieron limitarse a dos. Además,
Zv
y
Sv
no son imágenes especulares ideales entre sí. Ambos se crean girando en sentido antihorario, lo que conduce a un desequilibrio.
La tabla de orientación también contiene valores de mosaico para cada cuadrado en cada figura orientada. Sin embargo, con un estudio cuidadoso, queda claro que los valores para un tipo de tetrimino son siempre los mismos.
Los valores de mosaico son los índices de la tabla (pseudo-color) del patrón que se muestra a continuación.
Los mosaicos de
$7B
,
$7C
y
$7D
se encuentran directamente debajo de "ATIS" de la palabra "ESTADÍSTICAS". Estos son los tres tipos de cuadrados a partir de los cuales se hace el tetrimino.
Para los curiosos, diré que las avestruces y los pingüinos se usan al final del modo de tipo B. Este tema se trata en detalle en la sección "Final".
A continuación se muestra el resultado de modificar la ROM después de reemplazar
$7B
por
$29
. El corazón es el mosaico debajo del símbolo P en la tabla de patrones para todas las orientaciones T.
Las fichas de corazón permanecen en el campo de juego incluso después de que los Ts modificados se bloqueen en su lugar. Como se indica a continuación en la sección "Creación de Tetrimino", esto significa que el campo de juego almacena los valores reales de los índices de mosaico del Tetrimino jugado.
Los programadores de juegos permitieron usar 4 fichas separadas para cada figura, y no solo un tipo invariable de cuadrados. Esta es una característica útil que puede usarse para modificar la apariencia del juego. La tabla de patrones tiene mucho espacio vacío para nuevos mosaicos que pueden dar a cada tetrimino un aspecto único.
Las coordenadas de los cuadrados son muy fáciles de manipular. Por ejemplo, a continuación se muestra una versión modificada de los primeros cuatro triples en la tabla de orientación.
8A9C: FE 7B FE FE 7B 02 02 7B FE 02 7B 02 ; 00: T up
Este cambio es similar al siguiente:
{ { -2, -2 }, { 2, -2 }, { -2, 2 }, { 2, 2 }, }, -- 00: T up
El resultado es un tetrimino dividido.
Al mover un tetrimino dividido, sus cuadrados no pueden ir más allá de los límites del campo de juego y no pueden pasar a través de figuras previamente bloqueadas. Además, el juego prohíbe la rotación en esta orientación si conduce a que una casilla caiga fuera de los límites del campo de juego o al hecho de que la casilla se superpone a una casilla ya tendida.
El tetrimino dividido se bloquea en su lugar cuando hay soporte para cualquiera de sus cuadrados. Si la figura está bloqueada, los cuadrados que cuelgan en el aire continúan colgando.
El juego maneja tetriminos divididos como cualquier figura normal. Esto nos hace comprender que no hay una tabla adicional que almacene los metadatos de las figuras. Por ejemplo, podría haber una tabla que almacene el tamaño del cuadro delimitador de cada orientación para verificar las colisiones con el perímetro del campo de juego. Pero esa tabla no se usa. En cambio, el juego simplemente verifica los cuatro cuadrados justo antes de manipular la forma.
Además, las coordenadas de los cuadrados pueden ser cualquier valor; no se limitan al intervalo
[−2, 2]
. Por supuesto, los valores que exceden en gran medida este intervalo nos darán cifras inaplicables que no pueden caber en el campo de juego. Más importante aún, como se indicó en la sección "Estados de juego y modos de renderizado", cuando una figura está bloqueada en su lugar, el mecanismo para limpiar líneas rellenas escanea solo los desplazamientos de filas de −2 a 1 desde el cuadrado central de la figura; un cuadrado con una coordenada
y
fuera de este intervalo no se reconocerá.
Rotación Tetrimino
En una ilustración gráfica de la tabla de orientación, la rotación consiste en pasar de una matriz a una de las matrices a la izquierda o a la derecha con la transferencia de la serie si es necesario. Este concepto está codificado en una tabla a
$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
Para hacerlo más claro, moveremos cada columna de esta tabla a la fila de la tabla a continuación.
La mnemotecnia en los encabezados anteriores se puede interpretar como un índice de secuencia o clave de distribución. Por ejemplo, girando en sentido antihorario
Tu
nos da
Tl
, y girando en sentido horario
Tu
da
Tr
.
La tabla de rotación codifica secuencias enlazadas en cadena de ID de orientación; por lo tanto, podemos modificar las grabaciones para que la rotación transforme un tipo de tetrimino en otro. Esta técnica puede utilizarse potencialmente para aprovechar una fila no utilizada en la tabla de orientación.
Delante de la tabla de rotación hay un código para acceder a ella.
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;
Para la rotación en sentido antihorario, el índice de la tabla de rotación se resta duplicando la ID de orientación. Al agregarle 1, obtenemos el índice de rotación en el sentido de las agujas del reloj.
Las coordenadas
x
,
y
ID de orientación del tetrimino actual se almacenan en las direcciones
$0040
,
$0041
y
$0042
respectivamente.
El código usa una variable temporal para hacer una copia de seguridad del ID de orientación. Más tarde, después de cambiar la orientación, el código verifica que los cuatro cuadrados estén dentro de los límites del campo de juego y que ninguno de ellos se superponga con los cuadrados ya existentes (el código de verificación se encuentra en
$948B
, debajo del fragmento de código que se muestra arriba). Si la nueva orientación es incorrecta, se restaura la original, sin permitir que el jugador gire la figura.
Contando con una cruz, el controlador NES tiene ocho botones, cuyo estado está representado por el bit de dirección
$00B6
.
Por ejemplo,
$00B6
contendrá el valor
$81
mientras el jugador mantiene A e Izquierda.
Por otro lado,
$00B5
informa cuando se presionaron los botones; los bits
$00B5
son verdaderos solo durante una iteración del bucle del juego (1 cuadro procesado). El código usa
$00B5
para responder a presionar A y B. Cada uno de ellos necesita ser liberado antes de ser usado nuevamente.
$00B5
y
$00B6
son espejos de
$00F5
y
$00F6
. El código en las siguientes secciones usa estas direcciones indistintamente.
Crea Tetrimino
El campo de juego Nintendo Tetris consiste en una matriz con 22 filas y 10 columnas para que las dos primeras filas estén ocultas para el jugador.
Como se muestra en el siguiente código, al crear una figura de Tetrimino, siempre se encuentra en las coordenadas
(5, 0)
campo de juego.
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
A continuación se muestra una matriz de 5 × 5 superpuesta sobre este punto.
Ninguna de las matrices de creación tiene cuadrados por encima del punto de partida. Es decir, al crear un tetrimino, sus cuatro cuadrados se vuelven visibles de inmediato para el jugador. Sin embargo, si el jugador gira rápidamente la pieza antes de que tenga tiempo de soltarla, parte de la pieza se ocultará temporalmente en las dos primeras líneas del campo de juego.
Por lo general, pensamos que el juego termina cuando el montón llega a la cima. Pero, de hecho, esto no es del todo cierto. El juego termina cuando ya no es posible crear la siguiente pieza. Es decir, antes de la aparición de la figura, las cuatro celdas del campo de juego correspondientes a las posiciones de los cuadrados del tetrimino creado deberían estar libres. La figura puede estar bloqueada en su lugar de tal manera que parte de sus cuadrados aparezca en líneas numeradas negativamente y el juego no termine; sin embargo, en Nintendo Tetris, las líneas negativas son una abstracción relacionada solo con el tetrimino activo. Después de que la figura se bloquea (se convierte en mentira), solo se escriben en el campo cuadrados en líneas desde cero y más. Conceptualmente, resulta que las líneas numeradas negativamente se borran automáticamente después del bloqueo. Pero en realidad, el juego simplemente no almacena estos datos, cortando las partes superiores de las figuras.
El área visible del campo de juego 20 × 10 se almacena en
$0400
línea por línea, cada byte contiene el valor del mosaico de fondo. Las celdas vacías se denotan con el mosaico
$EF
, un cuadrado negro sólido.
Al crear una forma, se utilizan tres tablas de búsqueda. Si hay una ID de orientación arbitraria, la tabla en
$9956
nos da la ID de orientación al crear el tipo correspondiente 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
Es más fácil mostrar esto en la tabla.
Por ejemplo, todas las orientaciones de J están unidas a
Jd
.
La tabla en
$993B
contiene el tipo Tetrimino para la ID de orientación dada.
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
Para mayor claridad, mostraré todo en forma de tabla.
Veremos la tercera tabla de búsqueda en la siguiente sección.
Selección de tetrimino
Nintendo Tetris utiliza un registro de desplazamiento de retroalimentación lineal de 16 bits (LFSR) como su generador de números pseudoaleatorios (PRNG) en su configuración de Fibonacci. El valor de 16 bits se almacena como big-endian en las direcciones
$0017
-
$0018
. Se utiliza un número arbitrario de
$8988
como semilla.
80BC: LDX #$89
80BE: STX $0017
80C0: DEX
80C1: STX $0018
Cada número pseudoaleatorio posterior se genera de la siguiente manera: el valor se percibe como un número de 17 bits, y el bit más significativo se obtiene realizando XOR para los bits 1 y 9. Luego, el valor se desplaza hacia la derecha, descartando el bit menos significativo.
Este proceso ocurre en
$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
Curiosamente, los parámetros de la subrutina anterior se pueden configurar para que la función de llamada pueda especificar el ancho del registro de desplazamiento y la dirección en la que se puede encontrar en la memoria. Sin embargo, los mismos parámetros se usan en todas partes, por lo que podemos suponer que los desarrolladores tomaron prestado este código en alguna parte.
Para aquellos que quieran modificar aún más el algoritmo, lo escribí 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); }
Y todo este código puede exprimirse en una sola línea.
int generateNextPseudorandomNumber(int value) { return ((((value >> 9) & 1) ^ ((value >> 1) & 1)) << 15) | (value >> 1); }
Este PRNG genera continua y determinísticamente 32,767 valores únicos, comenzando cada ciclo desde la semilla original. Este es uno menos de la mitad de los números posibles que pueden caber en el registro, y cualquier valor en este conjunto puede usarse como semilla. Muchos de los valores fuera del conjunto crean una cadena que eventualmente conduce a un número del conjunto. Sin embargo, algunos números iniciales dan como resultado una secuencia infinita de ceros.
Para evaluar aproximadamente el rendimiento de este PRNG, generé una representación gráfica de los valores que crea en base a una oración con
RANDOM.ORG .
Al crear la imagen, PRNG se utilizó como generador de números pseudoaleatorios, en lugar de enteros de 16 bits. Cada píxel se colorea según el valor del bit 0. La imagen tiene un tamaño de 128 × 256, es decir, cubre toda la secuencia.
Además de las rayas apenas perceptibles en los lados superior e izquierdo, parece aleatorio. No aparecen patrones obvios.
Después de comenzar, el PRNG cambia constantemente el registro, trabajando al menos una vez por cuadro. Esto no sucede no solo en la pantalla de inicio y en las pantallas de menú, sino también cuando el tetrimino se encuentra entre las operaciones de creación de formas. Es decir, la figura que aparece a continuación depende del número de fotogramas que el jugador toma para colocar la figura. De hecho, el juego se basa en la aleatoriedad de las acciones de la persona que interactúa con él.
Durante la creación de la figura, el código se ejecuta en la dirección
$9907
, que selecciona el tipo de la nueva figura.
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;
En la dirección
$001A
almacena un contador del número de figuras creadas con el encendido. El incremento del contador se realiza mediante la primera línea de la subrutina, y dado que es un contador de un solo byte, después de cada 256 piezas vuelve a cero. Como el contador no se reinicia entre juegos, el historial de juegos anteriores afecta el proceso de selección de figuras. Esta es otra forma en que el juego usa al jugador como fuente de aleatoriedad.
La rutina convierte el byte más significativo del número pseudoaleatorio (
$0017
) a un tipo tetrimino y lo usa como el índice de la tabla ubicada en
$994E
para convertir el tipo a la ID de orientación de creación de forma.
994E: 02 ; Td
994F: 07 ; Jd
9950: 08 ; Zh
9951: 0A ; O
9952: 0B ; Sh
9953: 0E ; Ld
9954: 12 ; Ih
En la primera etapa de conversión, el contador de figuras creadas se agrega al byte superior. Luego se aplica una máscara para guardar solo los 3 bits inferiores. Si el resultado no es 7, entonces este es el tipo correcto de tetrimino, y si no es el mismo que la figura seleccionada anteriormente, entonces el número se usa como índice en la tabla para crear figuras. De lo contrario, se genera el siguiente número pseudoaleatorio y la máscara se aplica para obtener los 3 bits inferiores del byte superior, y luego se agrega la ID de orientación de creación de forma anterior. Finalmente, se realiza una operación de módulo para obtener el tipo correcto de tetrimino, que se utiliza como índice en la tabla de creación de formas.
Como el procesador no admite la división con el resto, este operador se emula restando repetidamente 7 hasta que el resultado sea menor que 7. La división con el resto se aplica a la suma del byte superior con la máscara aplicada y al ID de creación de orientación anterior. El valor máximo de esta suma es 25. Es decir, para reducirlo al resto de 4, solo se requieren 3 iteraciones.
Al comienzo de cada juego, el ID de orientación de creación de forma (
$0019
) se inicializa con un valor de
Tu
(
$00
). Este valor podría usarse potencialmente a
$9928
durante la creación de la primera forma.
Cuando se usa la ID de orientación anterior para crear una figura, en lugar del tipo anterior, Tetrimino agrega distorsión, porque los valores de la ID de orientación no se distribuyen uniformemente. Esto se muestra en la tabla:
Cada celda contiene un tipo de tetrimino, calculado al agregar el ID de orientación de la figura (columna) creada a un valor de 3 bits (fila), y luego aplicar el resto de la división por 7 a la suma. Cada fila contiene duplicados, porque
$07
y
$0E
dividen de manera uniforme a las 7, mientras que
$0B
y
$12
tienen un saldo común. Las líneas 0 y 7 son iguales porque están a una distancia de 7.
Hay 56 combinaciones de entrada posibles, y si los tipos de tetrimino resultantes se distribuyen uniformemente, entonces podemos esperar que en la tabla anterior, cada tipo aparezca exactamente 8 veces. Pero como se muestra a continuación, este no es el caso.
T y S aparecen con más frecuencia, y L e I, con menos frecuencia. Pero el código sesgado que usa el ID de orientación no se ejecuta cada vez que se llama a la subrutina.
Suponga que PRNG crea una secuencia de valores estadísticos independientes distribuidos uniformemente. Esto es realmente una suposición justa, dada la forma en que el juego intenta obtener la aleatoriedad correcta de las acciones del jugador. Agregar el número de figuras creadas a la dirección
$990C
no afectará la distribución, porque el número aumenta de manera uniforme entre llamadas. Usar la máscara de bits a
$990E
similar a aplicar la división por 8 con el resto, lo que tampoco afecta la distribución. Por lo tanto, la comprobación a
$9910
va a
invalidIndex
en 1/8 de todos los casos. Y la probabilidad de golpear cuando se verifica en la dirección
$9918
, donde se compara la cifra recién seleccionada con la cifra anterior, es 7/8, con una probabilidad de coincidencia de 1/7.
Esto significa que hay una posibilidad adicional de 7/8 × 1/7 = 1/8
estar adentro invalidIndex
. En general, hay un 25% de probabilidad de usar un código sesgado y un 75% de probabilidad de usar un código que seleccione Tetrimino de manera uniforme.En un conjunto de 224 tetriminos creados, la expectativa matemática es de 32 instancias para cada tipo. Pero en realidad el código crea la siguiente distribución:Es decir, despejando 90 líneas y alcanzando el nivel 9, el jugador recibirá una T y S extra y una L e I menos de lo que se espera estadísticamente.Tetrimino se eligen con las siguientes probabilidades:Parece que en la declaración de que el "palo largo" nunca aparece cuando es necesario, hay parte de la verdad (al menos para Nintendo Tetris).Tetrimino Shift
Nintendo Tetris utiliza el cambio automático retardado (DAS). Al hacer clic en "Izquierda" o "Derecha", el tetrimino se mueve instantáneamente una celda horizontalmente. Mientras mantiene presionado uno de estos botones de dirección, el juego cambia automáticamente la figura cada 6 cuadros con un retraso inicial de 16 cuadros.Este tipo de movimiento horizontal está controlado por el código en la dirección $89AE
. Como en el código de rotación, aquí se usa una variable temporal para hacer una copia de seguridad de las coordenadas en caso de que la nueva posición sea incorrecta. Tenga en cuenta que la comprobación le impide mover la pieza mientras el jugador presiona Abajo.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
Lanzando Tetrimino
La velocidad del descenso automático de Tetrimino es una función del número de nivel. Las velocidades se codifican como el número de cuadros renderizados para el descenso en la tabla ubicada en $898E
. Como NES opera a 60.0988 cuadros / s, puede calcular el período entre descensos y la velocidad.La tabla tiene un total de 30 entradas. Después del nivel 29, el valor de cuadros para el descenso es siempre 1.Un número entero de cuadros para el descenso no es una forma muy detallada de describir la velocidad. Como se muestra en el gráfico a continuación, la velocidad aumenta exponencialmente con cada nivel. De hecho, el nivel 29 es dos veces más rápido que el nivel 28.Con 1 cuadro / descenso, el jugador no tiene más de 1/3 de segundo para colocar la figura, después de lo cual comenzará a moverse. A esta velocidad de descenso, DAS evita que la figura llegue a los bordes del campo de juego hasta que se bloquee en su lugar, lo que para la mayoría de las personas significa un final rápido del juego. Sin embargo, algunos jugadores, especialmente Thor Akerlund , lograron derrotar a DAS con la rápida vibración de los botones cruzados ( D-pad
). En el código de cambio que se muestra arriba, se puede ver que mientras el botón de dirección horizontal se suelta a través del cuadro, es posible cambiar el tetrimino en los niveles 29 y superiores con media frecuencia. Este es un máximo teórico, pero cualquier vibración del pulgar por encima de 3.75 golpes / s puede vencer el retraso original de 16 cuadros.Si el descenso automático y controlado por el jugador (presionando "Abajo") coinciden y ocurren en un cuadro, el efecto no suma. Cualquiera o ambos de estos eventos hacen que la forma baje exactamente una celda en este cuadro.La lógica de control del disparador se encuentra en $8914
. La tabla del marco de descenso está debajo de la etiqueta . Como se mencionó anteriormente, en el nivel 29 y superior, la velocidad es constantemente igual a 1 obturador / marco. (dirección ) comienza el descenso cuando llega a ( ). El incremento se realiza en una dirección fuera de este fragmento de código. Durante el descenso automático o controlado, se restablece a 0. La variable ( ) se inicializa con el valor (en la dirección8914: 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
$0045
dropSpeed
$00AF
fallTimer
$8892
autorepeatY
$004E
$0A
$8739
), que se interpreta como −96. Una condición al principio causa un retraso inicial. El primer Tetrimino permanece suspendido en el aire en el punto de creación hasta autorepeatY
que aumenta a 0, lo que lleva 1.6 segundos. Sin embargo, cuando presiona Abajo en esta fase, se le autorepeatY
asigna instantáneamente 0. Es interesante que pueda mover y rotar la figura en esta fase del retraso inicial sin cancelarla.El incremento autorepeatY
se realiza mientras se mantiene presionado. Cuando llega a 3, se produce un descenso controlado por el hombre (descenso "suave") y se le autorepeatY
asigna 1. Por lo tanto, el descenso suave inicial requiere 3 cuadros, pero luego se repite en cada cuadro.Además, autorepeatY
aumenta de 0 a 1 solo cuando el juego reconoce que el jugador acaba de hacer clic en Abajo (en$00B5
), pero no reconoce mantener presionado. Esto es importante porque se autorepeatY
restablece a 0 cuando se crea un tetrimino (en la dirección $98E8
), lo que crea una característica importante: si el jugador mismo baja la figura y se bloquea, y continúa presionando "Abajo" al crear la siguiente figura, que a menudo ocurre en niveles altos, entonces Esto no conducirá a un suave descenso de la nueva figura. Para que esto suceda, el jugador debe soltar "Abajo" y luego presionar el botón nuevamente.El descenso potencialmente suave puede aumentar los puntos. holdDownPoints
( $004F
) aumenta con cada descenso, pero cuando se suelta, "Abajo" se restablece a 0. Por lo tanto, para ganar puntos, es necesario bajar el tetrimino en la cerradura con un descenso suave. El descenso suave a corto plazo, que puede ocurrir en el camino de la figura, no afecta los puntos. La cuenta se actualiza en$9BFE
, pero se holdDownPoints
restablece a 0 poco después, en la dirección $9C2F
.La verificación, que evita que el jugador realice un descenso suave con un desplazamiento horizontal de la figura, complica el conjunto de puntos. Significa que el último movimiento antes de bloquear la pieza en su lugar debe ser "Abajo".Cuando ocurre el descenso, tetriminoY
( $0041
) se copia a originalY
( $00AE
). Si la nueva posición creada por el incremento tetriminoY
resulta ser incorrecta (es decir, la figura empuja a través del piso del campo de juego o se superpone en cuadrados ya existentes), entonces el tetrimino permanece en la posición anterior. En este caso, se restauratetriminoY
y la figura se considera bloqueada. Esto significa que el retraso antes del bloqueo (el número máximo de cuadros que espera un tetrimino, que se mantiene en el aire antes del bloqueo) es igual al retraso en el descenso.El descenso rígido (caída instantánea) no es compatible con Nintendo Tetris.Deslizamiento y desplazamiento
El manual de Nintendo Tetris tiene un ejemplo ilustrativo de deslizamiento:El deslizamiento consiste en desplazarse a lo largo de la superficie de otras figuras o en el piso del campo de juego. Por lo general, se usa para empujar una figura debajo de un cuadrado saliente. El deslizamiento se puede realizar hasta que el temporizador de caída alcance la velocidad de descenso, después de lo cual la figura se bloqueará en su lugar. Un ejemplo animado se muestra a continuación.Por otro lado, el desplazamiento le permite empujar figuras en espacios que son inalcanzables de otra manera (ver más abajo).Al igual que el deslizamiento, el desplazamiento no es posible sin un retraso de bloqueo. Pero más allá de eso, el desplazamiento explota la forma en que el juego manipula las formas. Antes de mover o rotar la figura, el juego verifica que después de cambiar la posición todos los cuadrados tetriminos estarán en celdas vacías dentro de los límites del campo de juego. Tal verificación, como se muestra a continuación, no impide la rotación a través de bloques llenos cercanos. Como se indicó en la sección Descripción de Tetrimino, cada fila de la tabla de orientación contiene 12 bytes; por lo tanto, el índice en esta tabla se calcula multiplicando el ID de orientación del tetrimino activo por 12. Como se muestra a continuación, todas las multiplicaciones en la rutina se realizan utilizando cambios y sumas.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
Cada iteración del ciclo desplaza la posición del tetrimino por las coordenadas relativas de uno de los cuadrados de la tabla de orientación para obtener la ubicación de la celda correspondiente en el campo de juego. Luego verifica que las coordenadas de la celda estén dentro de los límites del campo de juego y que la celda en sí esté vacía.Los comentarios describen más claramente cómo verificar el espacio entre líneas. Además de las celdas en las líneas visibles, el código considera las dos líneas ocultas sobre el campo de juego como las posiciones legales de los cuadrados sin usar una condición compuesta. Esto funciona porque en el código adicional los números negativos representados por las variables de un solo byte son equivalentes a valores mayores que 127. En este caso, el valor mínimo es −2, que se almacena como94AA: 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 notación decimal).El índice del campo de juego es la suma cellY
multiplicada por 10 y cellX
. Sin embargo, cuando cellY
−1 ( $FF
= 255) o −2 ( $FE
= 254), el producto produce −10 ( $F6
= 246) y −20 ( $EC
= 236). Al estar en el intervalo, no cellX
puede ser más de 9, lo que da un índice máximo de 246 + 9 = 255, y esto está mucho más allá del final del campo de juego. Sin embargo, el juego se inicializa $0400
, $04FF
con un valor $EF
(de un mosaico vacío), creando otros 56 bytes adicionales de espacio vacío.Extraño ese chequeo de intervalocellX
realizado después de examinar la celda del campo de juego. Pero funciona correctamente en cualquier orden. Además, verificar el intervalo evita la condición compuesta, como se indica en el comentario a continuación. Los ejemplos de desplazamiento que se muestran a continuación son posibles debido a la forma en que este código verifica las posiciones.94D5: LDA $8A9C,X
94D8: CLC
94D9: ADC $0040 ; if (cellX >= 10) {
94DB: CMP #$0A ; return false;
94DD: BCS $94E9 ; }
Como se muestra a continuación, incluso puede realizar el deslizamiento con desplazamiento.AI aprovecha al máximo las capacidades de movimiento de Nintendo Tetris, incluidos el desplazamiento y el desplazamiento.Nivel 30 y superior
Después de alcanzar el nivel 30, parece que el nivel se restablece a cero.Pero el nivel 31 muestra que algo más está sucediendo:Los valores de nivel mostrados se encuentran en la tabla en la dirección $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
Como se muestra a continuación, la tabla de patrones se ordena de manera que las baldosas con $00
el $0F
son símbolos de glifos 0
en F
. Esto significa que cuando se muestra un dígito decimal o hexadecimal, el valor del dígito en sí mismo se usa como índice de la tabla de patrones. En nuestro caso, los valores de nivel se almacenan como decimal codificado en binario (BCD); cada mordisco de cada byte en la secuencia es un valor de mosaico.Desafortunadamente, parece que los diseñadores del juego asumieron que nadie pasaría el nivel 29 y, por lo tanto, decidieron insertar solo 30 entradas en la tabla. Los valores extraños mostrados son bytes diferentes después de la tabla. Solo $0044
se usa un byte (en la dirección ) para indicar el número de nivel , razón por la cual el juego gira lentamente alrededor de los 256 valores que se muestran a continuación.Los primeros 20 valores ordinales son en realidad otra tabla que almacena las compensaciones en el campo de juego para cada una de las 20 filas. Dado que el campo de juego comienza con y cada línea contiene 10 celdas, la dirección de una celda arbitraria es: Dado que el procesador no admite la multiplicación directamente, esta tabla de búsqueda proporciona una forma extremadamente rápida de obtener el producto. La tabla correspondiente ocupa los siguientes 40 bytes. Contiene 20 direcciones en formato little endian para la tabla de nombres 0 (un área de memoria VRAM que contiene los valores de los mosaicos de fondo). Son punteros a las líneas de desplazamiento del campo de juego . Los bytes restantes de los que se componen los valores de nivel mostrados son instrucciones.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
Filas y estadísticas
El número de filas completadas y estadísticas de tetrimino ocupan 2 bytes cada una en las siguientes direcciones.De hecho, estos valores se almacenan como BCD pequeños endian empaquetados de 16 bits. Por ejemplo, el número de filas se muestra a continuación, que es 123. Los bytes se cuentan de derecha a izquierda para que los dígitos decimales estén en orden.Sin embargo, los diseñadores de juegos asumieron que ninguno de los valores sería mayor que 999. Por lo tanto, la lógica de visualización procesa correctamente el primer byte como un BCD empaquetado, donde cada mordisco se usa como un valor de mosaico. Pero todo el segundo byte se usa realmente como el dígito decimal superior. Cuando los dígitos inferiores van de 99
a 00
, se produce el incremento normal del segundo byte. Como resultado, el segundo byte recorre los 256 mosaicos. Un ejemplo de esto se muestra a continuación.Después de borrar la línea, se ejecuta el siguiente código para incrementar el número de filas. Se realizan verificaciones para los dígitos medio e inferior para que permanezcan entre 0 y 9. Pero el dígito superior se puede aumentar infinitamente. Si después del incremento del número de filas, el dígito inferior es 0, entonces esto significa que el jugador acaba de completar un conjunto de 10 líneas y necesita aumentar el número de nivel. Como puede ver en el siguiente código, se realiza una verificación adicional antes del incremento de nivel. La segunda verificación está relacionada con el nivel de entrada seleccionado. Para ir a un cierto nivel , independientemente del nivel inicial, el jugador debe borrar9BA8: 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
; }
; }
X
10X
líneas Por ejemplo, si un jugador comienza en el nivel 5, permanecerá en él hasta que haya despejado 60 líneas, después de lo cual irá al nivel 6. Después de eso, cada 10 líneas adicionales conducirán a un aumento en el número de nivel.Para realizar esta verificación, el valor de las filas rellenas se copia de $0050
- $0051
a $00A8
- $00A9
. Luego, la copia se desplaza a la derecha 4 veces, lo que para un BCD empaquetado es similar a dividir por 10. El dígito decimal más pequeño se descarta, y los dígitos más altos y medios se desplazan por una posición, lo que da como resultado mordiscos $00A8
.Sin embargo, en la dirección, $9BEA
el número de nivel se compara directamente con el valor empaquetado del BCD $00A8
. No hay búsqueda en la tabla para convertir el valor BCD a decimal, y este es un claro error. Por ejemplo, en la imagen de arriba, el número de nivel debe compararse con $12
(18 en decimal) y no con 12. Por lo tanto, si un jugador decide comenzar en el nivel 17, el nivel en realidad irá a 120 filas, porque 18 es más que 17.La tabla muestra el número esperado de filas requeridas para la transición en cada nivel inicial. Se compara con lo que realmente sucede debido a un error.La cantidad esperada es la misma que la verdadera para los niveles iniciales 0–9. De hecho, la coincidencia para el nivel de entrada 9 es aleatoria; 10-15 también va al siguiente nivel con 100 filas, porque $10
- esto es 16 en forma decimal. La mayor diferencia entre lo esperado y lo real es de 60 filas.Sospecho que el error se debe a cambios de diseño en las últimas etapas de desarrollo. Mire la pantalla del menú, permitiendo al jugador elegir un nivel de entrada.No hay una explicación sobre cómo comenzar desde los niveles superiores a 9. Pero en el folleto de Nintendo Tetris, se revela este secreto:Parece que esta característica oculta fue inventada en el último momento. Tal vez se agregó muy cerca de la fecha de lanzamiento, lo que no permitió probarlo por completo.De hecho, la comprobación de la serie inicial contiene un segundo error relacionado con la salida de valores para el intervalo. A continuación hay comentarios en el código que explican mejor lo que sucede en un nivel bajo. La comparación se realiza restando y verificando el signo del resultado. Pero un número con signo de un solo byte se limita a −128 a 127. Si la diferencia es menor que −128, el número se transfiere y el resultado se convierte en un número positivo. Este principio se explica en los comentarios sobre el código. Al verificar que la diferencia está en este intervalo, se debe tener en cuenta que el número de nivel, cuando se incrementa a valores superiores a 255, realiza la transferencia a 0, y9BE8: LDA $0044
9BEA: CMP $00A8 ; if (level - [$00A8] < 0) {
9BEC: BPL $9BFB
9BEE: INC $0044 ; increment level
; }
9BE8: LDA $0044 ; difference = level - [$00A8];
9BEA: CMP $00A8 ; if (difference < 0 && difference >= -128) {
9BEC: BPL $9BFB
9BEE: INC $0044 ; increment level
; }
$00A8
potencialmente puede contener cualquier valor, porque se toma su mordisco superior $0051
, cuyo incremento puede ocurrir infinitamente.Estos efectos se superponen, creando períodos en los que el número de nivel permanece sin cambios por error. Los períodos ocurren a intervalos regulares de 2,900 filas, comenzando en 2,190 filas, y duran 800 filas. Por ejemplo, de 2190 ( L90
) a 2990 ( T90
), el nivel permanece igual a $DB
( 96
), como se muestra a continuación.El siguiente período pasa de 5090 a 5890, el nivel es constantemente igual a $AD
( 06
). Además, durante estos períodos, la paleta de colores tampoco cambia.Colorear Tetrimino
En cada nivel, a los azulejos tetrimino se les asignan 4 colores únicos. Los colores se toman de la tabla ubicada en $984C
. Sus registros se reutilizan cada 10 niveles. De izquierda a derecha: las columnas de la tabla correspondientes a las áreas negra, blanca, azul y roja de la imagen a continuación.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
Los valores corresponden a la paleta de colores NES.Los primeros 2 colores de cada entrada son siempre en blanco y negro. Sin embargo, el primer color se ignora realmente; independientemente del valor, se considera un color transparente a través del cual se asoma un fondo negro sólido.El acceso a la tabla de colores se realiza en la rutina en $9808
. El índice de la tabla de colores se basa en el número de nivel dividido por un resto de 10. El ciclo copia la entrada a las tablas de paleta en VRAM. La división con el resto se emula mediante una resta constante de 10 hasta que el resultado sea menor que 10. El comienzo de la subrutina con comentarios se muestra a continuación.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 ; }
Sin embargo, como se indicó en la sección anterior, la resta y la ramificación basadas en el signo de diferencia se usan en comparación. Un número con signo de un solo byte está limitado a −128 a 127. Los comentarios actualizados a continuación reflejan este principio. Los comentarios a continuación se simplifican aún más. Esta redacción revela un error en el código. La operación de división restante se omite por completo para niveles de 138 y superiores. En cambio, el índice se asigna directamente al número de nivel, que proporciona acceso a bytes mucho más allá del final de la tabla de colores. Como se muestra a continuación, esto incluso puede conducir a un tetrimino casi 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 ; }
A continuación se muestran los colores de los 256 niveles. Los mosaicos se organizan en 10 columnas para enfatizar el uso cíclico de la tabla de colores, violada en el nivel 138. Las filas y columnas en los encabezados se indican en decimal.Después de 255, el número de nivel vuelve a 0.Además, como se mencionó en la sección anterior, algunos niveles no cambian hasta que se eliminan 800 filas. Durante estos largos niveles, los colores permanecen sin cambios.Modo de juego
El modo de juego almacenado en la dirección $00C0
determina cuál de las diversas pantallas y menús se muestran actualmente al usuario.Como se muestra arriba, el juego tiene una rutina ingeniosamente escrita que actúa como una declaración de cambio usando la pequeña tabla de navegación endian ubicada inmediatamente después de la llamada. La lista anterior muestra las direcciones de todos los modos de juego. Tenga en cuenta que los modos "Juego" y "Demostración" usan el mismo código. Esta rutina nunca regresa. En cambio, el código usa la dirección de retorno; generalmente apunta a la instrucción que sigue inmediatamente a la llamada al salto a la subrutina (menos 1 byte), pero en este caso apunta a la tabla de salto. La dirección de retorno se saca de la pila y se almacena en - . Después de guardar la dirección de la tabla de salto, el código usa el valor en el registro A como índice y realiza la transición correspondiente.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]
El código puede usar esta rutina de cambio siempre que los índices estén cerca de 0 y no haya espacios o pocos entre los posibles casos.Pantalla de información legal
El juego comienza con una pantalla que muestra un aviso legal.En la parte inferior de la pantalla, Aleksey Pazhitnov es mencionado como el inventor, diseñador y programador del primer Tetris. En 1984, trabajando como desarrollador de computadoras en el Dorodnitsyn Computing Center (un instituto de investigación líder de la Academia de Ciencias de Rusia en Moscú), desarrolló un prototipo del juego en Electronics-60 (clon soviético DEC LSI-11 ). Se desarrolló un prototipo para un modo de texto monocromo verde en el que los cuadrados se indican mediante pares de corchetes []
. Con la ayuda del niño de 16 años Vadim Gerasimov y el ingeniero informático Dmitry Pavlovsky unos días después de la invención del juego, el prototipo fue trasladado a una PC IBM con MS DOS y Turbo Pascal. En el transcurso de dos años, perfeccionaron el juego juntos, agregando características como colores de tetrimino, estadísticas y, lo que es más importante, un código de tiempo y gráficos que permitió que el juego funcionara en una variedad de modelos de PC y clones.Desafortunadamente, debido a las peculiaridades de la Unión Soviética en ese momento, sus intentos de monetizar el juego no tuvieron éxito y al final decidieron compartir la versión para PC con sus amigos de forma gratuita. A partir de ese momento, "Tetris" comenzó a extenderse viralmente en todo el país y más allá, copiado de un disco a otro. Pero dado que el juego fue desarrollado por empleados de una agencia gubernamental, el estado lo poseía, y en 1987 la organización responsable del comercio internacional de tecnologías electrónicas se hizo cargo de la licencia del juego ( Electronorgtekhnika (ELORG)) La abreviatura V / O en la pantalla de información legal puede ser abreviada para Version Originale.La compañía británica de software Andromeda intentó obtener los derechos de Tetris y, antes de completar la transacción, otorgó la licencia del juego a otros proveedores, por ejemplo, el editor británico de juegos de computadora Mirrorsoft . Mirrorsoft, a su vez, lo sublicencia a Tengen , una subsidiaria de Atari Games. Tengen otorgó a Bullet-Proof Software los derechos para desarrollar un juego para computadoras y consolas en Japón, lo que resultó en Tetris para Nintendo Famicom . A continuación se muestra su pantalla de información legal.Curiosamente, en esta versión, el escolar Vadim Gerasimov es llamado el diseñador y programador original.Intentando asegurar la versión portátil de la próxima consola Game Boy, Nintendo usó el software Bullet-Proof para concluir un acuerdo exitoso directamente con ELORG. En el proceso de concluir el acuerdo, ELORG revisó su contrato con Andromeda, agregando que Andromeda solo obtuvo derechos para juegos para computadoras y máquinas recreativas. Debido a esto, Bullet-Proof Software tuvo que pagar regalías ELORG por todos los cartuchos vendidos a Famicom, porque los derechos que recibió de Tengen resultaron ser falsos. Pero a través de la reconciliación con ELORG, Bullet-Proof Software finalmente logró obtener los derechos mundiales de juegos de consola para Nintendo.Bullet-Proof Software otorgó la licencia de los derechos de juegos portátiles de Nintendo y juntos desarrollaron Game Boy Tetris, que se refleja en la pantalla de información legal a continuación.Con los derechos de juego de la consola global, Nintendo ha desarrollado la versión Tetris para NES que estamos explorando en este artículo. Luego, Bullet-Proof Software otorgó la licencia de los derechos de Nintendo, lo que le permitió continuar vendiendo cartuchos para Famicom en Japón.Esto fue seguido por una compleja batalla legal. Tanto Nintendo como Tengen exigieron que la parte contraria dejara de producir y vender su versión del juego. Como resultado, Nintendo ganó y cientos de miles de cartuchos Tengen Tetris fueron destruidos. El veredicto judicial también prohibió a otras compañías como Mirrorsoft crear versiones de consola.Pajitnov nunca recibió ninguna deducción de ELORG o del estado soviético. Sin embargo, en 1991 se mudó a los EE. UU. Y en 1996 con el apoyo del propietario del software Bullet-ProofHenka Rogers cofundó The Tetris Company , que le permitió beneficiarse de versiones para dispositivos móviles y consolas modernas.Es interesante mirar la pantalla de información legal como una ventana que da una idea del origen modesto del juego y las batallas subsiguientes por los derechos de propiedad intelectual, porque para la mayoría de los jugadores esta pantalla es solo un obstáculo molesto, cuya desaparición parece tener que esperar para siempre. El retraso se establece en dos contadores, contando secuencialmente de 255 a 0. La primera fase no se puede omitir, y la segunda se omite presionando el botón Iniciar. Por lo tanto, la pantalla de información legal se muestra al menos 4,25 segundos y no más de 8,5 segundos. Sin embargo, creo que la mayoría de los jugadores se rinden y dejan de presionar Start durante el primer intervalo, y debido a esto están esperando su finalización completa.El momento de las fases, así como el resto del juego, se rige por un controlador de interrupción desenmascarado llamado al comienzo de cada intervalo de supresión vertical, un corto período de tiempo entre la representación de cuadros de televisión. Es decir, cada 16.6393 milisegundos, el siguiente código interrumpe la ejecución normal del programa. El controlador comienza pasando los valores de los registros principales a la pila y recuperándolos después de la finalización para no interferir con la tarea interrumpida. La llamada actualiza VRAM, convirtiendo la descripción del modelo de memoria a lo que se muestra en la pantalla. Además, el controlador reduce el valor del contador de la pantalla de información legal si es mayor que cero. Desafío8005: 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()
realiza el paso requerido por el equipo de generación de cuadros. El controlador continúa trabajando incrementando el contador de trama, el pequeño valor endian de 16 bits almacenado en la dirección $00B1
, $00B2
que utiliza en diferentes lugares para controlar el tiempo. Después de eso, se genera el siguiente número pseudoaleatorio; como se mencionó anteriormente, esto sucede independientemente del modo al menos una vez por cuadro. El $8040
indicador de intervalo de supresión vertical se establece en la dirección , lo que significa que el controlador acaba de ejecutarse. Finalmente, se sondean los botones del controlador; El comportamiento de esta rutina se describe a continuación en la sección Demo.La bandera es verticalBlankingInterval
utilizada por la rutina discutida anteriormente. Continúa hasta que comienza la ejecución del controlador de interrupciones.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;
Esta rutina de bloqueo es utilizada por dos etapas de sincronización de la pantalla de información legal, que se ejecutan una tras otra. El script Lua AI evita este retraso al establecer ambos contadores en 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;
Demo
La demostración muestra unos 80 segundos de juego pregrabado. No solo muestra el archivo de video, sino que usa el mismo motor que en el juego. Durante la reproducción, se utilizan dos tablas. El primero, ubicado en la dirección $DF00
, contiene la siguiente secuencia de creación de tetrimino:TJTSZJTSZJSZLZJTTSITO JSZLZLIOLZLIOJTSITOJ
Al crear una figura, se selecciona al azar o se lee de la tabla, según el modo. El cambio ocurre en la dirección $98EB
. El tipo Tetrimino se extrae de los bits 6, 5 y 4 de cada byte. De vez en cuando, esta operación nos da un valor : el tipo incorrecto. Sin embargo, la tabla de la creación de formas ( ) utilizado para una conversión de tipo en Tetrimino orientación ID se encuentra realmente entre las dos tablas vinculadas: Significado98EB: LDA $00C0
98ED: CMP #$05
98EF: BNE $9903 ; if (gameMode == DEMO) {
98F1: LDX $00D3
98F3: INC $00D3
98F5: LDA $DF00,X ; value = demoTetriminoTypeTable[++demoIndex];
98F8: LSR
98F9: LSR
98FA: LSR
98FB: LSR
98FC: AND #$07
98FE: TAX ; tetriminoType = bits 6,5,4 of value;
98FF: LDA $994E,X
9902: RTS ; return spawnTable[tetriminoType];
; } else {
; pickRandomTetrimino();
; }
$07
$994E
993B: 00 00 00 00 ; T
993F: 01 01 01 01 ; J
9943: 02 02 ; Z
9945: 03 ; O
9946: 04 04 ; S
9948: 05 05 05 05 ; L
994C: 06 06 ; I
994E: 02 ; Td
994F: 07 ; Jd
9950: 08 ; Zh
9951: 0A ; O
9952: 0B ; Sh
9953: 0E ; Ld
9954: 12 ; Ih
9956: 02 02 02 02 ; Td
995A: 07 07 07 07 ; Jd
995E: 08 08 ; Zh
9960: 0A ; O
9961: 0B 0B ; Sh
9963: 0E 0E 0E 0E ; Ld
9967: 12 12 ; Ih
$07
lo obliga a leer más allá del final de la tabla, en el siguiente, que da Td
( $02
).Debido a este efecto, este esquema puede darnos una secuencia ilimitada pero reproducible de ID pseudoaleatorios de la orientación de las figuras creadas. El código funcionará porque cualquier dirección arbitraria en una secuencia cambiante de bytes no nos permite determinar dónde termina la tabla. De hecho, la secuencia en la dirección $DF00
puede ser parte de algo completamente ajeno a esto, especialmente teniendo en cuenta que la asignación de los 5 bits restantes que no son cero no está clara, y la secuencia generada demuestra repetibilidad.Durante la inicialización del modo de demostración, el índice de la tabla ( $00D3
) se restablece a la dirección $872B
.La segunda tabla de la demostración contiene un registro de los botones del gamepad codificados en pares de bytes. Los bits del primer byte corresponden a los botones.El segundo byte almacena el número de cuadros durante los cuales se presiona una combinación de botones.La tabla toma direcciones $DD00
, $DEFF
y consta de 256 pares. El acceso a él se realiza mediante la subrutina en la dirección $9D5B
. Dado que la tabla de botones de demostración tiene una longitud de 512 bytes, se requiere un índice de dos bytes para acceder a ella. El índice se almacena como little endian en - . Se inicializa con el valor de la dirección de la tabla , y su incremento se realiza mediante el siguiente código. Los programadores dejaron el procesamiento de entrada del jugador en el código, lo que nos permite ver el proceso de desarrollo y reemplazar la demostración con otro registro. El modo de grabación de demostración se activa cuando se asigna un valor.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
. En este caso, se inicia el siguiente código, destinado a escribir en la tabla de botones para la demostración. Sin embargo, la tabla se almacena en el PRG-ROM. Intentar escribir en él no afectará los datos guardados. En cambio, cada operación de escritura activa un cambio de banco, lo que resulta en el efecto de falla que se muestra a continuación.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;
Esto sugiere que los desarrolladores podrían ejecutar el programa parcial o totalmente en RAM.Para sortear este obstáculo, creé lua/RecordDemo.lua
uno ubicado en un zip con código fuente . Después de cambiar al modo de grabación de demostración, redirige las operaciones de escritura a la tabla en la consola Lua. A partir de él, los bytes se pueden copiar y pegar en la ROM.Para grabar su propia demostración, ejecute FCEUX y descargue el archivo ROM de Nintendo Tetris (Archivo | Abrir ROM ...). Luego abra la ventana Lua Script (Archivo | Lua | Nueva ventana Lua Script ...), busque el archivo o ingrese la ruta. Presione el botón Ejecutar para iniciar el modo de grabación de demostración y luego haga clic en la ventana FCEUX para cambiar el enfoque. Puede controlar las formas hasta que la tabla de botones esté llena. Después de eso, el juego volverá automáticamente al protector de pantalla. Haga clic en Detener en la ventana Lua Script para detener el script. Los datos grabados aparecerán en la Consola de salida, como se muestra en la figura a continuación.
Seleccione todos los contenidos y cópielos en el portapapeles (Ctrl + C). Luego ejecute el Editor Hex (Depuración | Editor Hex ...). En el menú Editor hexadecimal, seleccione Ver | Archivo ROM y luego Archivo | Ir a la dirección. En el cuadro de diálogo Ir a, ingrese 5D10 (dirección de la tabla de botones de demostración en el archivo ROM) y haga clic en Aceptar. Luego pegue el contenido del portapapeles (Ctrl + V).Finalmente, en el menú FCEUX, seleccione NES | Restablecer Si logró repetir todos estos pasos, la demostración debe ser reemplazada por su propia versión.Si desea guardar los cambios, seleccione Archivo | Guardar Rom como ... e ingrese el nombre del archivo ROM modificado, y luego haga clic en Guardar.De manera similar, puede ajustar la secuencia de tetriminos creados.Pantalla de la muerte
Como se mencionó anteriormente, la mayoría de los jugadores no pueden hacer frente a la velocidad del descenso de figuras en el nivel 29, lo que rápidamente lleva a la finalización del juego. Por lo tanto, los jugadores se asociaron con el nombre de "pantalla de la muerte". Pero desde un punto de vista técnico, la pantalla de muerte no permite que el jugador vaya más allá debido a un error en el que un descenso rápido en realidad no es un error, sino una característica. Los diseñadores fueron tan amables que permitieron que el juego continuara mientras el jugador podía soportar la velocidad sobrehumana.Aparece una verdadera pantalla de muerte en aproximadamente 1550 filas retraídas. Se manifiesta de diferentes maneras. A veces el juego se reinicia. En otros casos, la pantalla se vuelve negra. Por lo general, un juego se congela ("congela") inmediatamente después de eliminar una fila, como se muestra a continuación. Dichos efectos suelen ir precedidos de artefactos gráficos aleatorios.La pantalla de muerte es el resultado de un error en el código que agrega puntos al eliminar filas. La cuenta de seis caracteres se almacena como un pequeño BCD endian empaquetado de 24 bits y se encuentra en $0053
- $0055
. Para realizar conversiones entre el número de filas despejadas y los puntos obtenidos, se utiliza una tabla; cada entrada es un pequeño valor endian BCD empaquetado de 16 bits. Después de incrementar el número total de filas, y posiblemente el nivel, el valor en esta lista se multiplica por el número de nivel más uno, y el resultado se agrega a los puntos. Esto se demuestra claramente en la tabla del manual de 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
Como se muestra a continuación, la multiplicación se simula mediante un ciclo que agrega puntos a la puntuación. Se ejecuta después de bloquear la forma, incluso si no se borran las filas. Desafortunadamente, el Ricoh 2A03 no tiene un modo decimal binario 6502; él podría simplificar enormemente el cuerpo del ciclo. En cambio, la adición se realiza en pasos usando el modo binario. Cualquier dígito que exceda 9 después de la suma se obtiene esencialmente restando 10 e incrementando los dígitos de la izquierda. Por ejemplo, eso se convierte a . Pero tal esquema no está completamente protegido. Tomar : un cheque no puede convertir el resultado a9C31: 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
. Para compensar esto, ninguno de los dígitos decimales en las entradas en el cuadro de mando supera 6. Además, para poder usarlo, el último dígito de todas las entradas es siempre 0.Se necesita tiempo para completar este ciclo largo y complicado. A niveles altos, una gran cantidad de iteraciones afecta el momento del juego, ya que lleva más de 1/60 de segundo generar cada cuadro. Todo esto como resultado conduce a diversas manifestaciones de la "pantalla de la muerte".El script Lua AI limita el número de iteraciones en un bucle a 30, el valor máximo que los diseñadores podrían lograr según lo diseñado por los diseñadores, lo que elimina la pantalla de la muerte.Finales
En el folleto de Nintendo Tetris, el juego tipo A se describe de la siguiente manera:El juego recompensa a los jugadores que obtuvieron un número suficientemente grande de puntos en una de las cinco animaciones de los finales. La elección del final se basa completamente en los dos dígitos más a la izquierda de la puntuación de seis dígitos. Como se muestra a continuación, para obtener uno de los finales, el jugador debe anotar al menos 30,000 puntos. Vale la pena señalar que - es un espejo de direcciones - . La cuenta está duplicada en las direcciones - . Después de pasar la primera prueba, la siguiente animación de cambio selecciona la animación final.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;
; }
Al final, se lanzan cohetes de tamaño creciente desde la plataforma de lanzamiento junto a la Catedral de San Basilio. En el cuarto final, se muestra la nave espacial Buran, la versión soviética del transbordador espacial estadounidense. En el mejor final, la catedral se eleva en el aire, y un ovni cuelga sobre la plataforma de lanzamiento. A continuación se muestra una imagen de cada final y el puntaje asociado.En el modo de juego B-Type, se implementa otra prueba, que se describe en el folleto de Nintendo Tetris de la siguiente manera:Si el jugador borra con éxito 25 filas, el juego muestra el final, según el nivel inicial. Los finales para los niveles 0–8 consisten en animales y objetos que vuelan o corren en el cuadro, pasando misteriosamente detrás de la Catedral de San Basilio. El OVNI del mejor final del modo tipo A aparece en el final 3. En el final 4, aparecen pterosaurios voladores extintos, y en el final 7 se muestran los míticos dragones voladores. En las terminaciones 2 y 6, se muestran aves sin alas: pingüinos corriendo y avestruces. Al final de 5, el cielo se llena de BUENAS aeronaves (que no debe confundirse con las aeronaves Goodyear). Y al final de 8, muchos "Buranas" barren la pantalla, aunque en realidad solo había uno.La altura inicial (más 1) se utiliza como multiplicador, recompensando al jugador con una gran cantidad de animales / objetos por su mayor complejidad.En el mejor final del tipo B, se muestra un castillo lleno de personajes del universo de Nintendo: la princesa Peach aplaude, Kid Icarus toca el violín, Donkey Kong toca el gran tambor, Mario y Luigi bailan, Bowser toca el acordeón, Samus toca el violonchelo, Link - en una flauta, mientras las cúpulas de la Catedral de San Basilio se elevan en el aire. La cantidad de estos elementos que se muestran en el final depende de la altura inicial. A continuación se muestran imágenes de las 10 terminaciones.AI puede borrar rápidamente las 25 filas requeridas en el modo Tipo B en cualquier nivel y altura iniciales, lo que le permite ver cualquiera de las terminaciones. También vale la pena evaluar qué tan genial maneja grandes montones de bloques al azar.En las terminaciones 0–8, se pueden mover hasta 6 objetos en el cuadro. Las coordenadas y de los objetos se almacenan en una tabla ubicada en at $A7B7
. Las distancias horizontales entre los objetos se almacenan en una tabla en la dirección . Una secuencia de valores con un signo en la dirección determina la velocidad y la dirección de los objetos. Los índices de sprites se almacenan en . De hecho, cada objeto consta de dos sprites con índices adyacentes. Para obtener el segundo índice, debe agregar 1. Por ejemplo, un dragón consta deA7B7: 98 A8 C0 A8 90 B0 ; 0
A7BD: B0 B8 A0 B8 A8 A0 ; 1
A7C3: C8 C8 C8 C8 C8 C8 ; 2
A7C9: 30 20 40 28 A0 80 ; 3
A7CF: A8 88 68 A8 48 78 ; 4
A7D5: 58 68 18 48 78 38 ; 5
A7DB: C8 C8 C8 C8 C8 C8 ; 6
A7E1: 90 58 70 A8 40 38 ; 7
A7E7: 68 88 78 18 48 A8 ; 8
$A77B
A77B: 3A 24 0A 4A 3A FF ; 0
A781: 22 44 12 32 4A FF ; 1
A787: AE 6E 8E 6E 1E 02 ; 2
A78D: 42 42 42 42 42 02 ; 3
A793: 22 0A 1A 04 0A FF ; 4
A799: EE DE FC FC F6 02 ; 5
A79F: 80 80 80 80 80 FF ; 6
A7A5: E8 E8 E8 E8 48 FF ; 7
A7AB: 80 AE 9E 90 80 02 ; 8
$A771
A771: 01 ; 0: 1
A772: 01 ; 1: 1
A773: FF ; 2: -1
A774: FC ; 3: -4
A775: 01 ; 4: 1
A776: FF ; 5: -1
A777: 02 ; 6: 2
A778: 02 ; 7: 2
A779: FE ; 8: -1
$A7F3
A7F3: 2C ; 0: dragonfly
A7F4: 2E ; 1: dove
A7F5: 54 ; 2: penguin
A7F6: 32 ; 3: UFO
A7F7: 34 ; 4: pterosaur
A7F8: 36 ; 5: blimp
A7F9: 4B ; 6: ostrich
A7FA: 38 ; 7: dragon
A7FB: 3A ; 8: Buran
$38
y $39
. Los mosaicos para estos sprites están contenidos en las tablas de patrones a continuación.Examinamos la tabla central del patrón de arriba, se usa para mostrar el tetrimino y el campo de juego. Curiosamente, contiene el alfabeto completo, mientras que otros contienen solo una parte para ahorrar espacio. Pero aún más interesantes son los sprites de aviones y helicópteros en la tabla de patrones a la izquierda; no aparecen en los finales ni en otras partes del juego. Resultó que el avión y el helicóptero tienen índices de sprites $30
y $16
y se puede cambiar la tabla mostrada anteriormente, para verlos en acción.Desafortunadamente, las monturas de helicópteros no se muestran, pero los rotores principal y de cola están bellamente animados.2 jugadores contra
Nintendo Tetris contiene un modo incompleto para dos jugadores que puedes habilitar cambiando la cantidad de jugadores ( $00BE
) a 2. Como se muestra a continuación, aparecen dos campos de juego en el fondo del modo para un jugador.No hay borde entre los campos porque la región central del fondo es negro sólido. Los valores que se 003
muestran arriba de los campos de juego indican el número de filas despejadas por cada jugador. La única figura común para dos jugadores aparece en el mismo lugar que en el modo para un jugador. Desafortunadamente, se encuentra en el campo de juego correcto. Los cuadrados y otros mosaicos están coloreados incorrectamente. Y cuando el jugador pierde el juego se reinicia.Pero si ignora estos problemas, entonces el modo es bastante jugable. Cada jugador puede controlar independientemente las piezas en el campo de juego correspondiente. Y cuando un jugador escribe Doble, Triple o Tetris (es decir, borra dos, tres o cuatro filas), aparecen filas de basura con un cuadrado faltante en la parte inferior del campo de juego del oponente.Un campo adicional se encuentra en $0500
. A $0060
- $007F
espejo que normalmente forman $0040
- $005F
se utiliza para el segundo jugador.Probablemente, este modo interesante fue abandonado debido a un programa de desarrollo ocupado. O tal vez lo dejaron sin terminar intencionalmente. Una de las razones por las que se eligió a Tetris como el juego incluido con Nintendo Game Boy fue porque alentó la compra de Game Link Cable- un accesorio que conecta a dos Game Boys para lanzar 2 jugadores contra el modo. Este cable agregó un elemento de "socialidad" al sistema: alentó a los amigos a comprar un Game Boy para unirse a la diversión. Quizás Nintendo temía que si la versión de consola del juego tuviera 2 jugadores versus modo, entonces el poder "publicitario" de Tetris, que estimuló la compra de Game Boy, podría debilitarse.Musica y efectos de sonido
La música de fondo se activa cuando $06F5
se asigna uno de los valores enumerados en la tabla.Puede escuchar música no utilizada desde el protector de pantalla aquí . En el juego en sí, no suena nada durante la pantalla del protector de pantalla.Music-1 es una versión de " Dance of the Dragee Fairy ", música para la bailarina del tercer acto del pas de deux waltz "El cascanueces" de Tchaikovsky. La música final es una variación de los " Versos del torero ", un aria de la ópera Carmen Georges Bizet. Estas composiciones son arregladas por el compositor del resto de la música de Hirokazu Tanaka .Music-2 se inspiró en las canciones tradicionales rusas del folklore. Music-3 es misterioso, futurista y tierno; Durante un tiempo, fue el tono de llamada de atención al cliente de Nintendo of America.Para ayudar al jugador a caer en un estado de pánico cuando la altura del montón se acerca al techo del campo de juego, una versión de música de fondo comienza a reproducirse a un ritmo rápido ( $06
- $08
).Curiosamente, entre las composiciones musicales no hay " Chapman ", un tema famoso que suena en Game Boy Tetris.Los efectos de sonido se activan al grabar $06F0
y $06F1
, de acuerdo con la siguiente tabla.Estados de juego y modos de renderizado
Durante el juego, el estado actual del juego está representado por un número entero en la dirección $0048
. La mayoría de las veces tiene un significado $01
que indica que el jugador controla el tetrimino activo. Sin embargo, cuando la pieza está bloqueada en su lugar, el juego pasa gradualmente de un estado $02
a otro $08
, como se muestra en la tabla.La ramificación del código, según el estado del juego, se produce en la siguiente dirección $81B2
: en el estado de cambio, salta al código que asigna un valor que indica que la orientación no está especificada. El manejador nunca se llama; sin embargo, el estado del juego sirve como señal para otras partes del código. El estado permite al jugador cambiar, rotar y bajar el tetrimino activo: como se indicó en las secciones anteriores, las rutinas de cambio, rotación y descenso de la figura antes de ejecutar el código verifican las nuevas posiciones del tetrimino. La única forma de bloquear una forma en la posición incorrecta es crearla encima de una forma existente. En este caso, el juego termina. Como se muestra a continuación, el código de estado realiza esta verificación.81B2: LDA $0048
81B4: JSR $AC82 ; switch(playState) {
81B7: 2F 9E ; case 00: goto 9E2F; // Unassign orientationID
81B9: CF 81 ; case 01: goto 81CF; // Player controls active Tetrimino
81BB: A2 99 ; case 02: goto 99A2; // Lock Tetrimino into playfield
81BD: 6B 9A ; case 03: goto 9A6B; // Check for completed rows
81BF: 39 9E ; case 04: goto 9E39; // Display line clearing animation
81C1: 58 9B ; case 05: goto 9B58; // Update lines and statistics
81C3: F2 A3 ; case 06: goto A3F2; // B-Type goal check; Unused frame for A-Type
81C5: 03 9B ; case 07: goto 9B03; // Unused frame; Execute unfinished 2 player mode logic
81C7: 8E 98 ; case 08: goto 988E; // Spawn next Tetrimino
81C9: 39 9E ; case 09: goto 9E39; // Unused
81CB: 11 9A ; case 0A: goto 9A11; // Update game over curtain
81CD: 37 9E ; case 0B: goto 9E37; // Increment play state
; }
$00
orientationID
$13
9E2F: LDA #$13
9E31: STA $0042 ; orientationID = UNASSIGNED;
9E33: RTS ; return;
$00
$01
81CF: JSR $89AE ; shift Tetrimino;
81D2: JSR $88AB ; rotate Tetrimino;
81D5: JSR $8914 ; drop Tetrimino;
81D8: RTS ; return;
$02
. Si la posición bloqueada es correcta, marca las 4 celdas asociadas del campo de juego como ocupadas. De lo contrario, ella hace la transición a un estado : la ominosa cortina del final del juego.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
La cortina se dibuja desde la parte superior del campo de juego hacia abajo, descendiendo una línea cada 4 cuadros. curtainRow
( $0058
) se inicializa con un valor de −16, creando un retraso adicional de 0.27 segundos entre el bloqueo final y el inicio de la animación. En la dirección $9A21
en el estado del $0A
código que se muestra a continuación, se accede a la tabla de multiplicación, que se muestra erróneamente como números de nivel. Esto se hace para escalar curtainRow
en 10. Además, como se muestra arriba, el código en la dirección $9A51
comienza la animación final si la puntuación del jugador no es inferior a 30,000 puntos; de lo contrario, espera hacer clic en Inicio. El código se completa asignando un valor al estado del juego , pero no se llama al controlador correspondiente porque el juego se ha completado.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
Las líneas del campo de juego se copian de forma incremental en VRAM para mostrarlas. El índice de la fila actual a copiar está contenido en vramRow
( $0049
). Se $9A3C
vramRow
asigna un valor en la dirección curtainRow
, lo que finalmente hace que esta línea sea visible cuando se procesa.Las manipulaciones con VRAM se producen durante un intervalo de supresión vertical, que es reconocido por el controlador de interrupciones descrito en la sección "Pantalla de información legal". Llama a la subrutina que se muestra a continuación (marcada en los comentarios del controlador de interrupciones como render()
). El modo de renderizado es similar al modo de juego. Se almacena en la dirección y puede tener uno de los siguientes valores: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
Parte del modo de representación se $03
muestra a continuación. Como puede ver a continuación, pasa en VRAM una línea del campo de juego con un índice . Si es mayor que 20, la rutina no hace nada. La tabla ( ) contiene las direcciones VRAM en formato little endian, correspondientes a las líneas mostradas del campo de juego, desplazadas por 6 en modo normal y por −2 y 12 para el campo de juego en modo inacabado 2 Player Versus. Los bytes de esta tabla son parte de la lista de valores que se muestran erróneamente como números de nivel después del nivel 29. Los bytes adyacentes inferior y superior de cada dirección se obtienen por separado y se combinan esencialmente en una dirección de 16 bits que se utiliza en el ciclo de copia. Se ejecuta un incremento al final de la subrutina.952A: JSR $9725 ; copyPlayfieldRowToVRAM();
952D: JSR $9725 ; copyPlayfieldRowToVRAM();
9530: JSR $9725 ; copyPlayfieldRowToVRAM();
9533: JSR $9725 ; copyPlayfieldRowToVRAM();
copyPlayfieldRowToVRAM()
vramRow
vramRow
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 el valor llega a 20, se le asigna un valor de 32, lo que significa que la copia está completamente completa. Como se muestra arriba, solo se copian 4 líneas por fotograma.El controlador de estado $03
es responsable de reconocer las líneas completadas y eliminarlas del campo de juego. Durante 4 llamadas separadas, escanea los desplazamientos de línea [−2, 1]
cerca del centro del tetrimino (ambas coordenadas de todos los cuadrados del tetrimino están en este intervalo). Los índices de las filas completadas se almacenan en $004A
- $004D
; el índice registrado 0 se usa para indicar que no se encontraron filas completas en esta pasada. El controlador se muestra a continuación. La comprobación al principio no permite que el controlador se ejecute al transferir líneas del campo de juego a VRAM (controlador de estado9A6B: LDA $0049
9A6D: CMP #$20 ; if (vramRow < 32) {
9A6F: BPL $9A74 ; return;
9A71: JMP $9B02 ; }
9A74: LDA $0041 ; rowY = tetriminoY - 2;
9A76: SEC
9A77: SBC #$02 ; if (rowY < 0) {
9A79: BPL $9A7D ; rowY = 0;
9A7B: LDA #$00 ; }
9A7D: CLC
9A7E: ADC $0057
9A80: STA $00A9 ; rowY += lineIndex;
9A82: ASL
9A83: STA $00A8
9A85: ASL
9A86: ASL
9A87: CLC
9A88: ADC $00A8
9A8A: STA $00A8 ; rowIndex = 10 * rowY;
9A8C: TAY
9A8D: LDX #$0A
9A8F: LDA ($B8),Y
9A91: CMP #$EF ; for(i = 0; i < 10; i++) {
9A93: BEQ $9ACC ; if (playfield[rowIndex + i] == EMPTY_TILE) {
9A95: INY ; goto rowNotComplete;
9A96: DEX ; }
9A97: BNE $9A8F ; }
9A99: LDA #$0A
9A9B: STA $06F1 ; play row completed sound effect;
9A9E: INC $0056 ; completedLines++;
9AA0: LDX $0057
9AA2: LDA $00A9
9AA4: STA $4A,X ; lines[lineIndex] = rowY;
9AA6: LDY $00A8
9AA8: DEY
9AA9: LDA ($B8),Y
9AAB: LDX #$0A
9AAD: STX $00B8
9AAF: STA ($B8),Y
9AB1: LDA #$00
9AB3: STA $00B8
9AB5: DEY ; for(i = rowIndex - 1; i >= 0; i--) {
9AB6: CPY #$FF ; playfield[i + 10] = playfield[i];
9AB8: BNE $9AA9 ; }
9ABA: LDA #$EF
9ABC: LDY #$00
9ABE: STA ($B8),Y
9AC0: INY ; for(i = 0; i < 10; i++) {
9AC1: CPY #$0A ; playfield[i] = EMPTY_TILE;
9AC3: BNE $9ABE ; }
9AC5: LDA #$13
9AC7: STA $0042 ; orientationID = UNASSIGNED;
9AC9: JMP $9AD2 ; goto incrementLineIndex;
rowNotComplete:
9ACC: LDX $0057
9ACE: LDA #$00
9AD0: STA $4A,X ; lines[lineIndex] = 0;
incrementLineIndex:
9AD2: INC $0057 ; lineIndex++;
9AD4: LDA $0057 ; if (lineIndex < 4) {
9AD6: CMP #$04 ; return;
9AD8: BMI $9B02 ; }
9ADA: LDY $0056
9ADC: LDA $9B53,Y
9ADF: CLC
9AE0: ADC $00BC
9AE2: STA $00BC ; totalGarbage += garbageLines[completedLines];
9AE4: LDA #$00
9AE6: STA $0049 ; vramRow = 0;
9AE8: STA $0052 ; clearColumnIndex = 0;
9AEA: LDA $0056
9AEC: CMP #$04
9AEE: BNE $9AF5 ; if (completedLines == 4) {
9AF0: LDA #$04 ; play Tetris sound effect;
9AF2: STA $06F1 ; }
9AF5: INC $0048 ; if (completedLines > 0) {
9AF7: LDA $0056 ; playState = DISPLAY_LINE_CLEARING_ANIMATION;
9AF9: BNE $9B02 ; return;
; }
9AFB: INC $0048 ; playState = UPDATE_LINES_AND_STATISTICS;
9AFD: LDA #$07
9AFF: STA $06F1 ; play piece locked sound effect;
9B02: RTS ; return;
vramRow
$03
llamado en cada cuadro). Si se detectan filas llenas, se vramRow
restablece a 0, lo que fuerza una transferencia completa.lineIndex
( $00A9
) se inicializa con un valor de 0 y su incremento se realiza en cada pasada.A diferencia del estado del juego $0A
y la rutina de copia del campo del juego, que usa la tabla de multiplicación de direcciones $96D6
, un bloque que comienza con $9A82
multiplica rowY
por 10 usando turnos y adiciones:rowIndex = (rowY << 1) + (rowY << 3); // rowIndex = 2 * rowY + 8 * rowY;
Esto se hace solo porque está rowY
limitado por el intervalo [0, 20]
, y la tabla de multiplicación solo cubre [0, 19]
. El escaneo de filas puede extenderse más allá del final del campo de juego. Sin embargo, como se dijo anteriormente, el juego se inicializa $0400
, $04FF
con un valor$EF
(mosaico vacío), creando más de 5 líneas ocultas vacías adicionales debajo del piso del campo de juego.Un bloque que comienza $9ADA
es parte del modo incompleto de 2 Player Versus. Como se mencionó anteriormente, despejar las filas agrega escombros al campo de juego del oponente. La tabla en la dirección determina el número de filas de basura $9B53
: el ciclo en la dirección desplaza el material sobre la fila llena una línea hacia abajo. Aprovecha el hecho de que cada línea en una secuencia continua está separada de la otra por 10 bytes. El siguiente ciclo borra la línea superior. La animación de eliminación de filas se realiza durante el estado del juego , pero como se muestra a continuación, no ocurre en el controlador del estado del juego, que está completamente vacío.9B53: 00 ; no cleared lines
9B54: 00 ; Single
9B55: 01 ; Double
9B56: 02 ; Triple
9B57: 04 ; Tetris
$9AA6
$04
9E39: RTS ; return;
En cambio, durante el estado del juego $04
, se realiza la siguiente ramificación del modo de representación $03
. y los valores reflejados son necesarios para el modo inacabado 2 Player Versus. La subrutina se muestra a continuación . Se llama en cada cuadro, pero la condición al principio permite que se ejecute solo en cada cuarto cuadro. En cada pasada, recorre la lista de índices de filas completadas y borra 2 columnas en estas filas, moviéndose desde la columna central hacia afuera. Una dirección VRAM de 16 bits se construye de la misma manera que se muestra en la rutina de campo de copia. Sin embargo, en este caso, realiza un desplazamiento por el índice de columna obtenido de la tabla a continuación.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
Para limpiar la animación, se requieren 5 pases. Luego, el código pasa al siguiente estado del juego.El controlador de estado del juego $05
contiene el código descrito en la sección "Filas y estadísticas". El controlador termina con este código: la variable no se restablece hasta el final del estado del juego , después de lo cual se usa para actualizar el número total de filas y la puntuación. Esta secuencia permite ejecutar un error interesante. En el modo de demostración, debe esperar hasta que el juego recoja la fila completa y luego presione rápidamente Iniciar hasta que la animación para borrar la serie haya terminado. El juego volverá al protector de pantalla, pero si elige el momento adecuado, se guardará el valor . Ahora puedes iniciar el juego en modo A-Type. Cuando está bloqueado en lugar de la primera figura, el controlador de estado del juego9C9E: LDA #$00
9CA0: STA $0056 ; completedLines = 0;
9CA2: INC $0048 ; playState = B_TYPE_GOAL_CHECK;
9CA4: RTS ; return;
completedLines
$05
completedLines
$03
comienza a escanear filas completadas. No los encontrará, pero los dejará completedLines
sin cambios. Finalmente, cuando se cumple el estado del juego, el $05
número total de filas y la puntuación aumentarán, como si las hubiera puntuado.La forma más fácil de hacer esto es obtener la mayor cantidad, esperando que la demostración recolecte Tetris (habrá 2 de ellos en la demostración). Tan pronto como vea el parpadeo de la pantalla, haga clic en Inicio.
Después de comenzar un nuevo juego, la pantalla continuará parpadeando. Todo esto gracias al siguiente código llamado por el controlador de interrupciones. De hecho, si deja que la primera figura descienda automáticamente al piso del campo de juego, la puntuación aumentará en un valor aún mayor, porque ( ) también guardará su valor de la demostración. Esto es cierto incluso en los casos en que la demostración no llenó una sola fila. No se restablece hasta que se presiona el botón "Abajo". Además, si presiona Iniciar durante la animación de borrar la serie de combinaciones de Tetris en el modo de demostración, y luego espera a que la demostración comience nuevamente, no solo se contarán los puntos para Tetris en la demostración, sino que todo el tiempo se mezclará. Como resultado, la demo perderá el juego. Después de cortar el final del juego, puede volver al protector de pantalla haciendo clic en Inicio.9673: LDA #$3F
9675: STA $2006
9678: LDA #$0E
967A: STA $2006 ; prepare to modify background tile color;
967D: LDX #$00 ; color = DARK_GRAY;
967F: LDA $0056
9681: CMP #$04
9683: BNE $9698 ; if (completedLines == 4) {
9685: LDA $00B1
9687: AND #$03
9689: BNE $9698 ; if (frameCounter divisible by 4) {
968B: LDX #$30 ; color = WHITE;
968D: LDA $00B1
968F: AND #$07
9691: BNE $9698 ; if (frameCounter divisible by 8) {
9693: LDA #$09
9695: STA $06F1 ; play clear sound effect;
; }
; }
; }
9698: STX $2007 ; update background tile color;
holdDownPoints
$004F
holdDownPoints
El estado del juego $06
realiza una comprobación de objetivos para los juegos de tipo B. En el modo de tipo A, es esencialmente un marco no utilizado.El estado del juego $07
contiene exclusivamente la lógica incompleta de 2 jugadores contra. En el modo de un jugador, se comporta como un marco no utilizado.El estado del juego se $08
trata en las secciones "Creación de Tetrimino" y "Elección de Tetrimino".El estado del juego $09
no se usa. $0B
aumenta el estado del juego, pero también se ve sin usar.Y finalmente, el ciclo principal del juego:; 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 ; }