Reverse engineering Tetris on Nintendo to add Hard Drop

Nintendo Tetris is one of my favorite versions of Tetris. My only complaint is that it lacks the "Hard Drop" feature, which instantly drops the current shape and locks it in place. Let's add it





, , — «» « » — , , .





. — .





, .





Hard drop . , , , , , .





NES .





rust, NES ROM INES. NES Tetris ( - «Tetris(U)[!].nes»), ROM NES, NES Tetris, .





sha1 a99f922e9da20b2a27e4398348505d2e9d15271b.





$ cargo install nes-tetris-hard-drop-patcher   # install my tool
$ nes-tetris-hard-drop-patcher < 'Tetris (U) [!].nes' > tetris-hd.nes   # patch a NES Tetris ROM
$ fceux tetris-hd.nes   # run the result in an emulator
      
      



, ROM- NES Tetris. . ROM NES — fceux.





, ROM (IPS), . .





NES. , -, . , , , . , .





, Rust. , «» 12:





b.inst(Clc, ());                  // clear carry flag
b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x2)
b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x4)
b.inst(Sta(ZeroPage), 0x20);      // store current accumulator value at address 0x0020
b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x8)
b.inst(Adc(ZeroPage), 0x20);      // add the accumulator with the value at 0x0020 (x12)
      
      



rust NES. Rust , 1980- .





, NES .





, NES Mesen, . .





NES :





  • 8x8 ;





  • — , .





, — .





, , . , .





, , , , , .





, , :





— , , .





NES (, . .), OAMDMA. ( — OAM — , DMA — , .) OAMDMA NES , .





OAMDMA 0x4014. :





0xAB63  Lda(Immediate) 0x02       # load accumulator with 2
0xAB65  Sta(Absolute) 0x4014      # write accumulator to 0x4014
      
      



2 OAMDMA, 0x0200 0x02FF OAM. . 0x8A0A , .





0x0040 0x0041, 8 . NES 8x8 , , -, , . : 0x40 — x, 0x41 — Y .





0x42. 0 12, , -, , . (, “S”) 0x42. “ ”.





4 , . 0x40 0x41 — , . 0x8A9C, « ». 13 ( ) 12- . 3 4 :





  • y ( 0x41);





  • , ;





  • x ( 0x40).





OAM DMA . , , , , , , , hard drop. , , / , , .





— mesen, , , . , 0x00 0xFF! , mesen Monospace!





512 , 0xD6D0. , , , DMA OAM:





b.label("oam-dma-buffer-update");

// Call original function
b.inst(Jsr(Absolute), 0x8A0A);
// Return
b.inst(Rts, ());
      
      



(0x8A0A) .





DMA OAM rust NES.





:





0x8A0A  Lda(ZeroPage) 0x40
0x8A0C  Asl(Accumulator)
0x8A0D  Asl(Accumulator)
0x8A0E  Asl(Accumulator)
0x8A0F  Adc(Immediate) 0x60
0x8A11  Sta(ZeroPage) 0xAA
...
      
      



:





b.label("render-ghost-piece"); // function label so it can be called by name later

b.inst(Lda(ZeroPage), 0x40);
b.inst(Asl(Accumulator), ());
b.inst(Asl(Accumulator), ());
b.inst(Asl(Accumulator), ());
b.inst(Adc(Immediate), 0x60);
b.inst(Sta(ZeroPage), 0xAA);
...
      
      



DMA OAM, , . , oam-dma-buffer-update, :





b.label("oam-dma-buffer-update");

// Call new function
b.inst(Jsr(Absolute), "render-ghost-piece");
// Return
b.inst(Rts, ());
      
      



, , . , , , , 6.





b.label("oam-dma-buffer-update");  // Call original function first b.inst(Jsr(Absolute), 0x8A0A); // Render the ghost piece, passing the vertical offset argument in address 0x0028. b.inst(Lda(Immediate), 6); b.inst(Sta(ZeroPage), 0x28); b.inst(Jsr(Absolute), "render-ghost-piece"); // Return b.inst(Rts, ());b.label("oam-dma-buffer-update");

// Call original function first
b.inst(Jsr(Absolute), 0x8A0A);
// Render the ghost piece, passing the vertical offset argument in address 0x0028.
b.inst(Lda(Immediate), 6);
b.inst(Sta(ZeroPage), 0x28);
b.inst(Jsr(Absolute), "render-ghost-piece");
// Return
b.inst(Rts, ());
      
      



