Space War is a two-player space combat game, inspired by Race Track, as described in a Martin Gardner column, in which each player controls a ship and fires proximity-fused missiles at the opponent. Ship and missile state—including position, velocity, fuel, and damage—is packed into a three-dimensional string array a$(2,3,15), with individual bytes retrieved via a DEF FN v function that calls CODE on specific character positions. Joystick input is handled through the STICK keyword, and collision detection uses a parametric near-miss calculation based on relative missile and ship velocity vectors. The program includes a self-contained BASIC renumbering utility at line 9000 that patches line-number bytes directly in memory via PEEK and POKE, and a machine code module is saved and loaded separately as prcode at address 64256. Error handling at line 9500 exploits the error code in system variable address 23739 to recover from divide-by-zero errors (code 6) during near-miss calculation by substituting PI/2, and to distinguish game-over conditions from other errors.
Program Analysis
Program Structure
The program is organized into clearly delineated subroutine blocks, each introduced by a REM banner. The main entry point is line 8000 (setup), which initializes the display, dimensions the state array, reads initial ship data, and falls through to the main game loop at line 1000. The overall call hierarchy is:
- Line 8000 – Setup and title display
- Line 1000 – Main loop (iterates
a=1 TO 2for each player per frame) - Line 2000 – Get missile firing orders (reads joystick for aim)
- Line 2500 – Update missile movement and check collisions
- Line 3000 – Near-miss calculation (parametric intercept)
- Line 3200 – Check actual miss distance
- Line 3500 – Boom (explosion and damage)
- Line 4000 – Get ship move orders (joystick)
- Line 4500 – Update ship movement
- Line 2800 – Abort missile (erase and deactivate)
- Line 9000 – BASIC renumbering utility
- Line 9500 – Error handler
Lines 6000–7999 contain a dead code block for angle/vector testing; line 6000 opens with { GO TO 9500 (i.e., ON ERR GO TO 9500), effectively bypassing the block entirely during normal execution.
State Array Encoding
All game state for ships and missiles is stored in the three-dimensional string array a$(2,3,15), dimensioned at line 8020. The first index is player (1 or 2), the second is object (1=ship, 2=missile slot 1, 3=missile slot 2), and the third is the byte position within a 15-character record. Individual bytes are accessed with the user-defined function FN v(a,b,c) defined as CODE a$(a,b,c) at line 30.
The byte layout within each record (1-indexed) is:
| Byte(s) | Contents |
|---|---|
| 1 | Fuel / missile fuel; CHR$ 255 = inactive missile slot |
| 2 | Missile count (ship) / X aim offset (missile) |
| 3 | Damage (ship) / Y aim offset (missile) |
| 4–5 | Distance to opposing ship (dx, dy) |
| 6–7 | Current X, Y position |
| 8–9 | Previous X, Y position (one frame ago) |
| 10–11 | Track point (two frames ago) |
| 12–13 | Track point (three frames ago) |
| 14–15 | Track point (four frames ago) — used for trail erasure |
The string " " (five spaces) written to positions 1–5 of a ship record signals destruction, allowing the main loop to detect the condition with a$(a,1, TO 5)=" ". Similarly, " COPY " (with embedded spaces and a length of 6) in missile slot position 1 indicates an inactive missile.
Near-Miss and Collision Detection
The parametric near-miss calculation at lines 3000–3120 computes the time parameter t at which the missile and opposing ship are closest, using their velocity vectors. Direction cosines and sines are computed manually from the velocity components (lines 3010–3040), and the closing parameter formula at line 3100 is the standard dot-product time-of-closest-approach formula. If t<1 (the intercept is within the current step), the missile trajectory is shortened to tx=tx*t, ty=ty*t. The actual miss distance check at lines 3220–3270 then determines whether the explosion threshold of 12 units in both axes is met.
Error Handling as Control Flow
The ON ERR GO TO 9500 handler at line 20 (and reasserted at line 9540) is used deliberately as a control-flow mechanism. At line 9520, system variable address 23739 is peeked to read the BASIC error code: code 21 (BREAK) or 9 (STOP statement) routes to the “play again” prompt; code 6 (number too big / divide by zero, which arises from a zero-length velocity vector in the near-miss cosine calculation) is handled at line 9530 by setting t=PI/2 before resuming execution at the faulting line via GO TO PEEK 23736+256*PEEK 23737+1 at line 9550. This is a well-considered defensive strategy for the division in the SQR denominators.
Joystick and Display Idioms
Joystick reading uses the TS2068 STICK keyword. Line 320 does POKE 16384, STICK (1,1) — writing the joystick state to the first byte of the display file as a side-effect of updating an internal value. Lines 330–340 use POINT to read the pixel value at a specific screen coordinate to decode directional bits from the joystick result, a technique that maps the bit pattern written to video RAM back out via the graphics subsystem.
Drawing uses OVER 1 mode (set at line 8010) so that plotting a pixel twice erases it, enabling flicker-free animation without storing background data. Ship tracks are maintained as a rolling history of four past positions in the state record (bytes 8–15), and the oldest track segment is XOR-erased before the new position is drawn.
Variable Aliasing for Subroutine Calls
Because BASIC lacks procedure parameters, the target line numbers for frequently called internal subroutines (abort, nearmiss, check, boom) are stored in numeric variables at lines 8140–8160 and called with GO SUB abort etc. This is a well-known TS2068/Spectrum BASIC technique that allows the effective target to be changed at runtime and marginally improves readability.
Machine Code and Save/Load Structure
Lines 9800–9910 reveal that the program is designed to coexist with a machine code routine. Line 9800 performs a CLEAR 64255 to protect memory from address 64256 upward, patches the RAMTOP system variables at 26703–26704, and loads a code block named prcode. Line 9900 saves both the BASIC program (with auto-start at line 9800) and the code block of 1111 bytes at address 64256. Line 1090 calls RANDOMIZE USR 64628 when the key "s" is pressed, which is presumably a screen-capture or sound routine within that machine code block.
BASIC Renumbering Utility
Lines 9000–9120 implement an in-BASIC line renumberer. It accepts a start line, end line, and new starting number, then walks the BASIC program area using the standard line-record structure (2 bytes line number, 2 bytes length) via PEEK and POKE. It only renumbers line-number fields in the binary header of each line, not line-number literals inside GO TO or GO SUB statements, so it is a header-only renumberer. The program pointer starts from system variables at addresses 23635–23636 (PROG).
Notable Bugs and Anomalies
- Lines 4530–4540 reference
a$(a,b,1)(usingb) inside the ship move routine, but at this pointb=1(ship slot), so the index is equivalent toa$(a,1,1). The intent appears correct but the use ofbrather than the literal1is fragile — ifbwere ever non-1 on entry, fuel would be deducted from a missile slot instead. - At line 2505, the expression
LET a=(NOT a-1)+1is used to toggleabetween 1 and 2.NOT 1evaluates to 0 andNOT 2also evaluates to 0 in Spectrum BASIC (NOT returns 0 for any non-zero value), so this expression always evaluates to 1 regardless of the incoming value ofa. This means missile updates always operate on player 1’s missiles, which appears to be a bug — the intent was likely to switch to the opposing player. - Line 1100 checks
a$(o,2,1)=" COPY "but" COPY "is 6 characters long while position 1 of a 15-character slot would normally hold a single character; the substring comparison against a multi-character literal relies on BASIC’s string slice comparison semantics and will match if the first character of the slice equals the first character of the literal — since" COPY "starts with a space (CHR$ 32), this inadvertently checks only that byte 1 is a space, not the full six-character sentinel. - The commented-out line 360 (
REM IF px=0 AND py=0 THEN PAUSE 30) suggests a frame-rate throttle was considered but disabled.
Content
Source Code
10 REM ********* Space War ***********************************
20 ON ERR GO TO 9500
30 DEF FN v(a,b,c)=CODE a$(a,b,c): REM Value
40 DEF FN d(c)=FN v(a,b,c)-FN v(a,b,c+2)
50 GO TO 8000
100 REM *************** Get Coordinates ***********************
110 LET x=FN v(a,b,6): LET y=FN v(a,b,7): LET ox=FN v(a,b,8): LET oy=FN v(a,b,9)
120 LET dx=x-ox: LET dy=y-oy
130 RETURN
300 REM ****j** stick reading *********************************
310 LET px=0: LET py=0: REM Pointer x,y
320 POKE 16384, STICK (1,1)
330 LET px=px+s*((POINT (4,175) AND px<2 AND px+x<=254)-(POINT (5,175) AND px>-2 AND x-px>=1))
340 LET py=py+s*((POINT (7,175) AND py<2 AND py+y<=173)-(POINT (6,175) AND py>-2 AND y-py>=2))
350 PLOT x,y: DRAW 8*px/s,8*py/s
360 REM IF px=0 AND py=0 THEN PAUSE 30
370 LET ex= STICK (2,1)
380 PLOT x,y: DRAW 8*px/s,8*py/s
390 IF ex=0 THEN GO TO 320
400 RETURN
1000 REM ***************** Main Loop ***************************
1010 FOR a=1 TO 2
1020 INK a*2+3
1030 IF a$(a,1, TO 5)=" " THEN GO SUB 2500: LET px=0: LET py=0: GO SUB 4500: GO TO 1100
1040 GO SUB 2000: REM get missile firing orders
1050 GO SUB 2500: REM move & update missiles
1060 INK a*2+2
1070 GO SUB 4000: REM get ship move
1080 GO SUB 4500: REM Move ship
1090 IF INKEY$="s" THEN RANDOMIZE USR 64628
1100 IF a$(o,1, TO 5)=" " AND a$(o,2,1)=" COPY " AND a$(o,3,1)=" COPY " THEN STOP
1110 LET o=a: NEXT a
1120 GO TO 1000
2000 REM ************* Get Missile coords *********************
2010 LET b=1: GO SUB 100: IF a$(a,1,2)=CHR$ 0 OR a$(a,1, TO 5)=" " OR a$(o,1, TO 5)=" " THEN RETURN : REM find parent ship
2020 PRINT #1;AT 0,0;">>>>>>>>PLAYER #";a;" MISSILE<<<<<<<Dist. =";FN v(a,1,4);"x";FN v(a,1,5),"Missiles =";FN v(a,1,2);" "
2030 LET s=1: GO SUB 300: REM s=step size
2040 PRINT #1;AT 0,0;" "
2050 IF NOT FN v(a,1,2) OR (px=0 AND py=0) THEN RETURN : REM No missile fired
2060 LET a$(a,1,2)=CHR$ (FN v(a,1,2)-1)
2070 IF a$(a,2,1)=CHR$ 255 THEN GO TO 2100: REM if #2 is free, fire it
2080 IF a$(a,3,1)<>CHR$ 255 THEN LET b=3: LET tx=0: LET ty=0: PLOT x,y: GO SUB abort: REM if two other, abort #3
2090 LET a$(a,3)=a$(a,2)
2100 LET a$(a,2)=CHR$ 8+CHR$ (px+3)+CHR$ (py+3)+a$(a,1,4 TO ): REM Fuel,x&y aim, and past movement
2110 RETURN
2500 REM ***************update missile movement*****************
2505 LET a=o: LET b=1: GO SUB 100: LET a=(NOT a-1)+1: LET sx=x: LET sy=y: LET sox=ox: LET soy=oy
2510 FOR b=2 TO 3: GO SUB 100: IF a$(a,b,1)=" COPY " THEN GO TO 2620: REM If missile is inactive, skip update
2520 IF NOT FN v(a,b,1) THEN LET a$(a,b,2 TO 3)=CHR$ 0+CHR$ 0: REM if no fuel, no accel
2530 LET tx=(FN v(a,b,2)-3)*5+dx: LET ty=(FN v(a,b,3)-3)*5+dy: LET mx=tx+x: LET my=ty+y: REM Get new points..
2540 GO SUB nearmiss
2550 PLOT x,y: DRAW tx,ty
2565 IF t<=1 THEN GO SUB check: GO SUB abort: GO TO 2620
2570 IF mx>253 OR mx<2 OR my>173 OR my<2 THEN GO SUB abort: GO TO 2620: REM and check that it is not off screen
2580 IF a$(a,b,12 TO 15)<>a$(a,1,12 TO 15) THEN PLOT FN v(a,b,14),FN v(a,b,15): DRAW FN d(12),FN d(13): REM If track is different from ship, erase track
2590 LET a$(a,b,8 TO )=a$(a,b,6 TO ): REM move track data
2600 LET a$(a,b,4 TO 7)=CHR$ ABS dx+CHR$ ABS dy+CHR$ mx+CHR$ my: IF a$(a,b,1)>CHR$ 0 THEN LET a$(a,b,1)=CHR$ (CODE a$(a,b,1)-1): REM Update fuel& nex points
2620 NEXT b
2630 LET b=1: RETURN
2800 REM ******* abort missile *********************************
2810 FOR c=14 TO 8 STEP -2: REM Erase aborted missile
2820 IF a$(a,b,c-2 TO c-1)<>a$(a,1,c-2 TO c-1) THEN PLOT FN v(a,b,c),FN v(a,b,c+1): DRAW FN d(c-2),FN d(c-1)
2830 NEXT c
2840 LET a$(a,b,1)=" COPY "
2850 PLOT x,y: DRAW tx,ty
2860 RETURN
3000 REM ******* nearmiss **************************************
3010 LET mcos=(tx)/(SQR ((tx)*(tx)+(ty)*(ty)))
3020 LET msin=(ty)/(SQR ((ty)*(ty)+(tx)*(tx)))
3030 LET scos=(sx-sox)/(SQR ((sox-sx)*(sox-sx)+(soy-sy)*(soy-sy)))
3040 LET ssin=(soy-sy)/(SQR ((soy-sy)*(soy-sy)+(sox-sx)*(sox-sx)))
3050 LET mv=SQR (tx*tx+ty*ty)
3060 LET sv=SQR ((sox-sx)*(sox-sx)+(soy-sy)*(soy-sy))
3100 LET t=-((x-sox)*((mv*mcos)-(sv*scos))+(y-soy)*((mv*msin)-(sv*ssin)))/((mv*mv+sv*sv)-(2*mv*sv*(mcos*scos+msin*ssin)))
3110 IF t<1 THEN LET tx=tx*t: LET ty=ty*t
3120 RETURN
3200 REM ********** check miss *********************************
3220 LET mx=tx+x: LET my=ty+y
3230 LET sx=sox+t*sv*scos
3240 LET sy=soy+t*sv*ssin
3250 LET dx=mx-sx: REM Dist missile to opposing ship
3260 LET dy=my-sy: REM Dist missile to opposing ship
3270 IF ABS dx<12 AND ABS dy<12 THEN GO SUB boom
3280 RETURN
3500 REM ***** boom ********************************************
3520 LET a$(o,1,3)=CHR$ (FN v(o,1,3)+2+(ABS dx<8)+(ABS dy<8))
3530 CIRCLE mx,my,3: PRINT #1;AT 0,0;"*<><>>=<>*/ SHIP #";o;" HIT ,/-^?`#$Damage =";FN v(o,1,3),"Dist. ";INT dx;" x ";INT dy;" ": PAUSE 200
3540 IF FN v(o,1,3)<4 THEN RETURN
3550 FOR i=2 TO 10 STEP 3: CIRCLE mx,my,i: NEXT i
3560 PRINT OVER 0;AT 1,7;" Ship ";o;" destroyed "
3570 IF a$(o,2,1)=" COPY " AND a$(o,3,1)=" COPY " THEN STOP : REM if no active missiles, stop.
3580 LET a$(o,1, TO 5)=" "
3590 RETURN
4000 REM ****** Ship Move **************************************
4010 GO SUB 100: REM find parent ship
4020 PRINT #1;AT 0,0;"^^^^^^^PLAYER #";a;" SHIP MOVE^^^^^^"; FLASH FN v(a,1,3)>1;"Damage =";FN v(a,1,3), FLASH FN v(a,1,1)<20;"Fuel =";FN v(a,1,1);" "
4030 IF a$(a,1,1)=CHR$ 0 OR a$(a,1,3)>CHR$ 1 THEN PAUSE 80: LET px=0: LET py=0: RETURN : REM if damage or no fuel, no accel
4040 LET s=2: GO SUB 300: LET px=px/2: LET py=py/2: REM coarser step than missile
4050 RETURN
4500 REM *********update ship movement *************************
4510 PLOT FN v(a,1,14),FN v(a,1,15): DRAW FN d(12),FN d(13): CIRCLE FN v(a,1,6),FN v(a,1,7),2: REM erase track
4515 IF a$(a,1, TO 5)=" " THEN LET a$(a,1,8 TO )=a$(a,1,6 TO ): GO TO 4610
4520 LET mx=px*5+dx+FN v(a,1,6): LET my=py*5+dy+FN v(a,1,7): REM Get new points..
4530 LET a$(a,b,1)=CHR$ (CODE a$(a,b,1)-(ABS px) AND CODE a$(a,b,1))
4540 LET a$(a,b,1)=CHR$ (CODE a$(a,b,1)-(ABS py) AND CODE a$(a,b,1)): REM reduce fuel if burned
4550 IF FN v(a,b,3)>0 THEN LET a$(a,b,3)=CHR$ (CODE a$(a,b,3)-1): REM reduce damage
4560 IF mx>253 OR mx<2 OR my>173 OR my<2 THEN LET a$(a,1,3)=CHR$ 10: LET o=a: GO TO boom: REM and check that it is not off screen
4570 LET dx=ABS (FN v(a,1,6)-FN v(o,1,6)): REM Dist to opposing ship
4580 LET dy=ABS (FN v(a,1,7)-FN v(o,1,7)): REM Dist to opposing ship
4590 LET a$(a,1,4 TO )=CHR$ dx+CHR$ dy+CHR$ mx+CHR$ my+a$(a,1,6 TO )
4600 PLOT FN v(a,1,8),FN v(a,1,9): DRAW FN d(6),FN d(7): CIRCLE FN v(a,1,6),FN v(a,1,7),2
4610 RETURN
6000 { GO TO 9500: LET x1=0: LET y1=20
6010 LET i=2*PI/360
6020 PLOT 125,80: DRAW x1,y1
6030 PRINT x1,y1
6040 LET t=ATN (y1/x1)
6050 PRINT t,t/i
6060 INPUT x,y
6070 IF SGN x1=SGN x AND SGN y1=SGN y THEN LET t=t+PI
6080 PRINT t/i
6090 DRAW x,y
6100 PRINT "- ";ATN (y/x)/i,(t-ATN (y/x))/i
6110 PAUSE 0: CLS
7000 GO TO 6000
7999 STOP
8000 REM ******************** Setup ****************************
8010 OVER 1: BORDER 0: PAPER 0: INK 7: CLS
8015 GO SUB 8500
8020 DIM a$(2,3,15)
8030 LET x=0: LET y=0: LET b=1: REM x,y=screen coord,b=ship or missile select flag
8040 LET dx=0: LET dy=0: LET o=2: REM x&y movement from last pos; o=other player
8050 FOR a=1 TO 2: FOR c=1 TO 15
8060 READ x
8070 LET a$(a,1,c)=CHR$ x
8080 NEXT c: NEXT a
8090 DATA 150,10,0,255,255,3,87,3,87,3,87,3,87,3,87: REM ship #1
8100 DATA 150,10,0,255,255,252,87,252,87,252,87,252,87,252,87: REM ship #2
8110 FOR b=2 TO 3
8120 LET a$(1,b)=CHR$ 255+a$(1,1,2 TO ): LET a$(2,b)=CHR$ 255+a$(2,1,2 TO )
8130 NEXT b
8140 LET abort=2800
8150 LET nearmiss=3000
8155 LET check=3200
8160 LET boom=3500
8170 CIRCLE 252,87,1: CIRCLE 3,87,1: PLOT 3,3: DRAW 249,0: DRAW 0,169: DRAW -249,0: DRAW 0,-169
8180 LET b=1: FOR i=1 TO 2
8190 FOR a=1 TO 2
8200 GO SUB 100: READ px: READ py
8210 DATA 1,0,-1,0,1,0,-1,0,1,-1,-1,1,1,-1,-1,1
8220 GO SUB 4500
8230 NEXT a: NEXT i
8231 PRINT #1;AT 0,0;"Press any key to play": PAUSE 0: GO SUB 8500
8240 GO TO 1000
8500 REM ******* titles ****************************************
8510 PRINT AT 2,10;"************ * spacewar * ************"
8520 PRINT AT 5,15;"by"," Mark Fisher"," \* 1985"''" You are fighting a space duel. Each ship has 10 missiles and equal fuel."
8530 PRINT " Missiles have proximity fuses- they will explode if they pass within 15 units."
8540 PRINT " If damaged, you cannot change course, but you can still fire missiles."
8550 PRINT " If you go out of bounds, you will be destroyed."
8560 RETURN
9000 REM ********** Renumber and Save **************************
9010 INPUT "Start ";start
9020 INPUT "End ";end
9030 IF end>=9000 THEN LET end=9000
9040 INPUT "Start renumber using ";n
9050 LET x=PEEK 23635+256*PEEK 23636
9060 IF PEEK x*256+PEEK (x+1)<start THEN GO TO 9110
9070 IF PEEK x*256+PEEK (x+1)>=end THEN STOP
9080 POKE x,INT (n/256)
9090 POKE x+1,n-PEEK x*256
9100 LET n=n+10
9110 LET x=x+4+PEEK (x+2)+PEEK (x+3)
9120 GO TO 9060
9500 REM *********** error handling ****************************
9510 ON ERR RESET
9520 IF PEEK 23739=21 OR PEEK 23739=9 THEN GO TO 9560: REM replay?
9530 IF PEEK 23739=6 THEN LET t=PI/2
9540 ON ERR GO TO 9500
9550 GO TO PEEK 23736+256*PEEK 23737+1
9560 REM ***** replay ******************************************
9570 PRINT AT 3,8;"Play again? (y/n)"
9580 IF INKEY$="y" THEN RUN
9590 IF INKEY$="n" THEN STOP
9600 GO TO 9580
9610 REM ***** start up ****************************************
9800 CLEAR 64255: LET p=64261: POKE 26704,INT (p/256): POKE 26703,p-(INT (p/256)*256): LOAD ""CODE
9810 RUN
9900 CLEAR : SAVE "spacewar" LINE 9800: SAVE "prcode"CODE 64256,1111
9910 PRINT "rewind and verify:"'': VERIFY "spacewar": VERIFY "prcode"CODE
9999 INK 7: LIST 2500
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

