Pavement Panic (Amstrad CPC) by muckypaws
A downloadable game
========================================================
PAVEMENT PANIC
A 10-line arcade game for the Amstrad CPC
BASIC 10 Competition Entry - Category: PUR-120
------------------------------------------------------------
Author : Jason Brooks
Website : www.muckypaws.com
Date : 19th March 2026
========================================================
DESCRIPTION
-----------
Pavement Panic is a top-down arcade game for the Amstrad CPC.
You control an animated walking figure navigating four lanes of oncoming ghosts. The longer you survive, the higher your score -- and the faster the ghosts become!
Four lanes of bright cyan ghosts haunt the screen. The top two lanes carry ghosts travelling left, while the bottom two lanes carry ghosts travelling right. This creates a natural two-way road feel where crossing the central reservation is always the most dangerous moment.
The game starts at a manageable pace but gradually becomes more frantic. Each time a ghost completes a lap of the screen the global speed base increases slightly, and each ghost picks a new random speed from an ever-widening range. Early on the ghosts are slow and predictable; later they lurch across the road at wildly varying speeds, making every lane change a calculated risk.
The game features:
- A custom UDG ghost sprite with dome, eyes, scalloped skirt and two little feet
- A 4-frame animated walking sprite using the CPC's built-in CHR$(248)-CHR$(251) character set
- Progressive difficulty: ghost speeds increase every lap via a global base speed variable
- Sweep collision detection that catches fast-moving ghosts even when they jump over the player in a frame
- Persistent high score across multiple games
- Live high score update during play
- RANDOMIZE(TIME) seeding for a different game every time
HOW TO PLAY
-----------
O - Move LEFT
P - Move RIGHT
E - Move UP (change lane toward top of screen)
D - Move DOWN (change lane toward bottom of screen)
Avoid the bright cyan ghosts. Survive as long as possible.
Press SPACE to restart after being caught.
These controls follow the BASIC 10 competition standard layout, compatible with QWERTY, QWERTZ and AZERTY keyboards.
1 MODE 1:BORDER 0:INK 0,0:INK 1,8:INK 2,20:INK 3,26:SYMBOL 255,60,126,255,219,255,255,219,129:h=0:DIM ec(4),er(4),ed(4) 2 s=0:f=0:CLS:FOR r=2 TO 22 STEP 5:PEN 3:LOCATE 1,r:PRINT STRING$(40,"-"):NEXT r:RANDOMIZE(TIME):pc=20:pr=4:b=1.5 3 FOR i=1 TO 4:ec(i)=i*8:er(i)=i*5-1:ed(i)=i*2-5:NEXT i:PEN 1 4 LOCATE 14,24:PRINT"Score:";s;" Hi:";h:FOR i=1 TO 4:LOCATE ec(i),er(i):PRINT" ":ox=ec(i) 5 ec(i)=(ec(i)-1+ed(i)+400) MOD 40+1:IF pr=er(i) AND ABS(ec(i)-ox)<20 AND (pc-ox)*(pc-ec(i))<=0 THEN PEN 1:GOTO 10 6 IF ec(i)=1 OR ec(i)=40 THEN b=b-0.1*(b<6):ed(i)=SGN(ed(i))*(INT(RND*b)+1.5) 7 PEN 2:LOCATE ec(i),er(i):PRINT CHR$(255):NEXT i:s=s+1:h=h-(s-h)*(s>h) 8 LOCATE pc,pr:PRINT" ":k$=INKEY$:pr=pr+(k$="e")*5-(k$="d")*5:pr=((pr-4+20) MOD 20)+4 9 pc=pc+(k$="o")-(k$="p"):pc=(pc+39) MOD 40+1:f=f+1:PEN 3:LOCATE pc,pr:PRINT CHR$(248+(f MOD 4)):PEN 1:GOTO 4 10 LOCATE 10,13:PRINT"GAME OVER Score:";s;"HI:";h:LOCATE 14,15:PRINT"SPACE TO RESTART":WHILE INKEY$<>" ":WEND:GOTO 2
========================================================
LINE BY LINE BREAKDOWN
========================================================
LINE 1 - ONE TIME INITIALISATION
----------------------------------
1 MODE 1:BORDER 0:INK 0,0:INK 1,8:INK 2,20:INK 3,26:SYMBOL 255,60,126,255,219,255,255,219,129:h=0:DIM ec(4),er(4),ed(4)
MODE 1
Sets the screen to Mode 1: 40 columns x 25 rows, 4 colours.
BORDER 0
Sets the screen border to colour 0 (black).
INK 0,0 / INK 1,8 / INK 2,20 / INK 3,26
Maps the four colour indices to hardware colours:
Index 0 = black (background)
Index 1 = bright yellow (player, score display)
Index 2 = bright cyan / colour 20 (ghosts)
Index 3 = white (lane dividers)
SYMBOL 255,60,126,255,219,255,255,219,129
Defines CHR$(255) as a custom ghost sprite:
60 = 00111100 dome top
126 = 01111110 dome
255 = 11111111 solid body
219 = 11011011 eyes (two pixel gaps)
255 = 11111111 solid body
255 = 11111111 solid body
219 = 11011011 scalloped skirt
129 = 10000001 two little feet at outer edges
h=0
Initialises the high score to zero. The ONLY place h is set -- not reset on restart, so the high score persists across multiple games.
DIM ec(4),er(4),ed(4)
Declares the three ghost arrays. Placed on line 1 so the DIM is never re-executed on restart, which would cause a Locomotive BASIC "Array already dimensioned" error. The restart on line 10 jumps to line 2, safely skipping line 1.
LINE 2 - NEW GAME SETUP
------------------------
2 s=0:f=0:CLS:FOR r=2 TO 22 STEP 5:PEN 3:LOCATE 1,r:PRINT STRING$(40,"-"):NEXT r:RANDOMIZE(TIME):pc=20:pr=4:b=1.5
s=0:f=0
Resets current score and animation frame counter.
CLS
Clears the screen for a new game.
FOR r=2 TO 22 STEP 5:PEN 3:LOCATE 1,r:PRINT STRING$(40,"-"):NEXT r
Draws five horizontal rows of 40 dashes at rows 2, 7, 12, 17 and 22 as road lane dividers. PEN 3 (white).
Ghosts and player occupy rows 4, 9, 14 and 19 -- the gaps between the dividers.
RANDOMIZE(TIME)
Seeds the RNG from the CPC hardware timer. Ensures a different pattern of ghost speeds every game.
pc=20:pr=4
Places the player at the centre of the screen (column 20) in the top lane (row 4).
b=1.5
Resets the base speed to 1.5 for a fresh game. Ghost speeds start in the range 1-3 and grow from there.
LINE 3 - GHOST INITIALISATION
------------------------------
3 FOR i=1 TO 4:ec(i)=i*8:er(i)=i*5-1:ed(i)=i*2-5:NEXT i:PEN 1
FOR i=1 TO 4:ec(i)=i*8:er(i)=i*5-1:ed(i)=i*2-5:NEXT i
ec(i)=i*8
Staggers starting columns: 8, 16, 24, 32. Spreads ghosts evenly across the road at the start.
er(i)=i*5-1
Sets each ghost to its fixed lane row: 4, 9, 14, 19.
ed(i)=i*2-5
Sets initial speed and direction:
Ghost 1: -3 (left, fast) -- top lane
Ghost 2: -1 (left, slow) -- second lane
Ghost 3: +1 (right, slow) -- third lane
Ghost 4: +3 (right, fast) -- bottom lane
Top two lanes are oncoming (left), bottom two are outgoing (right). Crossing the centre is the most dangerous moment in the game.
PEN 1
Resets pen to 1 (yellow) ready for score display.
LINE 4 - SCORE DISPLAY AND GHOST ERASE
---------------------------------------
4 LOCATE 14,24:PRINT"Score:";s;" Hi:";h:FOR i=1 TO 4:LOCATE ec(i),er(i):PRINT" ":ox=ec(i)
LOCATE 14,24:PRINT"Score:";s;" Hi:";h
Prints score and high score at the bottom of the screen.
FOR i=1 TO 4
Begins the ghost update loop covering lines 4-7.
LOCATE ec(i),er(i):PRINT" "
Erases the ghost's current screen position before moving.
ox=ec(i)
Saves the ghost's current column BEFORE moving. Essential for the sweep collision check on line 5 -- if saved after the move it would always equal ec(i), making the sweep check impossible.
LINE 5 - GHOST MOVEMENT AND COLLISION DETECTION
------------------------------------------------
5 ec(i)=(ec(i)-1+ed(i)+400) MOD 40+1:IF pr=er(i) AND ABS(ec(i)-ox)<20 AND (pc-ox)*(pc-ec(i))<=0 THEN PEN 1:GOTO 10
ec(i)=(ec(i)-1+ed(i)+400) MOD 40+1
Moves the ghost and wraps it around the screen.
ec(i)-1 converts from 1-based to 0-based
+ed(i) adds the speed/direction delta
+400 adds 10*40 to guarantee a positive value before MOD, protecting against Locomotive BASIC's inconsistent MOD with negatives
MOD 40 wraps to range 0-39
+1 converts back to 1-based column
IF pr=er(i) AND ABS(ec(i)-ox)<20 AND (pc-ox)*(pc-ec(i))<=0
THEN PEN 1:GOTO 10
Three-part sweep collision check:
pr=er(i)
Player and ghost must be on the same row.
ABS(ec(i)-ox)<20
Distinguishes a real move from a wrap-around.
A real move at maximum speed covers at most ~7 columns. A wrap jumps 30+ columns. Threshold of 20 safely separates these two cases.
(pc-ox)*(pc-ec(i))<=0
Checks whether the player column lies between the ghost's old and new positions. When pc is between ox and ec(i), one factor is negative and one positive -- product is <=0. When pc is outside that range both factors share the same sign --product is positive (>0). Catches all pass-through cases regardless of ghost speed.
On collision: PEN 1 resets ink, GOTO 10 triggers game over.
LINE 6 - PROGRESSIVE DIFFICULTY AND SPEED RERANDOMISATION
----------------------------------------------------------
6 IF ec(i)=1 OR ec(i)=40 THEN b=b-0.1*(b<6):ed(i)=SGN(ed(i))*(INT(RND*b)+1.5)
IF ec(i)=1 OR ec(i)=40 THEN b=b-0.1*(b<6):ed(i)=SGN(ed(i))*(INT(RND*b)+1.5)
Triggers each time a ghost reaches either screen edge.
b=b-0.1*(b<6)
Increments the base speed by 0.1 each lap, capped at 6.
In Locomotive BASIC, (b<6) returns -1 when TRUE and 0 when FALSE. So -0.1*(-1) = +0.1 when below cap, and -0.1*(0) = 0 when at cap. No IF statement needed.
SGN(ed(i))
Returns -1 or +1, preserving lane direction.
INT(RND*b)+1.5
Generates a new random speed. As b grows the range of RND*b widens, so both the minimum and maximum possible speeds increase together:
b=1.5 (start): speeds roughly 1-3
b=3 (mid): speeds roughly 1-4
b=6 (max): speeds roughly 1-7
Late game ghosts become wildly unpredictable in speed while always travelling in their designated direction.
LINE 7 - GHOST DRAW, SCORE AND HIGH SCORE UPDATE
-------------------------------------------------
7 PEN 2:LOCATE ec(i),er(i):PRINT CHR$(255):NEXT i:s=s+1:h=h-(s-h)*(s>h)
PEN 2:LOCATE ec(i),er(i):PRINT CHR$(255)
Draws the ghost UDG in PEN 2 (bright cyan) at its new position.
NEXT i
Closes the ghost loop. All four ghosts have now been erased, moved, collision-checked, speed-updated if needed, and redrawn.
s=s+1
Increments the score by 1 for every game loop survived.
h=h-(s-h)*(s>h)
Updates the high score without an IF statement.
In Locomotive BASIC, (s>h) returns -1 for TRUE, 0 for FALSE.
When s>h: h = h-(s-h)*(-1) = h+(s-h) = s
When s<=h: h = h-(s-h)*(0) = h (unchanged)
The high score increments live on screen as the player beats their previous best.
LINE 8 - PLAYER ERASE AND VERTICAL MOVEMENT
--------------------------------------------
8 LOCATE pc,pr:PRINT" ":k$=INKEY$:pr=pr+(k$="e")*5-(k$="d")*5:pr=((pr-4+20) MOD 20)+4
LOCATE pc,pr:PRINT" "
Erases the player at the current position.
k$=INKEY$
Reads the currently pressed key (non-blocking).
pr=pr+(k$="e")*5-(k$="d")*5
Updates the player row using boolean arithmetic.
(k$="e") returns -1 if E is held, multiplied by 5 moves exactly one lane. E moves up, D moves down.
pr=((pr-4+20) MOD 20)+4
Wraps the player row within the valid lane range.
pr-4 shifts 4,9,14,19 to 0,5,10,15.
+20 keeps value positive before MOD.
MOD 20 wraps to 0,5,10,15.
+4 shifts back to 4,9,14,19.
LINE 9 - PLAYER HORIZONTAL MOVEMENT AND ANIMATION
--------------------------------------------------
9 pc=pc+(k$="o")-(k$="p"):pc=(pc+39) MOD 40+1:f=f+1:PEN 3:LOCATE pc,pr:PRINT CHR$(248+(f MOD 4)):PEN 1:GOTO 4
pc=pc+(k$="o")-(k$="p")
O moves left (decreases column), P moves right.
pc=(pc+39) MOD 40+1
Wraps the player column around the screen edges.
f=f+1
Increments the animation frame counter.
PEN 3:LOCATE pc,pr:PRINT CHR$(248+(f MOD 4))
Draws the player in PEN 3 (white) using one of the four built-in CPC walking figure UDG characters.
f MOD 4 cycles 0,1,2,3 giving CHR$(248)-CHR$(251).
These are factory-defined animation frames present in all CPC ROM versions -- no SYMBOL needed.
PEN 1:GOTO 4
Resets pen to yellow and loops back to line 4.
LINE 10 - GAME OVER AND RESTART
--------------------------------
10 LOCATE 10,13:PRINT"GAME OVER Score:";s;"HI:";h:LOCATE 14,15:PRINT"SPACE TO RESTART":WHILE INKEY$<>" ":WEND:GOTO 2
LOCATE 10,13:PRINT"GAME OVER Score:";s;"HI:";h
Displays game over with final score and high score.
LOCATE 14,15:PRINT"SPACE TO RESTART"
Prompts the player to press SPACE.
WHILE INKEY$<>" ":WEND
Busy-waits until SPACE is pressed.
GOTO 2
Restarts the game at line 2. Skips line 1 so the DIM statements are not re-executed and h is not reset, preserving the high score.
============================================================
DESIGN NOTES
============================================================
WHY BOOLEAN ARITHMETIC?
Locomotive BASIC returns -1 for TRUE and 0 for FALSE.
This allows conditional assignments and movements without
IF statements, saving significant characters per line:
pr=pr+(k$="e")*5-(k$="d")*5 (player movement)
h=h-(s-h)*(s>h) (high score update)
b=b-0.1*(b<6) (difficulty ramp cap)
WHY +400 IN THE WRAP FORMULA?
Locomotive BASIC's MOD operator can return unexpected results with negative values on some CPC ROM versions.
Adding 400 (10*40) guarantees the value passed to MOD is always positive, making the wrap reliable across all CPC models regardless of ghost speed or direction.
WHY CHR$(248)-CHR$(251)?
The Amstrad CPC ROM includes pre-defined UDG characters at CHR$(240)-CHR$(255). Characters 248-251 are four frames of a walking stick figure designed for animation.
Using these built-in frames provides a four-frame animated player sprite without spending any SYMBOL definitions or line characters defining it -- leaving the full SYMBOL budget available for the ghost sprite on line 1.
WHY IS b RESET ON LINE 2 BUT h IS NOT?
b (base speed) resets to 1.5 each game so every new game starts at the same difficulty. h (high score) is only set on line 1 which is skipped on restart, so the high score persists across games as intended.
WHY START LINE NUMBERS AT 1?
Renumbering from 10,20,30... to 1,2,3... saves one character on every line containing a GOTO or line number reference. This was essential to keep all lines within the 120 character PUR-120 limit, particularly line 1 which sits at 119 characters -- one short of the maximum.
=========================================================
TECHNICAL NOTES FOR THE JUDGES
=========================================================
SPRITE DESIGN
CHR$(255) - Custom ghost UDG defined via SYMBOL on line 1:
Row data: 60,126,255,219,255,255,219,129
00111100 <- dome top
01111110 <- dome
11111111 <- body
11011011 <- eyes (two pixel gaps)
11111111 <- body
11111111 <- body
11011011 <- scalloped skirt
10000001 <- two little feet at outer edges
CHR$(248)-CHR$(251) - Animated player, using the Amstrad CPC's factory-defined UDG characters (built-in walking figure animation frames, not redefined by this program).
COLOUR PALETTE
INK 0,0 = Black (background)
INK 1,8 = Bright Yellow (player and score display)
INK 2,20 = Bright Cyan (ghosts)
INK 3,26 = White (lane dividers)
LANE LAYOUT
Traffic directions are set on line 3 using ed(i)=i*2-5:
Lane 1 (top): ed = -3 (travels left, fast)
Lane 2: ed = -1 (travels left, slow)
Lane 3: ed = +1 (travels right, slow)
Lane 4 (bottom): ed = +3 (travels right, fast)
Top two lanes are oncoming, bottom two are outgoing.
Crossing the middle is always the riskiest moment.
PROGRESSIVE DIFFICULTY
Variable b (base speed) starts at 1.5 and increases by 0.1 each time any ghost completes a lap, capped at 6:
b=b-0.1*(b<6)
(When b<6, TRUE=-1 in Locomotive BASIC, so -0.1*(-1)=+0.1)
Each ghost's new speed on lap completion is:
INT(RND*b)+1.5
As b grows, RND*b spans a wider range, so both minimum and maximum possible speeds increase together:
b=1.5 (start): speeds roughly 1-3
b=3 (mid): speeds roughly 1-4
b=6 (max): speeds roughly 1-7
COLLISION DETECTION
Standard point collision fails when a ghost moves faster than 1 character per frame. Pavement Panic uses a sweep check instead:
ox = old ghost X position (saved before move)
ec(i) = new ghost X position (after move)
Collision fires when ALL THREE conditions are true:
1. Player and ghost are on the same row: pr=er(i)
2. The move was not a wrap-around: ABS(ec(i)-ox)<20
-- a real move covers at most ~7 columns at max speed; a wrap jumps 30+ columns
3. Player X lies between old and new positions:
(pc-ox)*(pc-ec(i))<=0
-- product is <=0 only when pc is between ox and ec(i), catching all pass-through cases
HIGH SCORE
Updated live every frame on line 7:
h=h-(s-h)*(s>h)
In Locomotive BASIC (s>h) returns -1 when true, so the expression adds (s-h) to h only when s exceeds h.
No IF statement required.
========================================================
SUBMISSION CHECKLIST
========================================================
[x] Program on disk image (PAVEMENTPANIC.DSK)
[x] Program source listing (PavementPanicListing.txt)
[x] Program description and instructions (this file)
[x] Emulator recommendation and run instructions (this file)
[x] Character count proof (PavementPanicListingProof.txt)
[x] Program breakdown (PavementPanicCodeExplanation.txt)
[x] Animated screenshot (PavementPanic.gif)
========================================================
| Published | 27 days ago |
| Status | Released |
| Author | BASIC 10Liner |
| Genre | Action |
| Tags | 10liner, 8-Bit, Amstrad CPC, basic, schneider-cpc |
| Content | No generative AI was used |
Download
Install instructions
========================================================
EMULATOR SETUP AND HOW TO RUN
========================================================
RECOMMENDED EMULATOR: JavaCPC Desktop
JavaCPC is a full-featured Amstrad CPC emulator written
in Java, accurately reproducing the CPC 464/664/6128
including the character set and UDG system used by this
program.
Download: https://sourceforge.net/projects/javacpc/
Platforms: Windows, macOS, Linux (requires Java Runtime)
HOW TO RUN (JavaCPC Desktop):
1. Launch JavaCPC Desktop
2. Go to Drive > Drive A > Insert Disk
3. Select PAVEMENTPANIC.DSK
4. At the BASIC prompt type:
RUN"PANIC"
and press ENTER
5. The game starts immediately
ALTERNATIVE EMULATOR: WinAPE (Windows only)
Download: https://www.winape.net
1. Launch WinAPE, select CPC 464
2. File > Drive A > Insert Disk Image
3. Select PAVEMENTPANIC.DSK
4. Type RUN"PANIC" and press ENTER
NOTE FOR JUDGES: The 10 lines may also be typed directly into any Amstrad CPC emulator at the BASIC prompt without the disk image. No LOAD statements are used; the program runs entirely from the listing.

Comments
Log in with itch.io to leave a comment.
very good
A video of the game in action