🡄 Previous

Next 🡆

Contents > General-purpose Computer

Example Program

The following program is designed to demonstrate the capabilities of the general-purpose computer. It is a bare-bones implementation of Tetris written in the assembly language detailed in the previous section.

The program begins by defining constants:

define FALL_SPEED 01

define MODE_ATTRACT     00
define MODE_PLAY        01
define MODE_CLEAR_LINES 02

define ACTION_DRAW 22 ; BNE
define ACTION_TEST 23 ; BEQ

define CELL_EMPTY 00
define CELL_SOLID FF ; Any nonzero cell is solid

define PLAYFIELD_WIDTH 0B

define SPAWN_X 05
define SPAWN_Y 02
define SPAWN_ROTATION 00

FALL_SPEED is the number of frames between gravity drops. While generating a frame, the program decrements the fall timer by one if it is nonzero. Otherwise, the program resets the fall timer to FALL_SPEED, and it executes a gravity drop. Since the frame rate is approximately 5.75 frames/sec, a FALL_SPEED of 01 translates to a gravity drop about every 350 milliseconds, a rate similar to level 5 in NES Tetris.

The program switches between the modes defined on lines 3–5. MODE_ATTRACT is a very basic attract mode that simply keeps the last top out displayed, or an empty playfield in the case of startup. It is unresponsive to controls except for the Start button, which causes the program to clear the playfield and to transition to MODE_PLAY. As its name suggests, MODE_PLAY is the mode where the player interacts with the falling pieces. When a piece locks into the pile, the program temporarily changes to MODE_CLEAR_LINES, during which it scans for and removes lines.

The program declares a single subroutine called drawOrTestTetromino that performs the actions defined on lines 7 and 8. ACTION_DRAW directs drawOrTestTetromino to paint four playfield cells corresponding to the blocks of a named tetromino, at a specified location and orientation, in a prescribed color. ACTION_TEST directs drawOrTestTetromino to verify that four playfield cells corresponding to the blocks of a named tetromino, at a specified location and orientation, are all empty. It enables the program to determine if a requested move is a valid move.

Since the computer does not provide a call stack, drawOrTestTetromino receives the tetromino type, coordinates, and orientation via global variables, and the action and the color via self-modifying code. Regarding the latter, the program overwrites instructions with the action constants (the opcodes for BNE and BEQ).

The remaining constants relate to the playfield. As revealed below, the playfield is a matrix with 23 rows and 11 columns (PLAYFIELD_WIDTH). Each byte contains the state of an individual cell. 00 (CELL_EMPTY) means the cell is empty. Any other value represents the color of a solid cell.

segment 0000
playfield:
;  0  1  2  3  4  5  6  7  8  9 10
  00 00 00 00 00 00 00 00 00 00 FF  ;  0
  00 00 00 00 00 00 00 00 00 00 FF  ;  1
  00 00 00 00 00 00 00 00 00 00 FF  ;  2
  00 00 00 00 00 00 00 00 00 00 FF  ;  3
  00 00 00 00 00 00 00 00 00 00 FF  ;  4
  00 00 00 00 00 00 00 00 00 00 FF  ;  5
  00 00 00 00 00 00 00 00 00 00 FF  ;  6
  00 00 00 00 00 00 00 00 00 00 FF  ;  7
  00 00 00 00 00 00 00 00 00 00 FF  ;  8
  00 00 00 00 00 00 00 00 00 00 FF  ;  9
  00 00 00 00 00 00 00 00 00 00 FF  ; 10
  00 00 00 00 00 00 00 00 00 00 FF  ; 11
  00 00 00 00 00 00 00 00 00 00 FF  ; 12
  00 00 00 00 00 00 00 00 00 00 FF  ; 13
  00 00 00 00 00 00 00 00 00 00 FF  ; 14
  00 00 00 00 00 00 00 00 00 00 FF  ; 15
  00 00 00 00 00 00 00 00 00 00 FF  ; 16
  00 00 00 00 00 00 00 00 00 00 FF  ; 17
  00 00 00 00 00 00 00 00 00 00 FF  ; 18
  00 00 00 00 00 00 00 00 00 00 FF  ; 19
  00 00 00 00 00 00 00 00 00 00 FF  ; 20
  00 00 00 00 00 00 00 00 00 00 FF  ; 21
  FF FF FF FF FF FF FF FF FF FF 00  ; 22

