Como eu ensinei AI a jogar Tetris para NES. Parte 1: análise do código do jogo

Neste artigo, explorarei a mecânica enganosamente simples do Nintendo Tetris e, na segunda parte, explicarei como criei uma IA que explora essas mecânicas.


Experimente você mesmo


Sobre o projeto


Para aqueles que não têm a perseverança, paciência e tempo necessários para dominar o Nintendo Tetris, criei uma IA que pode jogar por conta própria. Você pode finalmente chegar ao nível 30 e ainda mais. Você verá como obter o número máximo de pontos e observará a mudança interminável de contadores de linhas, níveis e estatísticas. Você aprenderá quais cores aparecem em níveis acima dos quais uma pessoa não pode subir. Veja até onde você pode ir.

Exigências


Para executar o AI, você precisa de um emulador universal NES / Famicom FCEUX . A inteligência artificial foi desenvolvida para o FCEUX 2.2.2 , a versão mais recente do emulador no momento da escrita.

Você também precisará do arquivo ROM do Nintendo Tetris (versão dos EUA). Tente pesquisar no Google .

Baixar


Descompacte lua/NintendoTetrisAI.lua deste arquivo zip de origem .

Lançamento


Inicie o FCEUX. No menu, selecione Arquivo | Abrir ROM ... Na caixa de diálogo Abrir arquivo, selecione o arquivo ROM do Nintendo Tetris e clique em Abrir. O jogo começará.

No menu, selecione Arquivo | Lua | Nova janela Lua Script ... Na janela Lua Script, digite o caminho para NintendoTetrisAI.lua ou clique no botão Procurar para encontrá-lo. Depois disso, clique em Executar.

O script em Lua o redirecionará para a primeira tela do menu. Deixe o tipo de jogo A-Type e você pode escolher qualquer música. Em computadores lentos, a música pode tocar muito rapidamente, então você deve desligá-la. Pressione Iniciar (Enter) para ir para a próxima tela do menu. No segundo menu, você pode usar as teclas de seta para alterar o nível inicial. Clique em Iniciar para iniciar o jogo. E aqui a IA assumirá o controle.

Se depois de selecionar um nível na segunda tela do menu, mantenha pressionado o botão A do gamepad (você pode alterar o layout do teclado no menu Config | Entrada ...) e pressione Iniciar, então o nível inicial será 10 a mais que o valor selecionado. O nível máximo de entrada é décimo nono.

Configuração


Para acelerar o jogo, abra o script Lua em um editor de texto. No início do arquivo, encontre a seguinte linha.

PLAY_FAST = false

Substitua false por true como mostrado abaixo.

PLAY_FAST = true

Salve o arquivo. Em seguida, clique no botão Reiniciar na janela Lua Script.

Nintendo Tetris Mechanics


Descrição de Tetrimino


Cada figura tetrimino corresponde a um nome de uma letra que se assemelha à sua forma.


Os designers do Nintendo Tetris definem arbitrariamente a ordem do tetrimino mostrada acima. As figuras são mostradas na orientação em que aparecem na tela, e o circuito cria uma imagem quase simétrica (talvez seja por isso que essa ordem foi escolhida). O índice de sequência fornece a cada tetrimino um ID numérico único. Identificadores de sequência e tipo são importantes no nível de programação; além disso, eles se manifestam na ordem das figuras exibidas no campo de estatísticas (veja abaixo).


