Skip to content

Mark what can be seen

Build a logical sight mask for a stationary guard.

  • Sinclair ZX Spectrum
  • BASIC

What can the guard see from one square? Hold it at the opening position while we build an answer we can inspect. This temporary version says SIGHT TEST at the top; movement and corner scans return in lesson 8.

Widen the candidate view

The guard at (7,5) faces east. Consider five successive columns in front of it. At distance one there is one candidate square; then three, five, seven and nine. This diagram shows candidates before walls remove any. It is an explanation of the rule, not a game capture.

An east-facing guard and five candidate columns, widening from one to nine squares.

For a target at forward distance depth, its sideways offset runs from 1-depth through depth-1. Check the path towards each candidate one forward step at a time. At step distance, the sideways displacement is floor(offset * distance / depth + 0.5). Here floor means round down to an integer; Sinclair BASIC calls that INT.

Trace this rule; the following is pseudocode, not another program to type:

For each candidate target:
  previous = guard position
  For distance from 1 through target depth:
      side = floor(offset * distance / depth + 0.5)
      sample = guard + forward * distance + sideways * side
      Reject target if sample is outside the map or in a wall
      If this step changes both column and row:
          Reject target if either square beside that diagonal is a wall
      previous = sample
  If no step rejected the target, mark it visible

Forward is (1,0) when facing east; sideways is (0,1). For any forward pair (dx,dy), the sideways pair is (−dy,dx). This rotates the same construction with the guard. Blocking either side of a diagonal prevents sight squeezing between touching wall corners. These are the game’s discrete sight rules, not a claim about physically exact vision.

At the opening, (9,4) and (9,6) are visible: the fan reaches beyond its centre line. (13,5) is six columns ahead, so it is outside the range. A different example distinguishes cover from range: from (7,12) facing north, (9,9) lies inside the candidate fan, but a wall blocks its sampled path.

Store the answer separately

We use a string v$ as a visibility mask: each position holds a space for unseen or ! for visible. Its index is 32*row+column, so (9,5) becomes 169. The stride is 32, matching the display’s columns, even though our map is only thirty columns wide. DIM reserves 608 characters, including unused margin positions; the last map square uses index 606.

The opening mask has eleven cells. Line 8900 begins with position, facing, visible count and change count, then lists those cell indices. For this first view every change is an addition. Decode 137 as row INT (137/32)=4, column 137-32*4=9: square (9,4).

Continue from Look around at corners. Remove its movement section and phase table as specified below. Keep a saved copy of that version if you want to compare it later.

Delete lines 400–440 in steps of ten, 455, 460, 8200–8800 in steps of ten.

Replace lines 100, 120, 140, 370, 1010, 4010.

Add lines 2020, 2070, 3000, 3005, 3010, 3020, 3030, 8900.

Delete a line by entering its number alone and pressing Enter. Then enter the replacements and additions below.

changes.bas
 100 DIM v$(608): LET phase=0
 120 LET x=3: LET y=16: LET gx=7: LET gy=5: LET gd=1
 140 GO SUB 1000: RESTORE 8900: READ gx,gy,gd,bn,n: GO SUB 3000: GO SUB 3300: GO SUB 2500
 370 IF v$(y*32+x)="!" OR (x=gx AND y=gy) THEN LET outcome=1: GO TO 4000