The green rectangular region is the visible playfield. It spans rows 2–21 and columns 0–9. tetrominoes spawn in the middle of its ceiling, centered at row 2 (SPAWN_Y) and column 5 (SPAWN_X).

Rows 0–1 constitute the vanish zone, the area above the ceiling that falling tetrominoes can rotate and even lock into. The initial rotation index, SPAWN_ROTATION, ensures none of the blocks of newly spawned tetrominoes start out in the vanish zone.

Row 22 and column 10 consist entirely of solid cells (CELL_SOLID), except for the element in the lower-right corner. Row 22 acts as the floor. Column 10 serves as both the left wall and the right wall since the matrix, like all data regions, is a one-dimensional array, and each of its row wraps into the next.

When a tetromino locks into the pile, the program scans four rows about the tetromino's center for lines. The scan ignores the visible playfield boundaries for performance and code simplicity. To prevent row 22 from inadvertently registering as a line, its rightmost element is an empty cell, and the program scans for lines of eleven solid cells, rather than ten. The eleven-solids check works on the visible rows due the solid elements of column 10.

To clear a line, the program shifts all rows above the line downward. To achieve that, the program overwrites each row with a copy of the preceding row. Since row 0 has no preceding row, the program fills it with ten empty cells followed by a solid cell.

At the start of a new game, the program resets the playfield to the state in the listing above.

The segment directive on line 19 is superfluous because no instructions or data appear before the playfield. But the directive makes it clear the playfield is located at the beginning of RAM. I.e., the label playfield resolves to address 0000.

At 23 × 11 = 253 bytes, the playfield fits in the first 256-byte page of memory with three bytes to spare. That enables the program to address a cell at (row,col) with code equivalent to:

M = 0;               // page zero

N = 11 * row + col;  // offset within page zero

The computer is connected to an external peripheral device, a window that serves as a proxy for the player’s physical monitor and keyboard. It presents a graphical representation of the visible playfield:

general-purpose-computer.png

The lower-three bits of each visible playfield byte determine the color of each cell per the following mapping.

ValuetetrominoColor
000(empty)Black
001TPurple
010JBlue
011ZRed
100OYellow
101SGreen
110LOrange
111ICyan

The computer and the window communicate via memory-mapped I/O. After the program updates the visible playfield in RAM, it writes 01 to 00FD, the drawFrame flag in the listing below.

segment 00FD
drawFrame:           00 ; 00 = generating frame; otherwise, finished generating frame
leftButton:          00 ; 00 = released; otherwise, pressed
rightButton:         00 ; 00 = released; otherwise, pressed

The window polls that location. When it reads a nonzero value, it copies the visible playfield from RAM to an array that backs the displayed image. Then, for each keyboard key that controls the game, the window writes the key's status—either 00 for released or 01 for pressed—to a key-specific address. Finally, it resets drawFrame to 00.

The window writes the statuses of the left and right arrow keys in leftButton and rightButton, respectively, the last-two bytes of memory page zero. The remaining keyboard key addresses appear later in the code because the tetrominoes table is aligned with the start of page one:

segment 0100
tetrominoes:
;  0  1  2  3
  FF 00 01 0B ; 0 0 td {  -1,   0,   1,  11 }
  F5 FF 00 0B ; 0 1 tl { -11,  -1,   0,  11 }
  FF 00 01 F5 ; 0 2 tu {  -1,   0,   1, -11 }
  F5 00 01 0B ; 0 3 tr { -11,   0,   1,  11 }

  FF 00 01 0C ; 1 0 jd {  -1,   0,   1,  12 }
  F5 00 0A 0B ; 1 1 jl { -11,   0,  10,  11 }
  F4 FF 00 01 ; 1 2 ju { -12,  -1,   0,   1 }
  F5 F6 00 0B ; 1 3 jr { -11, -10,   0,  11 }

  FF 00 0B 0C ; 2 0 zh {  -1,   0,  11,  12 }
  F6 00 01 0B ; 2 1 zv { -10,   0,   1,  11 }
  FF 00 0B 0C ; 2 2 zh {  -1,   0,  11,  12 }
  F6 00 01 0B ; 2 3 zv { -10,   0,   1,  11 }

  FF 00 0A 0B ; 3 0 o  {  -1,   0,  10,  11 }
  FF 00 0A 0B ; 3 1 o  {  -1,   0,  10,  11 }
  FF 00 0A 0B ; 3 2 o  {  -1,   0,  10,  11 }
  FF 00 0A 0B ; 3 3 o  {  -1,   0,  10,  11 }

  00 01 0A 0B ; 4 0 sh {   0,   1,  10,  11 }
  F5 00 01 0C ; 4 1 sv { -11,   0,   1,  12 }
  00 01 0A 0B ; 4 2 sh {   0,   1,  10,  11 }
  F5 00 01 0C ; 4 3 sv { -11,   0,   1,  12 }

  FF 00 01 0A ; 5 0 ld {  -1,   0,   1,  10 }
  F4 F5 00 0B ; 5 1 ll { -12, -11,   0,  11 }
  F6 FF 00 01 ; 5 2 lu { -10,  -1,   0,   1 }
  F5 00 0B 0C ; 5 3 lr { -11,   0,  11,  12 }

  FE FF 00 01 ; 6 0 ih {  -2,  -1,   0,   1 }
  EA F5 00 0B ; 6 1 iv { -22, -11,   0,  11 }
  FE FF 00 01 ; 6 2 ih {  -2,  -1,   0,   1 }
  EA F5 00 0B ; 6 3 iv { -22, -11,   0,  11 }

