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

Episode 7: Roaming the Maze

Previously we promised to add roaming to the map display soon — and, if we're talking US-time, it's still the same day.

Roaming the map display

The Olivetti M10, the NEC PC-8201A, Their maze & Its player marker.
(Sorry, no cook in this image.)

Encoding Movement

Directional encodings are really simple, just an integer in the range of 0..3, where 0 is up, 1 is left, etc. A player's position is thus described by a triple vector made of the X-coordinate, the Y-coordinate, and the heading/direction. Certainly we want to turn left and turn right and we will provide forward and backward movement. For this we'll add two arrays DX and DY for positional deltas corresponding to movements in the respective directions.

Maze Directions
(There is no way out.)

heading  code   dx    dy
   up      0     0    -1
  left     1    -1     0
  down     2     0     1
 right     3     1     0

In order to turn, we just iterate the direction (an increment represents a counter-clockwise turn) and limit the range by a MOD 4 operation. To move forward, we'll add the respective DX and DY values, to move backward we will subtract them. Before we move or turn, we'll erase the marker from the map and redraw it with updated coordinates. In case we bump into a wall, we just revert the movement again and there will be a short flicker to indicate the movement attempt. (We can't use a sound in order to not to disclose any player related informations to the opponent.)

Drawing the Player Marker

This is dead simple as we're using PSET to draw the marker pixel by pixel and PRESET to clear it again. We're using PSET/PRESET, because we would have to figure out relative offsets and pages across display blocks (segments) for each of these pixels anyway. (There is no guarantee that the respective pixels will be all in the same block and in the same row.) So there's nothing to win using a low-level approach in BASIC and we can leave this to the machine language implementation in ROM, which will be certainly faster.

The player marker is represented by a pattern of 4 pixels in a 3 × 3 square and we will store the relative positions (X/Y) of the active pixels in an array of a length of 4 (since there are 4 possible directions or headings).

100 REM drawing the player marker
110 REM ML: maze left position(in px), MT: maze top position
120 REM MX: player x position in the maze, MY: player y position
130 REM MD: direction/heading of the player marker
200 REM set up the marker definitions in array SM
210 DIM SM(3,3,2)
220 FOR I=0 TO 3
230 FOR J=0 TO 3:READ B1,B2:SM(I,J,1)=B1:SM(I,J,0)=B2:NEXT
240 NEXT
250 REM setup vars
260 ML=132:MT=5:MX=11:MY=7:MD=3
300 REM main, calling example: clear player, move, redraw
310 GOSUB 400:MX=MX+1:GOSUB 500
320 END
400 REM clear the player marker
410 X=ML+MX*3:Y=MT+MY*3
420 FOR I=0 TO 3:PRESET(X+SM(MD,I,0),Y+SM(MD,I,1)):NEXT
430 RETURN
500 REM draw the player marker
510 X=ML+MX*3:Y=MT+MY*3
520 FOR I=0 TO 3:PSET(X+SM(MD,I,0),Y+SM(MD,I,1)):NEXT
530 RETURN
1000 REM player maker definitions, active pixels (0..3: 4 x Y,X)
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

Note: We may optimize this in the future by collapsing the 3-dimensional array storing the pixel patterns into a 2-dimensional one or even into a flat 1D-array. Here, we keep it simple for testing.
Moreover, there are always 3 pixels in common when a player turns — another potential for optimization.

Reading the Keyboard

Reading the keyboard is a bit more interesting. We could keep it simple and us INKEY$, but the Kyocera Siblings have a keyboard buffer of up to 32 characters, which not only provides some "typeahead" but also makes laggy player controls.

Let's see how this is implemented. In the "houskeeping" section of the RAM, we find the following entry (here for the TRS-80 Model 100):

0xFFAA (65450) -  Number of characters in keyboard buffer.

0xFFAB (65451) -  Start of keyboard typeahead buffer (32 bytes)

We could inspect the keyboard buffer pointer regularly, say, in a loop, and in case we find a value other than zero, this will give us the offset to the last character typed. We may PEEK this address to get an ASCII value and reset the buffer pointer to zero by a POKE. This will give us always the last character typed. For movements we'll use cursor keys and IJKL as these are at the same positions in every layout (not true for WASD and AZERTY keyboards). For fire the space bar may be appropriate, moreover, we'll add "Q" for quit.

The keyboard buffer is found at different locations in the various models, so we'll keep its address in a variable of type SNG (single precision). Therefor we add another definition for any variable identifiers starting with "A" to be of single precision type.

And these are the addresses of the keyboard buffer offset pointer (with the buffer starting right at the next location):

Location of Keyboard Buffer
(First byte is buffer length/offset of last character, or zero.) 

TRS Model 100 .... 0xFFAA (65450)
NEC PC-8101A ..... 0xFE68 (65128)
Olivetti M10 ..... 0xFF6D (65389)

Here is a complete example (Model 100), mind the bang marking up the single-precision type of the numeric address literal (so we don't have to convert this to a negative number in order to fit 16-bits single precision):

100 REM reading the keyboard, most recent character only (Model 100)
110 DEFSNG A:DEFINT B,K:AK=65450!
120 B=PEEK(AK):IF B=0 THEN 120 :REM read the buffer offset
130 K=PEEK(AK+B):POKE AK,0     :REM get value of last char, reset
140 PRINT CHR$(K);:GOTO 120    :REM print it and resume

Since the values in the buffer are all ASCII, we may normalize them to upper case by masking bit 5 (32). So we define constants for 97 (LC, first lower case character) and 223 (UC, mask to get upper case characters).

The Program

Not much else to add. Our program is gaining BASIC-credibility by growing a bit crowded. And this while we're still trading speed for readability and are keeping most spaces …

10 REM Maze Roamer
20 DEFSNG A:DEFINT B-Z:SCREEN 0,0:CLS:PRINT"Setting up..."
29 REM M: 1 = PC-8201A, 2 = M10 (no modem), 3 = Model 100
30 P=PEEK(1):M = (P=148)*-1 + (P=35)*-2 + (P=51)*-3:IF M=0 THEN END
40 IF M=1 THEN AK=65128!:ELSE IF M=2 THEN AK=65389!: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
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 DIM DX(3),DY(3):FOR I=C0 TO C3:READ B1,B2:DX(I)=B1:DY(I)=B2: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 MW=31:MH=15:ML=132:MT=5:DIM M(MH,MW),BM(C9)
120 FOR Y=C0 TO C9:READ B:BM(Y)=B:NEXT
130 FOR Y=C0 TO MH:FOR X=C0 TO MW:READ B:M(Y,X)=B:NEXT:NEXT
199 REM == main  ==
200 CLS:GOSUB 700:MX=11:MY=7:MD=0:GOSUB 410
210 PRINT"Use cursor keys":PRINT"or IJKL to move,":PRINT"Q to quit."
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
499 REM key handling (left, right, bwd, fwd, fire, quit)
500 GOSUB 400:MD=(MD+C3) MOD C4:GOSUB 410:GOTO 220
520 GOSUB 400:MD=(MD+C1) MOD C4:GOSUB 410:GOTO 220
540 GOSUB 400:MX=MX+DX(MD):MY=MY+DY(MD):IF M(MY,MX)=C0 THEN MX=MX-DX(MD):MY=MY-DY(MD)
550 GOSUB 410:GOTO 220
560 GOSUB 400:MX=MX-DX(MD):MY=MY-DY(MD):IF M(MY,MX)=C0 THEN MX=MX+DX(MD):MY=MY+DY(MD)
570 GOSUB 410:GOTO 220
580 GOTO 220:REM shoot
590 END
699 REM maze display
700 SY=MT\C8:Y0=0:D=MT MOD C8+C2:ON M GOSUB 910,920,930
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 M GOSUB 960,970,980:RETURN
900 REM disable interrupts
910 EXEC 30437:RETURN
920 CALL 29558:RETURN
930 CALL 30300:RETURN
950 REM enable interrupts
960 EXEC 29888:RETURN
970 CALL 28998:RETURN
980 CALL 29756: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 maze maker defs (0..3:Y,X)
1005 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
1006 REM maze bit patterns
1007 DATA 1,3,7,14,28,56,112,224,192,128
1010 REM maze data
1011 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
1012 DATA 0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
1013 DATA 0,1,0,1,0,1,0,0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,1,0
1014 DATA 0,0,0,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,0
1015 DATA 0,1,0,1,0,0,1,0,0,0,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0,1,1,1,0,0,0
1016 DATA 0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,0,1,0,1,1,1,1,1,1,0,0,0,1,1,0,0
1017 DATA 0,1,0,1,0,0,0,0,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,0,1,1,0
1018 DATA 0,1,0,1,1,1,1,1,1,1,0,1,0,1,0,0,0,1,1,1,1,1,0,0,1,0,0,1,1,0,0,0
1019 DATA 0,1,0,0,0,1,0,0,0,1,1,1,0,1,1,1,0,0,0,0,0,1,0,1,1,1,0,1,0,0,1,0
1020 DATA 0,1,1,1,0,1,1,1,0,1,0,1,0,1,0,1,0,1,1,1,0,1,0,1,0,1,0,1,1,1,1,0
1021 DATA 0,0,0,1,0,1,0,1,0,1,0,1,1,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,1,0,1,0
1022 DATA 0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,0,1,0
1023 DATA 0,1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,1,0,1,0,1,0,1,0
1024 DATA 0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0
1025 DATA 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
1026 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

BTW, "SCREEN 0,0" in line 20 means, use screen 0 without displaying key labels. (Yes, there could be an external CRT, "SCREEN 1"!) — Maybe we should turn key labels back on, when we leave the program (line 590, "SCREEN 0,1")?

And here is the program on the Olivetti M10:

Roaming the maze on the Olivetti M10

Roaming the maze on the Olivetti M10.

And here on the NEC PC-8101A, proving the cross-platform claim:

Roaming the maze on the NEC PC-8101A

Roaming the maze on the NEC PC-8101A.

With a roamable maze on the screen and working player controls, we somewhat made our midterm goal. Even with the perspective view and networking still to come, we may imagine ourselves quite on schedule.

:-)

 

Next:   Episode 8: Creative Pico Murder — A Virtual Marketing Campaign

Previous:   Episode 6: The Round and the Square — Displaying the Maze Map

Back to the index.

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