;; snake.asm: Copyright (C) 2001 by Brian Raiter, under the GNU ;; General Public License. No warranty. ;; ;; To build: ;; nasm -f bin -o snake snake.asm && chmod +x snake ;; ;; Use 4, n, or the left-arrow key to turn the snake left. ;; Use 5, m, or the right-arrow key to turn the snake right. ;; Eating the blocks adds to your score and makes you grow longer. ;; Avoid the walls and your own tail. ;; ;; Note that this program assumes that it is running on a terminal ;; that has the VT100 line-drawing characters as the alternate ;; character set. If you're playing on a Linux console and you see a ;; bunch of lowercase letters instead of lines, try typing "echo -e ;; "\e(B\e)0" before starting the game. If that doesn't help (or if ;; instead you see a lot of non-ASCII characters) you may need to use ;; a console font with the VT100 character set in order to play. ;; ;; $Id: snake.asm,v 1.3 2006/02/09 07:49:34 konst Exp $ %include "system.inc" CPU 486 ;; Macros for accessing addresses in .bss via ebp. %define DATAOFF(addr) byte ebp + ((addr) - score) ;; ASCII character names %define NL 012q %define FF 014q %define SO 016q %define SI 017q %define ESC 033q %define CSI 233q ;; How the program tracks directions. (These values are also ORed ;; together to indicate the various VT-100 line-drawing characters.) %define NORTH 1 %define EAST 2 %define SOUTH 4 %define WEST 8 ;; Dimensions of the screen and the playing area. %define WIDTH 78 %define HEIGHT 24 %define CENTERPOINT ((HEIGHT / 2) * WIDTH + WIDTH / 2) %define RIGHTCOL '79' %define BOTTOMROW '24' ;; The number of turns it takes for the food to decay. %define FOOD_HALFLIFE 42 ;; Indices for messages to the user. %define helpmsgcode 51 %define endmsgcode 91 %define filledmsgcode 101 %define ouromsgcode 135 %define philosmsgcode 172 %define centurymsgcode 209 CODESEG ;; getkey retrieves a single character from standard input if one is ;; waiting to be read. ;; ;; input: ;; ecx = pointer to a byte in memory at which to store the character. ;; ;; output: ;; al = the contents of [ecx]. ;; [ecx] = the character input, or zero if no character was available, ;; or 'q' if standard input could not be read. ;; eax, ebx, and edx are altered. getkey: ;; Call select, waiting on stdin, and setting the time to wait to ;; zero. If select returns an error, return 'q' to the caller. If ;; select returns zero, return zero to the caller. Otherwise, the ;; read function is called to retrieve the input. _mov ebx, STDIN xor edx, edx mov [ecx], dl pusha mov ecx, fdset bts dword [ecx], ebx lea ebx, [byte edx + 1] xor esi, esi lea edi, [DATAOFF(timer)] mov [edi], esi mov [byte edi + 4], esi sys_select dec eax popa jl .return js .retquit inc edx sys_read ;; If read returned zero (i.e., EOF) or an error, return 'q' to the ;; caller. Otherwise, return the retrieved character. dec eax jz .return .retquit: mov byte [ecx], 'q' .return: mov al, [ecx] ret ;; zmove appends a cursor-positioning VT-100 escape sequence to the ;; given buffer. ;; ;; input: ;; eax = the zero-based coordinates to move the cursor to, in the form ;; y * WIDTH + x. ;; edi = the buffer to write the escape sequence to. ;; ;; output: ;; edi = the position following the escape sequence. ;; eax and edx are altered. zmove: mov dl, WIDTH div dl push eax inc eax aam bswap eax add eax, ESC | ('[00' << 8) stosd pop eax mov al, ah inc eax aam bswap eax shr eax, 8 add eax, ';00H' stosd ret ;; addvtchar appends a VT-100 cursor-position escape sequence followed ;; by a line-drawing character to the given buffer. ;; ;; eax = the zero-based coordinates to move the cursor to. ;; dl = the line-drawing character, given as a combination of ;; direction bit flags. ;; edi = the buffer to write the escape sequence to. ;; ;; output: ;; edi = the position following the line-drawing character. ;; eax, ebx, and edx are altered. addvtchar: push edx mov [DATAOFF(scr) + eax], dl call zmove pop eax mov edx, eax mov ebx, vtlines xlatb stosb ret ;; refresh writes the contents of outbuf to standard output. ;; ;; input: ;; edi = one past the last character in outbuf to write. ;; ;; output: ;; edi = outbuf. ;; eax, ebx, ecx, and edx are altered. refresh: mov eax, ESC | ('[' << 8) | (BOTTOMROW << 16) stosd mov eax, ';0H' | (SI << 24) stosd mov edx, edi mov edi, outbuf mov ecx, edi sub edx, ecx sys_write STDOUT tick: ret ;; The program begins here. START: ;; ebp is set to point to an address near the top of the .bss ;; section. It will retain this value throughout the program. mov ebp, score ;; The attributes of the tty connected to standard input are ;; retrieved, and then canonical-mode and input-echoing are turned ;; off. This puts the terminal in a mode similar to what ncurses calls ;; cbreak mode. The original attributes are remembered so that they ;; can be restored at the end. lea edx, [DATAOFF(termattrs)] sys_ioctl STDIN, TCGETS mov eax, [DATAOFF(termattrs.c_lflag)] push eax and eax, byte ~(ICANON | ECHO) mov [DATAOFF(termattrs.c_lflag)], eax sys_ioctl STDIN, TCSETS pop dword [DATAOFF(termattrs.c_lflag)] ;; A do-nothing signal hander is installed for SIGALRM, and then an ;; interval timer is set up to go off every 0.2 seconds. xor edx, edx lea ecx, [DATAOFF(sigact)] mov dword [byte ecx + Sigaction.sa_handler], tick sys_sigaction SIGALRM lea ecx, [DATAOFF(timer)] mov eax, 200 * 1000 mov [byte ecx + itimerval.it_interval_usec], eax mov [byte ecx + itimerval.it_value_usec], eax sys_setitimer ITIMER_REAL ;; The scr buffer is initialized by adding the outer walls of the ;; playing field. lea edi, [DATAOFF(scr)] mov al, SOUTH | EAST stosb mov al, EAST | WEST push byte WIDTH - 2 pop ecx rep stosb mov al, SOUTH | WEST stosb mov esi, edi mov al, NORTH | SOUTH stosb add edi, byte WIDTH - 2 stosb mov cl, ((HEIGHT - 3) * WIDTH) & 0xFF mov ch, ((HEIGHT - 3) * WIDTH) >> 8 rep movsb mov al, NORTH | EAST stosb mov al, EAST | WEST mov cl, WIDTH - 2 rep stosb mov byte [edi], NORTH | WEST ;; edi is initialized to point to the output buffer. It will continue ;; to point into this buffer for the rest of the program. mov edi, outbuf ;; The snake is set to begin in the middle of the playing field, and ;; is set to extend by one unit. A food block is placed there as well, ;; but having already expired so that the program will create a new ;; food block on the first iteration of the main loop. mov eax, CENTERPOINT mov [snake], eax mov byte [DATAOFF(scr) + eax], EAST | WEST mov [DATAOFF(foodpos)], eax ;; The current time is used to initialize our pseduorandom-number ;; generator, and is also used to select between EAST and WEST as the ;; snake's starting direction of movement. A Ctrl-L keystroke is ;; inserted into the input queue, so that the program will redraw ;; everything on the first iteration of the main loop. lea ebx, [DATAOFF(rndseed)] sys_gettimeofday add eax, [ebx] mov al, WEST jp .gowest mov al, EAST .gowest: mov [DATAOFF(dir)], al mov byte [DATAOFF(key + 1)], FF ;; The main loop, where the program will remain until the game ends. mainloop: ;; Examine the input queue. If a keystroke is waiting in there, then ;; remove it and use it as this iteration's keystroke. (If two ;; keystrokes are waiting there, remove the first one and move the ;; second one into its position.) lea ecx, [DATAOFF(key)] mov al, [byte ecx + 1] or al, al jz .readkey xor edx, edx cmp dl, [byte ecx + 2] jz .retlast xchg dl, [byte ecx + 2] .retlast: mov [byte ecx + 1], dl .retkey: mov [ecx], al jmp short .endkeys ;; Otherwise, check for an incoming character. If anything besides ESC ;; is returned (including zero, indicating no keys have been pressed), ;; then proceed with that key. Otherwise, check for a second ;; character. If anything besides '[' or 'O' is returned, then put the ;; character in the input queue and proceed with the ESC. Otherwise, ;; check for a third character. If anything besides 'C' or 'D' is ;; returned, then put it and the previous character into the input ;; queue and proceed with the ESC. Otherwise, replace the arrow-key ;; sequence with an 'm' or an 'n', as appropriate. .readkey: call getkey cmp al, ESC jnz .endkeys inc ecx call getkey cmp al, '[' jz .getthird cmp al, 'O' jnz .endkeys .getthird: inc ecx call getkey cmp al, 'C' jz .acceptarrow cmp al, 'D' jnz .endkeys .acceptarrow: add al, 'm' - 'C' movzx eax, al mov [byte ecx - 2], eax .endkeys: ;; If a 'q' was retrieved, exit the program at once. If a Ctrl-L was ;; retrieved, then redraw the screen. cmp al, 'q' jz near leavegame cmp al, FF jnz .continue ;; The screen is erased, and the VT-100 line-drawing character set is ;; selected. The scr array is read, and each element is translated ;; into one of the line-drawing characters and output, with a newline ;; inserted at the end of each row. Finally, the normal character set ;; is re-selected, and the program jumps to the routine to display the ;; current score. mov eax, ESC | ('[H' << 8) | (ESC << 24) stosd mov eax, '[J' | (SO << 16) | (SO << 24) stosd lea esi, [DATAOFF(scr)] mov ebx, vtlines push byte HEIGHT pop ecx .initoutloop: mov ch, WIDTH .lineoutloop: lodsb xlatb stosb dec ch jnz .lineoutloop dec ecx jz .outloopend mov al, 10 stosb jmp short .initoutloop .outloopend: mov al, SI stosb jmp near .drawscore .continue: ;; The pointers to the positions of the snake's head and tail are ;; retrieved. If the snake is currently growing, decrement the growth ;; counter and continue on. Otherwise, erase the end of the tail and ;; advance the tail pointer. mov esi, [DATAOFF(ringhead)] mov ebx, [DATAOFF(ringtail)] dec dword [DATAOFF(growing)] jns .growing inc dword [DATAOFF(growing)] mov eax, [snake + ebx*4] mov dl, 0 call addvtchar inc byte [DATAOFF(ringtail)] .growing: ;; The position of the snake's head is retrieved, and the current ;; direction of the snake's movement is used to advance the head to ;; the next position, which is stored in esi. mov esi, [snake + esi*4] mov al, [DATAOFF(dir)] rcr eax, 1 jnc .notnorth sub esi, byte WIDTH .notnorth: rcr eax, 1 adc esi, byte 0 rcr eax, 1 jnc .notsouth add esi, byte WIDTH .notsouth: rcr eax, 1 sbb esi, byte 0 .notwest: ;; The location of the food block is compared with the new position of ;; the head. If they intersect, the program jumps ahead. Otherwise, ;; the life counter for the food is decremented. If it reaches zero, ;; it is reset, and the value of the food is decremented. If that ;; reaches zero, the program skips ahead to the newfood routine. ;; Otherwise, the new value of the food is displayed at the food's ;; location. mov eax, [DATAOFF(foodpos)] sub eax, esi jns .positive neg eax .positive: dec eax jle .eating dec byte [DATAOFF(foodlife)] jnz .endfoodrelay dec byte [DATAOFF(foodvalue)] jz .newfood mov byte [DATAOFF(foodlife)], FOOD_HALFLIFE mov eax, [DATAOFF(foodpos)] call zmove mov eax, ESC | ('[7m' << 8) stosd mov eax, '0 ' | (ESC << 16) | ('[' << 24) add al, [DATAOFF(foodvalue)] stosd mov eax, '27m' | (SI << 24) stosd .endfoodrelay: jmp near .endfood ;; Eat the food! The current value of the food is added to the score, ;; and to the snake's growth counter, and the new score is displayed ;; to the right of the playing field in decimal. .eating: mov ecx, [DATAOFF(foodvalue)] add [DATAOFF(growing)], ecx shl dword [DATAOFF(growing)], 1 add [DATAOFF(score)], ecx .drawscore: mov eax, [DATAOFF(score)] mov bl, 10 mov edx, '[:;' | ((RIGHTCOL & 0x00FF) << 24) .updscoreloop: div bl push eax mov al, ESC stosb xchg eax, edx dec ah stosd xchg eax, edx mov al, (RIGHTCOL & 0xFF00) >> 8 stosb add ax, ('H' - ((RIGHTCOL & 0xFF00) >> 8)) | ('0' << 8) stosw pop eax mov ah, 0 or eax, eax jnz .updscoreloop ;; If the program arrived here due to a Ctrl-L keystroke, then the ;; program skips ahead again to the food-block display routine. .newfood: mov eax, [DATAOFF(foodpos)] mov ecx, [DATAOFF(foodvalue)] cmp byte [DATAOFF(key)], FF jz near .drawfood ;; The old food block is erased. dec eax call zmove mov al, ' ' stosb stosb stosb ;; The scr array is examined, and every location where there are three ;; blanks in a row is added to the spaces array. The new position of ;; the snake's head is temporarily marked so that the food will not be ;; placed there. xor [DATAOFF(scr) + esi], al xor ebx, ebx mov ecx, WIDTH * (HEIGHT - 1) .spaceloop: mov [spaces + ebx*4], ecx cmp dword [DATAOFF(scr - 2) + ecx], 256 adc ebx, byte 0 dec ecx jnz .spaceloop xor [DATAOFF(scr) + esi], al mov dl, filledmsgcode or ebx, ebx jz near endgame ;; Otherwise, a new pseudorandom number is generated, and scaled to be ;; between zero (inclusive) and seven times the number of locations in ;; the spaces array (exclusive). .spaceforrent: push ebx fld dword [seventothem31] fimul dword [esp] mov eax, [DATAOFF(rndseed)] mov edx, 1103515245 mul edx add eax, 12345 shl eax, 1 shr eax, 1 mov [DATAOFF(rndseed)], eax fimul dword [DATAOFF(rndseed)] fsub dword [onehalf] fistp dword [esp] pop eax ;; The random number is divided by seven. The quotient is the index to ;; the location for the new food block, and the remainder plus three ;; is the starting value for the food block. cdq mov cl, 7 div ecx mov eax, [spaces + eax*4] mov [DATAOFF(foodpos)], eax lea ecx, [byte edx + 3] mov [DATAOFF(foodvalue)], cl mov byte [DATAOFF(foodlife)], FOOD_HALFLIFE ;; The new food block is display at its chosen location. If the ;; program arrived here due to a Ctrl-L keystroke, it immediately ;; flushes the output buffer and returns to the top of the main loop. .drawfood: dec eax call zmove mov eax, ESC | ('[7m' << 8) stosd mov eax, ' 0 ' | (ESC << 24) add ah, cl stosd mov eax, '[27m' stosd cmp byte [DATAOFF(key)], FF jnz .endfood call refresh jmp near mainloop .endfood: mov al, SO stosb ;; dl is set to the current direction of movement, and dh is set to ;; the opposite direction. mov dl, [DATAOFF(dir)] mov dh, dl shl dh, 4 or dh, dl shr dh, 2 and dh, 15 ;; Having dispensed with the food block, the program now checks to see ;; if anything else is already at the new location of the snake's ;; head. If not, the new location is stored and the pointer to the ;; head is advanced. mov al, [DATAOFF(scr) + esi] or al, al jnz collide inc byte [DATAOFF(ringhead)] mov eax, [DATAOFF(ringhead)] mov [snake + eax*4], esi ;; The keystroke is retrieved, and, if appropriate, the program shifts ;; the snake's direction of movement to the right or the left. mov al, dl shl al, 4 or dl, al mov al, [DATAOFF(key)] cmp al, '4' jz .left cmp al, '6' jz .right or al, 'a' - 'A' cmp al, 'n' jz .left cmp al, 'm' jnz .endturn .right: shr dl, 2 .left: shr dl, 1 .endturn: and dl, 15 mov [DATAOFF(dir)], dl ;; The opposite of the old direction and the new direction are ;; combined to indicate the line-drawing character to display at the ;; current location. or dl, dh mov eax, esi call addvtchar ;; The contents of the output buffer are displayed, and the program ;; goes to sleep until the next SIGALRM arrives, whereupon it begins ;; the next iteration of the main loop. call refresh sys_pause jmp near mainloop ;; Crash! The snake has hit something, either a wall or its own ;; tail. The opposite of the direction of travel is combined with ;; whatever is already here to produce the updated line-drawing ;; character for this location. The program also checks to see if the ;; snake actually swallowed its own tail, as opposed to merely running ;; into it, and makes a smartaleck comment if so. collide: test al, dh pushf or al, dh mov dl, al mov eax, esi call addvtchar popf jz .noloop mov dl, ouromsgcode mov eax, [DATAOFF(ringtail)] cmp esi, [snake + eax*4] jz endgame .noloop: ;; If the game ended with a score of zero, the program provides a ;; brief help message in case the user doesn't know how the game ;; works. Other conditions of the game produce different messages. mov eax, [DATAOFF(score)] mov dl, helpmsgcode or eax, eax jz endgame mov dl, centurymsgcode cmp eax, byte 100 jz endgame mov dl, philosmsgcode cmp dword [DATAOFF(growing)], byte 15 ja endgame mov dl, endmsgcode ;; The game is over, and a closing message needs to be displayed. The ;; program reads the text from the messages array and into the output ;; buffer, which is then flushed. endgame: mov ebx, msgs movzx edx, dl lea esi, [ebx + edx] mov eax, SI | (ESC << 8) | ('[1' << 16) stosd .msgloop: lodsd dec esi or al, al jz leavegame mov cl, 4 xchg eax, edx .msgcharloop: mov al, 63 and al, dl xlatb stosb shr edx, 6 dec cl jnz .msgcharloop jmp short .msgloop leavegame: call refresh ;; The program restores the tty to its original settings, and exits. lea edx, [DATAOFF(termattrs)] sys_ioctl STDIN, TCSETS xchg eax, ebx sys_exit ;; The messages array. msgs: dd 0x49484745, 0x534F4E4A, 0x58575554, 0x2161205B, 0x65646362, 0x68672766 dd 0x2C6C6B69, 0x6F2F6E6D, 0x72317030, 0x34337332, 0x76753574, 0x79387736 dd 0xB01B0F3B, 0x39490AC8, 0x36C7A735, 0x1E74D8DF, 0xE3637CD7, 0x96D7E38C dd 0xA835FA0D, 0x368D763A, 0x74E36851, 0xD6623352, 0xB000C685, 0x72310AC9 dd 0x49D00D4D, 0x0A293000, 0xD7B5F171, 0xFA0D4E34, 0x368AA135, 0xD43535E8 dd 0x235B49F7, 0x04D95D38, 0x4E728A2A, 0xB000C4F7, 0x3A860AE8, 0x7E37D07E dd 0xDF48D96A, 0xDA157654, 0x353AD868, 0xEB4E37D4, 0xD7583634, 0x365617A0 dd 0x004DC396, 0xC80AE8B0, 0x7B4DA0E5, 0x48D5D161, 0xDD3654DF, 0xA6193687 dd 0x35370D69, 0x654D939C, 0xF68D4DC3, 0xC634D675, 0xB22C2000, 0xE0A4CC82 dd 0x08A23322, 0x3320AC8B, 0xCD34B829, 0xD4D939D1, 0x4D939D1C, 0x4EB4D0CD dd 0xD7E83687, 0x36F39A84, 0x9364E747, 0x2C2488CC, 0x0CD342B2, 0x5D4C4354 dd 0x34ED34E7, 0x35D60D4E, 0x5B69A4D7, 0xF40D5DF3, 0xA9FB4DBD, 0xD794D49A dd 0x364E40D4, 0xC8D34D35, 0x22C2688C, 0xD0CD342B, 0x36874EB4, 0x9A84D7E8 dd 0xE74736F3, 0x4CD6D364, 0x6860DA0E, 0x735274E3, 0x70D4EB39, 0xD939136F dd 0xCC8D34D4, 0xB22C2788, 0xD728D342, 0xD34335D4, 0xA0DA1D3A, 0x684D635F dd 0xF3526233, 0x39368D51, 0xCD6E54EB, 0xD4E5A8D0, 0x64E8CD38, 0x8CC8D353 dd 0x2B22C298, 0x2E0A4CC8 ;; Two floating-point values, used in the scaling of our pseudorandom ;; numbers. onehalf: dd 0x3F000000 ; 0.5 seventothem31: dd 0x31600000 ; 7 * 2^-31 ;; The table used to translate direction bitflags to VT-100 ;; line-drawing characters. vtlines: db ' xqmxxltqjqvkuwn' UDATASEG ALIGNB 4 ;; The attributes of the user's tty. termattrs: B_STRUC termios,.c_lflag ;; This chunk of memory doubles as a timeval structure and an ;; itimerval structure. timer: B_STRUC itimerval ;; The structure passed to the sigaction system call. sigact: B_STRUC Sigaction,.sa_handler ;; The player's current score. score: resd 1 ;; The snake's current direction of movement. dir: resd 1 ;; The value of the current food block, its decay counter, and its ;; position in the playing field. foodvalue: resd 1 foodlife: resd 1 foodpos: resd 1 ;; Pointers to the where the position of the snake's head and tail are ;; stored in the ring buffer. ringhead: resd 1 ringtail: resd 1 ;; The snake's growth counter. growing: resd 1 ;; The current seed value for the pseudorandom-number generator. rndseed: resd 2 ;; The input queue. key: resb 4 ;; The scr array. The snake and the walls of the playing field are ;; stored in this array as direction bitflags. scr: resb WIDTH * HEIGHT ;; The buffer used for storing the program's output. outbuf: resb 2169 ;; The ring buffer containing the locations that the snake currently ;; passes through. snake: resd 256 ;; The spaces array, used while selecting a new food block location. spaces: resd WIDTH * HEIGHT ;; The file description bit array used by the select system call. fdset: resb fd_set_size END