Snaker

Date: 198x
Type: Program
Platform(s): TS 2068
Tags: Arcade

Snaker is a snake-movement game where the player steers a growing snake around a playfield, eating dots while avoiding collisions with the snake’s own body and managing a countdown timer. The playfield is generated with randomly placed food dots using INK, PAPER, and FLASH attributes, and collision detection relies on reading screen attributes via the ATTR function rather than tracking object positions in arrays. Two custom UDG characters are defined—one for the snake body and one for the food dots—using POKE USR with bitmap data read from DATA statements. A 55-byte machine code routine is loaded into RAM at address 65200 via POKE, called through USR, and handles sound effects (writing to port 254) and screen operations using direct Z80 instructions. The game tracks waves, score accumulation based on remaining time and wave number, and maintains a top-10 high score table with four-character initials, stored in arrays across sessions within a single run.


Program Structure

The program is organized into functional blocks across its line numbers. Lines 2027 form an initialization routine: setting screen attributes, defining UDGs, poking machine code, and seeding the high score table. Lines 19 are the core game loop handling movement, collision, and display. Lines 1013 generate the playfield and advance the wave. Lines 1419 handle death, the high score screen, and the instructions screen. Line 30 is a cleanup/quit routine, and lines 35, 110, and 9990 form a tape-loading subroutine.

UDG Definition

Two UDG characters are defined in lines 2123. The program reads each character name ("\\o" for the snake body, a filled circle shape; "\\a" for the food dot, a smaller square) and eight bytes of bitmap data, then POKEs them into USR b$—the standard Spectrum method for writing UDG pixel patterns. This allows the playfield and snake to use readable, distinctive glyphs rather than standard character set symbols.

Machine Code Routines

Two machine code entry points are embedded at addresses 65200 and 65232 within a 55-byte block poked in line 24 from DATA at line 25. Both are invoked via USR assigned to a dummy variable u. The routine at 65200 appears to produce a sound effect by writing to I/O port 254 (OUT 254, opcode D3 FE), looping with a counter. The routine at 65232 is called during normal snake movement and food consumption. Because USR returns a value, assigning it to u is the standard BASIC idiom for calling machine code as a statement without losing the return value.

Collision Detection via ATTR

Rather than maintaining a full collision map or checking coordinate arrays, the game uses ATTR (x,y) to read the screen attribute byte at the snake’s new head position. A result of 242 identifies a food dot cell (INK 2, PAPER 6, FLASH 1, BRIGHT 0 encodes to this value), while 67 identifies the snake’s own body (INK 3, PAPER 0 encodes accordingly). This is a compact and efficient technique that offloads collision state entirely to the display file.

Snake Position Tracking

The snake’s segment positions are stored in parallel arrays x() and y(), dimensioned to 1005 elements each at line 2. The variable a is an index that advances by 2 each step (line 5), storing both a midpoint and the new head. The tail is erased by printing a space at position x(a-l), y(a-l), where l tracks the effective length. This sliding-window approach avoids shifting array contents on each move.

Movement and Direction Logic

Direction is encoded in c$ (a single INKEY$ character). At line 3, the deltas xt and yt are computed using Boolean arithmetic: 2*(c$="6")-2*(c$="7") gives horizontal movement, and 2*(c$="8")-2*(c$="5") gives vertical movement, corresponding to the Spectrum’s cursor keys. The step size of 2 is used because the playfield uses even-numbered columns (the food is placed at even coordinates in line 11), keeping the snake aligned to the grid.

Playfield Generation

Line 11 scatters food dots on even rows (0, 2, 4 … 18) using random even column positions. A mild collision-avoidance check LET q=q-2*(q=o) shifts one dot if both would land on the same column. Line 12 prints a row of UDG-A characters on odd rows to create a textured background pattern. The score display line at row 21 shows the countdown timer t, accumulated score s, current wave w, and remaining lives represented as snake-body glyphs conditionally printed based on sl.

Scoring and Wave Progression

