Space War

This file is part of and CATS Library Tape 1. Download the collection to get this file.
Developer(s): Mark Fisher
Date: 1986
Type: Program
Platform(s): TS 2068
Tags: Game

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:

  1. Line 8000 – Setup and title display
  2. Line 1000 – Main loop (iterates a=1 TO 2 for each player per frame)
  3. Line 2000 – Get missile firing orders (reads joystick for aim)
  4. Line 2500 – Update missile movement and check collisions
  5. Line 3000 – Near-miss calculation (parametric intercept)
  6. Line 3200 – Check actual miss distance
  7. Line 3500 – Boom (explosion and damage)
  8. Line 4000 – Get ship move orders (joystick)
  9. Line 4500 – Update ship movement
  10. Line 2800 – Abort missile (erase and deactivate)
  11. Line 9000 – BASIC renumbering utility
  12. 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
1Fuel / missile fuel; CHR$ 255 = inactive missile slot
2Missile count (ship) / X aim offset (missile)
3Damage (ship) / Y aim offset (missile)
4–5Distance to opposing ship (dx, dy)
6–7Current X, Y position
8–9Previous X, Y position (one frame ago)
10–11Track point (two frames ago)
12–13Track point (three frames ago)
14–15Track 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) (using b) inside the ship move routine, but at this point b=1 (ship slot), so the index is equivalent to a$(a,1,1). The intent appears correct but the use of b rather than the literal 1 is fragile — if b were ever non-1 on entry, fuel would be deducted from a missile slot instead.
  • At line 2505, the expression LET a=(NOT a-1)+1 is used to toggle a between 1 and 2. NOT 1 evaluates to 0 and NOT 2 also 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 of a. 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

Appears On

From Blackjack to Star Trek, Moon Lander to a first-person dungeon crawler — this tape packs 21 games and puzzles into one of CATS' earliest library volumes. Card sharks, arcade fans, and puzzle solvers alike will find something here.

Related Products

Related Articles

A single or two player space game, inspired by Race Track, as described in a Martin Gardner column. Uses a...

Related Content

Image Gallery

Space War

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.

Scroll to Top