This program implements a Pac-Man-style maze game called “Spacman” in which the player navigates a dot-eating character through a 20×20 maze while being pursued by two ghosts. The maze is stored as a 20×20 string array, with solid wall cells represented by the block graphic █ (two-cell wide), and dots encoded as periods within the array strings. Player movement is controlled via keys 5–8 for left, down, up, and right, with collision detection performed by comparing array cells against the wall character before allowing movement. Five UDG characters are defined via POKEs to USR addresses: directional Pac-Man mouth sprites for up, right, down, and left (UDGs \u, \r, \d, \l), plus a ghost sprite (UDG \a). The two ghosts use a simple AI that biases random movement toward the player’s current coordinates, and the game triggers a level-clear animation subroutine at scores of 180, 460, and 740 before resetting the maze.
Originally published in Sinclair Programs – type-in from issue #4 – 1982/Nov.
Program Analysis
Program Structure
The program is organized into several clearly separated functional blocks:
- Lines 1–3: Title REM and variable initialization (player position, ghost positions, score, direction).
- Lines 9–210: Maze definition and initial rendering. The 20×20 string array
a$is populated row by row, then printed. - Lines 1000–3130: Main game loop — input reading, player movement, dot collection, ghost AI, and collision detection.
- Lines 7000–7060: Level-clear animation subroutine showing the player sprite traversing the screen.
- Lines 9000–9080: UDG initialization subroutine using DATA and POKE to define five custom characters.
- Line 9999: SAVE statement with auto-run flag.
Maze Representation
The maze is stored as a DIM a$(20,20) two-dimensional string array, where each row is exactly 20 characters wide. Wall cells are represented by the block graphic █ (\::), and open passages contain either a dot "." or a space " " after being eaten. This dual-purpose array simultaneously acts as the game map and tracks which dots have been consumed — when the player moves onto a dot cell, that entry is replaced with a space (line 1000), and the score increments at line 1070. The printed maze is offset one column to the right by printing a leading "\::" before each row at line 210, effectively adding a left border wall.
UDG Sprite Definitions
Subroutine 9000 defines five UDG characters using POKE and READ from DATA statements:
| UDG | Lines | Represents |
|---|---|---|
\u | 9045 | Pac-Man facing up |
\r | 9050 | Pac-Man facing right |
\d | 9060 | Pac-Man facing down |
\l | 9070 | Pac-Man facing left |
\a | 9075 | Ghost |
The direction variable d$ is therefore both a movement indicator and a directly printable UDG sprite character — the current direction string is printed at the player’s position (line 1064), so the sprite automatically reflects the direction of travel without any additional logic.
Player Movement and Collision Detection
Lines 1060–1063 handle movement in all four directions. Before updating the player’s row (x) or column (y), each line checks that the destination cell in a$ is not a wall ("\::"). This array-lookup collision method is efficient and requires no separate collision map. The old player position is erased with a space at line 1055 before the new position is calculated and printed at line 1064 with INK 0.
Ghost AI
Two ghosts are tracked by coordinate pairs (gx,gy) and (gx1,gy1). Each ghost’s movement is decided by four independent checks (lines 1080–2010 for ghost 1; lines 3080–3110 for ghost 2). Each check uses the idiom INT (RND*2)+(gx>x) — the Boolean comparison yields 0 or 1 and is added to a random 0–1 value, giving a result of 0, 1, or 2. The move only occurs when this sum is non-zero AND the target cell is not a wall. This produces a bias toward the player rather than a strict chase, adding unpredictability. Ghost 1 is printed in INK 3 (yellow) and ghost 2 in INK 4 (green).
Score Milestones and Level Reset
Line 1001 checks for scores of 180, 460, and 740, which correspond to levels being cleared. On a match, the score is incremented by 100 (to prevent re-triggering), the level-clear animation at subroutine 7000 is called, and then execution jumps back to line 10 to rebuild and redisplay the maze. This means the maze resets completely but dot consumption is not tracked between levels — the array is reloaded from the literal string assignments each time.
Level-Clear Animation
Subroutine 7000 (lines 7000–7060) animates the Pac-Man sprite moving right across row 10 of the screen (columns 0–25) with a ghost trailing behind, using BEEP for sound effects. It then reverses the animation, sweeping left. The ghost sprite in this sequence is displayed with INK 5 (cyan). A fixed “O” is printed at column 29 as a static target element throughout the forward sweep.
Notable Techniques and Idioms
- Using
d$as both the direction state variable and a printable UDG character is an elegant space-saving technique. - The maze is defined entirely in string literals without any procedural generation, making level layouts hand-crafted.
- Line 1080 prints the ghost’s current cell content from
a$(gx,gy)to erase it before the ghost moves — this restores walls or dots beneath the ghost sprite correctly. - Dot-eating feedback uses
BEEP .1,-10(a low tone) immediately after incrementing the score. - Collision between player and either ghost triggers
FLASH 1on the player character followed bySTOP, providing a visual death indicator without a dedicated game-over routine.
Potential Bugs and Anomalies
- The ghost movement code at lines 1080–2010 does not update
a$(gx,gy)to restore the cell before moving, only printing it. If a ghost moves over a dot cell, the dot character is visually restored buta$still contains the dot, meaning the player could re-eat it for points — though in practice the ghost’s PRINT at line 1080 only affects the display. - At line 1001, the score is incremented by 100 to skip the trigger value, but the conditions are fixed values (180, 460, 740) rather than a general “all dots eaten” check, so level resets depend on reaching exact scores.
- Ghost 2’s movement (lines 3080–3110) checks
a$(gx1,gy1-1)for horizontal movement but uses the updatedgx1from the vertical movement checks immediately above, meaning vertical and horizontal movement can interact within the same frame.
Content
Source Code
1 REM spacman
2 GO SUB 9000
3 LET r=.01: LET s=0: LET gx=10: LET gx1=2: LET gy1=2: LET gy=10: LET x=19: LET y=2: LET d$="\r": BORDER 1: PAPER 1: INK 1: CLS : PAPER 7
9 DIM a$(20,20)
10 LET a$(1)="\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::"
20 LET a$(2)="\::......\::\::\::\::\::\::......\::"
30 LET a$(3)="\::.\::\::\::\::.\::\::\::\::\::\::.\::\::\::\::.\::"
40 LET a$(4)="\::..\::\::...\::\::\::\::...\::\::..\::"
50 LET a$(5)="\::\::....\::..\::\::..\::....\::\::"
60 LET a$(6)="\::..\::\::.\::\::....\::\::\::\::\::..\::"
70 LET a$(7)="\::.\::\::\::.\::\::\::.\::.......\::\::"
80 LET a$(8)="\::......\::\::...\::\::\::\::\::.\::\::"
90 LET a$(9)="\::\::\::\::\::\::..\::.\::...\::\::\::..\::"
100 LET a$(10)="\::......\::\::.\::\::\::...\::\::.\::"
110 LET a$(11)="\::\::\::.\::\::\::\::...\::\::\::.\::\::..\::"
120 LET a$(12)="\::.....\::\::.......\::\::.\::\::"
130 LET a$(13)="\::.\::\::\::\::\::.....\::\::.....\::"
140 LET a$(14)="\::...\::\::\::\::\::.\::..\::.\::.\::\::\::"
150 LET a$(15)="\::\::.\::\::.....\::\::......\::\::"
160 LET a$(16)="\::.....\::\::\::.\::\::.\::.\::\::..\::"
170 LET a$(17)="\::\::.\::\::\::\::\::\::.\::\::....\::\::.\::"
180 LET a$(18)="\::\::.\::\::.............\::\::"
190 LET a$(19)="\::.....\::\::.....\::\::\::\::..\::"
200 LET a$(20)="\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::"
210 PRINT ': FOR f=1 TO 20: PRINT "\::";a$(f): NEXT f
1000 LET a$(x,y)=" "
1001 IF s=180 OR s=460 OR s=740 THEN LET s=s+100: PAPER 1: CLS : GO SUB 7000: PAPER 1: CLS : PAPER 7: GO TO 10
1010 IF INKEY$="" THEN GO TO 1055
1020 IF INKEY$="5" THEN LET d$="\l"
1030 IF INKEY$="6" THEN LET d$="\d"
1040 IF INKEY$="7" THEN LET d$="\u"
1050 IF INKEY$="8" THEN LET d$="\r"
1055 PRINT AT x,y;" "
1060 IF d$="\u" AND a$(x-1,y)<>"\::" THEN LET x=x-1
1061 IF d$="\r" AND a$(x,y+1)<>"\::" THEN LET y=y+1
1062 IF d$="\d" AND a$(x+1,y)<>"\::" THEN LET x=x+1
1063 IF d$="\l" AND a$(x,y-1)<>"\::" THEN LET y=y-1
1064 PRINT AT x,y; INK 0;D$
1070 IF a$(x,y)="." THEN LET s=s+1: PRINT AT 0,0;s: BEEP .1,-10
1080 PRINT AT gx,gy;a$(gx,gy): IF INT (RND*2)+(gx>x) AND a$(gx-1,gy)<>"\::" THEN LET gx=gx-1
1090 IF INT (RND*2)+(gx<x) AND a$(gx+1,gy)<>"\::" THEN LET gx=gx+1
2000 IF INT (RND*2)+(gy>y) AND a$(gx,gy-1)<>"\::" THEN LET gy=gy-1
2010 IF INT (RND*2)+(gy<y) AND a$(gx,gy+1)<>"\::" THEN LET gy=gy+1
2015 PRINT AT gx,gy; INK 3;"\a"
2020 IF (gx=x AND gy=y) OR (gx1=x AND gy1=y) THEN PRINT AT x,y; FLASH 1;d$: STOP
3080 PRINT AT gx1,gy1;a$(gx1,gy1): IF INT (RND*2)+(gx1>x) AND a$(gx1-1,gy1)<>"\::" THEN LET gx1=gx1-1
3090 IF INT (RND*2)+(gx1<x) AND a$(gx1+1,gy1)<>"\::" THEN LET gx1=gx1+1
3100 IF INT (RND*2)+(gy1>y) AND a$(gx1,gy1-1)<>"\::" THEN LET gy1=gy1-1
3110 IF INT (RND*2)+(gy1<y) AND a$(gx1,gy1+1)<>"\::" THEN LET gy1=gy1+1
3120 PRINT AT gx1,gy1; INK 4;"\a"
3130 GO TO 1000
7000 FOR f=0 TO 25
7001 PRINT AT 10,29; INK 7;"O"
7010 PRINT AT 10,f; INK 3;" \a "; INK 6;"\r"
7020 BEEP .1,-15
7030 NEXT f
7035 BEEP .2,-20
7040 FOR f=25 TO 0 STEP -1
7050 PRINT AT 10,f; INK 5;"\a "; INK 6;"\l "
7055 BEEP 0.1,0
7060 NEXT f: CLS : RETURN
8999 STOP
9000 DATA 0,BIN 1000010,BIN 11100111,255,255,BIN 1111110,BIN 111100,BIN 11000
9010 DATA BIN 000011100,BIN 111110,BIN 1111100,BIN 1111000,BIN 1111000,BIN 1111100,BIN 111110,BIN 11100
9020 DATA 0,BIN 00011000,BIN 00111100,BIN 01111110,255,255,BIN 11100111,BIN 01000010,0
9030 DATA BIN 001110000,BIN 011111000,BIN 001111100,BIN 000111110,BIN 000111110,BIN 001111100,BIN 011111000,BIN 001110000
9035 DATA 24,60,126,219,219,255,219,145
9040 RESTORE 9000
9045 FOR f=0 TO 7: READ a: POKE USR "\u"+f,a: NEXT f
9050 FOR f=0 TO 7: READ a: POKE USR "\r"+f,a: NEXT f
9060 FOR f=0 TO 7: READ a: POKE USR "\d"+f,a: NEXT f
9070 FOR f=0 TO 7: READ a: POKE USR "\l"+f,a: NEXT f
9075 FOR f=0 TO 7: READ a: POKE USR "\a"+f,a: NEXT f
9080 RETURN
9999 SAVE "spacman" LINE 1: BEEP 1,33
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
