GRAVE-YARD

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

GRAVE-YARD is a two-character chase game in which the player maneuvers a ghost sprite around a bordered arena while a second ghost wanders randomly, and both characters must avoid collision with the arena’s underscore walls. The player moves using keys 5–8 (the standard Spectrum directional keys), with the player sprite changing shape based on the direction of travel using one of four UDGs (A–D). A 60-unit countdown timer drives the game’s pace, displayed at the bottom of the screen alongside a score that increments by three each time the wandering ghost (UDG F) catches the player’s ghost, triggering a respawn at fixed coordinates. Six UDGs are defined at startup via a DATA-driven POKE loop targeting USR “A” through USR “F”, covering directional player sprites, a collision sprite (UDG E), and the enemy sprite (UDG F). The high score is retained across rounds within the same session and displayed on the game-over screen alongside a prompt to restart.


Program Analysis

Program Structure

The program is organized into a main game loop with clearly separated initialization and setup subroutines:

  • 90009080: UDG definition — reads pixel data and POKEs six UDGs (A–F) via a loop over USR "A" to USR "F"+7.
  • 80008990: Game variable initialization — sets border/paper/ink, positions, score, timer, and initial sprite character.
  • 70007060: Screen setup — draws a bordered arena using INVERSE INK 6 underscores and prints the score line.
  • 50200: Main game loop — handles player input, player movement, enemy movement, collision detection, and timer management.
  • 10001060: Score/respawn handler — increments score, plays a beep sequence, and respawns both characters.
  • 20002050: Game-over sequence — displays score, updates high score, and prompts replay.

UDG Initialization

Subroutine 9000 defines six UDGs (A through F) by reading 8 bytes each from DATA statements at lines 90309080 and POKEing them into the UDG area. The loop iterates from USR "A" to USR "F"+7, covering all 48 bytes. UDGs A–D are directional player sprites (left, right, down, up), UDG E is a collision/capture sprite, and UDG F is the enemy ghost sprite.

Player Input and Movement

Lines 5170 implement a latching input scheme. INKEY$ is only accepted if it falls in the range "5""8"; otherwise the last direction stored in A$ is retained. Movement is computed using Boolean arithmetic:

  • V (row) changes by (A$="6")-(A$="7") — key 6 moves down, key 7 moves up.
  • H (column) changes by (A$="8")-(A$="5") — key 8 moves right, key 5 moves left.

The previous position is saved in V1, H1 before each move. If the new cell contains an underscore (wall), the move is cancelled by restoring V1, H1 and clearing A$ at line 90.

Directional Sprite Switching

Line 110 updates the player’s display character C$ based on the current direction key using concatenated Boolean string expressions — a common Spectrum BASIC idiom where a string multiplied by a true condition yields the string and by a false condition yields an empty string. The four cases map keys 5–8 to UDGs D, A, C, and B respectively. If no direction key is active (C$=""), the previous sprite D$ is retained (line 120).

Enemy Ghost Movement

The enemy ghost at coordinates (J,K) moves pseudo-randomly each frame (lines 150180). Its row and column each change by INT(RND*3)-1, giving a displacement of −1, 0, or +1. Like the player, it saves its prior position and cancels moves into wall cells. Collision with an empty cell (SCREEN$="") at line 175 triggers the catch event at line 1000.

Collision Detection via SCREEN$

Both player and enemy use SCREEN$ to detect the contents of the target cell before moving into it. An empty string result indicates the player and enemy have overlapped, triggering the score event at line 1000. An underscore character indicates a wall boundary, blocking movement.

Timer Mechanism

The timer variable TI is initialized to 60 and decremented by 0.2 each main loop iteration at line 125. This is a software timer driven by loop speed rather than the system clock. When TI drops below zero, the game-over routine at line 2000 is triggered. The display uses INT TI to show whole seconds.

Bugs and Anomalies

  • Line 1060 sets K=20 and then immediately overwrites it with K=16 — the intent was almost certainly H=16 to reset the player’s column, since H is the player’s horizontal coordinate. As written, H is never reset here, so the player respawns at whatever column they were at when the catch occurred.
  • Line 80 checks SCREEN$(V,H)="" after the player has already moved, meaning the check fires when the player moves into a cell occupied by the enemy — this is the intended catch condition, but the logic is inverted from what a reader might expect (empty screen = collision rather than a solid obstacle).
  • The BEEP .008,SC at line 130 pitches the movement sound to the current score, so the tone rises throughout the game as the score increases.

