Retrochallenge 2016/01:
Maze War for Olivetti M10 and NEC PC-8201A

Episode 11: A Path to the Maze

In the previous episode we were, once again, exploring the past, now it's time to look ahead, to see, what lies in front of us.

RC2016/01:Displaying a path to the maze

First person, Rogue-like. — A maze and a path.

DATAism

Currently, we've got a roamable maze, which isn't that bad at all, but we really should be advancing in our project. Today we're going to implement a path view relative to the player's position. It won't be the final perspective view, it's merely about drawing a symbolic representation, something of a distant remembrance of Rogue.

Maybe you were wondering, back in episode #5 when we were encoding the maze data, why we hadn't used some nifty 8-bit compression, so that our quite massive amount of encoded maze data would have looked some like this:

1000 REM maze data as bit vectors, just 3 bytes per row
1001 DATA 0,0,0,255,191,255,42,19,32,233,251,175,74,40,170,255,142,251,10,233,3,251
1002 DATA 42,63,34,238,161,238,170,174,168,186,170,42,170,170,235,171,191,10,8,5
1003 DATA 255,255,255,0,0,0

I've to confess, I had some ulteriors on this, and they were all about today's episode.

And here is our evil plan for maze domination. … Maze Domination! … Today, it may be just the Maze, but tomorrow, tomorrow, it's the World! … harhar … (exit, hallways echo of hollow laughter).

Er … Sorry … er … got carried away … somehow … er … however …

I was going to write some about a little plan (rubs hands), and this is what it is about: We're going to encode directions to any neighboring maze-cells into the individual data points. For this, we'll stay with our directional encoding (as in 0: up, 1: left, 2: down, 3: right) and raise them to powers of two. As we collect the data, we also encode the connections as directional bit-vectors. As before, a data point representing a wall will be just zero.

1019 REM maze data (encoded connections: 1: up, 2: left, 4: down, 8: right)
1020 DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
1021 DATA 0,12,10,14,10,14,10,10,10,14,10,10,14,2,0,8,14,10,10,10,10,14,10,10,10,14,10,10,10,10,6,0
1022 DATA 0,1,0,5,0,5,0,0,0,5,0,0,5,0,0,0,5,0,0,0,0,5,0,0,0,5,0,0,0,0,5,0
1023 DATA 0,0,0,5,0,9,14,10,10,3,0,12,11,14,10,10,11,14,10,6,0,5,0,12,10,11,6,0,12,10,3,0
1024 DATA 0,4,0,5,0,0,5,0,0,0,0,5,0,1,0,0,0,5,0,5,0,5,0,5,0,0,9,10,7,0,0,0
1025 DATA 0,13,10,15,10,10,11,10,10,10,10,7,0,0,0,4,0,5,0,9,10,11,10,11,6,0,0,0,9,6,0,0
1026 DATA 0,5,0,5,0,0,0,0,0,0,0,5,0,12,10,11,10,7,0,0,0,0,0,0,13,10,10,6,0,9,2,0
1027 DATA 0,5,0,9,10,14,10,10,10,6,0,5,0,5,0,0,0,9,10,10,10,6,0,0,5,0,0,13,2,0,0,0
1028 DATA 0,5,0,0,0,5,0,0,0,13,10,7,0,13,10,6,0,0,0,0,0,5,0,12,11,6,0,5,0,0,4,0
1029 DATA 0,9,10,6,0,13,10,6,0,5,0,5,0,5,0,5,0,12,10,6,0,5,0,5,0,5,0,9,14,10,7,0
1030 DATA 0,0,0,5,0,5,0,1,0,5,0,13,10,7,0,5,0,5,0,5,0,5,0,5,0,5,0,0,5,0,5,0
1031 DATA 0,4,0,5,0,5,0,0,0,5,0,5,0,5,0,5,0,5,0,5,0,5,0,5,0,9,14,10,7,0,5,0
1032 DATA 0,5,0,5,0,9,10,10,10,3,0,5,0,1,0,9,10,11,14,11,10,3,0,9,6,0,5,0,1,0,5,0
1033 DATA 0,5,0,5,0,0,0,0,0,0,0,5,0,0,0,0,0,0,5,0,0,0,0,0,5,0,5,0,0,0,5,0
1034 DATA 0,9,10,11,10,10,10,10,10,10,10,11,10,10,10,10,10,10,11,10,10,10,10,10,11,10,11,10,10,10,3,0
1035 DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

