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.
NES. , -, . , , , . , .
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 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», .