As 19 orientações usadas no Nintendo Tetris tetrimino são codificadas em uma tabela localizada em $8A9C da memória do console NES. Cada figura é representada como uma sequência de 12 bytes que pode ser dividida em triplos (Y, tile, X) que descrevem cada quadrado da figura. Os valores hexadecimais acima das coordenadas acima de $7F indicam números inteiros negativos ( $FF= −1 e $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


Na parte inferior da tabela, há um registro não utilizado, potencialmente dando a oportunidade de adicionar outra orientação. No entanto, em várias partes do código, $13 indica que o identificador de orientação do tetrimino ativo não recebe um valor.

Para facilitar a leitura, as coordenadas dos quadrados em decimal são mostradas abaixo.

-- { { 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 as orientações são colocadas em uma matriz 5 × 5.


Na figura acima, o quadrado branco indica o centro da matriz, o ponto de referência para a rotação da figura.

A tabela de orientação é apresentada graficamente abaixo.


O identificador de orientação (índice da tabela) é mostrado em hexadecimal no canto superior direito de cada matriz. E os mnemônicos inventados para este projeto são mostrados no canto superior esquerdo. u , r , d , l , h v são abreviações de “cima, direita, baixo, esquerda, horizontal e vertical”. Por exemplo, é mais fácil denotar a orientação de Jd do que $07 .

Matrizes contendo as orientações das figuras durante a criação são marcadas com uma moldura branca.

Tetrimino I, S e Z podem receber quatro orientações separadas, mas os criadores do Nintendo Tetris decidiram se limitar a dois. Além disso, Zv e Sv não são imagens espelhadas ideais um do outro. Ambos são criados girando no sentido anti-horário, o que leva a um desequilíbrio.

A tabela de orientação também contém valores de bloco para cada quadrado em cada figura orientada. No entanto, com um estudo cuidadoso, fica claro que os valores para um tipo de tetrimino são sempre os mesmos.

TJZOSLEu
7B7D7C7B7D7C7B

Os valores do bloco são os índices da tabela (pseudo-cor) do padrão mostrado abaixo.


Os blocos de $7D $7C , $7C e $7D estão localizados diretamente abaixo de "ATIS" da palavra "ESTATÍSTICAS". Estes são os três tipos de quadrados dos quais o tetrimino é feito.

Para os curiosos, direi que avestruzes e pingüins são usados ​​no final do modo B-Type. Este tópico é discutido em detalhes na seção "Finalização".

Abaixo está o resultado da modificação da ROM após a substituição de $7B por $29 . O coração é o ladrilho sob o símbolo P na tabela de padrões para todas as orientações T.


As peças do coração permanecem no campo de jogo mesmo depois que os Ts modificados são travados no lugar. Conforme indicado abaixo na seção "Criando Tetrimino", isso significa que o campo de jogo armazena os valores reais dos índices de peças do Tetrimino jogado.

Os programadores de jogos tornaram possível o uso de 4 peças separadas para cada figura, e não apenas um tipo de quadrados invariável. Este é um recurso útil que pode ser usado para modificar a aparência do jogo. A tabela de padrões possui muito espaço vazio para novos ladrilhos que podem dar a cada tetrimino uma aparência única.

As coordenadas dos quadrados são muito fáceis de manipular. Por exemplo, uma versão modificada dos quatro primeiros triplos na tabela de orientação é mostrada abaixo.

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

Essa alteração é semelhante à seguinte:

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

O resultado é um tetrimino dividido.


Ao mover um tetrimino dividido, seus quadrados não podem ir além dos limites do campo de jogo e não podem passar por figuras previamente bloqueadas no local. Além disso, o jogo proíbe a rotação nessa orientação se levar a um quadrado que caia fora dos limites do campo de jogo ou ao fato de o quadrado se sobrepor a um quadrado que já esteja.

O tetrimino dividido é travado no lugar quando há suporte para qualquer um de seus quadrados. Se a figura estiver bloqueada, os quadrados suspensos no ar continuam pendurados.

O jogo lida com tetriminos divididos como qualquer figura normal. Isso nos faz entender que não há tabela adicional armazenando os metadados das figuras. Por exemplo, poderia haver uma tabela armazenando o tamanho da caixa delimitadora de cada orientação para verificar colisões com o perímetro do campo de jogo. Mas essa tabela não é usada. Em vez disso, o jogo simplesmente verifica todos os quatro quadrados antes de manipular a forma.

Além disso, as coordenadas dos quadrados podem ser quaisquer valores; eles não estão limitados ao intervalo [−2, 2] . Obviamente, valores que excedem muito esse intervalo nos fornecerão números inaplicáveis ​​que não cabem no campo de jogo. Mais importante, como declarado na seção “Estados do jogo e modos de renderização”, quando uma figura está travada no lugar, o mecanismo de limpeza de linhas preenchidas varre apenas os deslocamentos das linhas de -2 a 1 do quadrado central da figura; um quadrado com uma coordenada y fora desse intervalo não será reconhecido.

Rotação Tetrimino


Em uma ilustração gráfica da tabela de orientação, a rotação consiste em mover de uma matriz para uma das matrizes à esquerda ou à direita com a transferência da série, se necessário. Esse conceito é codificado em uma tabela de $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 tornar mais claro, moveremos cada coluna desta tabela para a linha da tabela abaixo.
TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
Sentido anti-horárioTlTuTrTdJdJlJuJrZvZhOSvShLuLrLdLlIhIv
No sentido horárioTrTdTlTuJuJrJdJlZvZhOSvShLdLlLuLrIhIv

Os mnemônicos nos títulos acima podem ser interpretados como um índice de sequência ou chave de distribuição. Por exemplo, girar Tu sentido anti-horário fornece Tl e girar Tu no sentido horário dá Tr .

A tabela de rotação codifica sequências encadeadas em cadeia de IDs de orientação; portanto, podemos modificar as gravações para que a rotação transforme um tipo de tetrimino em outro. Essa técnica pode potencialmente ser usada para tirar proveito de uma linha não utilizada na tabela de orientação.

Na frente da tabela de rotação há um código para acessá-lo.

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 rotação no sentido anti-horário, o índice da tabela de rotação é subtraído dobrando o ID da orientação. Ao adicionar 1, obtemos o índice de rotação no sentido horário.

As coordenadas x , y e o ID da orientação do tetrimino atual são armazenados nos endereços $0040 , $0041 e $0042 respectivamente.

O código usa uma variável temporária para fazer backup do ID da orientação. Posteriormente, após alterar a orientação, o código verifica se todos os quatro quadrados estão dentro dos limites do campo de jogo e que nenhum deles se sobrepõe aos quadrados já existentes (o código de verificação está localizado em $948B , no fragmento de código mostrado acima). Se a nova orientação estiver incorreta, a original será restaurada, não permitindo que o reprodutor gire a figura.

Contando com uma cruz, o controlador NES possui oito botões, cujo status é representado pelo bit de endereço $00B6 .

76543210
UmBSelecioneIniciarPara cimaPara baixoPara a esquerdaPara a direita

Por exemplo, $00B6 conterá o valor $81 enquanto o jogador segura A e Esquerda.

Por outro lado, $00B5 informa quando os botões foram pressionados; os bits $00B5 são verdadeiros apenas durante uma iteração do loop do jogo (1 quadro renderizado). O código usa $00B5 para responder a pressionar A e B. Cada um deles precisa ser liberado antes de ser usado novamente.

$00B5 e $00B6 são espelhos de $00F5 e $00F6 . O código nas seções a seguir usa esses endereços de forma intercambiável.

Criar Tetrimino


O campo de jogo do Nintendo Tetris consiste em uma matriz com 22 linhas e 10 colunas, para que as duas primeiras linhas fiquem ocultas do jogador.


Como mostrado no código abaixo, ao criar uma figura do Tetrimino, ela sempre está localizada nas coordenadas (5, 0) campo de jogo.

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


Abaixo está uma matriz 5 × 5 sobreposta neste ponto.


Nenhuma das matrizes de criação possui quadrados acima do ponto de partida. Ou seja, ao criar um tetrimino, todos os quatro quadrados imediatamente se tornam visíveis para o jogador. No entanto, se o jogador girar rapidamente a peça antes que ela tenha tempo de cair, parte da peça ficará temporariamente oculta nas duas primeiras linhas do campo de jogo.

Geralmente pensamos que o jogo termina quando o heap chega ao topo. Mas, de fato, isso não é inteiramente verdade. O jogo termina quando não é mais possível criar a próxima peça. Ou seja, antes do aparecimento da figura, todas as quatro células do campo de jogo correspondentes às posições dos quadrados do tetrimino criado devem estar livres. A figura pode ser travada no lugar de maneira que parte de seus quadrados apareça em linhas numeradas negativamente, e o jogo não termine; no entanto, no Nintendo Tetris, as linhas negativas são uma abstração relacionada apenas ao tetrimino ativo. Depois que a figura é bloqueada (fica mentindo), apenas quadrados em linhas de zero e mais são gravados no campo. Conceitualmente, as linhas numeradas negativamente são automaticamente limpas após o bloqueio. Mas, na realidade, o jogo simplesmente não armazena esses dados, cortando as partes superiores das figuras.

A área visível do campo de jogo 20 × 10 é armazenada em $0400 linha por linha, cada byte contém o valor do bloco de plano de fundo. As células vazias são indicadas pelo bloco $EF , um quadrado preto sólido.

Ao criar uma forma, três tabelas de pesquisa são usadas. Se houver um ID de orientação arbitrário, a tabela em $9956 nos fornecerá o ID de orientação ao criar o tipo correspondente 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


É mais fácil mostrar isso na tabela.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TdTdTdTdJdJdJdJdZhZhOShShLdLdLdLdIhIh

Por exemplo, todas as orientações de J são anexadas a Jd .

A tabela em $993B contém o tipo de Tetrimino para o ID de orientação fornecido.

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 maior clareza, mostrarei tudo de forma tabular.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TTTTJJJJZZOSSLLLLII

Veremos a terceira tabela de pesquisa na próxima seção.

Seleção Tetrimino


O Nintendo Tetris usa um registro de troca de feedback linear de 16 bits (LFSR) como seu gerador de números pseudo-aleatórios (PRNG) em sua configuração de Fibonacci. O valor de 16 bits é armazenado como big endian nos endereços $0017 - $0018 . Um número arbitrário de $8988 usado como uma semente.

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


Cada número pseudo-aleatório subsequente é gerado da seguinte forma: o valor é percebido como um número de 17 bits e o bit mais significativo é obtido executando XOR para os bits 1 e 9. Em seguida, o valor é deslocado para a direita, descartando o bit menos significativo.


Esse processo ocorre em $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, os parâmetros da sub-rotina acima podem ser configurados para que a função de chamada possa especificar a largura do registro de deslocamento e o endereço no qual ele pode ser encontrado na memória. No entanto, os mesmos parâmetros são usados ​​em todos os lugares, para que possamos assumir que os desenvolvedores tenham emprestado esse código em algum lugar.

Para aqueles que desejam modificar ainda mais o algoritmo, escrevi em Java.

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

E todo esse código pode ser compactado em uma linha.

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

Esse PRNG gera contínua e deterministicamente 32.767 valores exclusivos, iniciando cada ciclo a partir da semente original. Esse é um menos da metade dos números possíveis que podem caber no registro e qualquer valor nesse conjunto pode ser usado como semente. Muitos dos valores fora do conjunto criam uma cadeia que eventualmente leva a um número do conjunto. No entanto, alguns números iniciais resultam em uma sequência infinita de zeros.

Para avaliar aproximadamente o desempenho desse PRNG, gerei uma representação gráfica dos valores criados com base em uma frase com RANDOM.ORG .


Ao criar a imagem, o PRNG foi usado como um gerador de números pseudo-aleatórios, em vez de números inteiros de 16 bits. Cada pixel é colorido com base no valor do bit 0. A imagem possui um tamanho de 128 × 256, ou seja, cobre toda a sequência.

Além das listras quase imperceptíveis nos lados superior e esquerdo, parece aleatório. Nenhum padrão óbvio aparece.

Após o início, o PRNG muda constantemente o registro, trabalhando pelo menos uma vez por quadro. Isso não acontece não apenas na tela inicial e nas telas de menu, mas também quando o tetrimino fica entre as operações de criação de formas. Ou seja, a figura que aparece a seguir depende do número de quadros que o jogador leva para colocar a figura. De fato, o jogo se baseia na aleatoriedade das ações da pessoa que interage com ele.

Durante a criação da figura, o código é executado no endereço $9907 , que seleciona o tipo da nova 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;


No endereço $001A armazena um contador do número de figuras criadas com a inicialização. O incremento do contador é realizado pela primeira linha da sub-rotina e, como é um contador de byte único, após cada 256 peças, ele volta a zero novamente. Como o contador não é zerado entre os jogos, o histórico dos jogos anteriores afeta o processo de seleção de figuras. Essa é outra maneira de o jogo usar o jogador como fonte de aleatoriedade.

A rotina converte o byte mais significativo do número pseudo-aleatório ( $0017 ) em um tipo de tetrimino e o utiliza como o índice da tabela localizada em $994E para converter o tipo no ID da orientação de criação da forma.

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


No primeiro estágio da conversão, o contador de figuras criadas é adicionado ao byte superior. Em seguida, uma máscara é aplicada para salvar apenas os 3 bits inferiores. Se o resultado não for 7, esse é o tipo correto de tetrimino e, se não for o mesmo que a figura anterior selecionada, o número será usado como um índice na tabela para criação de figuras. Caso contrário, o próximo número pseudo-aleatório é gerado e a máscara é aplicada para obter os 3 bits inferiores do byte superior e, em seguida, o ID da orientação de criação de forma anterior é adicionado. Finalmente, uma operação de módulo é realizada para obter o tipo correto de tetrimino, que é usado como um índice na tabela de criação de formas.

Como o processador não suporta a divisão com o restante, esse operador é emulado subtraindo repetidamente 7 até que o resultado se torne menor que 7. A divisão com o restante é aplicada à soma do byte superior com a máscara aplicada e ao ID anterior da orientação de criação de forma. O valor máximo dessa soma é 25. Ou seja, para reduzi-lo ao restante de 4, são necessárias apenas 3 iterações.

No início de cada jogo, o ID da orientação de criação da forma ( $0019 ) é inicializado com um valor de Tu ( $00 ). Esse valor pode ser potencialmente usado em $9928 durante a primeira criação de forma.

Ao usar o ID de orientação anterior para criar uma figura, em vez do tipo anterior, o Tetrimino adiciona distorção, porque os valores do ID de orientação não são distribuídos uniformemente. Isso é mostrado na tabela:

$ 00$02$07$08$0A$0B$0E$12
0 020 01340 04
13124515
24235626
353460 030 0
46450 0141
50 0561252
6160 02363
720 01340 04

Cada célula contém um tipo de tetrimino, calculado adicionando o ID de orientação da figura criada (coluna) a um valor de 3 bits (linha) e aplicando o restante da divisão por 7 à soma. Cada linha contém duplicatas, porque $07 e $0E divididos igualmente por 7, enquanto $0B e $12 têm um saldo comum. As linhas 0 e 7 são as mesmas porque estão a uma distância de 7.

Existem 56 combinações de entradas possíveis e, se os tipos de tetrimino resultantes forem distribuídos uniformemente, podemos esperar que, na tabela acima, cada tipo apareça exatamente 8 vezes. Mas, como mostrado abaixo, este não é o caso.

TipoFrequência
T9
J8
Z8
O8
S9
L7
Eu7

T e S aparecem com mais frequência, e L e I - com menos frequência. Mas o código inclinado usando o ID da orientação não é executado toda vez que a sub-rotina é chamada.

Suponha que o PRNG crie uma sequência de valores independentes estatísticos uniformemente distribuídos. Esta é realmente uma suposição justa, considerando como o jogo tenta obter a aleatoriedade correta das ações do jogador. Adicionar o número de figuras criadas ao endereço $990C não afetará a distribuição, porque o número aumenta igualmente entre as chamadas. O uso da máscara de bits em $990E semelhante à aplicação da divisão por 8 com o restante, o que também não afeta a distribuição. Portanto, a verificação em $9910 vai para invalidIndex em 1/8 de todos os casos. E a probabilidade de acertar ao verificar no endereço $9918 , onde o valor selecionado recentemente é comparado com o valor anterior, é 7/8, com probabilidade de coincidência de 1/7.Isso significa que há uma chance adicional de 7/8 × 1/7 = 1/8entrar invalidIndex. Em geral, há uma probabilidade de 25% de usar um código distorcido e uma probabilidade de 75% de usar um código que selecione o Tetrimino uniformemente.

Em um conjunto de 224 tetriminos criados, a expectativa matemática é de 32 instâncias para cada tipo. Mas, na verdade, o código cria a seguinte distribuição:

TipoFrequência
T33
J32.
Z32.
O32.
S33
L31
Eu31

Ou seja, limpando 90 linhas e atingindo o nível 9, o jogador receberá um T e S extra e um L e I a menos do que o esperado estatisticamente.

O Tetrimino é escolhido com as seguintes probabilidades:

TipoProbabilidade
T14,73%
J14,29%
Z14,29%
O14,29%
S14,73%
L13,84%
Eu13,84%

Parece que na afirmação de que o “bastão longo” que nunca aparece quando é necessário, existe parte da verdade (pelo menos para o Nintendo Tetris).

Tetrimino Shift


O Nintendo Tetris usa o Delayed Auto Shift (DAS). Clicar em "Esquerda" ou "Direita" move instantaneamente o tetrimino uma célula na horizontal. Manter um desses botões de direção pressionado faz com que o jogo mude automaticamente a figura a cada 6 quadros, com um atraso inicial de 16 quadros.

Esse tipo de movimento horizontal é controlado pelo código no endereço $89AE. Como no código de rotação, uma variável temporária é usada aqui para fazer backup das coordenadas , caso a nova posição esteja incorreta. Observe que o teste impede que você mova a peça enquanto o jogador pressiona para baixo.

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



Jogando Tetrimino


A velocidade da descida automática do Tetrimino é uma função do número do nível. As velocidades são codificadas como o número de quadros renderizados para descida na tabela localizada em $898E. Como o NES opera a 60.0988 quadros / s, é possível calcular o período entre descidas e velocidade.

NívelQuadros de descidaPeríodo (s / descida)Velocidade (células / s)
0 048..7991,25
143.7151,40
238..6321,58
333.5491,82
428..4662,15
523.3832,61
618.3003,34
713.2164,62
88.1337,51
96.10010,02
10-125.08312,02
13-154.06715,05
16-183.05020/03
19-282.03330.05
29+1.01760.10

A tabela possui um total de 30 entradas. Após o nível 29, o valor dos quadros para descida é sempre 1.

Um número inteiro de quadros para descida não é uma maneira muito detalhada de descrever a velocidade. Como mostra o gráfico abaixo, a velocidade aumenta exponencialmente a cada nível. De fato, o nível 29 é duas vezes mais rápido que o nível 28.


Com 1 quadro / descida, o jogador não tem mais que 1/3 de segundo para posicionar a figura, após o que começará a se mover. A essa velocidade de descida, o DAS não permite que a figura alcance as bordas do campo de jogo até que ela trave no lugar, o que para a maioria das pessoas significa um final rápido do jogo. No entanto, alguns jogadores, principalmente Thor Akerlund , conseguiram derrotar o DAS com a vibração rápida dos botões de cruz ( D-pad). No código de mudança mostrado acima, pode-se ver que, enquanto o botão de direção horizontal é liberado através do quadro, é possível mudar o tetrimino nos níveis 29 e acima com meia frequência. Esse é um máximo teórico, mas qualquer vibração do polegar acima de 3,75 toques / s pode anular o atraso original de 16 quadros.

Se a descida automática e controlada pelo jogador (pressionando "Para baixo") coincidir e ocorrer em um quadro, o efeito não será adicionado. Um ou ambos os eventos fazem com que a forma diminua exatamente uma célula nesse quadro.

A lógica de controle do acionador está localizada em $8914. A tabela do quadro de descida está sob o rótulo . Como mencionado acima, no nível 29 e acima, a velocidade é constantemente igual a 1 obturador / quadro. (endereço ) inicia a descida quando atinge ( ). O incremento é realizado em um endereço fora deste fragmento de código. Durante a descida automática ou controlada, é redefinido para 0. A variável ( ) é inicializada com o valor (no endereço

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

; game just started
; initial Tetrimino hanging at spawn point

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

; player just pressed down ending startup delay

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

playing:

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

; left/right not pressed

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

; player exclusively just presssed down

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

8936: JMP $8973 ; goto lookupDropSpeed;

autorepeating:

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

; down released

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

downPressed:

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

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

8956: INC $004F ; holdDownPoints++;

drop:

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

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

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

; the piece is locked

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

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

8972: RTS ; return;

lookupDropSpeed:

8973: LDA #$01 ; tempSpeed = 1;

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

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

noTableLookup:

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

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

8986: JMP $8972 ; return;

incrementAutorepeatY:

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


lookupDropSpeed

fallTimer$0045dropSpeed$00AFfallTimer$8892

autorepeatY$004E$0A$8739), que é interpretado como -96. Uma condição no início causa um atraso inicial. O primeiro Tetrimino permanece suspenso no ar no ponto de criação até autorepeatYaumentar para 0, o que leva 1,6 segundos. No entanto, quando você pressiona Para baixo nesta fase, é autorepeatYatribuído 0 instantaneamente. É interessante que você possa mover e girar a figura nessa fase do atraso inicial sem cancelá-la.

O incremento autorepeatYé realizado enquanto mantém pressionado. Quando atinge 3, ocorre uma descida controlada pelo homem (descida "suave") e é autorepeatYatribuída a 1. Portanto, a descida suave inicial requer 3 quadros, mas depois é repetida em cada quadro.

Além disso, ele autorepeatYaumenta de 0 para 1 somente quando o jogo reconhece que o jogador acabou de clicar em Para baixo (em$00B5), mas não reconhece manter pressionado. Isso é importante porque é autorepeatYredefinido para 0 ao criar um tetrimino (no endereço $98E8), o que cria um recurso importante: se o próprio jogador abaixa a figura e ele é bloqueado, e continua pressionando "Baixo" ao criar a próxima figura, o que geralmente acontece em níveis altos, então isso não levará a uma descida suave da nova figura. Para que isso aconteça, o jogador deve soltar “Down” e pressionar o botão novamente.

Uma descida potencialmente suave pode aumentar pontos. holdDownPoints( $004F) aumenta a cada descida, mas quando liberado, “Down” é redefinido para 0. Portanto, para marcar pontos, é necessário abaixar o tetrimino na fechadura com uma descida suave. A descida suave de curto prazo, que pode ocorrer no caminho da figura, não afeta os pontos. A conta é atualizada em$9BFE, mas é holdDownPointsredefinido para 0 logo após, no endereço $9C2F.

A verificação, que impede o jogador de fazer uma descida suave com um deslocamento horizontal da figura, complica o conjunto de pontos. Isso significa que o último movimento antes de travar a peça no lugar deve ser "Para baixo".

Quando a descida ocorre, tetriminoY( $0041) é copiado para originalY( $00AE). Se a nova posição criada pelo incremento tetriminoYestiver incorreta (ou seja, a figura empurra o chão do campo de jogo ou se sobrepõe a quadrados já existentes), o tetrimino permanece na posição anterior. Nesse caso, ele é restauradotetriminoYe a figura é considerada bloqueada. Isso significa que o atraso antes do bloqueio (o número máximo de quadros que um tetrimino espera, mantendo o ar antes do bloqueio) é igual ao atraso na descida.

A descida rígida (queda instantânea) não é suportada no Nintendo Tetris.

Deslizamento e rolagem


O manual do Nintendo Tetris tem um exemplo ilustrado de um deslize:


Deslizar consiste em deslocar-se ao longo da superfície de outras figuras ou ao longo do chão do campo de jogo. Geralmente é usado para empurrar uma figura sob um quadrado pendente. O deslizamento pode ser realizado até que o cronômetro de queda atinja a velocidade de descida, após o qual a figura será travada no lugar. Um exemplo animado é mostrado abaixo.


Por outro lado, a rolagem permite inserir figuras em espaços que são inatingíveis de qualquer outra maneira (veja abaixo).


Como deslizar, a rolagem não é possível sem um atraso de bloqueio. Além disso, a rolagem explora a maneira como o jogo manipula formas. Antes de mover ou girar a figura, o jogo verifica se, após alterar a posição, todos os quadrados do tetrimino estarão em células vazias dentro dos limites do campo de jogo. Essa verificação, como mostrado abaixo, não impede a rotação através de blocos preenchidos nas proximidades. Conforme declarado na seção Descrição do Tetrimino, cada linha da tabela de orientação contém 12 bytes; portanto, o índice nesta tabela é calculado multiplicando o ID da orientação do tetrimino ativo por 12. Como mostrado abaixo, todas as multiplicações na rotina são executadas usando turnos e adições.

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 iteração do ciclo muda a posição do tetrimino pelas coordenadas relativas de um dos quadrados da tabela de orientação para obter a localização da célula correspondente no campo de jogo. Ela então verifica se as coordenadas da célula estão dentro dos limites do campo de jogo e se a própria célula está vazia.

Os comentários descrevem mais claramente como verificar o espaçamento entre linhas. Além das células nas linhas visíveis, o código considera as duas linhas ocultas acima do campo de jogo como as posições legais dos quadrados sem usar uma condição composta. Isso funciona porque no código adicional os números negativos representados por variáveis ​​de byte único são equivalentes a valores maiores que 127. Nesse caso, o valor mínimo é −2, que é armazenado como

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


cellY$FE(254 em notação decimal).

O índice do campo de jogo é a soma cellYmultiplicada por 10 e cellX. No entanto, quando cellY−1 ( $FF= 255) ou −2 ( $FE= 254), o produto produz −10 ( $F6= 246) e −20 ( $EC= 236). Estando no intervalo, não cellXpode ser superior a 9, o que fornece um índice máximo de 246 + 9 = 255, e isso é muito mais do que o fim do campo de jogo. No entanto, o jogo é inicializado $0400- $04FFcom um valor $EF(de um bloco vazio), criando outros 56 bytes adicionais de espaço vazio.

Estranho que verificação de intervalocellXrealizada após examinar a célula do campo de jogo. Mas funciona corretamente em qualquer ordem. Além disso, a verificação do intervalo evita a condição composta, conforme indicado no comentário abaixo. Os exemplos de rolagem mostrados abaixo são possíveis devido à maneira como esse código verifica as posições.

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






Como mostrado abaixo, você pode até deslizar com rolagem.


A IA aproveita ao máximo as capacidades de movimentação do Nintendo Tetris, incluindo deslizar e rolar.

Nível 30 e acima


Depois de atingir o nível 30, parece que o nível é redefinido para zero.


Mas o nível 31 mostra que algo mais está acontecendo:


Os valores de nível exibidos estão localizados na tabela no endereço $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 mostrado abaixo, a tabela padrão é ordenado para que as telhas com $00on $0Fsão símbolos para glifos 0no F. Isso significa que, ao exibir um dígito decimal ou hexadecimal, o valor do próprio dígito é usado como o índice da tabela de padrões. No nosso caso, os valores de nível são armazenados como decimal com código binário (BCD); cada mordidela de cada byte na sequência é um valor de bloco.


Infelizmente, parece que os designers do jogo presumiram que ninguém passaria no nível 29 e, portanto, decidiram inserir apenas 30 entradas na tabela. Valores estranhos exibidos são bytes diferentes após a tabela. Apenas um byte (no endereço $0044) é usado para indicar o número do nível , e é por isso que o jogo alterna lentamente os 256 valores mostrados abaixo.

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

Os primeiros 20 valores ordinais são na verdade outra tabela que armazena deslocamentos no campo de jogo para cada uma das 20 linhas. Como o campo de jogo começa e cada linha contém 10 células, o endereço de uma célula arbitrária é: Dado que o processador não suporta multiplicação diretamente, esta tabela de pesquisa fornece uma maneira extremamente rápida de obter o produto. A tabela correspondente ocupa os próximos 40 bytes. Ele contém 20 endereços no formato little endian para a tabela nomeada 0 (uma área de memória VRAM que contém os valores dos blocos de segundo plano). Eles são ponteiros para as linhas de deslocamento do campo de jogo . Os bytes restantes a partir dos quais os valores de nível exibidos são compostos são instruções.

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



Linhas e estatísticas


O número de linhas concluídas e estatísticas do tetrimino ocupa 2 bytes cada nos seguintes endereços.

EndereçosQuantidade
0050 - 0051Classificações
03F0 - 03F1T
03F2 - 03F3J
03F4 - 03F5Z
03F6 - 03F7O
03F8 - 03F9S
03FA - 03FBL
03FC - 03FDEu

De fato, esses valores são armazenados como pequenos BCDs endian compactados de 16 bits. Por exemplo, o número de linhas é mostrado abaixo, que é 123. Os bytes são contados da direita para a esquerda para que os dígitos decimais fiquem na ordem.


No entanto, os designers de jogos assumiram que nenhum dos valores seria maior que 999. Portanto, a lógica de exibição processa corretamente o primeiro byte como um BCD compactado, em que cada petisco é usado como um valor de bloco. Mas o segundo byte inteiro é realmente usado como o dígito decimal superior. Quando os dígitos inferiores passam de 99para 00, ocorre o incremento normal do segundo byte. Como resultado, o segundo byte percorre todos os 256 blocos. Um exemplo disso é mostrado abaixo.


Depois de limpar a linha, o código a seguir é executado para aumentar o número de linhas. As verificações são realizadas para os dígitos médio e inferior, para que permaneçam entre 0 e 9. Mas o dígito superior pode ser aumentado infinitamente. Se após o incremento do número de linhas o dígito inferior for 0, isso significa que o jogador acabou de completar um conjunto de 10 linhas e você precisará aumentar o número do nível. Como você pode ver no código abaixo, uma verificação adicional é realizada antes do incremento de nível. A segunda verificação está relacionada ao nível de entrada selecionado. Para ir para um determinado nível , independentemente do nível inicial, o jogador deve limpar

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






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

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

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

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

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


X10Xlinhas Por exemplo, se um jogador começa no nível 5, ele permanecerá nele até completar 60 linhas, após o que passará para o nível 6. Depois disso, a cada 10 linhas adicionais, você aumentará o número do nível.

Para executar essa verificação, o valor das linhas preenchidas é copiado de $0050- $0051para $00A8- $00A9. Em seguida, a cópia é deslocada para a direita 4 vezes, o que para um BCD compactado é semelhante a dividir por 10. O menor dígito decimal é descartado e os dígitos mais altos e médios são deslocados em uma posição, resultando em mordidelas $00A8.


No entanto, no endereço, $9BEAo número do nível é diretamente comparado ao valor compactado do BCD $00A8. Não há pesquisa na tabela para converter o valor BCD em decimal, e esse é um erro claro. Por exemplo, na figura acima, o número do nível deve ser comparado com $12(18 em decimal), e não 12. Portanto, se um jogador decidir começar no nível 17, o nível passará para 120 linhas, porque 18 é mais que 17.

A tabela mostra o número esperado de linhas necessárias para a transição em cada nível inicial. É comparado ao que realmente acontece devido a um bug.

0 012345678910111213141516171819
10203040.50.60708090100110120130140150160170180190200
10203040.50.60708090100100100100100100100110120130140

O valor esperado é o mesmo que verdadeiro para os níveis iniciais de 0 a 9. De fato, a coincidência para o nível de entrada 9 é aleatória; 10-15 também vai para o próximo nível com 100 linhas, porque $10- este é 16 na forma decimal. A maior diferença entre o esperado e o real é de 60 linhas.

Suspeito que o bug se deva a alterações de design nos estágios posteriores do desenvolvimento. Veja a tela do menu, permitindo ao jogador escolher um nível de entrada.


Não há explicação sobre como começar a partir dos níveis acima de 9. Mas no livreto Nintendo Tetris, esse segredo é revelado:


Parece que esse recurso oculto foi inventado no último momento. Talvez tenha sido adicionado muito perto da data de lançamento, o que não permitiu testá-lo completamente.

De fato, a verificação da série inicial contém um segundo erro relacionado à saída de valores para o intervalo. Abaixo estão os comentários no código que explicam melhor o que acontece em um nível baixo. A comparação é realizada subtraindo e verificando o sinal do resultado. Mas um número assinado de byte único é limitado a -128 a 127. Se a diferença for menor que -128, o número é transferido e o resultado se torna um número positivo. Este princípio é explicado nos comentários no código. Ao verificar se a diferença está nesse intervalo, deve-se levar em consideração que o número do nível, ao aumentar para valores maiores que 255, efetua a transferência para 0 e

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

9BEE: INC $0044 ; increment level
; }




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

9BEE: INC $0044 ; increment level
; }


$00A8potencialmente, ele pode conter qualquer valor, porque sua mordidela superior é retirada $0051, cujo incremento pode ocorrer infinitamente.

Esses efeitos se sobrepõem, criando períodos nos quais o número do nível permanece inalterado por engano. Os períodos ocorrem em intervalos regulares de 2.900 linhas, iniciando em 2.190 linhas e duram 800 linhas. Por exemplo, de 2190 ( L90) a 2990 ( T90), o nível permanece igual a $DB( 96), como mostrado abaixo.


O próximo período acontece de 5090 a 5890, o nível é constantemente igual a $AD( 06). Além disso, durante esses períodos, a paleta de cores também não muda.

Desenho de Tetrimino para colorir


Em cada nível, as peças de tetrimino recebem 4 cores únicas. As cores são retiradas da tabela localizada em $984C. Seus registros são reutilizados a cada 10 níveis. Da esquerda para a direita: as colunas da tabela correspondentes às áreas em preto, branco, azul e vermelho da imagem abaixo.

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





Os valores correspondem à paleta de cores do NES.


As 2 primeiras cores de cada entrada são sempre em preto e branco. No entanto, a primeira cor é realmente ignorada; independentemente do valor, é considerada uma cor transparente através da qual um fundo preto sólido é exibido.

O acesso à tabela de cores é realizado na rotina em $9808. O índice da tabela de cores é baseado no número do nível dividido por um restante de 10. O ciclo copia a entrada para as tabelas da paleta na VRAM. A divisão com o restante é emulada por uma subtração constante de 10 até o resultado ser menor que 10. O início da sub-rotina com comentários é mostrado abaixo.

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


No entanto, conforme declarado na seção anterior, subtração e ramificação com base no sinal de diferença são usadas em comparação. Um número assinado de byte único é limitado a -128 a 127. Os comentários atualizados abaixo refletem esse princípio. Os comentários abaixo são mais simplificados. Esta redação revela um erro no código. A operação de divisão restante é completamente ignorada para níveis de 138 e superiores. Em vez disso, o índice é atribuído diretamente ao número do nível, que fornece acesso a bytes muito além do final da tabela de cores. Como mostrado abaixo, isso pode até levar a um tetrimino quase invisível.

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





Abaixo estão as cores de todos os 256 níveis. As peças são organizadas em 10 colunas para enfatizar o uso cíclico da tabela de cores, violada no nível 138. Linhas e colunas nos cabeçalhos são indicadas em decimal.


Após 255, o número do nível retorna a 0.

Além disso, como mencionado na seção anterior, alguns níveis não mudam até que 800 linhas sejam removidas. Durante esses longos níveis, as cores permanecem inalteradas.

Modo de jogo


O modo de jogo armazenado no endereço $00C0determina qual das várias telas e menus é atualmente exibida ao usuário.

ValorDescrição do produto
00Tela de informações legais
01Tela inicial
02Menu Tipo de Jogo
03Menu de níveis e alturas
04Jogo / pontuação máxima / final / pausa
05Demo

Como mostrado acima, o jogo possui uma rotina inteligentemente escrita que atua como uma instrução de troca usando a pequena tabela de navegação endian localizada imediatamente após a chamada. A lista acima mostra os endereços de todos os modos de jogo. Observe que os modos "Jogo" e "Demo" usam o mesmo código. Essa rotina nunca volta. Em vez disso, o código usa o endereço de retorno; normalmente aponta para a instrução imediatamente após a chamada para o salto para a sub-rotina (menos 1 byte), mas neste caso aponta para a tabela de salto. O endereço de retorno é retirado da pilha e armazenado em - . Depois de salvar o endereço da tabela de salto, o código usa o valor no registro A como um índice e executa a transição correspondente.

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]


