Nintendo Tetris es una de mis versiones favoritas de Tetris. Mi única queja es que carece de la función "Hard Drop", que instantáneamente deja caer la forma actual y la bloquea en su lugar. Vamos a agregarlo
, , — «» « » — , , .
. — .
, .
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», .