FORTRESS

Date: 198x
Type: Program
Platform(s): TS 2068
Tags: Game

FORTRESS is a two-player (or one-player-vs-computer) strategy game played on a network of 18 labeled nodes arranged across the screen. The attacker tries to reach the defender’s position by moving along connecting edges, while the defender attempts to block progress by using up passage tokens. Node positions and connectivity are stored in parallel arrays: screen coordinates in p() and q(), adjacency lists in l$(), and zone classifications in c(), all initialized from DATA statements. The computer AI (when playing as attacker or defender) uses a scored evaluation array g(4) to select the best adjacent node each turn, weighting moves toward the defender, away from the defender’s zone, and penalizing already-used passages marked with asterisks. A custom UDG character is defined for the attacker token using 16 bytes of DATA POKEd into memory at USR "[UDG-A]".


Program Analysis

Program Structure

The program is organized into a set of well-separated subroutines and game-loop segments. Entry is at line 100, with the main menu at line 200. The flow branches into attacker turn (lines 1000–1240) and defender turn (lines 2000–2140), with win detection and replay at lines 7000–7070. Computer AI for the attacker occupies lines 8500–8660, and for the defender lines 8000–8050. Initialization of all map data is handled by the subroutine at lines 9000–9060, consuming DATA from lines 9070–9090.

  • Lines 5–50: Map drawing subroutine (nodes and edges)
  • Line 60: Status message display/flash subroutine
  • Lines 70–75: Attacker/defender token display subroutines
  • Lines 100–190: Initialization (UDGs, arrays, defaults)
  • Lines 200–230: Main menu and game start
  • Lines 500–530: Turn router (dispatches between attacker and defender)
  • Lines 1000–1240: Attacker’s turn (human)
  • Lines 2000–2140: Defender’s turn (human)
  • Line 3000–3020: Single-player role selection
  • Lines 7000–7070: Win announcement and replay
  • Lines 8000–8050: Computer defender logic
  • Lines 8500–8660: Computer attacker AI with scoring
  • Line 8700: UDG shape data
  • Lines 9000–9090: Map data initialization

Data Structures

The 18 nodes are represented by several parallel arrays, all dimensioned and populated from DATA in the 9000 subroutine:

ArrayDimensionsContents
p(18,2)18×2Screen row/column for node labels (PRINT AT coordinates)
q(18,2)18×2Pixel coordinates for PLOT/DRAW graphics
c(18)18Zone classification (1, 2, or 3) for each node
l$(18,4)18×4 stringOriginal adjacency lists — node letters for each of up to 4 neighbors
m$(18,4)18×4 stringWorking copy of adjacency lists; used passages replaced with *

Pixel coordinates are derived from print positions with the formula q(j,1) = 4 + p(j,2)*8 and q(j,2) = 172 - p(j,1)*8, mapping character-cell positions to the pixel grid.

Node and Passage System

Each node is identified by a letter A–R (nodes 1–18 map to CHR$(65)–CHR$(82)). When the attacker moves from node X to node Y, both the entry in m$(X) pointing to Y and the reverse entry in m$(Y) pointing to X are replaced with "*", marking that passage as consumed. This one-way depletion mechanic is the core of the game: passages are finite, and a node surrounded by used passages (m$(ap)="****") results in a defender win.

Turn Routing and Player Encoding

The variable no encodes the game mode and player role. In two-player mode, no=2. In single-player mode, the player’s choice of “a” (attacker) or “d” (defender) is stored as no = CODE i$, giving 97 or 100 respectively. The turn router at lines 500–530 checks these values: no=100 triggers the computer attacker AI at line 8500, and no=97 triggers the computer defender logic at line 8000, while both dispatch correctly for the human’s own turn.

Computer AI

The attacker AI uses a scoring array g(4), one entry per possible move. On the first move (g=1), a random adjacent node is chosen. On subsequent moves, each candidate is scored:

  • -30: passage already used (pm = -22, i.e., ASCII of * minus 64)
  • +30: move directly captures the defender
  • +15: destination is in the same zone as the defender
  • -10: destination is in zones 1–2 or zone 4 (attacker’s own area)
  • -1 per already-used passage: penalizes nodes that are becoming dead ends

The best-scoring move is selected; if the best score is -30, all moves are blocked and the defender wins immediately. The defender AI (lines 8000–8050) uses a pre-computed c() zone lookup to pick a safe adjacent node for the defender to move to.

Graphics

The map is drawn using PLOT and DRAW with INK 4 (green) for edges, connecting nodes via pixel-coordinate differences. The DRAW statement in line 25 uses relative coordinates: q(dest,1)-q(src,1) and q(dest,2)-q(src,2). Used passages are erased from the display using DRAW OVER 1 at line 1180, XORing the line away. Labels are printed with PRINT AT using the character-cell coordinate arrays.

UDG Definition

Line 100 POKEs 16 bytes of shape data (from line 8700) into memory starting at USR "[UDG-A]", defining two custom UDG characters (each 8 bytes) for the attacker and defender tokens. These are displayed in INK 2 (red) and INK 1 (blue) respectively via the subroutines at lines 70 and 75.