O código pode usar essa rotina de comutação desde que os índices estejam próximos de 0 e não haja espaços ou poucos entre os casos possíveis.

Tela de informações legais


O jogo começa com uma tela mostrando um aviso legal.


Na parte inferior da tela, Aleksey Pazhitnov é mencionado como o inventor, designer e programador do primeiro Tetris. Em 1984, trabalhando como desenvolvedor de computadores no Dorodnitsyn Computing Center (um instituto de pesquisa líder da Academia Russa de Ciências em Moscou), ele desenvolveu um protótipo do jogo na Electronics-60 (clone soviético DEC LSI-11 ). Um protótipo foi desenvolvido para um modo de texto monocromático verde no qual os quadrados são indicados por pares de colchetes []. Com a ajuda do estudante de 16 anos Vadim Gerasimov e do engenheiro de computação Dmitry Pavlovsky alguns dias após a invenção do jogo, o protótipo foi portado para um PC IBM com MS DOS e Turbo Pascal. Ao longo de dois anos, eles aperfeiçoaram o jogo juntos, adicionando recursos como cores do tetrimino, estatísticas e, mais importante, um código de tempo e gráficos que permitia que o jogo funcionasse em uma variedade de modelos e clones de PC.

Infelizmente, devido às peculiaridades da União Soviética na época, suas tentativas de monetizar o jogo não tiveram êxito e, no final, eles decidiram compartilhar a versão para PC com seus amigos de graça. A partir desse momento, "Tetris" começou a se espalhar viralmente por todo o país e além, copiado de disco para disco. Mas desde que o jogo foi desenvolvido por funcionários de uma agência governamental, o estado era o proprietário e, em 1987, a organização responsável pelo comércio internacional de tecnologias eletrônicas assumiu o licenciamento do jogo ( Electronorgtekhnika (ELORG)) A abreviatura V / O na tela de informações legais pode ser abreviada para Version Originale.