, . mesen, , , , 0x0020 0x0028. 256 « » , . 8 X, Y , .





0x20 0x27 X, Y :





b.label("compute-hard-drop-distance"); // function label so it can be called by name later

const SHAPE_TABLE: Address = 0x8A9C;
const ZP_PIECE_COORD_X: u8 = 0x40;
const ZP_PIECE_COORD_Y: u8 = 0x41;
const ZP_PIECE_SHAPE: u8 = 0x42;
// Multiply the shape by 12 to make an offset into the shape table,
// storing the result in IndexRegisterX.
b.inst(Lda(ZeroPage), ZP_PIECE_SHAPE);  // read shape index into accumulator
b.inst(Clc, ());               // clear carry flag to prepare for arithmetic
b.inst(Rol(Accumulator), ());  // rotate left: index * 2
b.inst(Rol(Accumulator), ());  // rotate left: index * 4
b.inst(Sta(ZeroPage), 0x20);   // store index * 4 at 0x0020
b.inst(Rol(Accumulator), ());  // rotate left: index * 8
b.inst(Adc(ZeroPage), 0x20);   // add to 0x0020: index * 12
b.inst(Tax, ());               // transfer accumulator to IndexRegisterX
// Store absolute X,Y coords of each tile by reading relative coordinates from shape table
// and adding the piece offset, storing the result in zero page 0x20..=0x27.
for i in 0..4 { // this is a rust loop - the assembly generated inside will be generated 4 times
    b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read Y offset from shape table
    b.inst(Clc, ());                                  // clear carry flag to prepare for addition
    b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);          // add to Y coordinate of piece
    b.inst(Sta(ZeroPage), 0x21 + (i  2));            // store the result in zero page
    b.inst(Inx, ());                                  // increment IndexRegisterX to sprite index
    b.inst(Inx, ());                                  // increment IndexRegisterX to X offset
    b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read X offset from shape table
    b.inst(Clc, ());                                  // clear carry flag to prepare for addition
    b.inst(Adc(ZeroPage), ZP_PIECE_COORD_X);          // add to X coordinate of piece
    b.inst(Sta(ZeroPage), 0x20 + (i  2));            // store the result in zero page
    b.inst(Inx, ());                                  // increment IndexRegisterX to next tile
}
      
      



! Y 0x20 0x27, . mesen, , , 0x0400, 0xEF — « ». , - 0xEF.





, , for rust, . . rust 4 , .





const BOARD_TILES: Address = 0x0400;
const EMPTY_TILE: u8 = 0xEF;
const BOARD_HEIGHT: u8 = 20;

b.inst(Ldx(Immediate), 0);   // Load 0 into IndexRegisterX - this will be our loop counter

b.label("start-ghost-depth-loop"); // This is a label - a target for branch instructions

for i in 0..4 { // the assembly in this rust loop will be emitted 4 times

    // Increment the Y component of the coordinate
    b.inst(Inc(ZeroPage), 0x21 + (i * 2));

    // Break out of the loop if the tile is off the bottom of the board
    b.inst(Lda(ZeroPage), 0x21 + (i * 2));
    b.inst(Cmp(Immediate), BOARD_HEIGHT);
    b.inst(Bpl, LabelRelativeOffset("end-ghost-depth-loop"));

    // Multiply the Y component of the coordinate by 10 (the number of columns)
    b.inst(Asl(Accumulator), ());
    b.inst(Sta(ZeroPage), 0x28); // store Y * 2
    b.inst(Asl(Accumulator), ());
    b.inst(Asl(Accumulator), ()); // accumulator now contains Y * 8
    b.inst(Clc, ());
    b.inst(Adc(ZeroPage), 0x28); // accumulator now contains Y * 10

    // Now add the X component to get the row-major index of the cell
    b.inst(Adc(ZeroPage), 0x20 + (i * 2));

    // Load the tile at that coordinate
    b.inst(Tay, ());
    b.inst(Lda(AbsoluteYIndexed), BOARD_TILES);

    // Test whether the tile is empty, breaking out of the loop if it is not
    b.inst(Cmp(Immediate), EMPTY_TILE);
    b.inst(Bne, LabelRelativeOffset("end-ghost-depth-loop"));
}
// Increment counter and loop
b.inst(Inx, ());
b.inst(Jmp(Absolute), "start-ghost-depth-loop");