Each row of the tetrominoes table contains the coordinates of four blocks of a rotated tetromino encoded as signed offsets relative to the tetromino’s center. Since the playfield is a one-dimensional data region representing a two-dimensional matrix of width eleven, the signed offset of a block at (x,y) is 11 * y + x.

The program addresses each table element with code equivalent to:

M = 1;  // page one

N = 16 * tetrominoType + 4 * tetrominoRotation + blockIndex;  // offset within page one

The orientation of the falling tetromino, tetrominoRotation, is in the range [0,3] regardless of the tetromino type, tetrominoType, because the orientations of the Z-, S-, and I-tetrominoes appear twice, and the orientation of the O-tetromino appears four times:

Tetrominoes Table

Above, the leftmost column comprises the spawn orientations, the way the pieces initially appear. And the steps of clockwise rotation span left-to-right across each row. This is the same way pieces rotate in NES Tetris.

The window writes the remaining keyboard key statuses to bytes immediately following the tetrominoes table.

segment 0170
startButton:         00 ; 00 = released; otherwise, pressed
ccwRotateButton:     00 ; 00 = released; otherwise, pressed 
cwRotateButton:      00 ; 00 = released; otherwise, pressed
downButton:          00 ; 00 = released; otherwise, pressed

ccw and cw are abbreviations for “counterclockwise” and “clockwise”, respectively.

Here is the full key mapping:

Status VariableKey
startButtonEnter
leftButton
rightButton
downButton
ccwRotateButtonZ
cwRotateButtonX

Next, the program declares the game state variables:

tetrominoType:       00 ; 00--06 (T, J, Z, O, S, L, I)
tetrominoRotation:   00 ; 00--03
tetrominoX:          00 ; 00--09
tetrominoY:          00 ; 02--15

lastRotation:        00 ; 00--03
lastX:               00 ; 00--09

frameCounter:        00 ; 00--FF (wraps around)

seedHigh:            89 ; randomizer
seedLow:             88
nextBit:             00

i:                   00 ; loops index
origin:              00 ; playfield index corresponding to tetromino center
tetrominoesIndex:    00 ; tetrominoes table index corresponding to a tetromino block

fallTimer:           00 ; 00 = drop tetromino
mode:                00 ; 00 = attract, 01 = play, 02 = clear lines
minY:                00 ; minimal locked tetromino Y (00--16)

The variables on lines 95–98 describe the falling tetromino. Prior to servicing a move request, the program backs up the tetromino's orientation and x-coordinate in the variables on lines 100–101. If the program discovers the request is invalid, then it restores the original values from the backups.

When a tetromino spawns, the program updates the 16-bit value spread across seedHigh and seedLow by it passing through a randomization function. The frameCounter—a variable the program increments at the start of each frame—influences the randomization function. It provides a source of entropy based on the timing of the player's actions. The program stores the function's single-bit output in nextBit.

The code comments describe the rest of the variables, though minY deserves extra explanation. minY stores the row index of the highest tetromino on the pile. The program uses it to improved the performance of clearing lines. Specifically, the program does not shift the empty rows above the highest tetromino because empty space looks the same shifted or not.

That completes the data region. The rest of the program consists of instructions, beginning with its entry point:

main: ; ------------------------------------------------------------------------------------------------------

SMN drawFrame
SEA 01
STA                     ; render frame

SMN frameCounter
LDA
INC
STA                     ; ++frameCounter;

SMN mode
LDB
SEA MODE_PLAY
SUB                     ; if (mode == MODE_PLAY) {
BEQ playing             ;   goto playing;
                        ; }
SEA MODE_CLEAR_LINES
SUB                     ; if (mode == MODE_CLEAR_LINES) {
BEQ clearLines          ;   goto clearLines;
                        ; }
SMN startButton
LDA                     ; if (startButton == 0) {
BEQ main;               ;   goto main;
                        ; }

The program signals the window to display the current playfield state by writing 01 to drawFrame. It does not wait for the window to write 00 back to drawFrame because the window runs at blistering speed compared to the program.

The program increments the frameCounter by one.

In play mode and clear lines mode, the program jumps to respective handlers. Otherwise, the program is in attract mode, where it monitors the Start button's status. If the button is released, the program loops back to the entry point. Else it continues to:

SMN minY                ; // Start button pressed
SEA 16
STA                     ; minY = 22;

SEA F1                  ; A = 0xF1; // 22 * PLAYFIELD_WIDTH - 1, index of last element of row 21
SEB CELL_EMPTY
SMN playfield
clearLoop:
TAN
STB                     ; playfield[A] = CELL_EMPTY;
DEC                     ; if (--A != 0) {
BNE clearLoop           ;   goto clearLoop;
                        ; }
STB                     ; playfield[0] = CELL_EMPTY;

SMN 00F1                ; MN = 0x00F1; // 22 * PLAYFIELD_WIDTH - 1, address of last element of row 21 
SEB CELL_SOLID
edgeLoop:
STB                     ; *MN = CELL_SOLID;
TNA
SEB PLAYFIELD_WIDTH
SUB
TAN                     ; MN -= PLAYFIELD_WIDTH;
SEB FF
SUB                     ; if (*MN != -1) {
BNE edgeLoop            ;   goto edgeLoop;
                        ; }

The program resets minY to the floor index, it clears rows 0–21, and it resolidifies column 10. Then the program spawns the first tetromino of the game:

spawn:
SMN mode
SEA MODE_PLAY
STA                     ; mode = MODE_PLAY;
SMN tetrominoRotation
SEA SPAWN_ROTATION
STA                     ; tetrominoRotation = SPAWN_ROTATION;
SMN tetrominoX
SEA SPAWN_X
STA                     ; tetrominoX = SPAWN_X;
SMN tetrominoY
SEA SPAWN_Y
STA                     ; tetrominoY = SPAWN_Y;
SMN fallTimer
SEA FALL_SPEED
STA                     ; fallTimer = FALL_SPEED;

The program finally changes to play mode. It happens here, rather than earlier, because this spawn code also executes at the end of clear lines mode to resume play mode.

The program positions and orients the tetromino at the spawn point, and it resets the fallTimer to the maximum value to prevent the tetromino from gravity dropping immediately after spawning.

Next, the program randomly chooses the tetromino type:

randomlyChoose:
SEB 02
SMN seedLow
LDA
AND
SMN nextBit
STA
SMN seedHigh
LDA
AND
SMN nextBit
LDB
XOR
BEQ bit9Clear
SEA 80
bit9Clear:
STA                     ; nextBit = ((seedHigh & 0x02) ^ (seedLow & 0x02)) << 6;

SMN seedHigh
LDA
SEB 01
AND
BEQ bit8Clear
SEB 80
bit8Clear:
SMN seedLow
LDA
RS1
OR
STA                     ; seedLow = (seedHigh << 7) | (seedLow >>> 1);

SMN nextBit
LDB
SMN seedHigh
LDA
RS1
OR
STA                     ; seedHigh = nextBit | (seedHigh >>> 1);

SMN frameCounter
LDB
XOR
SEB 1F
AND
TAB
LS3
SUB
RS5
TAB                     ; B = ((seedHigh ^ frameCounter) & 0x1F) * 7 / 32;
SMN tetrominoType
LDA
SUB                     ; if (B == tetrominoType) {
BEQ randomlyChoose      ;   goto randomlyChoose;
                        ; }
STB                     ; tetrominoType = B;