A empresa de software britânica Andromeda tentou obter direitos sobre a Tetris e, antes da conclusão da transação, sublicenciou o jogo para outros fornecedores, por exemplo, a editora britânica de jogos de computador Mirrorsoft . A Mirrorsoft, por sua vez, a sublicenciou para a Tengen , uma subsidiária da Atari Games. Tengen concedeu ao Bullet-Proof Software os direitos de desenvolver um jogo para computadores e consoles no Japão, o que resultou em Tetris para a Nintendo Famicom . Abaixo está sua tela de informações legais.


Curiosamente, nesta versão, o estudante Vadim Gerasimov é chamado de designer e programador original.

Tentando proteger a versão portátil do console Game Boy, a Nintendo usou o Bullet-Proof Software para concluir um acordo bem-sucedido diretamente com a ELORG. No processo de conclusão do acordo, a ELORG revisou seu contrato com a Andrômeda, acrescentando que a Andrômeda só tinha direitos de jogos para computadores e máquinas de fliperama. Por esse motivo, o Bullet-Proof Software teve que pagar royalties da ELORG por todos os cartuchos vendidos para a Famicom, porque os direitos que recebeu de Tengen acabaram sendo falsos. Mas, por meio da reconciliação com a ELORG, a Bullet-Proof Software finalmente conseguiu obter direitos mundiais de jogos de console para a Nintendo.