See the modified JS-program used to aggregate the maze data here.

We already know, what's left and right of us, thank's to the modular operations we apply when performing a turn. Now we add some DATA statements to provide the respective connection codes. And while we're at it, we may encode as well the relative turns, to save some runtime. By this, we get some like the following:

1002 REM directions (0..3:dx,dy)
1003 DATA 0,-1, -1,0, 0,1, 1,0
1004 REM turns (L: 0..3, R: 0..3)
1005 DATA 1,2,3,0, 3,0,1,2
1006 REM directional codes (2^0..3)
1007 DATA 1,2,4,8

We'll read the turns into arrays DL and DR respectively, and do the same with our new directional connection codes for array DD. Moreover, we save some other happy little bits (no Bob Ross didn't paint those) and runtime, as well, and store the current values of DX, DY, DL, and DR in simple variables of the same name (yes, these are separate name­spaces), like "DX=DX(MD)", where MD is our current heading/direction.

And while we're at optimizations, you may recall the active pixels of the map-marker for any of the four headings, being 4 pixels each, which have to be cleared and repainted whenever the player moves or turns:

1010 DATA 0,1, 1,0, 1,1, 1,2
1020 DATA 0,1, 1,0, 1,1, 2,1
1030 DATA 1,0, 1,1, 1,2, 2,1
1040 DATA 0,1, 1,1, 1,2, 2,1

Obviousy, for any turn, there's only a single pixel that must be erased and another one that must be set. We can optimize on this by storing the changing pixels in another data statement, to be read into array ST:

1010 REM maze marker turn pixel mods (Y,X)
1011 DATA 2,1,1,0, 1,2,2,1, 0,1,1,2, 1,0,0,1

Whenever the player turns left, the pixel described by the first pair of relative offsets is to be erased and the second pair gives the position of the pixel to be newly set. If a right turn occurs, it's the other way round, but using the old direction as the subscript.

Drawing the Path

Back to our little plan (no, we had already enough of this villain theme). Now, that we have the connections encoded in our maze data and know what's relatively left and right of us, it's easy to collect a path (array V) of data points lying straight ahead of the player, starting at the very position in the maze:

   DIM V(7)                                      :REM path for view-port
   MW=31:MH=15                                    :REM maze dimensions
   MX=11:MY=7:MD=0                                :REM init player
   DX=DX(MD):DY=DY(MD)
   DL=DL(MD):DR=DR(MD)

   GOTO <collect-path>

<collect-path>:
   Y=MY:X=MX                                      :REM copy current position
   FOR I=0 TO 7
   V(I)=M(Y,X)                                    :REM collect path
   X=X+DX:Y=Y+DY                                  :REM advance the data cursors
   IF (X<0) OR (X>MW) OR (Y<0) OR (Y>MH) THEN <render-view>
   NEXT

<render-view>:
   VW=0                                           :REM flag: hit wall
   VL=DD(DL):VR=DD(DR)                            :REM get code for left & right
   FOR I=0 TO 5
   PRINT CHR$(27);"Y";CHR$(32+7-I);CHR$(32+6);    :REM set cursor using ANSI escapes
   IF VW THEN PRINT "   ";:GOTO <iterate>         :REM Hit wall, just spaces
   IF V(I) AND VL THEN PRINT ".";:ELSE PRINT " "; :REM left of the path
   IF I=0 THEN PRINT "o"; ELSE PRINT ".";         :REM on the path
   IF V(I) AND VR THEN PRINT ".";:ELSE PRINT " "; :REM right of the path
   VW=VW OR V(I+1)=0                              :REM update wall flag
<iterate>:
   NEXT
   GOTO <next-task>

We render the path from the bottom up, starting at the current player position, represented by an "o". For any passage in front of us, or to the left or right, we'll render a dot, if there's a wall, or it's hidden by a wall in front of us, it's just blanks.

And this is what we get (Virtual T):

Running in Virtual T