Score is accumulated at two points: on completing a wave (s=s+t*w at line 10, rewarding speed and wave depth) and on losing a life (s=s+w*(550-t) at line 14). The food counter p is checked against 20 at line 6; eating 20 dots advances to the next wave. The snake starting length bl increases by 3 each wave up to a cap of 31, and the time limit t decreases as 550-10*a where a=bl+1, making each wave harder.

High Score Table

Lines 1516 implement a 10-entry high score table using numeric array h(10) and string array h$(10,4) for four-character initials. A reverse loop from 10 to 2 shifts entries downward using the Boolean expression h(f-(h(f-1)<s)), a compact insertion-sort idiom. The table is pre-seeded in line 26 with scores 10000 down to 1000 and initials "MF ".

Instructions and Menu Loop

Line 17 draws a shrinking rectangular border using OVER 1 (XOR plotting), looping by re-executing itself: GO TO 17+(yl=-1 OR INKEY$="Y")-16*(INKEY$="N")+13*(INKEY$="Q"). This single-line conditional branch dispatches to line 18 (instructions), line 1 (new game), or line 30 (quit) depending on keypress, without any explicit IF statement. Line 19 similarly busy-waits for a keypress with GO TO 19*(INKEY$=""), branching to line 0 (falling through to start) when any key is pressed.

Notable Anomalies

  • Line 6 checks p=20 after incrementing p, but p is initialized only at line 10 (wave start), not at line 2 (life start), so a player resuming after a lost life retains the previous food count — potentially a minor bug.
  • Line 8 prints both the tail-erase and two snake segments in a single PRINT statement using multiple AT items, which is efficient but means the display update is not atomic with the movement logic.
  • The subroutine at line 100 (called from line 20) and the related code at lines 35 and 110 suggest a tape-loading wrapper that was part of the distribution mechanism; line 35 prompts to start the tape and performs a LOAD "".
  • Line 9999 saves the program with LINE 20, auto-starting at the initialization block rather than line 1.

Image Gallery

