MS-DOS Games Assembly C/C++

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.

Tetrisk in play
Tetrisk in play

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 game screen
The game screen

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
Finding the program's own segments: the code, and the picture 64k after it

The tables the game draws from are all in the code segment:

OffsetHolds
0242hThe score, eight bytes, one decimal digit in each, and the points waiting to be added to each digit
025Ch, 026BhThe left and right wells: the piece, where it is in the grid and on the screen, and how fast it falls
0970hThe two wells' grids, 16 bytes by 18 rows each
2000hThe pieces, 8 shapes in 4 rotations, each a 4x4 grid of bytes
2400hThe blocks the pieces are built from, an 8x8 picture for each of the 8 pieces
2800hThe score's digits, 0 to 9 and a blank one, 6x9 pixels each
2C00hA font of 128 characters, 3x5 dots each, stored as 15 bits in a word
3000h, 3100hTwo wave tables for the Lissajous figure, 256 entries each, from 0 to 31
3200hThe scrolling message, a column of LED dots in each byte
Where the game keeps its tables in the code segment

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
Switching to mode 13h, blacking out the palette from the cleared screen, and copying the picture in

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
The main loop: the two wells, then the four displays, then the random seed and the Esc key

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:

##............##
##............##
##............##
      ...
##............##
################
################
A well's grid: 12 cells of play between walls, above a solid floor (trimmed)

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
Moving down a pixel, and at each whole cell, checking for a landing (trimmed)

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
Laying the piece's 4x4 shape over the grid and setting carry if it overlaps anything

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
Scrolling the LED panel left by a dot and lighting the new column from the message

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
  ...
Counting the score up a step at a time, carrying into the digit on the left (trimmed)

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
Wiping the oscilloscope and plotting 256 dots from the two wave tables

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
Switching the character at the cursor every 16 frames

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:

The original game concept screen
The original game concept screen

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:

The concept version's sprite sheet
The concept version's sprite sheet

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