# Note: Additional comments added by NM, marked by "NM". # Note that the sample Makefile in the comments shows that the entry # point to the program is _tetris (see the ld line). # Note that the author has used a lot of macros here. # READ "main" FIRST, BY GOING TO THE LABEL _tetris NEAR THE END OF THE # FILE. THAT IS THE ENTRY POINT OF THE PROGRAM, I.E. THE PLACE AT WHICH # EXECUTION STARTS. #Tetris in x86 assembly #Copyright 2000,2001 dburr@ug.cs.usyd.edu.au, under the terms of the GPL #For the latest version, see http://bordello.2y.net:8000/programs/tetris # #Notes: #Uses VT100 terminal codes to position the cursor and to draw coloured text. #I also assume that this requires a > 2.0 Linux kernel which supports #sys_newselect (also uses sys_write, sys_read, sys_nanosleep, sys_exit, #sys_times and sys_ioctl). I have only tested it with 2.0.x, 2.2.x and 2.4.x #kernels. I'm pretty sure that this will work with 386+ processors. # # [NM] note that in the sample Makefile below, the entry point is specified # via the -e option to ld; note also the definition of various # quantities via -defsym; this is similar to a #define, but supplied # externally to calling "as", on the command line; GCC has the same # thing with -D #Example Makefile: #--start-- #NAME = tetris #ENTRY = _tetris # #SYMS = -defsym instructionsX=12 #SYMS += -defsym instructionsY=19 #SYMS += -defsym width=10 #SYMS += -defsym xoffset=3 #SYMS += -defsym height=16 #SYMS += -defsym yoffset=2 #SYMS += -defsym wait=50 #SYMS += -defsym scoredrop=2 #SYMS += -defsym scorelockin=3 #SYMS += -defsym scoreline=100 #SYMS += -defsym scoretetris=1000 #SYMS += -defsym speedup=10 #SYMS += -defsym colour=1 # #$(NAME): $(NAME).o # ld -s -o $@ -m elf_i386 $^ -e $(ENTRY) # #$(NAME).o: $(NAME).s # as -o $@ $^ $(SYMS) # #clean: # rm -f $(NAME).o $(NAME) #--end-- #Explanation of the symbols which can be changed to suit personal taste: #instructionsX, instructionsY: The offset of the instructions from the #top, left hand corner of the screen. #width, height: The dimensions of the playing area #xoffset, yoffset: The offset of the playing area from the top, left hand #corner of the screen. #wait: Controls the speed of the game. Lower number equals faster play. #scoredrop: Number of points scored for 'dropping' a piece (with spacebar). #scorelockin: Number of points scored for 'locking' a piece in. #scoreline: Number of points scored for eliminating a line (for 1-3 lines) #scoretetris: Number of points scored for a 'tetris' (ie eliminating 4 lines #at once). #speedup: The game will get twice as fast for every n lines. #colour: Set this to 0 if you want to compile without colours (eg to #play over Nifty Telnet on MacOS or the built-in telnet client on Windows) .macro sys_newselect xor %eax, %eax #smaller than writing to %eax directly mov $142, %al #new sys_select xor %ebx, %ebx mov $1, %bl #highest is 0 (stdin), plus 1 bts $0, -128(%esp) lea -128(%esp), %ecx #Take some stack for the fd_set struct xor %edx, %edx #writefds xor %esi, %esi #exceptfds mov $timeout, %edi int $0x80 .endm .macro sys_nanosleep length xor %eax, %eax mov $162, %al #sys_nanosleep movl $0, -8(%esp) #seconds movl \length, -4(%esp) #nanoseconds lea -8(%esp), %ebx xor %ecx, %ecx #ignore remainder int $0x80 .endm .macro sys_exit xor %eax, %eax mov $1, %al #sys_exit xor %ebx,%ebx #with 0 status int $0x80 .endm .macro sys_times xor %eax, %eax mov $43, %al #sys_times xor %ebx,%ebx #NULL int $0x80 .endm # [NM] one of the OS services is service number 54, ioctl; this is # generally called from C/C++ instead of assembly language, but as with # any system call, it can be done from assembly language; in the case of # a terminal window, what this service does is allow the programmer to # access characteristics of the screen, e.g. move the cursor, blank out # the screen, etc.; all the current information (e.g. current position # of the cursor) is recorded in a termios struct, maintained by the OS # (type "man termios" to see what's in it; also "man ioctl"); to change # something, e.g. move the cursor, one first gets a copy of the current # struct, then changes the relevant field in it, and writes the copy # back to the OS' original struct; the change is instantaneous .macro getterm lea -60(%esp), %edx #big enough for a termios struct mov $0x5401, %ecx #TCGETS call sys_ioctl .endm .macro setterm #No need to set %edx again mov $0x5403, %ecx #TCSETSW call sys_ioctl .endm # [NM] the VT100 code to move the cursor to row r, column c consists of # first Esc, then [r;cH where r and c are given in 2-character string # format, e.g. the string "12" for row 12; that's a total of 8 bytes; # the part of the .data segment labeled vt100_position contains these 8 # bytes; in order to move the cursor, we must fill in the r and c # portions of these bytes, and then write the 8 bytes to the screen; the # macro twodigits below converts r or c (it's called twice from # mvaddstr, once for r and then for c) from a number to a 2-character # string, and places it into vt100_position; the last two arguments # specify where to write within vt100_position (for row or column); the # inc instruction is apparently there because the VT100 scheme starts # numbering at 1 instead of 0 #Write the chars equivalent to 'source' into vt100_position .macro twodigits source first second mov \source, %ax inc %ax movb $10, %bl divb %bl add $0x30,%al movb %al, vt100_position+\first add $0x30, %ah movb %ah, vt100_position+\second .endm # [NM] in the [n]curses package, the macro mvaddstr moves the cursor # (thus the "mv" in the name) to row y, column x, and adds (thus # "addstr") to the screen at that point, replacing what was there # before, and then advancing the cursor to the point just after the # string that was written; in the variant here, the numbers y and x must # be converted to strings, as explained in my comments for the macro # twodigits #Named in honour of the ncurses function .macro mvaddstr y x string length twodigits \y, 2, 3 twodigits \x, 5, 6 mov $vt100_position, %ecx xor %edx, %edx mov $8, %dl call sys_write mov \string, %ecx xor %edx, %edx mov \length, %dl call sys_write .endm #Mask off the bits for the n'th block .macro bitMask n .if 4-\n shr $8-2*\n, %dx .endif and $0x303, %dx #Lower two bits of each mov yposition, %al add %dl, %al .endm #Put the y location of the n'th block in %ax, x location in %bx .macro screenoffset n bitMask \n mov xposition, %bx add %dh, %bl .endm #Make %ax the offset of the n'th block from the start of the screen array #where %dx is the piece in question .macro pieceoffset n bitMask \n imul $width, %ax add xposition, %ax shr $8, %dx add %dx, %ax .endm #Make real use of gas macros .macro storeLoop from=1, to=4 .if 4-\from movw (%esp), %dx .else pop %dx .endif pieceoffset \from movb %bl, (%eax, %ecx) .if \to-\from storeLoop "(\from+1)", \to .endif .endm .macro collisionLoop from=1, to=4 .if 1-\from movw (%esp), %dx .endif pieceoffset \from .if colour cmpb $0x30, (%ebx, %eax) .else cmpb $0x0, (%ebx, %eax) .endif .if 4-\from jnz collisionTest_over .endif .if \to-\from collisionLoop "(\from+1)", \to .endif .endm .macro drawLoop from=1, to=4 .if 1-\from movw (%esp), %cx .endif .if 4-\from mov 2(%esp), %dx .else pop %cx pop %dx .endif screenoffset \from call drawblock .if \to-\from drawLoop "(\from+1)", \to .endif .endm .data quitstring: .ascii "'q' to quit, arrow keys to move" scorestring: .ascii "Score: " linestring: .ascii "Lines: " namestring: .ascii "Daniel's Tetris" blankstring: .ascii " " exitstring: .ascii "User exitted" newline: .ascii "\n" #Also used after the previous string loserstring: .ascii "Loser\n" creditstring: .ascii "Tetris in 3k, by dburr@ug.cs.usyd.edu.au\n" score: .hword 0 timeout: .long 0 .long 0 #No delay while checking stdin vt100_position: .byte 0x1b .ascii "[12;13H" .if colour vt100_colour: #The proper english way of spelling the word! .byte 0x1b .ascii "[44m" .else vt100_invert: .byte 0x1b .ascii "[07m" .endif vt100_clear: .byte 0x1b .ascii "[2J" vt100_cursor: .byte 0x1b .ascii "[?25l" yposition: .byte 0 xposition: .hword 2 sleepcount: .byte 0 shapeStarts: .byte 2, 3, 5, 7, 11, 15, 19 shapeIndex: #This data contains the positions of the blocks in each shape #Each requires 16 bits: x1<<14|x2<<12|x3<<10|x4<<8|y1<<6|y2<<4|y3<<2|y4 .hword 0b0000010100010110 #0<<14|0<<12|1<<10|1<<8|0<<6|1<<4|1<<2|2 .hword 0b0100100100010001 #1<<14|0<<12|2<<10|1<<8|0<<6|1<<4|0<<2|1 .hword 0b0000010100010100 #0<<14|0<<12|1<<10|1<<8|0<<6|1<<4|1<<2|0 .hword 0b0000000000011011 #0<<14|0<<12|0<<10|0<<8|0<<6|1<<4|2<<2|3 .hword 0b0001101100000000 #0<<14|1<<12|2<<10|3<<8|0<<6|0<<4|0<<2|0 .hword 0b0001011000000101 #0<<14|1<<12|1<<10|2<<8|0<<6|0<<4|1<<2|1 .hword 0b0100010000010110 #1<<14|0<<12|1<<10|0<<8|0<<6|1<<4|1<<2|2 .hword 0b0001011001000101 #0<<14|1<<12|1<<10|2<<8|1<<6|0<<4|1<<2|1 .hword 0b0100010100010110 #1<<14|0<<12|1<<10|1<<8|0<<6|1<<4|1<<2|2 .hword 0b0001100100000001 #0<<14|1<<12|2<<10|1<<8|0<<6|0<<4|0<<2|1 .hword 0b0000000100011001 #0<<14|0<<12|0<<10|1<<8|0<<6|1<<4|2<<2|1 .hword 0b0001101000000001 #0<<14|1<<12|2<<10|2<<8|0<<6|0<<4|0<<2|1 .hword 0b0001000000000110 #0<<14|1<<12|0<<10|0<<8|0<<6|0<<4|1<<2|2 .hword 0b0000011000010101 #0<<14|0<<12|1<<10|2<<8|0<<6|1<<4|1<<2|1 .hword 0b0101010000011010 #1<<14|1<<12|1<<10|0<<8|0<<6|1<<4|2<<2|2 .hword 0b0001010100000110 #0<<14|1<<12|1<<10|1<<8|0<<6|0<<4|1<<2|2 .hword 0b0001100000000001 #0<<14|1<<12|2<<10|0<<8|0<<6|0<<4|0<<2|1 .hword 0b0000000100011010 #0<<14|0<<12|0<<10|1<<8|0<<6|1<<4|2<<2|2 .hword 0b1000011000010101 #2<<14|0<<12|1<<10|2<<8|0<<6|1<<4|1<<2|1 linesgone: .hword 0 #number of lines eliminated so far in the game currentwait: .byte wait #gets smaller as the game gets faster .bss buffer: .byte 0, 0 #for arrow keys we read two rotation: .byte 0 #overwrite with a random rotation blockType: .byte 0 #overwrite with a random block type .if colour currentcolour: .byte 0 #overwrite with random colour .endif stringbuffer: .fill 5 screen: .fill width*height lastrand: .long 0 .globl _tetris .text # [NM] most random number generators work something like the following; # the next random number, call it n, is generated from the last one, say # m, by multiplying m by a huge fixed number, adding another huge fixed # number, and then doing a mod operation by another huge fixed number; # years of research have found very good choices for those huge fixed # numbers; the algorithm below is similar #Return a 4-bit number in %al that is no greater than %cl rand: movl lastrand, %eax mov %eax, %ebx imul $1664525, %eax add $1013904223, %eax shr $10, %eax xor %ebx, %eax movl %eax, lastrand andb $0x7, %al cmp %al, %cl jb rand ret #Requires the string to write in %ecx, length in %edx sys_write: xor %eax, %eax mov $4, %al #sys_write xor %ebx, %ebx mov $1, %bl #stdout int $0x80 ret #Requires the length to read in %edx sys_read: xor %eax, %eax mov $3, %al #sys_read xor %ebx, %ebx #fd stdin mov $buffer, %ecx #buffer int $0x80 ret #Requires the number of the ioctl in %ecx, address for termios struct in %edx sys_ioctl: xor %eax, %eax mov $54, %al #sys_ioctl xor %ebx, %ebx #stdin int $0x80 ret #Take the current entry from the shapeIndex and push it on the stack coords: xor %edx, %edx xor %eax, %eax mov blockType, %al test %al, %al jz coords_noIndex mov shapeStarts-1(%eax), %dl coords_noIndex: add rotation, %dl shl $1, %dl #because each entry is 2 bytes pop %eax pushw shapeIndex(%edx) jmp *%eax #Write the block into the screen array at xposition, yposition storePiece: addw $scorelockin, score decb yposition .if colour movb currentcolour, %cl .else movb $0xff, %cl .endif call drawShape mov yposition, %al test %al, %al jz gameover call coords xor %eax, %eax .if colour mov currentcolour, %bl .else mov $0xff, %bl .endif mov $screen, %ecx storeLoop #There are 4 squares in the current piece. Test the lines which these #occupy to see if they are part of a complete line. If so, remove, redraw #Also adds to the score and speeds the game up if necessary elimline: mov yposition, %dl xor %eax, %eax mov %dl, %al #%al will contain the ypositions to test add $4, %dl xor %dh, %dh #number of lines eliminated in %dh cmpb $height-1, %dl jl elimline_skip mov $height-1, %dl #%dl contains one more than the last value to test elimline_skip: xor %ebx, %ebx mov $width, %bl imul %eax, %ebx add $screen, %ebx #ebx contains the start of the line xor %ecx, %ecx elimline_test: inc %cl #%ecx contains the x position to test .if colour cmpb $0x30, (%ecx, %ebx) #test this for each position in line .else cmpb $0, (%ecx, %ebx) #test this for each position in line .endif je elimline_linedone #ie: don't eliminate this line cmpb $width-2, %cl jne elimline_test inc %dh add $width, %ebx elimline_loop: dec %ebx movb -width(%ebx), %cl movb %cl, (%ebx) cmp $screen+width, %ebx jne elimline_loop elimline_linedone: inc %al cmp %al, %dl jne elimline_skip mov %dh, %ch #for testing linesgone later cmpb $4, %dh je elimline_tetris shr $8, %dx imul $scoreline, %dx addw %dx, score jmp elimline_finished elimline_tetris: addw $scoretetris, score elimline_finished: shr $8, %cx movw linesgone, %ax mov $speedup, %bl div %bl mov %al, %dl addw %cx, linesgone movw linesgone, %ax div %bl cmp %al, %dl je elimline_samespeed shrb $1, currentwait elimline_samespeed: test %cl, %cl jz elimline_noredraw call redraw elimline_noredraw: movb $0, yposition movw $2, xposition movb $0, sleepcount jmp playgame #Draw the current blockType at xposition,yposition (offset from xoffset, #yoffset). Will be coloured depending on %cl. Update score drawShape: call coords push %cx drawLoop .if colour movb $0x30, vt100_colour+3 mov $vt100_colour, %ecx .else movb $0x30, vt100_invert+3 mov $vt100_invert, %ecx .endif xor %edx, %edx mov $5, %dl call sys_write mvaddstr $instructionsY+2, $instructionsX, $scorestring, $7 mov score, %ax call numbertostring mvaddstr $instructionsY+3, $instructionsX, $linestring, $7 mov linesgone, %ax call numbertostring ret #Write the number in %ax numbertostring: mov $10, %bx mov $stringbuffer+5, %ecx numbertostring_loop: dec %ecx xor %dx,%dx div %bx add $0x30, %dl movb %dl, (%ecx) test %ax,%ax jnz numbertostring_loop mov $stringbuffer+5, %edx sub %ecx, %edx call sys_write ret #Requires the y coord in %al, x coord in %bx, val to colour in %cl drawblock: xor %ah, %ah add $xoffset,%bx add $yoffset,%ax push %ax push %bx .if colour movb %cl, vt100_colour+3 mov $vt100_colour, %ecx .else test %cl, %cl jz drawblock_out sub $0xf8, %cl drawblock_out: add $0x30, %cl movb %cl, vt100_invert+3 mov $vt100_invert, %ecx .endif xor %edx, %edx mov $5, %dl call sys_write pop %cx pop %ax shl $1, %cx mvaddstr %ax, %cx, $blankstring, $2 ret #Redraw the playing area (doesn't update score) redraw: xor %ax, %ax #y redraw_outer: xor %ebx, %ebx #x redraw_inner: push %ebx push %ax xor %ecx, %ecx mov $width, %cl imul %eax, %ecx mov screen(%ebx, %ecx), %cx call drawblock pop %ax pop %ebx inc %bl cmpb $width, %bl jl redraw_inner inc %ax cmpb $height, %al jl redraw_outer ret gameover: # [NM] here we must restore the screen, and especially the keyboard; to # see why, try playing the game but killing it with ctrl-C; you'll see # that the keyboard suddenly becomes inoperable in that window (to # restore it, hit ctrl-j then "reset" then ctrl-j again) .if colour movw $0x3030, vt100_colour+2 mov $vt100_colour, %ecx .else movb $0x30, vt100_invert+3 mov $vt100_invert, %ecx .endif xor %edx, %edx mov $5, %dl call sys_write movb $'h',vt100_cursor+5 mov $vt100_cursor, %ecx xor %edx, %edx mov $6, %dl call sys_write cmpb $'q',buffer jne gameover_loser mvaddstr $instructionsY+4, $0, $exitstring, $13 jmp gameover_quit gameover_loser: mvaddstr $instructionsY+4, $0, $loserstring, $6 gameover_quit: mov $scorestring, %ecx xor %edx, %edx mov $7, %dl call sys_write mov score, %ax call numbertostring mov $newline, %ecx xor %edx, %edx mov $1, %dl call sys_write mov $creditstring, %ecx xor %edx, %edx mov $41, %dl call sys_write getterm or $10,-48(%esp) #c_lflag |= (ICANON|ECHO) setterm sys_exit #Test the shape for any collision. If collision, then the zero flag will #NOT be set collisionTest: call coords xor %eax, %eax mov $screen, %ebx collisionLoop collisionTest_over: pop %dx ret #Writes the number of rotations of blockType into %cl numberrots: xor %ebx, %ebx mov blockType, %bl test %bl,%bl jz numberrots_zeroshape add $shapeStarts, %ebx mov (%ebx), %cl sub -1(%ebx), %cl jmp numberrots_done numberrots_zeroshape: mov shapeStarts, %cl numberrots_done: ret _tetris: # [NM] the next 3 lines (2 macro calls and an andb) unset # canonical mode, so keyboard input is instant, i.e. no waiting # for the user to hit the Enter key; the echo is also unset getterm andb $245,-48(%esp) #c_lflag &= ~(ICANON|ECHO) setterm sys_times mov %eax, lastrand #seed the randomizer # [NM] in the old days, cursor movement on a terminal was done # by printing a certain sequence of bytes to the screen; it was # different from each brand/model of terminal, but eventually # Digital Equipment Corporation's VT100 terminal type became a # standard, and today almost all terminal windows (e.g. xterm) # emulate a VT100 terminal # [NM] the next 4 lines of code write the code for clearing (i.e. # blanking out) a VT100 screen; if you check earlier in the file, # you'll see that vt100_clear is this: # vt100_clear: # .byte 0x1b # .ascii "[2J" # [NM] that first byte is the ASCII code for ESC (the Escape key), # which is a preface for all the VT100 cursor-movement codes; in # other words, to clear a VT100 screen, one sends ESC[2J to the # screen; try it yourself, by compiling and running this C # program: # main() # # { char esc = 27; // ASCII code for ESC # # printf("%c[2J",esc); # # } # [NM] one more thing: the author does a write to the screen so # often (duh!) that he has collected the code to do so into a # subroutine, which he has named sys_write, but which is simply # a call to the usual OS function write(), OS call #4: # sys_write: # xor %eax, %eax # mov $4, %al #sys_write # xor %ebx, %ebx # mov $1, %bl #stdout # int $0x80 # ret # [NM], so, here is the code to clear the screen: mov $vt100_clear, %ecx xor %edx, %edx mov $4, %dl call sys_write # [NM] these lines initialize the cursor position mov $vt100_cursor, %ecx xor %edx, %edx mov $6, %dl call sys_write .if colour mov $vt100_colour, %ecx .else mov $vt100_invert, %ecx .endif xor %edx, %edx mov $5, %dl call sys_write # [NM] in the old days, before VT100 really became standard, # there needed to be a way for people to write programs which # would work on any terminal type; for example, if you were # writing a text editor, like vi, you certainly would not want # to have to write a different version for each terminal type; # so, UNIX developers wrote the package named "curses" (get the # pun?); the programmer would simply call functions in this # package, and those functions would worry about how to make a # certain operation (e.g. cursor up one line) work; they would # do this by lookups in a database of all known terminal types # and their various cursor-movement codes, but the point is that # a programmer writing, say, vi could program cursor movements # without knowing what kind of terminal the user would use; the # author of Tetris here has written his own functions like that # (linking in curses from the C library would make the game too # big) and has even used the same macro names, e.g. mvaddstr # (see more comments on mvaddstr at its definition above); there # are lots of tutorials on ncurses on the Web mvaddstr $instructionsY, $instructionsX, $namestring, $15 mvaddstr $instructionsY+1, $instructionsX, $quitstring, $31 xor %al,%al mov $screen,%ebx tetris_yloop: .if colour movb $0x31, (%ebx) #red for the playing arena movb $0x31, width-1(%ebx) .else movb $0xff, (%ebx) movb $0xff, width-1(%ebx) .endif xor %ecx, %ecx mov $1, %cl tetris_yloop_inner: .if colour movb $0x30, (%ebx, %ecx) #init to black .else movb $0, (%ebx, %ecx) .endif inc %cl cmpb $width-1, %cl jl tetris_yloop_inner add $width, %ebx inc %al cmpb $height-1, %al jl tetris_yloop mov $width*(height-1)+screen, %ebx xor %eax, %eax tetris_xloop: .if colour movb $0x31, (%eax,%ebx) .else movb $0xff, (%eax,%ebx) .endif inc %al cmpb $width, %al jl tetris_xloop call redraw playgame: mov $6, %cl #7 shapes call rand movb %al, blockType call numberrots dec %cl call rand movb %al, rotation .if colour mov $6, %cl call rand add $0x31, %al mov %al, currentcolour mov %al, %cl .else movb $0xff, %cl .endif call drawShape call collisionTest jnz gameover playgame_keyloop: sys_nanosleep $250000 incb sleepcount movb currentwait, %cl cmpb %cl, sleepcount je playgame_slept sys_newselect test %eax, %eax jz playgame_keyloop playgame_checkkey: xor %edx, %edx mov $1, %dl call sys_read cmpb $'q', buffer je gameover .if colour mov $0x30, %cl .else xor %cl, %cl .endif call drawShape cmpb $' ', buffer jne playgame_checkarrow playgame_droploop: incb yposition call collisionTest jz playgame_droploop addw $scoredrop, score jmp storePiece playgame_checkarrow: cmpb $0x1b, buffer #check for arrow key jne playgame_checkdone xor %edx, %edx mov $2, %dl call sys_read movb buffer+1, %al cmpb $'D', %al #Left arrow je playgame_leftarrow cmpb $'C', %al #Right Arrow je playgame_rightarrow cmpb $'B', %al #Down Arrow je playgame_downarrow cmpb $'A', %al #Up Arrow jne playgame_checkdone xor %ah, %ah mov rotation, %al push %ax inc %al call numberrots divb %cl movb %ah, rotation call collisionTest jz playgame_checkdone pop %cx mov %cl, rotation jmp playgame_checkdone playgame_leftarrow: decw xposition call collisionTest jz playgame_checkdone incw xposition jmp playgame_checkdone playgame_rightarrow: incw xposition call collisionTest jz playgame_checkdone decw xposition jmp playgame_checkdone playgame_downarrow: incb yposition call collisionTest jz playgame_checkdone decb yposition jmp playgame_checkdone playgame_slept: .if colour mov $0x30, %cl #black to overwrite .else xor %cl, %cl .endif call drawShape incb yposition call collisionTest jnz storePiece movb $0, sleepcount playgame_checkdone: .if colour movb currentcolour, %cl .else movb $0xff, %cl .endif call drawShape jmp playgame_keyloop