1010 PRINT AT 0,1; INK 5; BRIGHT 1;"NIGHT PATROL";AT 0,20;"SIGHT TEST"
2020 IF v$(r*32+c)="!" THEN LET shade=6
2070 IF v$(r*32+c)="!" AND ((r=y AND c=x) OR (r=3 AND c=27) OR (r=16 AND c=3)) THEN LET shade=2
3000 IF n=0 THEN RETURN
3005 FOR j=1 TO n: READ cell: LET v$(cell)="!"
3010 LET r=INT (cell/32): LET c=cell-32*r
3020 PRINT AT r+1,c; PAPER 6; INK 7; BRIGHT 1;" "
3030 NEXT j: LET r=y: LET c=x: GO SUB 2000: RETURN
4010 IF outcome=1 THEN PRINT AT 1,1; INK 2; BRIGHT 1;"SPOTTED! Use the hiding places. "
8900 DATA 7,5,1,11,11,168,137,169,201,138,170,202,139,171,140,172
Complete program at this stage
night-patrol.bas
  10 GO SUB 7000: GO SUB 7100
 100 DIM v$(608): LET phase=0
 120 LET x=3: LET y=16: LET gx=7: LET gy=5: LET gd=1
 130 LET got=0: LET outcome=0: LET beat=0: LET steps=0
 140 GO SUB 1000: RESTORE 8900: READ gx,gy,gd,bn,n: GO SUB 3000: GO SUB 3300: GO SUB 2500
 150 LET tick=PEEK 23672
 200 LET k$=INKEY$
 210 IF k$="q" OR k$="Q" THEN GO TO 9000
 220 IF k$="r" OR k$="R" THEN GO SUB 8100: GO TO 100
 230 LET elapsed=PEEK 23672-tick: IF elapsed<0 THEN LET elapsed=elapsed+256
 240 IF elapsed<8 THEN GO TO 200
 250 LET k$=INKEY$
 260 LET dx=0: LET dy=0
 270 IF k$="i" OR k$="I" THEN LET dy=-1
 280 IF k$="k" OR k$="K" THEN LET dy=1
 290 IF k$="j" OR k$="J" THEN LET dx=-1
 300 IF k$="l" OR k$="L" THEN LET dx=1
 310 LET nx=x+dx: LET ny=y+dy
 320 IF m$(ny,nx)=CHR$ 145 THEN GO TO 370
 330 IF nx=x AND ny=y THEN GO TO 370
 340 LET r=y: LET c=x: LET x=nx: LET y=ny: GO SUB 2000
 350 LET steps=steps+1: LET r=y: LET c=x: GO SUB 2000
 370 IF v$(y*32+x)="!" OR (x=gx AND y=gy) THEN LET outcome=1: GO TO 4000
 380 IF x=27 AND y=3 AND got=0 THEN LET got=1: GO SUB 2500
 390 IF x=3 AND y=16 AND got=1 THEN LET outcome=2: GO TO 4000
 470 LET tick=PEEK 23672: GO TO 200
