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:
| Array | Dimensions | Contents |
|---|---|---|
p(18,2) | 18×2 | Screen row/column for node labels (PRINT AT coordinates) |
q(18,2) | 18×2 | Pixel coordinates for PLOT/DRAW graphics |
c(18) | 18 | Zone classification (1, 2, or 3) for each node |
l$(18,4) | 18×4 string | Original adjacency lists — node letters for each of up to 4 neighbors |
m$(18,4) | 18×4 string | Working 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)-1per 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 1to erase it — a common Spectrum technique for temporary messages without attribute clutter. - Uppercase-to-lowercase conversion uses
CODE i$-32andCHR$(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$andm$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
jis 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 leavesj=4after 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 showingc(j)values of 1, 2, or 3.
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 100Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