The program employs the same Fibonacci linear-feedback shift register (LFSR) at the core of the pseudorandom number generator in NES Tetris. As shown below, the program generates the next random bit, nextBit, by XORing bit-1s of seedHigh and seedLow.

LFSR

seedHigh and seedLow collectively operate as a 16-bit register. The program logical right shifts it, discarding the lowest bit, and shifting-in nextBit.

The program initializes seedHigh and seedLow to 89 and 88, respectively, the same arbitrary quantities used in NES Tetris.

The LFSR generates a sequence of 32,767 unique values before cycling, one less-than half the numbers that fit in a 16-bit register. To prevent that deterministic sequence from producing a constant tetromino order, the program bases its selection on seedHigh XORed with the frameCounter. Since the program increments the frameCounter at the beginning of every frame, even during attract mode, the XOR makes the tetromino order dependent on when the player hits the Start button. The number of frames between spawns also affects the order. And the player’s inputs influence that number.

The program reduces the XORed value to the range [ 0, 6 ] because there are seven tetrominoes. Since the computer does not provide a modulo instruction, the program performs the reduction with the following formula, where v is the lower-five bits of the XORed value.

7 * v / 32

Since v[ 0, 31 ], the formula multiplies 7 by a fraction less-than 1.0, producing a value in the correct range when floored.

The program employs a transformed version of the formula:

= (8 * v - v) / 32

= ((v << 3) - v) / 32

= ((v << 3) - v) >>> 5

If the randomly chosen tetromino is the same as the last one, the program picks again. That simple strategy does not prevent alternating duplicate pieces. Nor does it inhibit droughts.

Next, the program verifies the tetromino spawned into empty space by invoking drawOrTestTetromino:

SMN drawOrTest
SEA ACTION_TEST
STA                     ; drawOrTest = ACTION_TEST;
JSR drawOrTestTetromino ; if (drawOrTestTetromino()) { // verify tetromino spawned into empty space
BEQ keepPosition        ;   goto playing;
                        ; }

SMN mode                ; // Bad tetromino position. It's game over.
SEA MODE_ATTRACT
STA                     ; mode = MODE_ATTRACT;

JMP endFall             ; goto endFall; // draws the tetromino that failed to spawn

Based on the action constant stored to drawOrTest, drawOrTestTetromino either paints or examine the four playfield cells corresponding to the blocks of the falling tetromino. But unlike the inputs that describe the falling tetromino—tetrominoType, tetrominoRotation, tetrominoX, and tetrominoYdrawOrTest is not a global variable. It is a label that resolves to the address an instruction inside of drawOrTestTetromino. When the program “assigns” drawOrTest, it modifies its own code.

ACTION_TEST causes drawOrTestTetromino to set the zero flag iff all four examined cells are empty. If they are, the program jumps to the play handler below. Otherwise, a top out occurred. In that case, the program switches to attract mode, and it jumps to endFall. There, the program draws the tetromino at the invalid location, overlaid on solid blocks, to provide a visual indication of spawn failure. Then the program loops back to the beginning, where it waits for player to press the Start button once again.

playing:                ; // Play handler
SMN drawOrTest
SEA ACTION_DRAW
STA                     ; drawOrTest = ACTION_DRAW;
SMN drawCell+1
SEA CELL_EMPTY
STA                     ; *(drawCell+1) = CELL_EMPTY;
JSR drawOrTestTetromino ; drawOrTestTetromino(); // erase tetromino

SMN tetrominoRotation
LDA
SMN lastRotation
STA                     ; lastRotation = tetrominoRotation;
SMN tetrominoX
LDA
SMN lastX
STA                     ; lastX = tetrominoX;

SMN leftButton
LDA                     ; if (leftButton == 0) {
BEQ testRightButton     ;   goto testRightButton;
                        ; }

SMN tetrominoX          ; // Left button pressed
LDA
DEC
STA                     ; --tetrominoX;
JMP testCcwRotateButton ; goto testCcwRotateButton;

testRightButton:
SMN rightButton
LDA                     ; if (rightButton == 0) {
BEQ testCcwRotateButton ;   goto testCcwRotateButton;
                        ; }

SMN tetrominoX          ; // Right button pressed
LDA
INC
STA                     ; ++tetrominoX;

testCcwRotateButton:
SEB 03                  ; B = 3;
SMN ccwRotateButton
LDA                     ; if (ccwRotateButton == 0) {
BEQ testCwRotateButton  ;   goto testCwRotateButton;
                        ; }