O software à prova de balas sublicenciou os direitos dos jogos portáteis da Nintendo e juntos eles desenvolveram o Game Boy Tetris, que é refletido na tela de informações legais abaixo.


Com direitos globais de jogos de console, a Nintendo desenvolveu a versão Tetris para NES que estamos explorando neste artigo. Em seguida, o software à prova de balas sublicenciou os direitos da Nintendo, o que lhe permitiu continuar vendendo cartuchos para a Famicom no Japão.

Isto foi seguido por uma complexa batalha jurídica. Tanto a Nintendo quanto a Tengen exigiram que a parte contrária parasse de produzir e vender sua versão do jogo. Como resultado, a Nintendo venceu e centenas de milhares de cartuchos Tengen Tetris foram destruídos. O veredicto do tribunal também proibiu várias outras empresas, como a Mirrorsoft, de criar versões de console.

Pajitnov nunca recebeu deduções da ELORG ou do estado soviético. No entanto, em 1991 ele se mudou para os EUA e em 1996, com o apoio do proprietário do Bullet-Proof SoftwareHenka Rogers co-fundou a The Tetris Company , que lhe permitiu lucrar com versões para dispositivos móveis e consoles modernos.

É interessante olhar para a tela de informações legais como uma janela que dá uma idéia da origem modesta do jogo e das batalhas subsequentes pelos direitos de propriedade intelectual, porque para a maioria dos jogadores essa tela é apenas um obstáculo irritante, cujo desaparecimento parece ter que esperar para sempre. O atraso é definido por dois contadores, contando sequencialmente de 255 a 0. A primeira fase não pode ser pulada e a segunda é pulada pressionando o botão Iniciar. Portanto, a tela de informações legais é exibida por pelo menos 4,25 segundos e não mais do que 8,5 segundos. No entanto, acho que a maioria dos jogadores desiste, parando de pressionar Iniciar durante o primeiro intervalo e, por isso, eles estão aguardando a conclusão completa.

O tempo das fases, assim como o resto do jogo, é governado por um manipulador de interrupções não mascarado chamado no início de cada intervalo de apagamento vertical, um curto período de tempo entre a renderização de quadros de televisão. Ou seja, a cada 16.6393 milissegundos, a execução normal do programa é interrompida pelo seguinte código. O manipulador começa passando os valores dos principais registradores para a pilha e recuperando-os após a conclusão, para não interferir na tarefa interrompida. A chamada atualiza o VRAM, convertendo a descrição do modelo de memória para o que é exibido na tela. Além disso, o manipulador reduz o valor do contador da tela de informações legais se for maior que zero. Desafio

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

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