Notable Techniques and Idioms

  • The status message at line 60 is flashed by printing it, delaying with an empty FOR loop, then reprinting it with OVER 1 to erase it — a common Spectrum technique for temporary messages without attribute clutter.
  • Uppercase-to-lowercase conversion uses CODE i$-32 and CHR$(CODE i$+32) to shuttle between the user’s letter input and internal node numbering.
  • INKEY$ debouncing is performed at lines 1220 and 2120 by waiting until the key is released before proceeding.
  • The adjacency strings in l$ and m$ use exactly 4 characters, with "*" as a sentinel for depleted or unused passages, and "****" indicating total blockade.
  • The RESTORE at line 9010 explicitly targets line 9000 to ensure DATA is read from the correct point, independent of earlier RESTORE calls in the initialization sequence.

Potential Bugs and Anomalies

  • Line 8050 uses the expression 1050+(1020 AND no=97) to jump either to line 1050 (two-player or computer-as-attacker path) or line 2070 (computer-as-defender path). The value 1020 added to 1050 gives 2070, an elegant arithmetic branch, but relies on Spectrum BASIC’s boolean result being numeric 1 for true.
  • At line 1060, variable j is used as the FOR loop counter but is also set to 4 at line 1070 to force early loop termination — a deliberate early-exit idiom, not a bug, though it leaves j=4 after the NEXT at line 1080.
  • The computer defender AI at lines 8000–8050 references array c(ap) and offsets the result by checking equality with 3, suggesting the defender is constrained to nodes in zone 3 (a–c, nodes 1–3), which matches the DATA showing c(j) values of 1, 2, or 3.

Image Gallery

