;; snake1k, entry for the 2002 minigame competition ;; by Paolo Ferraris (pieffe8@libero.it) - public domain ;; works with any ZX Spectrum (16k/48k/128k/etc...) and ;; most Spectrum emulators - assemblable with TASM dataaddr: .equ 7000h lives .equ 0 level .equ 1 timeelapsed .equ 2 ; 2 bytes ballpos .equ 4 ; two bytes (x,y) balldir .equ 6 ; two bytes (dx,dy) currlevel .equ 8 ; address of mapdata for current level nextlevel .equ 10 ; address of mapdata for next level numapples .equ 12 ; apples on the screen uppsnake1addr .equ 64h snake1addr .equ 6400h uppsnake2addr .equ 65h snake2addr .equ 6500h direction .equ 0 ; 0=N, 1=S,2=E,3=W,8=not created,16=dead currx .equ 1 ; head coordinates curry .equ 2 toshow .equ 3 ; how much snake not shown yet lastx .equ 4 ; tail coordinates lasty .equ 5 movehowmany .equ 6 ; (snake1) how much straight do you have to go mapaddress .equ 8000h ; map uppmapaddress .equ 80h getcoords: .equ 22B0h moveup .equ 0 movedown .equ 1 moveright .equ 2 moveleft .equ 3 stringink .equ 16 ; ASCII code of ink stringpaper .equ 17 ; paper stringflash .equ 18 ; etc stringbright .equ 19 ; etc stringat .equ 22 ; ASCII code of AT beginline: .ORG 23755 ;; 0 RANDOMIZE: RANDOMIZE USR : REM .byte 0 ; line number (higher part) .byte 0 ; line number (lower part) .word endline-beginline-4 .byte 0F9h ; RANDOMIZE .byte ':' ; .byte 0F9h ; RANDOMIZE .byte 0C0h ; USR .byte '0' .byte 0Eh ; number begins .byte 0 ; .byte 0 ; .word start ; address of start code .byte 0 .byte ':' ; .byte 0EAh ; REM start: LD IX,dataaddr LD HL,0005h ; level,lives LD (dataaddr+lives),HL ;; new level (or new game) newlevel: LD HL,dataaddr+level RES 3,(HL) ; level 8 -> 0 LD A,(HL) INC (HL) ; increment level AND A ; was the level 0? (now 1) LD HL,levels JR Z,isnextlevel ; if yes, the map is in level LD HL,(dataaddr+nextlevel) ; if no, the one following the current isnextlevel: LD (dataaddr+currlevel),HL ; save that value INC (IX+lives) ; temporary increase the number of lives lifelost: DEC (IX+lives) ; life lost JR Z,start ; if no lives left, restart CALL resetstage ; regenerate the board LD B,50 ; wait one second mainloop: HALT DJNZ mainloop mainloop2: LD HL,snake1addr+direction ; snake 1 BIT 3,(HL) ; is it dead? CALL NZ,createsnake1 ; create it (if possible) CALL Z,movesnake2 ; if created, move it LD HL,snake2addr+direction ; snake 2 BIT 4,(HL) ; is lenght 0? JR NZ,newlevel ; if so, go to next level LD A,(snake2addr+curry) SUB 2 ; is the snake out of the board CP 33 JR NC,noobstacle ; if so, move the snake same direction CALL readkey ; read the new direction and if it hits an JR NZ,lifelost ; obstacle, if so, dead noobstacle: CALL movesnake1 ; move snake ;; move the ball CALL moveball ;; put the ball LD A,6 CALL putitem ;; if snake2 (you) is going out, time is stopped and ;; movements go faster LD A,(snake2addr+curry) LD B,2 DEC A JR Z,mainloop ;; increase time elapsed LD HL,(dataaddr+timeelapsed) LD BC,128 ADD HL,BC LD (dataaddr+timeelapsed),HL ;; lower the vertical bar EX DE,HL LD C,192 LD A,D CALL getcoords ; in ROM. Input: C=xcord, A=ycoord LD (HL),A LD A,D ;; if time over, handle it CP 181 CALL Z,resettime ;; move the ball ;; wait for 2/25th of second LD B,4 JR mainloop lifelost2: ;; eliminate two elements from the stack POP HL POP HL JR lifelost ;; handle the ball movement moveball: ;; remove the ball LD DE,(dataaddr+ballpos) CALL removeitem ;; test if same direction is okay LD BC,(dataaddr+balldir) CALL testballmove RET Z ;; if not, flip the y direction and try again CALL testballmovey RET Z ;; flip the x direction XOR A SUB C LD C,A ;; try again (the original direction with x flipped) CALL testballmovey RET Z ;; try the opposite direction testballmovey: ;; flip y direction XOR A SUB B LD B,A testballmove: ;; calculate the new possible ball position EX DE,HL LD A,H ADD A,B LD D,A LD A,L ADD A,C LD E,A ;; check if the ball hits the head of the snake PUSH HL LD H,uppsnake2addr CALL checksame POP HL JR Z,lifelost2 ; life lost (POP twice!) ;; is the new osition occupied by an obstacle? CALL getitem EX DE,HL RET NZ ; if so, that is not a good position ;; save the new ball position and direction LD (dataaddr+ballpos),HL LD (dataaddr+balldir),BC EX DE,HL RET ;; handle the snake movements movesnake2: CALL randmove ; new direction for snake 1 movesnake1: ;; check if it hits no obstacles CALL isgoodmove CALL Z,movehead ; if so, move the head ;; check if there is a part of snake to be shown LD L,toshow LD A,(HL) DEC (HL) AND A RET NZ ; if so, the tail stands still LD (HL),A ;; move the tail CALL 2AEFh ; read DE from (HL) CALL removeitem ; remove the tail ;; if the tail was outside the board, place a wall LD A,D SUB 2 CP 33 CALL NC,putwall ;; tail=head? CALL checksame JR Z,snakedestroyed; if so, serpent destroyed or out ;; save the next direction of the snake PUSH DE SET 7,D SET 6,E LD A,(DE) POP DE nextandmemorize: CALL getnext JP 1921h ; write DE in (HL) and RET ;; write the information of dead snake snakedestroyed: LD L,direction LD (HL),24 RET ;; move snake 1 randomically randmove: ;; will change direction if obstacle or moved straight enough CALL isgoodmove LD L,movehowmany JR NZ,changedirection DEC (HL) RET NZ ; no change direction changedirection: LD (HL),1 ;; is there a good direction? LD A,3 seenextdirection: CALL isgoodmovex JR Z,directionexists ; if yes, choose one of them! skipdirection: SUB 1 RET C JR seenextdirection ;; how long will move in the new direction (1 to 6) directionexists: LD A,6 CALL getrandnum INC A LD (HL),A ;; find a direction in which it is possible to go, and memorize it otherdirection: LD A,4 CALL getrandnum CALL isgoodmovex JR NZ,otherdirection LD L,direction LD (HL),A RET createsnake1: ;; try to remove the wall CALL removewall1 RET NZ ; if no wall, exit LD DE,snake1init createsnakecommon: ;; copy the initial data of the snake LD BC,7 EX DE,HL LDIR EX DE,HL RET ;; test the direction is not opposite to the previous one also isgoodmovex: PUSH HL LD L,direction LD C,A XOR (HL) POP HL DEC A LD A,C JR NZ,isgoodmove2 INC C ; C != 255, so Z is resetted RET ;; read the new direction, and test if it is good (hit obstacle) readkey: LD L,direction LD A,0EFh IN A,(254) RRCA LD B,0FFh readkeyloop: INC B BIT 2,B JR NZ,isgoodmove RRCA JR C,readkeyloop ;; opposite direction? LD A,(HL) XOR B DEC A JR Z,isgoodmove LD (HL),B ;; test if the direction is good isgoodmove: LD L,direction LD A,(HL) CALL 2AEFh ; increases HL, reads DE (snake position) isgoodmove2: PUSH DE CALL getnext LD B,A CALL getitem JR Z,exitgoodmove CP 5 exitgoodmove: LD A,B POP DE RET ;; A=what is in the position of the upper door, Z if empty space getupperdoor: LD DE,0112h ;; A=what is in the position of DE, Z if empty space getitem: SET 7,D LD A,(DE) RES 7,D CP 255 RET ;; calculate the new position relative to DE, with direction A getnext: LD B,A INC B DJNZ notup DEC D notup: DJNZ notdown INC D notdown: DJNZ notright INC E notright: DEC B RET NZ DEC E RET ;; remove item in DE ;; D=y coordinate, E=x coordinate removeitem: LD A,255 JR putitem ;; handle the apple removal appleremoved: ;; increase the amount of snake to be shown PUSH HL LD L,toshow LD A,(HL) ADD A,5 LD (HL),A POP HL ;; still apples left? If not remove wall 1 DEC (IX+numapples) RET NZ ;; remove, if it is there, wall 1 removewall1: PUSH DE CALL getupperdoor CP 4 CALL Z,removeitem POP DE RET ;; move the head movehead: ;; memorize the new direction PUSH DE SET 7,D SET 6,E LD (DE),A POP DE ;; calculate the new position and draw it CALL nextandmemorize JR putitem putwall: LD A,4 ;; put object A in DE putitem: PUSH AF CALL getitem CP 5 CALL Z,appleremoved CALL writeblock POP AF SET 7,D LD (DE),A RES 7,D ;; draw object memorized in coordinate DE ;; (uses XOR, so it creates/eliminates the object from the screen) writeblock: ;; is there an object? CALL getitem RET Z ; if not, do nothing PUSH HL PUSH DE PUSH BC ;; multiply the x,y coordinates by 5 LD H,D LD L,E ADD HL,HL ADD HL,HL ADD HL,DE ;; find the tile address for that object LD B,A INC B LD DE,tileset-6 LD A,E continuesumming: ADD A,6 DJNZ continuesumming LD E,A ;; draw the object whose tile is in DE and coordinates in HL LD B,7 ; 7 lines EX DE,HL writeblockloop: LD A,(HL) PUSH HL PUSH DE PUSH BC LD L,A ; we place the byte in L, 0 in H, LD H,0 ; and we will shift it left many times PUSH HL LD C,E LD A,D CALL getcoords ; in ROM. Input: C=xcord, A=ycoord ; Output: HL screen address, A=byte position ; destroys: A,BC,HL EX DE,HL ; DE <- address of two bytes in screen POP HL ; HL <- the byte to write printbyteloop: ADD HL,HL ; 8-A times INC A CP 8 JR NZ,printbyteloop LD A,(DE) XOR H LD (DE),A INC DE LD A,(DE) XOR L LD (DE),A POP BC POP DE POP HL INC D INC HL DJNZ writeblockloop INC B ; Z flag off POP BC POP DE POP HL RET ;; draw the map drawlines: drawlinesloop: LD A,(HL) INC HL CP 192 RET Z JR NC,newcoords ; >=192 LD B,A AND 3 LD C,A XOR B RRCA RRCA LD B,A drawloop: PUSH BC LD A,C CALL getnext CALL putwall POP BC DJNZ drawloop JR drawlinesloop ;; get the new coordinates newcoords: SUB 192 LD E,A LD D,(HL) INC HL CALL putwall JR drawlinesloop ;; check if DE is the address of the head of the snake checksame: PUSH HL LD L,currx LD A,E CP (HL) JR NZ,endschecksame INC L LD A,D CP (HL) endschecksame: POP HL RET resetstage: ;; initial ball position and direction LD HL,0202h LD (dataaddr+ballpos),HL DEC H DEC L LD (dataaddr+balldir),HL ;; 0 apples LD (IX+numapples),0 ;; clear the screen LD HL,16384 LD DE,16385 LD BC,6144 LD (HL),0 LDIR LD BC,767 LD (HL),7 LDIR ;; write level and lives LD C,(IX+level) LD HL,levelstr CALL printstringandlownumber LD C,(IX+lives) LD HL,livesstr CALL printstringandlownumber ;; clear the address map LD HL,mapaddress clearmaploop: LD (HL),255 INC HL BIT 6,H JR Z,clearmaploop ;; draw the border LD HL,borders CALL drawlines ;; draw the board LD HL,(dataaddr+currlevel) CALL drawlines LD (dataaddr+nextlevel),HL LD HL,8012h ; invisible obstacle out of the box LD (HL),0 LD HL,snake1addr LD (HL),8 ; snake 1 is dead INC H ;; create snake 2 ;; remove wall LD DE,2312h CALL removeitem ;; address of snake 2 data, and go on common LD DE,snake2init CALL createsnakecommon ;; time over resettime: ;; put the door 1 CALL getupperdoor CALL Z,putwall ;; draw the time bar LD DE,1536+192 drawnext: LD C,E LD A,D CALL getcoords ; in ROM. Input: C=xcord, A=ycoord LD (HL),192 INC D LD A,D CP 181 JR NZ,drawnext ;; current time LD HL,5*256+128 LD (dataaddr+timeelapsed),HL LD B,10 ; apples to be added newposition: ;; get random coordinates - empty space CALL getrandnum33 ADD A,2 LD D,A CALL getrandnum33 ADD A,2 LD E,A CALL getitem JR NZ,newposition ;; put the apple and increase their number LD A,5 CALL putitem INC (IX+numapples) DJNZ newposition RET getrandnum33: LD A,33 ;; gets a random number from 0 to A-1 inclusive. in A ;; It implements the RND subroutine that is in ROM but ;; using only integer operations, thus FASTER! getrandnum: PUSH DE PUSH HL PUSH BC PUSH AF LD HL,(5C76h) INC HL LD A,H OR L JR NZ,calcrnd LD HL,65461 JR calcrndresult calcrnd: LD A,75 CALL multiply16x8 AND A SBC HL,BC JR C,calcrndresult DEC HL calcrndresult: LD (5C76h),HL POP AF CALL multiply16x8 LD A,C POP BC POP HL POP DE RET multiply16x8: EX DE,HL LD HL,0 LD B,8 multiplyloop: ADD HL,HL RL C ADD A,A JR NC,multiplynocarry ADD HL,DE JR NC,multiplynocarry INC C multiplynocarry: DJNZ multiplyloop RET printstringandlownumber: printstring: LD A,(HL) INC HL CP 255 JR Z,printnumber RET Z RST 10h JR printstring printnumber: CALL 2D29h ; C to FP stack JP 2DE3h ; print number in FP stack ;; "lives" livesstr: .byte stringat .byte 9 .byte 25 .byte stringpaper .byte 0 .byte stringink .byte 6 .text "LIVES" .byte stringat .byte 10 .byte 27 .byte stringink .byte 5 .byte 255 ;; "level" levelstr: .byte stringat .byte 6 .byte 25 .byte stringpaper .byte 0 .byte stringink .byte 6 .text "LEVEL" .byte stringat .byte 7 .byte 27 .byte stringink .byte 5 .byte 255 snake1init: .byte movedown .byte 18 .byte 0 .byte 10 .byte 18 .byte 1 .byte 2 borders: .byte 1+192 .byte 1 .byte 34*4+moveright .byte 34*4+movedown .byte 34*4+moveleft .byte 33*4+moveup .byte 192 levels: ;; 1 .byte 9+192 .byte 6 .byte 24*4+movedown .byte 27+192 .byte 6 .byte 24*4+movedown .byte 192 ;; 2 .byte 7+192 .byte 7 .byte 22*4+moveright .byte 18+192 .byte 7 .byte 21*4+movedown .byte 192 ;; 3 .byte 7+192 .byte 7 .byte 22*4+moveright .byte 7+192 .byte 12 .byte 22*4+movedown .byte 29+192 .byte 12 .byte 22*4+movedown .byte 192 ;; 4 .byte 13+192 .byte 7 .byte 6*4+moveleft .byte 22*4+movedown .byte 6*4+moveright .byte 23+192 .byte 7 .byte 6*4+moveright .byte 22*4+movedown .byte 6*4+moveleft .byte 192 ;; 5 .byte 7+192 .byte 7 .byte 22*4+movedown .byte 11*4+moveright .byte 18+192 .byte 18 .byte 18+192 .byte 7 .byte 11*4+moveright .byte 22*4+movedown .byte 192 ;; 6 .byte 7+192 .byte 7 .byte 22*4+movedown .byte 12+192 .byte 7 .byte 22*4+movedown .byte 29+192 .byte 7 .byte 22*4+movedown .byte 24+192 .byte 7 .byte 22*4+movedown .byte 192 ;; 7 .byte 6+192 .byte 6 .byte 24*4+moveright .byte 6+192 .byte 9 .byte 10*4+moveright .byte 21*4+movedown .byte 30+192 .byte 9 .byte 10*4+moveleft .byte 21*4+movedown .byte 192 ;; 8 .byte 6+192 .byte 6 .byte 24*4+moveright .byte 6+192 .byte 9 .byte 24*4+moveright .byte 6+192 .byte 12 .byte 22*4+movedown .byte 9+192 .byte 12 .byte 22*4+movedown .byte 30+192 .byte 12 .byte 22*4+movedown .byte 27+192 .byte 12 .byte 22*4+movedown .byte 192 tileset: ;; note: every bitmap uses a byte from the next tile ;; move up .byte 00000000b .byte 00000000b .byte 00111000b .byte 00111000b .byte 00111000b .byte 00111000b ;; move down .byte 00111000b .byte 00111000b .byte 00111000b .byte 00111000b .byte 00111000b .byte 00000000b ;; move right .byte 00000000b .byte 00000000b .byte 11111000b .byte 11111000b .byte 11111000b .byte 00000000b ;; move right .byte 00000000b .byte 00000000b .byte 00111110b .byte 00111110b .byte 00111110b .byte 00000000b .byte 00000000b .byte 01111100b .byte 01111100b .byte 01111100b .byte 01111100b .byte 01111100b .byte 00000000b .byte 00000100b .byte 00111000b .byte 00111000b .byte 00111000b .byte 00000000b .byte 00000000b .byte 00111000b .byte 01000100b .byte 01000100b .byte 01000100b .byte 00111000b ;; note: moveup=0 snake2init: .byte moveup .byte 18 .byte 36 .byte 10 .byte 18 .byte 35 .byte 13 ; end BASIC line endline: .end