8011: DEC $00C3 ; legalScreenCounter1--;

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

801B: JSR $AB5E ; initializeOAM();

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

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

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

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

8042: JSR $9D51 ; pollControllerButtons();

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

804A: RTI ; resume interrupted task


render()initializeOAM()executa a etapa exigida pelo equipamento de geração de quadros. O manipulador continua a trabalhar incrementando o contador de quadros - o pequeno valor endian de 16 bits armazenado no endereço $00B1- $00B2que ele usa em locais diferentes para tempo controlado. Depois disso, o seguinte número pseudo-aleatório é gerado; como mencionado acima, isso acontece independentemente do modo pelo menos uma vez por quadro. O $8040sinalizador de intervalo de apagamento vertical é definido no endereço , o que significa que o manipulador acabou de ser executado. Finalmente, os botões do controlador são pesquisados; o comportamento dessa rotina é descrito abaixo na seção Demo.

O sinalizador é verticalBlankingIntervalusado pela rotina discutida acima. Ele continua até que a execução do manipulador de interrupção comece.

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;


Essa rotina de bloqueio é usada por dois estágios de tempo da tela de informações legais, que são executados um após o outro. O script Lua AI ignora esse atraso definindo os dois contadores como 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


A demo mostra cerca de 80 segundos de jogabilidade pré-gravada. Ele não apenas exibe o arquivo de vídeo, mas usa o mesmo mecanismo do jogo. Durante a reprodução, duas tabelas são usadas. O primeiro, localizado no endereço $DF00, contém a seguinte sequência de criação do tetrimino:

TJTSZJTSZJSZLZJTTSITO JSZLZLIOLZLIOJTSITOJ

Ao criar uma figura, ela é selecionada aleatoriamente ou lida da tabela, dependendo do modo. A troca ocorre no endereço $98EB. O tipo tetrimino é extraído dos bits 6, 5 e 4 de cada byte. De tempos em tempos, essa operação nos dá um valor - o tipo errado. No entanto, a tabela a criação de formas ( ) utilizado para uma conversão de tipo em Tetrimino orientação ID é na verdade localizada entre as duas tabelas ligadas: Significado

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

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

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

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


$07$994E

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


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


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


$07obriga a ler além do final da tabela, na próxima, que fornece Td( $02).

Devido a esse efeito, esse esquema pode nos fornecer uma sequência ilimitada, mas reproduzível, de IDs pseudo-aleatórios da orientação das figuras criadas. O código funcionará porque qualquer endereço arbitrário em uma sequência variável de bytes não nos permite determinar onde a tabela termina. De fato, a sequência no endereço $DF00pode ser parte de algo completamente não relacionado a isso, especialmente considerando que o objetivo dos 5 bits diferentes de zero restantes não é claro, e a sequência gerada demonstra repetibilidade.

Durante a inicialização do modo de demonstração, o índice da tabela ( $00D3) é redefinido para o endereço $872B.

A segunda tabela da demonstração contém um registro dos botões do gamepad codificados em pares de bytes. Os bits do primeiro byte correspondem aos botões.

76543210
UmBSelecioneIniciarPara cimaPara baixoPara a esquerdaPara a direita

O segundo byte armazena o número de quadros durante os quais uma combinação de botões é pressionada.

A tabela ocupa endereços $DD00- $DEFFe consiste em 256 pares. O acesso é realizado pela sub-rotina no endereço $9D5B. Como a tabela de botões de demonstração tem 512 bytes, é necessário um índice de dois bytes para acessá-la. O índice é armazenado como pouco endian em - . É inicializado com o valor do endereço da tabela e seu incremento é realizado pelo seguinte código. Os programadores deixaram o processamento de entrada do player no código, o que nos permite analisar o processo de desenvolvimento e substituir a demo por outro registro. O modo de gravação demo é ativado quando um valor é atribuído.

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. Nesse caso, o código a seguir é iniciado, destinado a gravar na tabela de botões da demonstração. No entanto, a tabela é armazenada no PRG-ROM. Tentar gravar nele não afetará os dados salvos. Em vez disso, cada operação de gravação aciona um comutador de banco, resultando no efeito de falha mostrado abaixo.

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;





Isso sugere que os desenvolvedores possam executar o programa parcial ou totalmente na RAM.

Para contornar esse obstáculo, criei lua/RecordDemo.luaum localizado em um zip com código-fonte . Após alternar para o modo de gravação demo, ele redireciona as operações de gravação para a tabela no console Lua. A partir dele, os bytes podem ser copiados e colados na ROM.

Para gravar sua própria demonstração, execute o FCEUX e faça o download do arquivo ROM do Nintendo Tetris (Arquivo | Abrir ROM ...). Em seguida, abra a janela Lua Script (Arquivo | Lua | Nova janela Lua Script ...), navegue até o arquivo ou insira o caminho. Pressione o botão Executar para iniciar o modo de gravação demo e clique na janela do FCEUX para mudar o foco para ele. Você pode controlar as formas até que a tabela de botões esteja cheia. Depois disso, o jogo retornará automaticamente ao protetor de tela. Clique em Parar na janela Lua Script para parar o script. Os dados gravados aparecerão no console de saída, como mostrado na figura abaixo.


Selecione todo o conteúdo e copie para a área de transferência (Ctrl + C). Em seguida, execute o Hex Editor (Debug | Hex Editor ...). No menu Editor hexadecimal, selecione Exibir | Arquivo ROM e depois Arquivo | Ir para Endereço. Na caixa de diálogo Goto, digite 5D10 (endereço da tabela de botões de demonstração no arquivo ROM) e clique em Ok. Em seguida, cole o conteúdo da área de transferência (Ctrl + V).


Por fim, no menu FCEUX, selecione NES | Reset Se você conseguiu repetir todas essas etapas, a demonstração deve ser substituída por sua própria versão.

Se você deseja salvar as alterações, selecione Arquivo | Salvar Rom como ... e digite o nome do arquivo ROM modificado e clique em Salvar.

De maneira semelhante, você pode ajustar a sequência de tetriminos criados.

Tela da morte


Como mencionado acima, a maioria dos jogadores não consegue lidar com a velocidade da descida das figuras no nível 29, o que rapidamente leva à conclusão do jogo. Portanto, os jogadores que ele se associou ao nome "tela da morte". Mas, do ponto de vista técnico, a tela da morte não permite que o jogador vá mais longe devido a um bug no qual uma descida rápida não é realmente um bug, mas um recurso. Os designers foram tão gentis que permitiram que o jogo continuasse enquanto o jogador era capaz de suportar velocidade sobre-humana.

Uma tela verdadeira da morte aparece em aproximadamente 1550 linhas retraídas. Manifesta-se de maneiras diferentes. Às vezes o jogo é reiniciado. Em outros casos, a tela fica preta. Normalmente, um jogo congela ("congela") imediatamente após a exclusão de uma linha, conforme mostrado abaixo. Tais efeitos são frequentemente precedidos por artefatos gráficos aleatórios.


A tela de morte é o resultado de um erro no código que adiciona pontos ao excluir linhas. A conta de seis caracteres é armazenada como um BCD little endian compactado de 24 bits e está localizada em $0053- $0055. Para realizar conversões entre o número de linhas limpas e pontos obtidos, uma tabela é usada; cada entrada é um BCD de 16 bits com pouco valor endian. Depois de incrementar o número total de linhas e, possivelmente, o nível, o valor nesta lista é multiplicado pelo número do nível mais um e o resultado é adicionado aos pontos. Isso está claramente demonstrado na tabela do manual do 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 mostrado abaixo, a multiplicação é simulada por um ciclo que adiciona pontos à pontuação. É executado após o bloqueio da forma, mesmo que nenhuma linha seja limpa. Infelizmente, o Ricoh 2A03 não possui um modo decimal binário 6502; ele poderia simplificar bastante o corpo do ciclo. Em vez disso, a adição é realizada em etapas usando o modo binário. Qualquer dígito que exceda 9 após a adição é obtido essencialmente subtraindo 10 e incrementando os dígitos à esquerda. Por exemplo, isso é convertido em . Mas esse esquema não está totalmente protegido. Tomar : uma verificação não pode converter o resultado em

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


$07 + $07 = $0E$14$09 + $09 = $12$18. Para compensar isso, nenhum dos dígitos decimais nas entradas do scorecard excede 6. Além disso, para poder usá-lo, o último dígito de todas as entradas é sempre 0.

Leva um tempo para concluir esse ciclo longo e complicado. Em níveis altos, um grande número de iterações afeta o tempo do jogo, porque leva mais de 1/60 segundo para gerar cada quadro. Tudo isso, como resultado, leva a várias manifestações da "tela da morte".

O script Lua AI limita o número de iterações em um loop para 30 - o valor máximo que os designers podem alcançar conforme projetado pelos designers, o que elimina a tela mortal.

Finais


No livreto Nintendo Tetris, o jogo A-Type é descrito da seguinte maneira:


O jogo recompensa os jogadores que marcaram um número suficientemente grande de pontos em uma das cinco animações dos finais. A escolha do final é inteiramente baseada nos dois dígitos mais à esquerda da pontuação de seis dígitos. Como mostrado abaixo, para obter um dos finais, o jogador deve marcar pelo menos 30.000 pontos. Vale ressaltar que - é um espelho de endereços - . A conta é duplicada nos endereços - . Depois de passar no primeiro teste, a animação final é selecionada pela seguinte instrução switch.

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

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


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



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

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

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

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

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


No final, foguetes de tamanho crescente são lançados a partir da plataforma de lançamento ao lado da Catedral de São Basílio. No quarto final, a espaçonave Buran é mostrada - a versão soviética do ônibus espacial americano. No melhor final, a própria catedral se eleva no ar e um OVNI paira sobre a plataforma de lançamento. Abaixo está uma imagem de cada final e a pontuação associada a ele.
30000–49999
50000–69999
70000–99999
100000–119999
120000+

No modo de jogo do tipo B, outro teste é implementado, descrito no livreto Nintendo Tetris da seguinte maneira:


Se o jogador limpa com sucesso 25 linhas, o jogo mostra o final, dependendo do nível inicial. As terminações dos níveis de 0 a 8 consistem em animais e objetos voando ou correndo no quadro, passando misteriosamente atrás da Catedral de São Basílio. O UFO do melhor final do modo A-Type aparece no final 3. No final 4, pterossauros voadores extintos aparecem e no final 7 são mostrados os míticos dragões voadores. Nos finais 2 e 6, são mostrados pássaros sem asas: pingüins e avestruzes correndo. No final de 5, o céu está cheio de aeronaves BOAS (não confundir com aeronaves da Goodyear). E no final de 8, muitas "Buranas" varrem a tela, embora na realidade houvesse apenas uma.

A altura inicial (mais 1) é usada como multiplicador, recompensando o jogador com um grande número de animais / objetos por maior complexidade.

No melhor final do tipo B, é mostrado um castelo cheio de personagens do universo Nintendo: a princesa Peach bate palmas, Kid Icarus toca violino, Donkey Kong bate no grande tambor, Mario e Luigi dançam, Bowser toca acordeão, Samus toca violoncelo, Link - em uma flauta, enquanto as cúpulas da Catedral de São Basílio voam no ar. A quantidade desses elementos mostrados no final depende da altura inicial. Abaixo estão as imagens dos 10 finais.


A IA pode limpar rapidamente todas as 25 linhas necessárias no modo Tipo B em qualquer nível e altura inicial, o que permite ver qualquer um dos finais. Também vale a pena avaliar o quão legal ele lida com grandes montes de blocos aleatórios.

Nas terminações de 0 a 8, até 6 objetos podem se mover no quadro. As coordenadas y dos objetos são armazenadas em uma tabela localizada em at $A7B7. As distâncias horizontais entre os objetos são armazenadas em uma tabela no endereço . Uma sequência de valores com um sinal no endereço determina a velocidade e a direção dos objetos. Os índices de Sprite são armazenados em . De fato, cada objeto consiste em dois sprites com índices adjacentes. Para obter o segundo índice, você precisa adicionar 1. Por exemplo, um dragão consiste em

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


$A77B

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


$A771

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


$A7F3

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


$38e $39. As peças para esses sprites estão contidas nas tabelas de padrões abaixo.


Examinamos a tabela central do padrão acima, usada para exibir o tetrimino e o campo de jogo. Curiosamente, ele contém o alfabeto inteiro, enquanto outros contêm apenas parte dele para economizar espaço. Mas ainda mais interessantes são os sprites de aeronaves e helicópteros na tabela de padrões à esquerda; eles não aparecem nos finais ou em outras partes do jogo. Descobriu-se que o avião eo helicóptero tem índices sprites $30e $16e você pode mudar a tabela mostrada acima, para vê-los em ação.



Infelizmente, as montagens de helicóptero não são exibidas, mas os rotores principal e de cauda são lindamente animados.

2 Jogadores Versus


O Nintendo Tetris contém um modo incompleto para dois jogadores que você pode ativar alterando o número de jogadores ( $00BE) para 2. Como mostrado abaixo, dois campos de jogo são exibidos no plano de fundo do modo para um jogador.


Não há borda entre os campos porque a região central do plano de fundo é preta sólida. Os valores 003mostrados acima dos campos de jogo indicam o número de linhas limpas por cada jogador. A única figura comum para dois jogadores aparece no mesmo local que no modo single player. Infelizmente, ele está localizado no campo de jogo certo. Quadrados e outros ladrilhos estão coloridos incorretamente. E quando o jogador perde o jogo reinicia.

Mas se você ignorar esses problemas, o modo é bastante reproduzível. Cada jogador pode controlar independentemente as peças no campo de jogo correspondente. E quando um jogador digita Double, Triple ou Tetris (isto é, limpa duas, três ou quatro linhas), as linhas de lixo com um quadrado ausente aparecem na parte inferior do campo de jogo do oponente.

Um campo adicional está localizado em $0500. A $0060- $007F, geralmente sendo um espelho $0040- $005F, são usados ​​para o segundo jogador.

Provavelmente, esse modo interessante foi abandonado devido a um cronograma de desenvolvimento ocupado. Ou talvez ele tenha sido deixado intencionalmente inacabado. Uma das razões pelas quais Tetris foi escolhido como o jogo fornecido com o Nintendo Game Boy foi porque incentivou a compra do cabo Game Link.- um acessório que conecta dois Game Boys para iniciar o modo 2 jogadores versus. Esse cabo adicionou um elemento de "socialidade" ao sistema - incentivou os amigos a comprar um Game Boy para participar da diversão. Talvez a Nintendo tenha medo de que, se a versão para console do jogo tivesse 2 jogadores versus o modo, o poder de "publicidade" de Tetris, que estimulou a compra do Game Boy, poderia ser enfraquecido.

Música e efeitos sonoros


A música de fundo é ativada quando $06F5um dos valores listados na tabela é atribuído.

ValorDescrição do produto
01Música não utilizada na tela inicial
02Alvo do modo B-Type alcançado
03Música-1
04Música-2
05Música-3
06Música-1 allegro
07Music-2 allegro
08Música-3 allegro
09Tela de Parabéns
0AFinais
0BAlvo do modo B-Type alcançado
Você pode ouvir música não utilizada no protetor de tela aqui . No próprio jogo, nada soa durante a tela de proteção de tela.

Music-1 é uma versão de " Dance of the Dragee Fairy ", música para a bailarina do terceiro ato da valsa do pas de deux "The Nutcracker" de Tchaikovsky. A música final é uma variação dos " Versos do Toureiro ", uma ária da ópera Carmen Georges Bizet. Essas composições são organizadas pelo compositor do resto da música de Hirokazu Tanaka .

A Music-2 foi inspirada nas músicas tradicionais do folclore russo. Music-3 é misterioso, futurista e terno; Por um tempo, foi o toque do telefone de suporte ao cliente da Nintendo of America.

Para ajudar o jogador a entrar em pânico quando a altura da pilha se aproxima do teto do campo de jogo, uma versão da música de fundo começa a tocar rapidamente ( $06- $08).

Curiosamente, entre as composições musicais não existe " Chapman ", um famoso tema que soa em Game Boy Tetris.

Os efeitos sonoros são acionados pela gravação em $06F0e $06F1, de acordo com a tabela a seguir.

O endereçoValorDescrição do produto
06F002A cortina do final do jogo
06F003Foguete no final
06F101Seleção de opção de menu
06F102Seleção da tela do menu
06F103Tetrimino Shift
06F104Tetris recebido
06F105Rotação Tetrimino
06F106Novo nível
06F107Tetrimino Lock
06F108Chilrear
06F109Limpeza de linhas
06F10ALinha preenchida

Estados do jogo e modos de renderização


Durante a jogabilidade, o estado atual do jogo é representado por um número inteiro no endereço $0048. Na maioria das vezes, tem um significado $01que indica que o jogador controla o tetrimino ativo. No entanto, quando a peça é travada no lugar, o jogo passa gradualmente de estado $02para estado $08, conforme mostrado na tabela.

CondiçãoDescrição do produto
00ID de orientação não atribuído
01O jogador controla o tetrimino ativo
02Bloqueio de Tetrimino no campo de jogo
03Verificando linhas preenchidas
04Exibir animação de limpeza de linha
05Atualizando linhas e estatísticas
06Verificando o alvo do modo B
07Não usado
08Criar Próximo Tetrimino
09Não usado
0AAtualização da cortina de jogo
0BIncremento do estado do jogo

A ramificação do código, dependendo do estado do jogo, ocorre no seguinte endereço $81B2: No estado do comutador, ele salta para o código que atribui um valor indicando que a orientação não está especificada. O manipulador nunca é chamado; no entanto, o estado do jogo serve como um sinal para outras partes do código. O estado permite que o jogador alterne, gire e abaixe o tetrimino ativo: Conforme declarado nas seções anteriores, as rotinas de deslocamento, rotação e abaixamento da figura antes de executar o código, verifique as novas posições do tetrimino. A única maneira de bloquear uma forma na posição incorreta é criá-la em cima de uma forma existente. Nesse caso, o jogo termina. Como mostrado abaixo, o código de status executa essa verificação.

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


$00orientationID$13

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

9E33: RTS ; return;


$00

$01

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

81D8: RTS ; return;