Source Code

    1 GO TO 100
    5 CLS :FOR j=1 TO 18
   10 FOR k=1 TO 4
   15 PLOT INK 4;q(j,1),q(j,2)
   20 IF l$(j,k)> CHR$ (j+64) THEN GO TO 35
   25 DRAW INK 4;q(CODE l$(j,k)-64,1)-q(j,1),q(CODE l$(j,k)-64,2)-q(j,2)
   30 NEXT k
   35 NEXT j
   40 FOR j=1 TO 18:PRINT AT p(j,1),p(j,2); CHR$ (j+64):NEXT j
   50 RETURN 
   60 PRINT AT 21,0;e$:FOR j=1 TO 100:NEXT j:PRINT AT 21,0; OVER 1;e$:RETURN 
   70 PRINT INK 2; AT p(ap,1),p(ap,2);"[UDG-A]":RETURN 
   75 PRINT INK 1; AT p(dp,1),p(dp,2);"[UDG-B]":RETURN 
  100 POKE 23658,0:RESTORE :FOR j=0 TO 15:READ a:POKE USR "[UDG-A]"+j,a:NEXT j
  150 GO SUB 9000:BORDER 6:PAPER 6:BRIGHT 1:INK 9:CLS 
  160 DIM m$(18,4):FOR j=1 TO 18:LET m$(j)=l$(j):NEXT j
  170 LET ap=18:LET dp=4
  180 DIM e$(20)
  190 LET w$="":LET g=0:RANDOMIZE 
  200 CLS :PRINT ' TAB 11;"FORTRESS"'''"1: 1 Player game"''"2: 2 Player game"
  210 LET i$= INKEY$:IF i$<"1" OR i$>"2" THEN GO TO 210
  220 LET no=2:IF i$="1" THEN GO SUB 3000
  230 GO SUB 5:GO SUB 70:GO SUB 75
  500 IF no <>100 THEN GO TO 1000
  510 GO TO 8500
  520 IF no <>97 THEN GO TO 2000
  530 GO TO 8000
 1000 PRINT AT 21,0;"Attacker move <"; INK 2;"[UDG-A]"; INK 9;")"
 1010 IF m$(ap)="****" THEN LET w$="DEFENDER":GO TO 7000
 1020 LET i$= INKEY$
 1030 IF i$<"a" OR i$>"r" THEN GO TO 1020
 1040 LET e$="NO PASSAGE"
 1050 FOR J=1 TO 4
 1060 IF m$(ap,j)= CHR$ (CODE i$-32) THEN GO TO 1100
 1070 IF m$(ap,j)="*" AND l$(ap,j)= CHR$ (CODE i$-32) THEN LET e$="PASSAGE USED":LET j=4
 1080 NEXT j
 1090 GO SUB 60:GO TO 1000
 1100 LET nap= CODE i$-96
 1110 LET m$(ap,j)="*"
 1120 FOR j=1 TO 4
 1130 IF m$(nap,j)= CHR$ (ap+64) THEN LET m$(nap,j)="*"
 1140 NEXT j
 1150 IF nap=dp THEN LET w$="ATTACKER"
 1160 LET x=ap:LET y=nap:IF nap>ap THEN LET x=nap:LET y=ap
 1170 PLOT q(x,1),q(x,2)
 1180 DRAW OVER 1; INK 4;q(y,1)-q(x,1),q(y,2)-q(x,2)
 1190 PRINT AT p(ap,1),p(ap,2); CHR$ (ap+64)
 1200 LET ap=nap
 1210 GO SUB 70
 1220 IF INKEY$=i$ THEN GO TO 1220
 1230 IF w$="" THEN GO TO 520
 1240 GO TO 7000
 2000 PRINT AT 21,0;"Defender move ("; INK 1;"[UDG-B]"; INK 9;")"
 2010 LET i$= INKEY$
 2020 IF i$="" OR i$= CHR$ 13 THEN GO TO 2010
 2030 IF i$="a" OR i$="b" OR i$="d" THEN GO TO 2070
 2040 LET e$="STAY IN YOUR AREA"
 2050 GO SUB 60
 2060 GO TO 2000
 2070 LET ndp= CODE i$-96
 2080 IF ndp=dp THEN GO TO 2010
 2090 IF ndp=ap THEN LET w$="DEFENDER"
 2100 PRINT AT p(dp,1),p(dp,2); CHR$ (dp+64)
 2110 LET dp=ndp:GO SUB 75
 2120 IF INKEY$=i$ THEN GO TO 2120
 2130 IF w$="" THEN GO TO 500
 2140 GO TO 7000
 3000 PRINT '"A: You will be the ATTACKER"''"D: You will be the DEFENDER"
 3010 LET i$= INKEY$:IF i$ <>"a" AND i$ <>"d" THEN GO TO 3010
 3020 LET no= CODE i$:RETURN 
 7000 IF w$(1)="D" AND no=100 THEN LET w$="YOU"
 7010 IF no=97 OR (no=100 AND w$(1)="A") THEN LET w$="I"
 7020 PRINT AT 21,0;w$;" WIN";:IF no=2 THEN PRINT "S";
 7030 PRINT "             ":REM 13 spaces
 7040 INPUT "P= Play again : S= Stop ";i$
 7050 IF i$ <>"p" AND i$ <>"s" THEN GO TO 7040
 7060 IF i$="s" THEN GO TO 9999
 7070 GO TO 150
 8000 LET i$= CHR$ (c(ap)+64+(c(ap)=3))
 8010 PRINT AT 21,0;"MY MOVE ";i$;" (Press ENTER)"
 8020 IF INKEY$ <> CHR$ 13 THEN GO TO 8020
 8030 PRINT AT 21,17;"      ":REM 6 spaces
 8040 LET i$= CHR$ (CODE i$+32)
 8050 GO TO 1050+(1020 AND no=97)
 8500 LET g=g+1:IF g=1 THEN LET i$=m$(ap, INT (RND*4)+1):GO TO 8010
 8510 DIM g(4):FOR j=1 TO 4
 8520 LET pm= CODE m$(ap,j)-64
 8530 IF pm=-22 THEN LET g(j)=-30:GO TO 8600
 8540 IF c(pm)=c(dp) THEN LET g(j)=15:GO TO 8600
 8550 IF pm=dp THEN LET g(j)=30:GO TO 8600
 8560 IF pm<3 OR pm=4 THEN LET g(j)=-10:GO TO 8600
 8570 FOR k=1 TO 4
 8580 IF m$(pm,k)= CHR$ 42 THEN LET g(j)=g(j)-1
 8590 NEXT k
 8600 NEXT j
 8610 LET k=g(1)
 8620 FOR j=2 TO 4:IF g(j)>k THEN LET k=g(j)
 8630 NEXT j
 8640 IF k=-30 THEN LET w$="DEFENDER":GO TO 7000
 8650 FOR j=1 TO 4:IF g(j) <>k THEN NEXT j
 8660 LET i$=m$(ap,j):GO TO 8010
 8700 DATA 60,60,60,126,255,255,60,60,102,255,255,126,60,60,24,24
 9000 DIM p(18,2):DIM q(18,2)
 9010 RESTORE 9000:FOR j=1 TO 18:READ p(j,1),p(j,2)
 9020 LET q(j,1)=4+p(j,2)*8:LET q(j,2)=172-p(j,1)*8
 9030 NEXT j
 9040 DIM c(18):FOR j=1 TO 18:READ c(j):NEXT j
 9050 DIM l$(18,4):FOR j=1 TO 18:READ l$(j):NEXT j
 9060 RETURN 
 9070 DATA 1,12,1,18,3,6,3,15,3,24,5,10,5,20,7,8,7,22,9,6,9,24,11,9,11,21,13,13,13,17,14,8,14,22,16,15
 9080 DATA 1,2,3,3,3,2,1,1,2,2,1,3,3,2,1,1,2,3
 9090 DATA "BCDF","ADEG","AFHJ","ABFG","BGIK","ADCH","BDEI","CFJL","GEKM","CHLP","EIMQ","HJPN","IKQO","LOPR","MNQR","JLNR","MKOR","NOPQ"
 9998 SAVE "FORTRESS" LINE 100

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.