SMN tetrominoRotation   ; // CCW button pressed
LDA
DEC
AND
STA                     ; tetrominoRotation = (tetrominoRotation - 1) & 3;

JMP validatePosition    ; goto validatePosition;

testCwRotateButton:
SMN cwRotateButton
LDA                     ; if (cwRotateButton == 0) {
BEQ validatePosition    ;   goto validatePosition;
                        ; }

SMN tetrominoRotation   ; // CW button pressed
LDA
INC
AND
STA                     ; tetrominoRotation = (tetrominoRotation + 1) & 3;

validatePosition:
SMN drawOrTest
SEA ACTION_TEST
STA                     ; drawOrTest = ACTION_TEST;
JSR drawOrTestTetromino ; if (drawOrTestTetromino()) { // verify tetromino shifted/rotated into empty space
BEQ keepPosition        ;   goto keepPosition;
                        ; }

SMN lastRotation        ; // Bad tetromino position. Undo shift or rotation.
LDA
SMN tetrominoRotation
STA                     ; tetrominoRotation = lastRotation;
SMN lastX
LDA
SMN tetrominoX
STA                     ; tetrominoX = lastX;

The play handler calls drawOrTestTetromino to paint the falling tetromino with empty cells, erasing it from the playfield. In another example of self-modifying code, the handler stores the paint value, CELL_EMPTY, to drawCell+1, the address of an instruction operand in drawOrTestTetromino.

The handler backs up the orientation and the x-coordinate of the falling tetromino.

If the Left button is pressed, the handler decrements the x-coordinate by one. Otherwise, if the Right button is pressed, the handler increments the x-coordinate by one.

If the Counterclockwise Rotation button is pressed, the handler decrements the orientation by one, such that zero wraps to three. Otherwise, if the Clockwise Rotation button is pressed, the handler increments the orientation by one, such that three wraps to zero.

The handler verifies the manipulated tetromino is fully within empty space by calling drawOrTestTetromino. If it is not, the handler restores the original orientation and x-coordinate from the backups.

Next, the handler deals with drops:

keepPosition:
SMN downButton
LDA
SMN fallTimer           ; if (downButton == 0) {
BEQ updateFallTimer     ;   goto updateFallTimer;
                        ; }

SEA 00                  ; // Down button pressed
STA                     ; fallTimer = 0;                  

updateFallTimer:
LDA                     ; if (fallTimer != 0) {
BNE decFallTimer        ;   goto decFallTimer;
SEA FALL_SPEED          ; } 
STA                     ; fallTimer = FALL_SPEED;

SMN tetrominoY          ; // Drop tetromino
LDA
INC
STA                     ; ++tetrominoY;
JSR drawOrTestTetromino ; if (drawOrTestTetromino()) { // verify tetromino dropped into empty space
BEQ endFall             ;   goto endFall;
                        ; }

SMN tetrominoY          ; // Bad tetromino position. Undo drop and lock tetromino in place.
LDA
DEC
STA                     ; --tetrominoY;

TAB
SMN minY
LDA
SUB
BMI keepMinY
STB                     ; minY = min(minY, tetrominoY);

keepMinY:
SMN mode
SEA MODE_CLEAR_LINES
STA                     ; mode = MODE_CLEAR_LINES;

decFallTimer:
SMN fallTimer
LDA
DEC
STA                     ; --fallTimer;

endFall:
SMN drawOrTest
SEA ACTION_DRAW
STA                     ; drawOrTest = ACTION_DRAW;
SMN tetrominoType
LDA
INC
SMN drawCell+1
STA                     ; *(drawCell+1) = tetrominoType + 1;
JSR drawOrTestTetromino ; drawOrTestTetromino(); // draw tetromino

JMP main                ; goto main;

If the Down button is pressed, the handler assigns fallTimer zero.

If fallTimer is zero, the handler resets fallTimer to FALL_SPEED, and it increments the tetromino's y-coordinate by one. If the dropped tetromino is not fully within empty space per a check by drawOrTestTetromino, the handler decrements the tetromino's y-coordinate by one, restoring its position to where it now locks into the pile. Afterwards, the handler updates minY based off the new pile height, and it switches to clear lines mode.

Then the handler flows into the code that normally runs when fallTimer is nonzero. It decrements fallTimer by one, and it calls drawOrTestTetromino to draw the tetromino, which may now be at a different location and orientation from where it was erased at the start of the play handler. The paint value is tetrominoType + 1 because zero represents an empty cell.