Key Variable Summary

VariablePurpose
V, HPlayer row and column
V1, H1Previous player position (for wall revert)
J, KEnemy ghost row and column
J1, K1Previous enemy position (for wall revert)
A$Current/latched direction key
C$Current player UDG sprite character
D$Previous player UDG sprite (fallback)
SCCurrent score
HISession high score
TICountdown timer (software-driven)

Image Gallery

Source Code

  10 REM "GRAVE-YARD"
   20 GO SUB 9000:LET HI=0
   30 GO SUB 8000
   40 GO SUB 7000
   50 PRINT AT V,H;" "
   51 IF INKEY$<"5" OR INKEY$>"8" THEN GO TO 55
   53 LET A$= INKEY$
   55 LET V1=V:LET H1=H
   60 LET v=v+(a$="6")-(a$="7")
   70 LET H=H+(A$="8")-(A$="5")
   80 IF SCREEN$ (V,H)="" THEN GO TO 1000
   90 IF SCREEN$ (V,H)="_" THEN LET V=V1:LET H=H1:LET A$="":GO TO 55
  100 PRINT AT V,H; INK 3;C$
  105 LET D$=C$
  110 LET C$=("[UDG-A]" AND A$="7")+("[UDG-B]" AND A$="8")+("[UDG-C]" AND A$="6")+("[UDG-D]" AND A$="5")
  120 IF C$="" THEN LET C$=D$
  125 PRINT AT 21,16;"TIME "; INT TI;" " :LET TI=TI-.2:IF TI<0 THEN GO TO 2000
  130 BEEP .008,SC
  140 PRINT AT J,K;" "
  150 LET J1=J:LET K1=K
  160 LET J=J+ INT (RND*3)-1:LET K=K+ INT (RND*3)-1
  170 IF SCREEN$ (J,K)="_" THEN LET j=j1:LET K=K1:GO TO 150
  175 IF SCREEN$ (J,K)="" THEN GO TO 1000
  180 PRINT AT J,K; INK 5;"[UDG-F]"
  200 GO TO 50
 1000 PRINT AT J,K;"[UDG-E]"
 1010 FOR A=1 TO 3
 1020 LET SC=SC+1
 1030 PRINT AT 21,0;"SCORE ";SC
 1040 BEEP .7,SC
 1050 NEXT A
 1060 LET J=4:LET K=20:LET V=10:LET K=16:GO TO 40
 2000 PRINT AT 21,16; PAPER 1;"TIME 0":BEEP 2,-10:CLS 
 2010 PRINT AT 2,12; PAPER 2;"GAME OVER"
 2020 PRINT AT 5,10; PAPER 2;"YOU SCORED ";SC
 2030 IF SC>HI THEN LET HI=SC
 2040 PRINT AT 18,6; PAPER 6;"HIGHEST SCORE TODAY";HI
 2050 INPUT PAPER 1;"PRESS "; PAPER 2;"ENTER"; PAPER 1;" TO PLAY AGAIN "; LINE A$:GO TO 30
 7000 CLS :PRINT INVERSE 1; INK 6;"________________________________"
 7010 FOR A=1 TO 19
 7020 PRINT INVERSE 1; INK 6;"_"; AT A,31;"_"
 7030 NEXT A
 7040 PRINT INVERSE 1; INK 6;"________________________________"
 7050 PRINT AT 21,0;"SCORE ";SC
 7060 RETURN 
 8000 BORDER 0:PAPER 0:INK 9:CLS 
 8010 LET V=10:LET H=16
 8020 LET J=4:LET K=20
 8030 LET SC=0:LET TI=60
 8040 LET C$="[UDG-A]"
 8050 LET A$=""
 8990 RETURN 
 9000 FOR A= USR "A" TO USR "F"+7
 9010 READ USER:POKE A,USER
 9020 NEXT A:RETURN 
 9030 DATA 60,153,255,153,24,189,255,189
 9040 DATA 236,68,229,255,255,229,68,238
 9050 DATA 189,255,189,24,153,255,153,60
 9060 DATA 119,34,167,255,255,167,34,119
 9070 DATA 56,124,238,198,238,238,254,254
 9080 DATA 56,56,16,124,16,16,40,68
 9998 REM A B C D E F
 9999 REM [UDG-A] [UDG-B] [UDG-C] [UDG-D] [UDG-E] [UDG-F] 

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