--- title: "FORTRESS" id: 71036 type: "computer_media" slug: "fortress-2" url: "http://localhost/computer_media/fortress-2/" markdown_url: "http://localhost/computer_media/fortress-2.md" published_at: "2026-08-26T07:58:11+00:00" modified_at: "2026-08-26T08:00:06+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2026/08/fortress.png" alt: "FORTRESS screen" excerpt: "A strategic network-traversal game where an attacker navigates 18 connected nodes to breach a fortress, with single-player AI that scores possible moves using a weighted evaluation system." 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/" genre: - name: "Game" slug: "game" taxonomy: "genre" url: "http://localhost/type/game/" media_type: "Program" download_url: "https://archive.org/download/timex-sinclair-software-archive/FORTRESS%20%28198x%29%28-%29%28TS2068%29%28US%29%28Program%29.zip" tsrun_member: "FORTRESS (198x)(-)(TS2068)(US)(Program).tap" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2026/08/fortress.png" alt: "FORTRESS screen" media_type_tags: "Game" --- # FORTRESS 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) - `-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. ## 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 ```