Finally, the handler jumps back to the beginning of the program. If the tetromino locked into the pile, the program will jump from there to the clear lines handler below. It scans for and vanishes lines, without displaying a clearing animation.

clearLines:             ; // Clear lines handler
SMN i
SEA 03
STA                     ; i = 3; // loop 4 times, from i = 3 down to 0.

SMN origin
LDA
SMN tetrominoX
LDB
SUB
SEB PLAYFIELD_WIDTH
ADD
SMN origin
STA                     ; origin = PLAYFIELD_WIDTH * (tetrominoY + 1); // row below tetromino center

clearLinesLoop:
SMN minY
LDA
LS3
LDB
ADD
ADD
ADD
SEB 16
SUB
BNE notLine0
SEA PLAYFIELD_WIDTH
notLine0:
DEC
SMN minN+1
STA                     ; *(minN+1) = PLAYFIELD_WIDTH * max(1, minY - 2) - 1; // minimum index to copy from

SMN origin
LDA
SMN playfield
TAN                     ; MN = playfield + origin;

SEB 0A                  ; B = PLAYFIELD_WIDTH - 1;
scanLine:
LDA                     ; if (*MN == CELL_EMPTY) {
BEQ continueClearLines  ;   goto continueClearLines;
                        ; }
TBA
DEC                     ; if (--B < 0) { 
BMI copyLines           ;   goto copyLines; // Found a line         
TAB                     ; }

TNA
INC
TAN                     ; ++N;

JMP scanLine            ; goto scanLine;

TBA
DEC
TAB                     ; if (--B >= 0) {
BPL scanLine            ;   goto scanLine;
                        ; }

As visualized above, the tetrominoes table centers pieces in 5×5 matrices, none of which contain solid cells in its bottom row. This means, the handler only needs to check rows in the range [ tetrominoY−2, tetrominoY+1 ] for lines. To do so, it loops backwards over that range, using i as the loop index.

origin is a pointer to the row to be checked. The handler initializes it to the index of row tetrominoY+1's first element. At the end of each iteration, if handler did not find a line, it moves origin to the next row by decrementing it by PLAYFIELD_WIDTH. However, if the handler found a line, it shifts all the rows above the line downward. In that case, the handler does not decrement origin because the row that shifted into the cleared line is the next row to be checked.

As previously mentioned, the handler does not shift the empty rows above the pile because empty space looks the same shifted or not. The first row above the pile guaranteed to be completed empty is either row minY−3 or 0, whichever is larger. In yet another example of self-modifying code, the handler assigns the byte at minN+1 to the index of the last element of that row.

In the inner loop on lines 434–459, the handler examines a row, including column 10. If the handler detects an empty cell, it breaks out of the loop, and it decrements origin as described above. If it does not detect any empty cells, then the handler discovered a line, and it runs the following code to clear the line.

copyLines:              ; // Clear line by copying down the lines above it
SEB PLAYFIELD_WIDTH
TNA
SUB
TAN                     ; N -= PLAYFIELD_WIDTH;
LDA
TAM                     ; M = *MN;
TNA
ADD
TAN                     ; N += PLAYFIELD_WIDTH;
TMA                     ; A = M;
SEB 00
TBM                     ; M = 0;
STA                     ; *MN = A;

TNA
DEC
TAN
minN:
SEB 00                  ; *** self-modifying code [minN+1] ***
SUB                     ; if (--N != *(minN+1)) {
BNE copyLines           ;   goto copyLines;
                        ; }
SEA 09
TAN                     ; N = 9;
SEB CELL_EMPTY
clearTopLine:
STB                     ; *MN = CELL_EMPTY;

TNA
DEC
TAN                     ; if (--N >= 0) {
BPL clearTopLine        ;   goto clearTopLine;
                        ; }
SMN minY
LDA
INC
STA                     ; ++minY;

JMP continueClear       ; goto continueClear;

continueClearLines:
SMN origin
LDA
SEB PLAYFIELD_WIDTH
SUB
STA                     ; origin -= PLAYFIELD_WIDTH;

continueClear:
SMN i
LDA
DEC
STA                     ; if (--i >= 0) {
BPL clearLinesLoop      ;   goto clearLinesLoop;
                        ; } else {
JMP spawn               ;   goto spawn;
                        ; }