b.label("end-ghost-depth-loop");
      
      



, IndexRegisterX , , . :





// Return depth via accumulator
b.inst(Txa, ());  // transfer IndexRegisterX to accumulator
b.inst(Rts, ());  // return
      
      



DMA OAM:





b.label("oam-dma-buffer-update");

// Call original function first
b.inst(Jsr(Absolute), 0x8A0A);
// Compute distance from current piece to drop destination, placing result in accumulator
b.inst(Jsr(Absolute), "compute-hard-drop-distance");
// Check if the distance is 0, and skip rendering the ghost piece in this case
b.inst(Beq, LabelRelativeOffset("after-render-ghost-piece"));
// Render the ghost piece, passing the vertical offset argument in address 0x0028.
b.inst(Sta(ZeroPage), 0x28);
b.inst(Jsr(Absolute), "render-ghost-piece");
b.label("after-render-ghost-piece");
// Return
b.inst(Rts, ());
      
      



:





Hard Drop 

, , — , «» . «» , , hard drop.





, , , , — , «», , .





, , 0x4016, , , , .





, . , , . . 20 , , . , 20 . — , — . , , .





:





@@ -116912,9 +116912,175 @@
 0x89B8  Lda(ZeroPage) 0xB5
 0x89BA  And(Immediate) 0x03
 0x89BC  Bne(Relative) 0x15
-0x89BE  Lda(ZeroPage) 0xB6
-0x89C0  And(Immediate) 0x03
-0x89C2  Beq(Relative) 0x45
+0x89D3  Lda(Immediate) 0x00
+0x89D5  Sta(ZeroPage) 0x46
+0x89D7  Lda(ZeroPage) 0xB6
+0x89D9  And(Immediate) 0x01
+0x89DB  Beq(Relative) 0x0F
...
      
      



, :





0x89AE  Lda(ZeroPage) 0x40
0x89B0  Sta(ZeroPage) 0xAE
0x89B2  Lda(ZeroPage) 0xB6
0x89B4  And(Immediate) 0x04
0x89B6  Bne(Relative) 0x51 (relative: 0x51, absolute: 0x8A09)
0x89B8  Lda(ZeroPage) 0xB5
0x89BA  And(Immediate) 0x03
0x89BC  Bne(Relative) 0x15 (relative: 0x15, absolute: 0x89D3)
0x89BE  Lda(ZeroPage) 0xB6
0x89C0  And(Immediate) 0x03
0x89C2  Beq(Relative) 0x45 (relative: 0x45, absolute: 0x8A09)
...
      
      



0x00B5 0x00B6. mesen , 0xB5 , 0xB6 . , «» .





, DMA OAM. , , — :





b.label("handle-controls");

// Call the original function
b.inst(Jsr(Absolute), 0x89AE);

// Return
b.inst(Rts, ());
      
      



, «». :





b.label("handle-controls");

const CONTROLLER_STATE: u8 = 0xB6;
const CONTROLLER_BIT_UP: u8 = 0x08;

// Call the original function
b.inst(Jsr(Absolute), 0x89AE);

// Skip to the end if the UP bit of the controller state is not set
b.inst(Lda(ZeroPage), CONTROLLER_STATE);
b.inst(And(Immediate), CONTROLLER_BIT_UP);
b.inst(Beq, LabelRelativeOffset("controller-end"));

// Set the current piece's Y coordinate to 7
b.inst(Lda(Immediate), 7);
b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);

b.label("controller-end");

// Return
b.inst(Rts, ());
      
      



, «»:





7 , . compute-hard-drop-distance, , , Y, :





b.label("handle-controls");

const CONTROLLER_STATE: u8 = 0xB6;
const CONTROLLER_BIT_UP: u8 = 0x08;

// Call the original function
b.inst(Jsr(Absolute), 0x89AE);

// Skip to the end if the UP bit of the controller state is not set
b.inst(Lda(ZeroPage), CONTROLLER_STATE);
b.inst(And(Immediate), CONTROLLER_BIT_UP);
b.inst(Beq, LabelRelativeOffset("controller-end"));

// Compute distance from current piece to drop destination, placing result in accumulator
b.inst(Jsr(Absolute), "compute-hard-drop-distance");

