Tetrisk is a falling-blocks game with a twist: there are two wells, and one set of controls moves a piece in each of them at the same time.
All of it, the code, the tables and the screen, is in one program, TETRISK.EXE. The code on this page is disassembled from it: the instructions are the game's, and the names are added to make them readable.
The game
Two pieces fall at once, one in each well, and the keys move both of them. Left and right move both pieces, and up rotates both, so a move that suits one well has to suit the other as well. Down drops both. Hold Left Shift with down to drop only the left piece, or Right Shift to drop only the right one.

The wells don't run at the same speed. The left piece falls a pixel every 11 frames and the right one a pixel every 14, so they drift out of step. There are eight pieces, the usual seven and a single square, and each new piece is picked at random, along with which way round it starts.
A full row scores 5. Clearing more than one row at a time is worth much more, because each extra row adds its 5 one decimal place further left: two rows score 55, three 555, and four 5,555.
The screen
The screen was painted in Deluxe Paint: the two purple wells with a grey splitter between them, set in a circuit board, with an LED message panel at the top left, an LED score at the top right, an oscilloscope screen in the middle on the right, and a small green screen at the bottom left:

The picture and its palette are stored in the program itself, 64,000 bytes of pixels at offset 10000h and 768 bytes of palette after them, so the game has no data files to load.
Inside the program
The program starts at offset 100h, as a .COM file would, and the first thing it does is work out where its parts are. The code segment holds the code and the tables, and the picture is 64k further on:
mov ax, cs
mov cs:[code_seg], ax
add ax, 0C00h
mov cs:[spare_seg], ax
add ax, 0400h
mov cs:[picture_seg], ax
The tables the game draws from are all in the code segment:
| Offset | Holds |
|---|---|
| 0242h | The score, eight bytes, one decimal digit in each, and the points waiting to be added to each digit |
| 025Ch, 026Bh | The left and right wells: the piece, where it is in the grid and on the screen, and how fast it falls |
| 0970h | The two wells' grids, 16 bytes by 18 rows each |
| 2000h | The pieces, 8 shapes in 4 rotations, each a 4x4 grid of bytes |
| 2400h | The blocks the pieces are built from, an 8x8 picture for each of the 8 pieces |
| 2800h | The score's digits, 0 to 9 and a blank one, 6x9 pixels each |
| 2C00h | A font of 128 characters, 3x5 dots each, stored as 15 bits in a word |
| 3000h, 3100h | Two wave tables for the Lissajous figure, 256 entries each, from 0 to 31 |
| 3200h | The scrolling message, a column of LED dots in each byte |
Starting up
The program prints its name, LKS Tetrisk v1.0, and fades the text screen down to black. Then it switches to mode 13h and sets every colour to black. It does that without a table of zeros: setting a mode clears the screen, so it points the BIOS at the first 768 bytes of the screen itself. Then the picture is copied to the screen while it's still invisible:
mov ax, 0013h
int 10h
mov es, cs:[screen_seg]
mov ax, 1012h
mov bx, 0000h
mov cx, 0100h
mov dx, 0000h
int 10h
mov ds, cs:[picture_seg]
mov es, cs:[screen_seg]
xor si, si
xor di, di
mov cx, 8000h
rep movsw
Then the palette fades up from black to the picture's colours, and the game starts. When Esc is pressed, it goes back to text mode.
The main loop
Each frame does the same things in the same order. It empties the BIOS keyboard buffer, since the game reads the keys itself, and waits for the vertical retrace. Then it handles each well in turn: it takes the piece off the screen, moves it, and draws it again, and if it has landed, starts a new one. Then it draws the displays:
game_loop:
call flush_keys
call wait_retrace
mov bp, offset left_well
mov byte ptr cs:[bp+0Bh], 0
call erase_piece
call move_piece
call draw_piece
cmp byte ptr cs:[bp+0Bh], 0
jz game_right
call new_piece
jc game_end
game_right:
mov bp, offset right_well
mov byte ptr cs:[bp+0Bh], 0
call erase_piece
call move_piece
jnc game_nokey
cli
xor al, al
out 60h, al
jmp $+3
jmp $+3
sti
game_nokey:
call draw_piece
cmp byte ptr cs:[bp+0Bh], 0
jz game_displays
call new_piece
jc game_end
game_displays:
call draw_score
call scroll_message
call draw_lissajous
call flash_cursor
inc byte ptr cs:[frame]
and byte ptr cs:[frame], 7
in al, 40h
xor cs:[seed], al
in al, 60h
mov cs:[last_key], al
cmp al, 81h
jz game_end
jmp game_loop
game_end:
ret
Both wells read the same key from the keyboard port, which is how one key moves both pieces. After the right well has used it, the game writes 0 to the port so that the same press isn't read again next frame, with two jumps to the very next instruction after it to give the hardware a moment. The random seed is stirred every frame with the low byte of the timer chip's counter.
Rendering
Everything is drawn straight onto the screen. There's no second buffer: anything that moves is taken off by copying the picture back over it, from the copy of the picture in memory, and drawn again in its new place.
The wells
The wells, and the grey splitter between them, are part of the picture. What's in them is kept in a grid for each well, 16 bytes wide and 18 rows deep. The play area is 12 cells wide and 16 rows deep, with two cells of wall down each side and a two-row floor, all filled in so that a piece can't move into them:
##............##
##............##
##............##
...
##............##
################
################
Each cell is 8x8 pixels on the screen. When a piece lands, the number of its kind is written into the cells it covers. When a row of 12 is full, it's cleared from the screen by copying that strip of the picture back, and the rows above move down to fill the gap, a step every eight frames.
The pieces
A piece is a 4x4 grid of bytes, one table entry for each of its 4 rotations, so the piece's number times 16 finds its shape. Its number is a byte with the shape in the top bits and the rotation in the bottom two, so turning it is adding one to the bottom two bits.
A falling piece moves down a pixel at a time, but it only checks whether it can go further when it reaches a whole cell, every eight pixels. If it can't, it goes back up and stays where it is:
dec byte ptr cs:[bp+2]
and byte ptr cs:[bp+2], 7
add word ptr cs:[bp+5], 0140h
cmp byte ptr cs:[bp+2], 7
jnz fall_keys
add word ptr cs:[bp+3], 10h
call check_piece
jnc fall_keys
mov byte ptr cs:[bp+0Bh], 1
sub word ptr cs:[bp+3], 10h
sub word ptr cs:[bp+5], 0140h
mov byte ptr cs:[bp+2], 0
call place_piece
ret
The pixel position moves on 140h, one row of the screen, and the grid position moves on 10h, one row of the grid. Checking is the same for every move: the piece's 16 bytes are laid over the grid, and if any filled byte of the piece meets a filled cell, it's blocked:
check_piece:
mov di, cs:[bp+3]
xor ah, ah
mov al, cs:[bp+0]
mov si, ax
rol si, 1
rol si, 1
rol si, 1
rol si, 1
add si, 2000h
mov dh, 4
check_row:
mov dl, 4
check_cell:
test byte ptr cs:[si], 0FFh
jz check_next
test byte ptr cs:[di], 0FFh
jz check_next
stc
ret
check_next:
inc si
inc di
dec dl
jnz check_cell
add di, 12
dec dh
jnz check_row
clc
ret
Moving left or right, or rotating, makes the change, checks, and puts it back if it's blocked. Drawing the piece copies its kind's 8x8 block onto the screen for each filled byte of the shape, and taking it off copies the same squares back from the picture.
The LED message
The panel at the top left is an LED display 41 dots wide and 7 high, with the dots on every other pixel across and down. Every other frame, the whole display is copied one dot to the left, and a new column of dots goes in at the right. Each byte of the message is one column, a bit for each dot, and a byte with the top bit set means start again:
scroll_message:
xor byte ptr cs:[message_frame], 1
jnz message_scroll
ret
message_scroll:
mov es, cs:[screen_seg]
push es
pop ds
mov di, 08CBh
mov si, 08CDh
mov dh, 7
message_row:
mov cx, 41
rep movsw
add di, 022Eh
add si, 022Eh
dec dh
jnz message_row
mov bx, cs:[message_pos]
mov al, cs:[bx]
test al, 80h
jnz message_restart
mov di, 091Dh
mov dh, 7
message_dot:
mov byte ptr es:[di], 3Dh
rcr al, 1
jnc message_unlit
mov byte ptr es:[di], 30h
message_unlit:
add di, 0280h
dec dh
jnz message_dot
inc bx
mov cs:[message_pos], bx
ret
message_restart:
mov bx, 3200h
mov cs:[message_pos], bx
ret
Copying a word moves two pixels, one dot and the gap after it, and the rows are 280h, two screen rows, apart. Colour 30h is a lit LED, bright green, and 3Dh an unlit one, dark green. The message reads LKS TETRISK, then © LKS 1994, over and over.
The score
The score at the top right is eight bytes, one decimal digit in each, and each digit has a count of points waiting to be added to it. Every frame, each digit with points waiting goes up by one, so the score counts up on the panel rather than jumping. A digit going past 9 goes back to 0 and adds one to the waiting count of the digit on its left:
draw_score:
mov bx, offset score+7
score_digit:
cmp byte ptr cs:[bx+9], 0
jz score_next
inc byte ptr cs:[bx]
dec byte ptr cs:[bx+9]
cmp byte ptr cs:[bx], 10
jnz score_next
mov byte ptr cs:[bx], 0
inc byte ptr cs:[bx+8]
score_next:
dec bx
cmp bx, offset score-1
jnz score_digit
...
That's also why clearing more rows at once scores so much more: each row cleared in one go puts its 5 into the waiting count one digit further left.
Then the eight digits are drawn, 6x9 pixels each, from the digit pictures. Leading zeros are drawn with the blank digit, an unlit 8, so the panel looks like an LED display with only the digits it needs lit.
The Lissajous figure
The oscilloscope screen in the middle on the right shows a Lissajous figure: 256 dots, each one's height from one wave table and its position across from the other. Each frame, the 32x32 screen is wiped by copying the picture back over it, and the starting points in the two tables move on by 1 and by 2, so the figure keeps changing shape:
draw_lissajous:
mov es, cs:[screen_seg]
mov ds, cs:[picture_seg]
mov si, 588Fh
mov di, si
xor ch, ch
mov dh, 32
lissajous_wipe:
mov cl, 16
rep movsw
add si, 0120h
mov di, si
dec dh
jnz lissajous_wipe
add byte ptr cs:[wave_x], 1
add byte ptr cs:[wave_y], 2
mov dl, cs:[wave_x]
mov dh, cs:[wave_y]
mov cx, 256
lissajous_dot:
mov si, 588Fh
xor bh, bh
mov bl, dh
add bx, 3000h
mov al, cs:[bx]
mov ah, 0A0h
mul ah
shl ax, 1
add si, ax
xor bh, bh
mov bl, dl
add bx, 3100h
xor ah, ah
mov al, cs:[bx]
add si, ax
mov al, 6Fh
sub al, [si]
mov es:[si], al
add dl, 1
add dh, 1
loop lissajous_dot
ret
The row is the table value times 160, shifted left once, which is times 320 without a multiply by 320. Each dot's colour is 6Fh minus the colour of the picture underneath it, rather than one fixed colour, so the dots stand out from the screen's colours wherever they land.
The cursor
The small screen at the bottom left has a flashing cursor. Every 16 frames, the character at the cursor is switched between character 0 and character 7Fh of the 3x5 font:
flash_cursor:
inc byte ptr cs:[cursor_frame]
and byte ptr cs:[cursor_frame], 0Fh
jz cursor_flash
ret
cursor_flash:
xor byte ptr cs:[cursor_char], 7Fh
mov al, cs:[cursor_char]
mov dl, cs:[cursor_x]
mov dh, cs:[cursor_y]
call draw_char
ret
A character is a word of the font, and drawing it shifts the word right a bit at a time with RCR, lighting a dot in colour 30h for each 1 and leaving it unlit, 3Dh, for each 0, three dots to a row for five rows. Character 7Fh is a chequered block and character 0 a small arrow, so the cursor flickers between the two.
Not finished
- There's no game over. A new piece always gets a place, so the game only ends when Esc is pressed.
- The screen at the bottom left only ever shows its cursor.
The concept version
The source code below, TETRISK1.A, is an earlier concept for the game. Its screen had the wells in a different layout, with the TETRISK logo along the top:

It loaded its screen, and a sheet of sprites, from a data file built by a small C program, and drew a mouse pointer, a conveyor between the wells and a clock, but no pieces yet:

Its two pictures are stored upside down in their BMP files. A BMP keeps its rows bottom first, and I've never known why Microsoft chose to store bitmaps that way. So to make them easier to read, I stored these two upside down myself, and the rows come out of the file in the normal top-to-bottom order. They're shown the right way up here; the original BMP files, and their previews on the source pages, are left as they are.
Source code
Game
- TETRISK.EXE MS-DOS program · 131,584 bytes · the game, with its screen and tables built in
- tetrisk.jsdos js-dos bundle · the game, ready to run in DOSBox
Concept version
- TETRISK1.A Assembly · 1370 lines · the concept: the screen, the pointer, the conveyor and the clock
- TETMAK10.C C · 77 lines · packs the concept's palette and pictures into a data file
- TETRISKW.BMP Bitmap · 320x200 · the concept screen, stored upside down
- TETRISKC.BMP Bitmap · 320x200 · the concept's sprite sheet, stored upside down
See also
- Tetrisk - The game, playable in the browser at tetrisk.org