ファミコンでテトリスをリバースエンジニアリングしてハードドロップを追加

ニンテンドーテトリスは、私のお気に入りのテトリスバージョンの1つです。私の唯一の不満は、現在の形状を即座にドロップして所定の位置にロックする「ハードドロップ」機能がないことです。追加しましょう





, , — «» « » — , , .





. — .





, .





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