--- title: "Space War" id: 54507 type: "computer_media" slug: "space-war" url: "http://localhost/computer_media/space-war/" markdown_url: "http://localhost/computer_media/space-war.md" published_at: "2024-06-02T15:57:24+00:00" modified_at: "2026-03-30T21:45:26+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2019/02/20230809-041332.jpg" excerpt: "A two-player space duel with proximity-fused missiles, joystick control, and clever state-packing inside a string array — all in BASIC." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 2068" slug: "ts2068" taxonomy: "post_tag" url: "http://localhost/tag/ts2068/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" indiv: - name: "Mark Fisher" slug: "mark-fisher" taxonomy: "indiv" url: "http://localhost/indiv/mark-fisher/" genre: - name: "Game" slug: "game" taxonomy: "genre" url: "http://localhost/type/game/" media_contents: - id: 51212 title: "CATS Library Tape 1" type: "computer_media" url: "http://localhost/computer_media/cats-library-tape-1/" media_type: "Program" programmers: - name: "Mark Fisher" slug: "mark-fisher" taxonomy: "indiv" url: "http://localhost/indiv/mark-fisher/" mediadate: "1986" images: - url: "http://localhost/wp-content/uploads/2024/06/space-war-2.png" - url: "http://localhost/wp-content/uploads/2024/06/space-war-1.png" article_media: - id: 42918 title: "Duel" type: "article" url: "http://localhost/article/duel/" media_type_tags: "Game" --- # Space War Space War is a two-player space combat game, inspired by [Race Track, as described in a Martin Gardner column](https://web.archive.org/web/20190724161306/http://chesswanks.com/txt/RaceTrack.html), 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 | | --- | --- | | 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)` (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. ## 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<<<<<<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)=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 ```