// Add the current piece's Y coordinate
b.inst(Clc, ());
b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);

// Update the current piece's Y coordinate with the result
b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);

b.label("controller-end");

// Return
b.inst(Rts, ());
      
      



!





. , «», . hard drop’a .





mesen, , 0x0045, , ( ). , 13 . 13, , .





13 . - , 13 . !





/tmp/log.txt:





cat /tmp/log.txt | sort | uniq --count | sort --numeric-sort
      
      



. , 13 , :





13 0x8958  Lda(Immediate) 0x00
13 0x895A  Sta(ZeroPage) 0x45
      
      



, 0x0045!





:





0x8980  Lda(ZeroPage) 0x45    # load the timer value
0x8982  Cmp(ZeroPage) 0xAF    # compare with the value at 0x00AF
0x8984  Bpl(Relative) 0xD2 (relative: D2, absolute: 8958)  # branch if it was higher
0x8986  Jmp(Absolute) 0x8972
0x8972  Rts(Implied)
0x8958  Lda(Immediate) 0x00  # load 0 into the accumulator
0x895A  Sta(ZeroPage) 0x45   # store the accumulator (0) in the timer
      
      



0, 13 . — (0x8984), , 13 — , . , , , , 0xAF, , , .





0x00AF mesen, , , , 0x0045. , , 0x00AF , ! hard drop 0x00AF:





b.label("handle-controls");

const CONTROLLER_STATE: u8 = 0xB6;
const CONTROLLER_BIT_UP: u8 = 0x08;
const TIMER: u8 = 0x45;
const TIMER_MAX: u8 = 0xAF;

// Call the original function
b.inst(Jsr(Absolute), 0x89AE);

// Skip to the end if the UP bit of the controller state is not set
b.inst(Lda(ZeroPage), CONTROLLER_STATE);
b.inst(And(Immediate), CONTROLLER_BIT_UP);
b.inst(Beq, LabelRelativeOffset("controller-end"));

// Compute distance from current piece to drop destination, placing result in accumulator
b.inst(Jsr(Absolute), "compute-hard-drop-distance");

// Add the current piece's Y coordinate
b.inst(Clc, ());
b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);

// Update the current piece's Y coordinate with the result
b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);

// Set the timer to its maximum value
b.inst(Lda(ZeroPage), TIMER);
b.inst(Sta(ZeroPage), TIMER_MAX);

b.label("controller-end");

// Return
b.inst(Rts, ());
      
      



, , . , , . mesen, , 0x004E . 0. 0 hard drop’a .





b.label("handle-controls");

const CONTROLLER_STATE: u8 = 0xB6;
const CONTROLLER_BIT_UP: u8 = 0x08;
const TIMER: u8 = 0x45;
const TIMER_MAX: u8 = 0xAF;
const TIMER_FIRST_TICK: u8 = 0x4E;

// Call the original function
b.inst(Jsr(Absolute), 0x89AE);

// Skip to the end if the UP bit of the controller state is not set
b.inst(Lda(ZeroPage), CONTROLLER_STATE);
b.inst(And(Immediate), CONTROLLER_BIT_UP);
b.inst(Beq, LabelRelativeOffset("controller-end"));

// Compute distance from current piece to drop destination, placing result in accumulator
b.inst(Jsr(Absolute), "compute-hard-drop-distance");

// Add the current piece's Y coordinate
b.inst(Clc, ());
b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);

// Update the current piece's Y coordinate with the result
b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);

// Set the timer to its maximum value
b.inst(Lda(ZeroPage), TIMER);
b.inst(Sta(ZeroPage), TIMER_MAX);

// Clear the first tick timer
b.inst(Lda(Immediate), 0x00);
b.inst(Sta(ZeroPage), TIMER_FIRST_TICK);

b.label("controller-end");

// Return
b.inst(Rts, ());
      
      



, !

github. IPS, , , . , hard drop, , .



, , — « Unity», .





, :





  • Data Scientist





  • Data Analyst





  • Data Engineering









  • Fullstack- Python





  • Java-





  • QA- JAVA





  • Frontend-









  • C++





  • Unity





  • -





  • iOS-





  • Android-









  • Machine Learning





  • "Machine Learning Deep Learning"





  • " Data Science"





  • " Machine Learning Data Science" 





  • "Python -"





  • " "









  • DevOps








All Articles