Death Race is a two-entity chase game in which the player maneuvers a directional car sprite around a bordered arena while a randomly moving enemy car pursues them, with a 60-unit countdown timer adding urgency. The player uses keys 5–8 for movement, and the car’s heading is represented by one of four custom UDG characters (a–d) that change depending on the last directional key pressed. Six UDGs are defined via POKE and DATA statements: four directional player cars, one collision explosion graphic, and one enemy car. Collision detection relies on SCREEN$ to test whether the destination cell contains a blank space, a border underscore, or another sprite. The score increments on each successful collision with the enemy, and a high-score variable persists across rounds within the same session.
Program Analysis
Program Structure
The program is organized into a main game loop and several subroutines with well-defined responsibilities:
- Lines 10–200: Main game loop — handles player input, movement, collision detection, enemy movement, and display.
- Lines 1000–1060: Collision/scoring handler — awards a point, plays a sound, resets positions, and restarts the round.
- Lines 2000–2050: Game-over sequence — displays score, compares to high score, prompts for replay.
- Lines 7000–7060: Arena drawing subroutine — draws a bordered play field using inverse underscores.
- Lines 8000–8990: Variable initialization subroutine — sets player/enemy positions, score, timer, and initial sprite direction.
- Lines 9000–9080: UDG loader — POKEs six custom characters (a–f) from DATA statements.
- Lines 9500–9510: REM comments labeling the six UDGs for reference.
UDG Sprite Definitions
Six UDGs are loaded into memory by subroutine 9000, which iterates from USR "a" to USR "f"+7, POKEing 8 bytes each from six DATA lines. Their roles are:
| UDG | Escape | Role |
|---|---|---|
| a | \a | Player car facing up |
| b | \b | Player car facing right |
| c | \c | Player car facing down |
| d | \d | Player car facing left |
| e | \e | Collision/explosion graphic |
| f | \f | Enemy car sprite |
Player Movement and Direction Sprite
Movement uses keys 5–8 in the standard Sinclair layout. Lines 60–70 update vertical (v) and horizontal (h) coordinates using Boolean arithmetic: LET v=v+(a$="6")-(a$="7") neatly adds or subtracts 1 depending on which key is held. Line 51 only updates a$ when a valid movement key is detected, preserving the last direction when no key is pressed.
Line 110 updates the current car sprite c$ based on a$, using string concatenation with Boolean masking — e.g., ("\a" AND a$="7") returns the UDG character if the condition is true, or an empty string otherwise. Line 120 falls back to the previous sprite d$ if no directional key is active, so the car always displays a valid heading.
Collision Detection via SCREEN$
All collision detection is performed by reading back screen content with SCREEN$, avoiding the need for separate coordinate arrays. Three distinct outcomes are handled:
SCREEN$(v,h)=""(line 80) — destination is empty, movement is allowed; but this also triggers the scoring event at line 1000 if it coincides with the enemy position overwrite. Actually, a blank cell at the player’s new position goes to line 1000, implying a hit on the enemy (whose cell was just cleared at line 140).SCREEN$(v,h)="_"(line 90) — wall collision; reverts position and clears direction.- Any other content (line 100 proceeds) — cell is occupied by the enemy sprite, causing a score event at line 1000 via the blank-cell test after the enemy moves.
The enemy uses the same technique at lines 170–175: if its randomly chosen new cell contains an underscore it retries, and if it contains a blank it triggers line 1000.
Enemy Movement
The enemy moves pseudo-randomly each frame. Line 160 computes j=j+INT(RND*3)-1 and similarly for k, giving a ±1 or 0 delta in each axis each tick. If the target cell is a wall ("_"), the move is retried (lines 170–150 loop back), though there is no limit on retries, which could theoretically stall in a tight corner. The enemy is drawn in INK 5 (cyan) to distinguish it from the player.
Timer and Scoring
The timer ti is initialized to 60 and decremented by 0.2 each main loop iteration at line 125, giving approximately 300 loops before time expires. Score sc increments by 1 per collision (three BEEP tones are played in a FOR loop at lines 1010–1050). A session high score hi is maintained across replays but is reset only at line 20 on first run.
Notable Bugs and Anomalies
- Line 1060 duplicate assignment:
LET k=20: LET v=10: LET k=16—kis assigned twice; the final value is 16, and the intermediate assignment of 20 is dead code. The intended value ofh(the player’s horizontal position) is likely meant to be reset here as well, buthis never reassigned in line 1060, so the player’s column position is not reset between scoring events. - Line 80 logic: The condition
SCREEN$(v,h)=""is used to detect a hit, relying on the enemy having just been erased at line 140. This is fragile and depends on execution order; if the enemy and player happen to be on the same cell without the erase having occurred, the check may not fire correctly. - Line 100 uses uppercase
V,Hinstead of lowercasev,h. In Sinclair BASIC, variable names are case-sensitive only for string variables; numeric variable names are case-insensitive, soVandvrefer to the same variable. This is not a bug but is inconsistent style.
Display and Sound
The arena border is drawn using inverse INK 6 (yellow) underscores filling the top and bottom rows and the leftmost and rightmost columns. Sound feedback is continuous: line 130 emits a short BEEP each loop iteration at pitch sc, so the tone rises as the score increases. Game-over and scoring events use longer BEEPs with fixed or score-relative pitches.
Content
Source Code
10 REM DEATH RACE from Games for Your Timex-Sinclair 2000, p.63
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$=("\a" AND a$="7")+("\b" AND a$="8")+("\c" AND a$="6")+("\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;"\f"
200 GO TO 50
1000 PRINT AT j,k;"\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,26; 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$="\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 238,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
9500 REM a b c d e f
9510 REM \a \b \c \d \e \f
9998 SAVE "Race" LINE 1: BEEP .2,15
9999 VERIFY ""
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