Virtual T, dressed somewhat Olivetti-esque.

Note: Contrary to my last rant, the key detection of Virtual T isn't that bad, if running at speeds beyond realistic emulation.

And this is the program at its current state. We also added support for the TRS-80 Model 102 and the Kyotronic 85, as promissed in last episode. Also, we rename the model-ID from M to G for clarity, with a bow to the Gestalt-ID of classic Macs.

10 REM Maze Roamer with Path View
20 DEFSNG A:DEFINT B-Z:SCREEN 0,0:CLS:PRINT"Setting up..."
29 REM G: 1=PC-8201A, 2=M10 (no modem), 3=Model 100, 4=Model 102, 5=KC85
30 B=PEEK(1):G= -(B=148) -(B=35)*2 -(B=51)*3 -(B=167)*4 -(B=225)*5
35 IF G=0 THEN SCREEN 0,1:PRINT "Sorry, model not supported. Gestalt:";B:END
40 IF G=1 THEN AK=65128!:ELSE IF G=2 THEN AK=65389!:ELSE IF G=5 THEN AK=65387!:ELSE AK=65450!
50 C0=0:C1=1:C2=2:C3=3:C4=4:C5=5:C6=6:C7=7:C8=8:C9=9:CA=10:CL=50:CP=64
60 UC=223:LC=97:CK=27:ES=27:EB=32
70 PA=185:PB=186:PC=254:PD=255:DIM SA(9),SB(9):SG=-1:SH=0
80 FOR I=C0 TO C9:READ B1,B2:SA(I)=B1:SB(I)=B2:NEXT
90 DX=0:DY=0:DIM DX(3),DY(3):FOR I=C0 TO C3:READ B1,B2:DX(I)=B1:DY(I)=B2:NEXT
92 DL=0:DR=0:DIM DL(3),DR(3)
94 FOR I=C0 TO C3:READ B:DL(I)=B:NEXT
96 FOR I=C0 TO C3:READ B:DR(I)=B:NEXT
98 DIM DD(3):FOR I=C0 TO C3:READ B:DD(I)=B:NEXT
99 REM setup the maze
100 DIM SM(3,3,2):FOR I=C0 TO C3:FOR Y=C0 TO C3:READ B1,B2:SM(I,Y,C1)=B1:SM(I,Y,C0)=B2:NEXT:NEXT
110 DIM ST(3,1,1):FOR I=C0 TO C3:FOR Y=C0 TO C1:READ B1,B2:ST(I,Y,C1)=B1:ST(I,Y,C0)=B2:NEXT:NEXT
120 MW=31:MH=15:ML=132:MT=5:DIM M(MH,MW),BM(C9)
130 FOR Y=C0 TO C9:READ B:BM(Y)=B:NEXT
140 FOR Y=C0 TO MH:FOR X=C0 TO MW:READ B:M(Y,X)=B:NEXT:NEXT
150 REM setup view port
160 DIM V(C7)
199 REM == main  ==
200 CLS:GOSUB 700:MX=11:MY=7:MD=0:DX=DX(MD):DY=DY(MD):DL=DL(MD):DR=DR(MD)
210 PRINT CHR$(ES);"Y";CHR$(EB+C7);CHR$(EB+22);"Crsr,IJKL,Q:quit";
215 GOSUB 410:GOSUB 610
220 B=PEEK(AK):IF B=C0 THEN 220
230 K=PEEK(AK+B):POKE AK,C0:IF K>CK THEN ON K-CK GOTO 500,520,540,560
240 IF K>=LC THEN K=K AND UC
250 ON INSTR("JLKI Q",CHR$(K)) GOTO 520,500,560,540,580,590
260 GOTO 220
298 REM == subroutines ==
299 REM segement/pos select
300 SH=SG:SG=SX\CL:IF SY>C3 THEN SG=SG+C5
310 IF (SG<>SH) THEN OUT PA,SA(SG):OUT PB,SB(SG)
320 OUT PC,(SY MOD C4)*CP OR SX MOD CL:RETURN
399 REM clear/draw marker
400 X=ML+MX*C3:Y=MT+MY*C3:FOR I=C0 TO C3:PRESET(X+SM(MD,I,C0),Y+SM(MD,I,C1)):NEXT:RETURN
410 X=ML+MX*C3:Y=MT+MY*C3:FOR I=C0 TO C3:PSET(X+SM(MD,I,C0),Y+SM(MD,I,C1)):NEXT:RETURN
420 X=ML+MX*C3:Y=MT+MY*C3:PRESET(X+ST(MD,T0,C0),Y+ST(MD,T0,C1)):PSET(X+ST(MD,T1,C0),Y+ST(MD,T1,C1)):RETURN
499 REM key handling (right, left, bkwd, fwd, fire, quit)
500 T0=C1:T1=C0:GOSUB 420:MD=DR:DX=DX(MD):DY=DY(MD):DL=DL(MD):DR=DR(MD):GOTO 610
520 MD=DL:T0=C0:T1=C1:GOSUB 420:DX=DX(MD):DY=DY(MD):DL=DL(MD):DR=DR(MD):GOTO 610
540 GOSUB 400:MX=MX+DX:MY=MY+DY:IF M(MY,MX)=C0 THEN MX=MX-DX:MY=MY-DY:GOSUB 410:GOTO 220
550 GOSUB 410:GOTO 610
560 GOSUB 400:MX=MX-DX:MY=MY-DY:IF M(MY,MX)=C0 THEN MX=MX+DX:MY=MY+DY:GOSUB 410:GOTO 220
570 GOSUB 410:GOTO 610
580 GOTO 220:REM shoot
590 SCREEN 0,1:END
600 REM view port, get cells
610 Y=MY:X=MX
620 FOR I=C0 TO C7:V(I)=M(Y,X):X=X+DX:Y=Y+DY:IF (X<C0) OR (X>MW) OR (Y<C0) OR (Y>MH) THEN 640
630 NEXT
639 REM render *ersatz* view
640 VW=C0:VL=DD(DL):VR=DD(DR)
645 FOR I=C0 TO C5:PRINT CHR$(ES);"Y";CHR$(EB+C7-I);CHR$(EB+C6);
650 IF VW THEN PRINT "   ";:GOTO 675
655 IF V(I) AND VL THEN PRINT ".";:ELSE PRINT " ";
660 IF I=0 THEN PRINT "o"; ELSE PRINT ".";
665 IF V(I) AND VR THEN PRINT ".";:ELSE PRINT " ";
670 VW=VW OR V(I+C1)=C0
675 NEXT:GOTO 220
699 REM maze display
700 SY=MT\C8:Y0=0:D=MT MOD C8+C2:ON G GOSUB 910,920,930,930,940
710 Y1=Y0+C1:Y2=Y0+C2:Y3=Y0+C3:B0=BM(D)
720 IF (Y1<=MH) AND (D<C7) THEN B1=BM(D+C3):ELSE B1=C0
730 IF (Y2<=MH) AND (D<C4) THEN B2=BM(D+C6):ELSE B2=C0
740 IF (Y3<=MH) AND (D=C0) THEN B3=BM(D+C9):ELSE B3=C0
750 SX=ML:GOSUB 300
760 FOR MX=C0 TO MW:IF M(Y0,MX)=C0 THEN B=B0 ELSE B=C0
770 IF B1 THEN IF M(Y1,MX)=C0 THEN B=B OR B1
780 IF B2 THEN IF M(Y2,MX)=C0 THEN B=B OR B2
790 IF B3 THEN IF M(Y3,MX)=C0 THEN B=B OR B3
800 FOR I=C0 TO C2:IF SX MOD CL=C0 THEN GOSUB 300
810 OUT PD,B:SX=SX+C1:NEXT:NEXT
820 Y0=Y0+(CA-D)\C3:IF Y0>MH THEN 840
830 D=(D+C1)MOD C3:SY=SY+C1:GOTO 710
840 ON G GOSUB 960,970,980,980,990:RETURN
900 REM disable interrupts
910 EXEC 30437:RETURN
920 CALL 29558:RETURN
930 CALL 30300:RETURN
940 CALL 29450:RETURN
950 REM enable interrupts
960 EXEC 29888:RETURN
970 CALL 28998:RETURN
980 CALL 29756:RETURN
990 CALL 28906:RETURN
1000 REM port patterns for segments (0..9: PA,PB)
1001 DATA 1,0,2,0,4,0,8,0,16,0,32,0,64,0,128,0,0,1,0,2
1002 REM directions (0..3:dx,dy)
1003 DATA 0,-1,-1,0,0,1,1,0
1004 REM turns (L: 0..3, R: 0..3)
1005 DATA 1,2,3,0, 3,0,1,2
1006 REM directional codes (2^0..3)
1007 DATA 1,2,4,8
1008 REM maze maker defs (0..3:Y,X)
1009 DATA 0,1,1,0,1,1,1,2, 0,1,1,0,1,1,2,1, 1,0,1,1,1,2,2,1, 0,1,1,1,1,2,2,1
1010 REM maze marker turn pixel mods (Y,X)
1011 DATA 2,1,1,0, 1,2,2,1, 0,1,1,2, 1,0,0,1
1012 REM maze bit patterns
1013 DATA 1,3,7,14,28,56,112,224,192,128
1019 REM maze data
1020 DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
1021 DATA 0,12,10,14,10,14,10,10,10,14,10,10,14,2,0,8,14,10,10,10,10,14,10,10,10,14,10,10,10,10,6,0
1022 DATA 0,1,0,5,0,5,0,0,0,5,0,0,5,0,0,0,5,0,0,0,0,5,0,0,0,5,0,0,0,0,5,0
1023 DATA 0,0,0,5,0,9,14,10,10,3,0,12,11,14,10,10,11,14,10,6,0,5,0,12,10,11,6,0,12,10,3,0
1024 DATA 0,4,0,5,0,0,5,0,0,0,0,5,0,1,0,0,0,5,0,5,0,5,0,5,0,0,9,10,7,0,0,0
1025 DATA 0,13,10,15,10,10,11,10,10,10,10,7,0,0,0,4,0,5,0,9,10,11,10,11,6,0,0,0,9,6,0,0
1026 DATA 0,5,0,5,0,0,0,0,0,0,0,5,0,12,10,11,10,7,0,0,0,0,0,0,13,10,10,6,0,9,2,0
1027 DATA 0,5,0,9,10,14,10,10,10,6,0,5,0,5,0,0,0,9,10,10,10,6,0,0,5,0,0,13,2,0,0,0
1028 DATA 0,5,0,0,0,5,0,0,0,13,10,7,0,13,10,6,0,0,0,0,0,5,0,12,11,6,0,5,0,0,4,0
1029 DATA 0,9,10,6,0,13,10,6,0,5,0,5,0,5,0,5,0,12,10,6,0,5,0,5,0,5,0,9,14,10,7,0
1030 DATA 0,0,0,5,0,5,0,1,0,5,0,13,10,7,0,5,0,5,0,5,0,5,0,5,0,5,0,0,5,0,5,0
1031 DATA 0,4,0,5,0,5,0,0,0,5,0,5,0,5,0,5,0,5,0,5,0,5,0,5,0,9,14,10,7,0,5,0
1032 DATA 0,5,0,5,0,9,10,10,10,3,0,5,0,1,0,9,10,11,14,11,10,3,0,9,6,0,5,0,1,0,5,0
1033 DATA 0,5,0,5,0,0,0,0,0,0,0,5,0,0,0,0,0,0,5,0,0,0,0,0,5,0,5,0,0,0,5,0
1034 DATA 0,9,10,11,10,10,10,10,10,10,10,11,10,10,10,10,10,10,11,10,10,10,10,10,11,10,11,10,10,10,3,0
1035 DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

As for the real thing, the state of affairs on the Olivetti M10:

RC2016/01:Displaying a path on the Olivetti M10

And on the NEC PC-8201A:

RC2016/01:Displaying a path on the NEC-PC8201A

Mind the characters in the line below the maze rendering differently on the Olivetti and on the NEC.

Now, we've only to render this as a perspective in a view-port, pseudo-3D …

— Stay tuned! —

 

Next:   Episode 12: A Screen With a View

Previous:   Episode 10: Cross-Platform ROM Diving

Back to the index.

— This series is part of Retrochallenge 2016/01. —