$02. Se a posição bloqueada estiver correta, marcará as 4 células associadas do campo de jogo como ocupadas. Caso contrário, ela faz a transição para um estado - a cortina sinistra do fim do jogo.

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


A cortina é desenhada do topo do campo de jogo para baixo, descendo uma linha a cada 4 quadros. curtainRow( $0058) é inicializado com um valor de -16, criando um atraso adicional de 0,27 segundos entre o bloqueio final e o início da animação. No endereço $9A21no estado do $0Acódigo mostrado abaixo, a tabela de multiplicação é acessada, que é exibida erroneamente como números de nível. Isso é feito na escala curtainRowde 10. Além disso, como mostrado acima, o código no endereço $9A51inicia a animação final se a pontuação do jogador não for inferior a 30.000 pontos; caso contrário, ele espera clicar em Iniciar. O código é concluído atribuindo um valor ao estado do jogo , mas o manipulador correspondente não é chamado porque o jogo está concluído.

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

As linhas do campo de jogo são copiadas incrementalmente para o VRAM para exibi-las. O índice da linha atual a ser copiada está contido em vramRow( $0049). Um $9A3C vramRowvalor é atribuído no endereço curtainRow, o que torna essa linha visível ao renderizar.

As manipulações com VRAM ocorrem durante um intervalo de apagamento vertical, que é reconhecido pelo manipulador de interrupção descrito na seção "Tela de informações legais". Ele chama a sub-rotina mostrada abaixo (marcada nos comentários do manipulador de interrupções como render()). O modo de renderização é semelhante ao modo de jogo. Ele é armazenado no endereço e pode ter um dos seguintes 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

ValorDescrição do produto
00Tela com tela proteção de informações e tela
01Telas de menu
02Tela de Parabéns
03Jogo e demo
04Animação final

Parte do modo de renderização é $03mostrada abaixo. Como você pode ver abaixo, ele passa no VRAM uma linha do campo de jogo com um índice . Se maior que 20, a rotina não faz nada. A Tabela ( ) contém os endereços VRAM no formato little endian correspondente às linhas exibidas do campo de jogo deslocadas 6 em modo normal e por -2 e 12 para o campo em modo inacabado 2 Player Versus. Os bytes desta tabela fazem parte da lista de valores que são erroneamente exibidos como números de nível após o nível 29. Os bytes inferiores e superiores adjacentes de cada endereço são obtidos separadamente e são essencialmente combinados em um endereço de 16 bits, usado no ciclo de cópia. Um incremento é executado no final da sub-rotina.

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


copyPlayfieldRowToVRAM()vramRowvramRow

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

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

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

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

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

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

974F: JMP $9767 ; } else {

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

975B: JMP $9767 ; } else {

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

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

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

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

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

977E: RTS ; return;


vramPlayfieldRows$96EA

vramRow. Se o valor atingir 20, será atribuído um valor 32, o que significa que a cópia está totalmente concluída. Como mostrado acima, apenas 4 linhas são copiadas por quadro.

O manipulador de estado $03é responsável por reconhecer as linhas concluídas e removê-las do campo de jogo. Durante 4 chamadas separadas, ele verifica as [−2, 1]deslocações de linha próximas ao centro do tetrimino (ambas as coordenadas de todos os quadrados do tetrimino estão nesse intervalo). Os índices de linhas concluídas são armazenados em $004A- $004D; o índice 0 registrado é usado para indicar que nenhuma linha concluída foi encontrada nesta passagem. O manipulador é mostrado abaixo. A verificação no início não permite que o manipulador execute ao transferir linhas do campo de jogo para VRAM (manipulador de estado

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

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

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

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

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

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

9A9E: INC $0056 ; completedLines++;

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

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

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

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

9AC9: JMP $9AD2 ; goto incrementLineIndex;

rowNotComplete:

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

incrementLineIndex:

9AD2: INC $0057 ; lineIndex++;

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

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

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

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

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

9AFB: INC $0048 ; playState = UPDATE_LINES_AND_STATISTICS;

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

9B02: RTS ; return;


vramRow$03chamado em todos os quadros). Se forem detectadas linhas preenchidas, ela será vramRowredefinida para 0, o que força uma transferência completa.

lineIndex( $00A9) é inicializado com o valor 0 e seu incremento é realizado em cada passagem.

Diferentemente do estado do jogo $0Ae da rotina de cópia do campo de jogo, que usa a tabela de multiplicação de endereços $96D6, um bloco que começa com $9A82multiplica rowYpor 10 usando turnos e adições:

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

Isso é feito apenas porque é rowYlimitado pelo intervalo [0, 20], e a tabela de multiplicação cobre apenas [0, 19]. A varredura de linhas pode se estender além do final do campo de jogo. No entanto, como dito anteriormente, o jogo inicializa $0400- $04FFcom um valor$EF(ladrilho vazio), criando mais de 5 linhas ocultas vazias adicionais sob o piso do campo de jogo.

Um bloco que começa com $9ADAfaz parte do modo incompleto de 2 Player Versus. Como mencionado acima, limpar as linhas adiciona detritos ao campo de jogo do oponente. O número de linhas de lixo é determinado pela tabela no endereço $9B53: O ciclo no endereço desloca o material acima da linha preenchida uma linha para baixo. Ele aproveita o fato de que cada linha em uma sequência contínua é separada da outra por 10 bytes. O próximo loop limpa a linha superior. A animação de limpeza de linha é executada durante o estado do jogo , mas, como mostrado abaixo, não ocorre no manipulador de estado do jogo, que está completamente vazio.

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


$9AA6

$04

9E39: RTS ; return;

Em vez disso, durante o estado do jogo $04, a próxima ramificação do modo de renderização é executada $03. e valores espelhados são necessários para o modo inacabado 2 Player Versus. A sub-rotina é mostrada abaixo . É chamado em todos os quadros, mas a condição no início permite que ele seja executado apenas em cada quarto quadro. Em cada passagem, ele percorre a lista de índices de linhas concluídas e limpa 2 colunas nessas linhas, movendo-se da coluna central para fora. Um endereço VRAM de 16 bits é construído da mesma maneira que é mostrado na rotina do campo de cópia. No entanto, nesse caso, ele executa um deslocamento pelo índice da coluna obtido da tabela abaixo.

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 animação de limpeza, são necessários 5 passes. Em seguida, o código prossegue para o próximo estado do jogo.

O manipulador de estado do jogo $05contém o código descrito na seção "Linhas e estatísticas". O manipulador termina com este código: A variável não é redefinida até o final do estado do jogo , após o qual é usada para atualizar o número total de linhas e a pontuação. Essa sequência permite que um bug interessante seja executado. No modo de demonstração, você precisa esperar até o jogo reunir a linha completa e pressionar rapidamente Iniciar até que a animação para limpar a série termine. O jogo retornará ao protetor de tela, mas se você escolher a hora certa, o valor será salvo. Agora você pode iniciar o jogo no modo A-Type. Quando bloqueado no lugar da primeira figura, o manipulador de estado do jogo

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

9CA2: INC $0048 ; playState = B_TYPE_GOAL_CHECK;

9CA4: RTS ; return;


completedLines$05completedLines$03inicia a verificação de linhas concluídas. Ele não os encontrará, mas os deixará completedLinesinalterados. Finalmente, quando o estado do jogo for cumprido, o $05número total de linhas e a pontuação aumentará, como se você as tivesse marcado.

A maneira mais fácil de fazer isso é obter o maior valor, aguardando a demo coletar o Tetris (haverá 2 deles na demo). Assim que a tela piscar, clique em Iniciar.


Depois de iniciar um novo jogo, a tela continuará piscando. Tudo isso graças ao seguinte código chamado pelo manipulador de interrupções. De fato, se você deixar o primeiro número descer automaticamente para o chão do campo de jogo, a pontuação aumentará em um valor ainda maior, porque ( ) também salvará seu valor na demonstração. Isso ocorre mesmo nos casos em que a demonstração não preencheu uma única linha. Não é reiniciado até que o botão "Para baixo" seja pressionado. Além disso, se você clicar em Iniciar durante a animação para limpar as linhas de combinação do Tetris no modo de demonstração e aguardar a demonstração iniciar novamente, não apenas os pontos do Tetris serão contados na demonstração, mas todo o tempo será misturado. Como resultado, a demo perderá o jogo. Depois de terminar o jogo, você pode retornar ao protetor de tela clicando em Iniciar.

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

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

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

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

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

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

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

; }
; }
; }

9698: STX $2007 ; update background tile color;


holdDownPoints$004FholdDownPoints



O estado do jogo $06executa uma verificação de destino para jogos do tipo B. No modo A-Type, é essencialmente um quadro não utilizado.

O estado do jogo $07contém exclusivamente a lógica 2 Player Versus incompleta. No modo single player, ele se comporta como um quadro não utilizado.

O estado do jogo é $08discutido nas seções “Criando Tetrimino” e “Escolhendo Tetrimino”.

O estado do jogo $09não é usado. $0Baumenta o estado do jogo, mas também parece não utilizado.

E, finalmente, o ciclo principal do jogo:

; while(true) {

8138: JSR $8161 ; branchOnGameMode();

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

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

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

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

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

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

Source: https://habr.com/ru/post/pt420725/


All Articles