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.
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:
And here on the NEC PC-8101A, proving the cross-platform claim:
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.
2016-01-16(17 GMT), Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2016/01. —