Source Code

    0 REM                             \o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o       \o a ZX SPECTRUM game by \o       \o     M.F. van Vuren    \o       \o                       \o       \o     GRAPHIC A=\a       \o       \o     GRAPHIC O=\o       \o       \o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o\o
    1 LET s=0:LET w=0:LET t=0:LET sl=3:LET bl=1:GO TO 10
    2 LET x=20:LET y=30:DIM x(1005):DIM y(1005):LET l=bl+1:LET a=l:LET t=550-10*a:LET c$="7":FOR f=1 TO bl:LET x(f)=x:LET y(f)=(y-bl)+f:PRINT AT x(f),y(f); INK 3;"\o":LET u= USR 65232:NEXT f
    3 LET xt=0:LET yt=0:LET xt=2*(c$="6")-2*(c$="7"):LET yt=2*(c$="8")-2*(c$="5")
    4 IF x+xt<0 OR x+xt>20 OR y+yt<0 OR y+yt>30 THEN GO TO 9
    5 LET x=x+xt:LET y=y+yt:LET x(a)=x+(xt=-2)-(xt=2):LET y(a)=y+(yt=-2)-(yt=2):LET x(a+1)=x:LET y(a+1)=y:LET a=a+2:LET l=l+1
    6 IF ATTR (x,y)=242 THEN LET u= USR 65232:LET p=p+1:IF p=20 THEN GO TO 10
    7 IF ATTR (x,y)=67 OR t <=0 THEN GO TO 14
    8 PRINT AT x(a-l),y(a-l);" "; AT x(a-2),y(a-2); INK 3;"\o"; AT x,y;"\o"
    9 LET c$= INKEY$:LET t=t-1:PRINT AT 21,5;t;" ":GO TO 3+(c$="")
   10 LET p=0:LET w=w+1:LET bl=bl+3*(bl<31):LET s=s+t*w:CLS 
   11 FOR f=0 TO 18 STEP 2:LET q=(INT (RND*14)*2)+2:LET o= INT (RND*16)*2:LET q=q-2*(q=o):PRINT AT f,q; INK 2; PAPER 6; FLASH 1;"\a"; AT f,o;"\a":NEXT f
   12 FOR f=1 TO 19 STEP 2:PRINT AT f,1;"\a \a \a \a \a \a \a \a \a \a \a \a \a \a \a":NEXT f
   13 PRINT AT 21,0;,,; AT 21,0;"TIME:"; AT 21,9;"SCORE:";s; AT 21,22;"WAVE:";w; AT 21,30;"\o" AND sl >=2;"\o" AND sl=3:GO TO 2
   14 PRINT AT x(a-2),y(a-2);"\o":LET u= USR 65200:FOR f=a-l TO a-2:PRINT AT x(f),y(f);" ":NEXT f:LET sl=sl-1:LET s=s+w*(550-t):GO TO 13+2*(sl=0)
   15 CLS :PRINT AT 2,12; INK 3;"SNAKER":IF s>h(10) THEN PRINT AT 4,7;"CONGRATULATIONS!!!":INPUT "YOUR INITIALS,PLEASE!!!"; LINE n$:FOR f=10 TO 2 STEP -1:LET h(f)=h(f-(h(f-1)<s)):LET h$(f)=h$(f-(h(f-1)<s)):IF h(f)<s THEN LET h(f-1)=s:LET h$(f-1, TO 4)=n$
   16 NEXT f:PRINT AT 6,4;"HI-SCORE:"; AT 6,21;"INITIALS:":FOR f=8 TO 17:PRINT AT f,2;f-7; AT f,6;h(f-7); AT f,21;h$(f-7):NEXT f:PRINT AT 20,8;"DO YOU REQUIRE"'" PLAYING INSTRUCTIONS?(Y or N)":PRINT #1;"     Or Press 'Q' to QUIT":LET x=0:LET y=0:LET xl=255:LET yl=175
   17 OVER 1:PLOT x,y:DRAW xl,0:DRAW 0,yl:DRAW -xl,0:DRAW 0,-yl+1:LET x=x+1:LET y=y+1:LET xl=xl-2:LET yl=yl-2:OVER 0:GO TO 17+(yl=-1 OR INKEY$="Y")-16*(INKEY$="N")+13*(INKEY$="Q")
   18 CLS :FOR f=7 TO 4 STEP -1:INK f:PRINT AT 2,12;"SNAKER"; AT 4,0;"Move snake with the CURSOR keys","and munch the dots in the field","Try to avoid a crash with the","body of the snake","When you come further into the","game the snake starts with a","greater while your time","is less"; AT 13,11;"Good luck!":LET u= USR 65232:NEXT f
   19 GO TO 19*(INKEY$="")
   20 GO SUB 100:BORDER 0:PAPER 0:INK 4:BRIGHT 1:CLEAR 65199:RESTORE 
   21 FOR f=1 TO 2:READ b$:FOR a=0 TO 7:READ x:POKE USR b$+a,x:NEXT a:NEXT f
   22 DATA "\o",60,126,126,255,255,126,126,60
   23 DATA "\a",0,0,60,60,60,60,0,0
   24 FOR f=65200 TO 65254:READ x:POKE f,x:NEXT f
   25 DATA 58,72,92,31,31,31,230,7,225,229,95,14,0,22,15,126,230,16,131,211,254,65,16,254,35,21,32,243,13,32,238,201,1,80,0,33,0,5,237,66,17,1,0,229,197,205,181,3,193,225,124,167,32,240,201
   26 DIM h(10):DIM h$(10,4):LET s=10000:FOR f=1 TO 10:LET h(f)=s:LET h$(f)="MF ":LET s=s-1000:NEXT f:POKE 23658,8
   27 GO TO 15
   30 STOP :POKE 23658,0:BRIGHT 0:INK 9:CLEAR 
   35 PRINT AT 10,8;"START THE TAPE":LOAD ""
  110 POKE 23733,255:POKE 23676,255
 9990 RETURN 
 9999 CLEAR :SAVE "snaker" LINE 20

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.