At the start of this snippet, N contains the index of the last element of the line to be cleared.

In the inner loop on lines 461–483, the handler copies the nonempty rows above the line down by one row. The loop is equivalent to the following pseudocode.

M = 0;
do {
  *MN = *(MN - PLAYFIELD_WIDTH);
} while (--N != *(minN+1));

In the inner loop on lines 484–494, the handler fills row 0 with empty cells, except for column 10. The loop is necessary for the rare case that a tetromino locks into the vanish zone.

The handler increments minY by one because clearing a line reduces the height of the pile by that amount.

At the end of the outer loop, the handler decrements i by one, and if it did not find a line, it decrements origin by PLAYFIELD_WIDTH.

The only remaining code is the definition of the drawOrTestTetromino subroutine:

drawOrTestTetromino: ; ---------------------------------------------------------------------------------------
; drawOrTest        - 22 = draw, 23 = test
; drawCell+1        - cell to draw
;
; tetrominoType     - type
; tetrominoRotation - rotation
; tetrominoX        - x
; tetrominoY        - y
;
; z: 0 = some solid, 1 = all empty

SMN i
SEA 03
STA                     ; i = 3; // Loop 4 times, once for each tetromino block

SMN tetrominoY
LDA
LS3
LDB
ADD
ADD
ADD
SMN tetrominoX
LDB
ADD
SMN origin
STA                     ; origin = PLAYFIELD_WIDTH * tetrominoY + tetrominoX;

SMN tetrominoType
LDA
LS4
TAB
SMN tetrominoRotation
LDA
LS2
ADD                     ; A = 16 * tetrominoType + 4 * tetrominoRotation;
SMN tetrominoesIndex
STA                     ; tetrominoesIndex = A;

SMN origin
LDB                     ; B = origin;

drawLoop:

SMN tetrominoes
TAN
LDA
ADD
SMN playfield
TAN

drawOrTest:
BNE drawCell            ; *** self-modifying code [BNE = draw, BEQ = test] ***

LDA                     ; if (playfield[tetrominoes[tetrominoesIndex] + origin] != 0) {
BNE endDrawLoop         ;   goto endDrawLoop;
                        ; } else {
JMP incDrawLoop         ;   goto incDrawLoop;
                        ; }

drawCell:
SEA 00                  ; *** self-modifying code [ 00 = empty; otherwise solid ] ***
STA                     ; playfield[tetrominoes[tetrominoesIndex] + origin] = *(drawCell+1);

incDrawLoop:
SMN i
LDA                     ; if (i == 0) {
BEQ endDrawLoop         ;   goto endDrawLoop;
                        ; }
DEC
STA                     ; --i;

SMN tetrominoesIndex
LDA
INC
STA                     ; A = ++tetrominoesIndex;

JMP drawLoop            ; goto drawLoop;

endDrawLoop:

RTS                     ; return; // -------------------------------------------------------------------------

The program prepares three variables on lines 530–559. It assigns origin to the playfield index corresponding to the center of the falling tetromino. It assigns tetrominoesIndex to the index of the first element of the tetrominoes table row associated with the type and orientation of the falling tetromino. And it assigns i to three to make the loop on lines 561–598 iterate over the four tetromino blocks.

When the program stores ACTION_DRAW or ACTION_TEST to drawOrTest, it changes the instruction on line 571 to BNE or BEQ, respectively. When the program arrives at that line, the zero flag is reset due to the ADD instruction on line 566. That ADD cannot set the zero flag because the player cannot to move the falling tetromino into the upper-right corner of the vanish zone. Consequentially, ACTION_TEST directs the program to lines 573–577, and ACTION_DRAW directs the program to lines 580–581.

In both pathways, the program accesses the playfield byte at tetrominoes[tetrominoesIndex] + origin, the center of the falling tetromino offset by the relative location of one of its blocks. In the ACTION_TEST pathway, the program reads from that location. If a read byte is a solid cell (nonzero), the program returns from the subroutine with the zero flag reset. Otherwise, if all read bytes are empty cells (all zeros), the program returns from the subroutine with the zero flag set, thanks to line 585. In the ACTION_DRAW pathway, the program writes the value at drawCell+1 to that location.

The subroutine employs self-modifying code twice because the alternative—moving values in and out of memory—imposes complexity and a performance cost.

At the end of the loop, the program decrements i by one, and it increments tetrominoesIndex by one.

🡄 Previous

Next 🡆