1000 BORDER 0: PAPER 0: INK 7: BRIGHT 0: CLS
1010 PRINT AT 0,1; INK 5; BRIGHT 1;"NIGHT PATROL";AT 0,20;"SIGHT TEST"
1020 FOR r=1 TO 18: PRINT AT r+1,1; INK 1; BRIGHT 1;m$(r): NEXT r
1022 LET r=3: LET c=27: GO SUB 2000
1024 LET r=y: LET c=x: GO SUB 2000
1030 PRINT AT 20,1;"I up  J left  K down  L right"
1040 PRINT AT 21,1;"R retry              Q quit";
1050 RETURN
2000 LET z$=" ": LET colour=7: LET shade=0
2010 IF m$(r,c)=CHR$ 145 THEN LET z$=CHR$ 145: LET colour=1
2020 IF v$(r*32+c)="!" THEN LET shade=6
2030 IF r=3 AND c=27 AND got=0 THEN LET z$=CHR$ 146: LET colour=5
2040 IF r=16 AND c=3 THEN LET z$=CHR$ 147: LET colour=4
2050 IF r=gy AND c=gx THEN LET z$=g$(gd): LET colour=6
2060 IF r=y AND c=x THEN LET z$=CHR$ 144: LET colour=7
2070 IF v$(r*32+c)="!" AND ((r=y AND c=x) OR (r=3 AND c=27) OR (r=16 AND c=3)) THEN LET shade=2
2080 PRINT AT r+1,c; PAPER shade; INK colour; BRIGHT 1;z$: RETURN
2500 PRINT AT 1,1; INK 5;"TAKE THE FILE. RETURN TO EXIT. "
2510 IF got=1 THEN PRINT AT 1,1; INK 4;"FILE TAKEN. GET BACK TO EXIT.  "
2520 RETURN
3000 IF n=0 THEN RETURN
3005 FOR j=1 TO n: READ cell: LET v$(cell)="!"
3010 LET r=INT (cell/32): LET c=cell-32*r
3020 PRINT AT r+1,c; PAPER 6; INK 7; BRIGHT 1;" "
3030 NEXT j: LET r=y: LET c=x: GO SUB 2000: RETURN
3300 LET r=gy: LET c=gx: GO SUB 2000: RETURN
4000 LET r=y: LET c=x: GO SUB 2000
4010 IF outcome=1 THEN PRINT AT 1,1; INK 2; BRIGHT 1;"SPOTTED! Use the hiding places. "
4020 IF outcome=2 THEN PRINT AT 1,1; INK 4; BRIGHT 1;"FILE RECOVERED. CLEAN GETAWAY! "
4030 PRINT AT 20,1;"R tries again. Q quits.      "
4040 GO SUB 8000
4050 IF k$="r" OR k$="R" THEN GO SUB 8100: GO TO 100
4060 IF k$="q" OR k$="Q" THEN GO TO 9000
4070 GO TO 4040
7000 RESTORE 7500: FOR j=0 TO 31: READ a: POKE USR "a"+j,a: NEXT j
7010 DIM a(4): DIM b(4): LET a(1)=1: LET b(2)=1: LET a(3)=-1: LET b(4)=-1
7020 LET g$=">v<"+CHR$ 94: RETURN
7100 DIM m$(18,30): RESTORE 7200
7110 FOR r=1 TO 18: READ m$(r): FOR c=1 TO 30
7120 IF m$(r,c)="#" THEN LET m$(r,c)=CHR$ 145
7130 NEXT c: NEXT r: RETURN
7200 DATA "##############################"
7210 DATA "##############################"
7220 DATA "###########  ############# ###"
7230 DATA "#####        #######     # ###"
7240 DATA "#####                      ###"
7250 DATA "#####     ######   #     #####"
7260 DATA "###### #########  ##### ######"
7270 DATA "######   ############   ######"
7280 DATA "######   ############   ######"
7290 DATA "######  ##############  ######"
7300 DATA "####   ################   ####"
7310 DATA "####   ####  ##########   ####"
7320 DATA "#####        #######     #####"
7330 DATA "#####                    #####"
7340 DATA "#####     ######   #     #####"
7350 DATA "##     #########  ############"
7360 DATA "##############################"
7370 DATA "##############################"
7500 DATA 24,24,0,60,90,24,36,66
7510 DATA 255,128,128,255,8,8,8,255
7520 DATA 126,66,90,66,90,66,126,0
7530 DATA 126,66,66,74,66,66,126,0
8000 GO SUB 8100
8010 LET k$=INKEY$: IF k$="" THEN GO TO 8010
8020 RETURN
8100 IF INKEY$<>"" THEN GO TO 8100
8110 RETURN
8900 DATA 7,5,1,11,11,168,137,169,201,138,170,202,139,171,140,172
9000 PAPER 0: INK 7: BRIGHT 0: PRINT AT 21,0;"Finished. RUN to try again.     ";: STOP

Display the mask and use it

The routine at 3000 reads the eleven cells, marks them in v$ and prints yellow paper at their screen positions. The per-square drawing routine also consults v$, so restoring a square preserves its lit background. It uses red paper under a visible player, file or entrance to keep the symbol distinct.

Line 370 checks v$(y*32+x)="!" after a player move, alongside contact with the guard. The program reads logical visibility. Changing a screen colour cannot make a hidden square dangerous.

RUN. The guard must stay at (7,5), facing east. Walk into the displayed fan to be spotted. Retry, then use the surrounding walls and unlit passages to fetch the file. A successful return is still possible; this diagnostic stage isolates sight, not the objective.

Is every unlit square protected by a wall?

Show the explanation

No. Some are outside the fan’s direction or range; others are candidates blocked by walls. Both are unseen now, but a turn or step can change which ones are safe.

Sources

Steven Vickers, edited by Robin Bradbeer, ZX Spectrum BASIC Programming, second edition (Sinclair Research, 1983), chapter 8, chapter 12, chapter 16.

Run it here

This unit's program, built for you.

Program
night-patrol.tap
Machine
ZX Spectrum 48K (PAL)

Download night-patrol.tap