Episode 12: A Screen With a View
It's more than a week since previous episode and we really ought to hurry up for a final run.
An entire week passed without a chance to continue on the project, bummer. (Caught a cold, bussy catching up, day off for teaching — final presentations, actually pleasant, presentations better than expected, some were really good —, birthdays to be congratulated for and to be celebrated, you name it….) On the good side, there were still some quiet moments to contemplate the project — and so we were able to come up with some really nifty plan for implementing the viewport…
There is no good masterplan for real without a plan, and here is our's:
Sorry for this one. As for the real one, our considerations are along the following lines:
Since we've found out that we're going to write to the display low-level by talking to its controllers over the serial line, we reconsider the layout of the viewport a bit. We're going for a window size of 100 × 64 pixels, spanning over 4 adjacent display blocks (segments 0 and 1 in the upper half of the display and segments 5 and 6 in the lower half). By this we're able to divide the viewport into 4 symmetrical quadrants, with the vanishing point in the exact center.
And here is the plan to our plan:
Symmetry is nice thing, but for our purpose it's a thing of real beauty: Since we discovered that we may write to the display both left-to-right and right-to-left (by setting the direction of the auto-increment/decrement of the offset counter), we may use — more or less — the same code and definitions to write to left and right sides. And with a few adaptions we may also do the bottom half of the viewport, just add some code to reverse the vertical direction.
At closer inspection, there are some not so few areas (green) that are never to be updated, since there's no state of the viewport, where we would write to these areas. And then, there are some areas that will change only, if there was a frontally blocking wall before and the updated view hasn't any or the new blocking wall is farther away into the distance. Summing these areas up, they make up for the better half of the viewport. Good news!
Moreover, as we divide the viewport into 7 slices of depth, we may compare the new state to the previous one and will only have to update those regions of the screen that have actually changed. Even more good news!
Principally, a given left or righ part of any slice into the depth may have one of the following states:
- A continuous wall of the corridor leading ahead into the depth.
- A passage sideways into another corridor.
- Both may have either an edge or none.
Edges are to be drawn at corners of the maze only (following the Alto-implementation). With the semantics well defined, we may consider the layout at a per-pixel level.
Based on our layout, we may observe that there are:
- rows of successive pixels that are forming a line at a constant slope,
- rows of successive pixels that are just iterating the same pixel or pattern of pixels,
- individual offset locations which are either showing an edge or the pixels of a continuous line.
Let's dive right into it…
We may provide an extensive list of all pixels for any part of a sloped line, so that we may iterate over them:
REM line pixels for iterations REM 1..128: sloped downwards (k=-0.5), 128..1 sloped upwards (k=0.5) DATA 1, 1, 2, 2, 4, 4, 8, 8, 16, 16 DATA 32, 32, 64, 64, 128, 128, 64, 64, 32, 32 DATA 16, 16, 8, 8, 4, 4, 2, 2, 1, 1
Quite alike, we may come up with a list of pixels that are used for both horizontal and vertical lines. (Vertical line segments are either from any vertical pixel position filled downwards [x..128] for the upper half of the display, or in reversed direction for the lower part of the viewport [x..1].)
REM pixels for repeats DATA 0,1,2,4,8,16,32,64,128 :REM single pixels or horizontal lines DATA 255,254,252,248,240,224,192,128 :REM vertical lines REM inverse direction (bottom up) DATA 0,128,64,32,16,8,4,2,1 DATA 255,127,63,31,15,7,3,1
We're going to make use of them in some nifty way: We're going to implement a simple language for path drawing, consisting of just 4 commands:
- Move to position row (page) y, offset x.
- Repeat, until offset x is reached, the pixel with the given subscript.
- Iterate over the line pixel patterns to position x, starting with index i.
- Depending on the edge-state, draw either the pixel with the 1st subscript or the 2nd one.
As we may see, each of these commands is just three bytes long. Moreover, thanks to symmetry, we've only to provide the definitions for the first quadrant and may derive directions for the other quadrants from this.
We use this tiny command language to describe 3 states, namely, a wall on the side, a passage to the side, or a frontal blocking wall on any of the 7 depth-slices:
REM viewport commands struct, 1st quadrant, per level and mode REM 1..set position (page, offset) REM 2..repeat byte (stop-pos, idx of repeat-pixels) REM 3..iterate line from offset (stop-pos, idx of line-patterns) REM 4..alternate pixel ege:no-edge (idx of repeat-pixels, alternate idx) DATA 0 :REM level/distance 0 DATA 10 :REM mode 0: side-wall (10 commands) DATA 1,0,0, 3,5,10 :REM codes for row/page 0 DATA 1,1,0, 2,5,0, 2,6,1, 4,9,1 :REM codes for row/page 1 DATA 1,2,7, 4,9,0 :REM codes for row/page 2 DATA 1,3,7, 4,9,0 :REM codes for row/page 3 DATA 9 :REM mode 1: passage to the side (9 commands) DATA 1,0,0, 2,5,0 DATA 1,1,0, 2,6,1, 4,9,1 DATA 1,2,7, 4,9,0 DATA 1,3,7, 4,9,0 DATA 6 :REM mode 2: frontally blocking wall (6 commands) DATA 1,1,8, 2,49,1 DATA 1,2,8, 2,49,0 DATA 1,3,8, 2,49,0 DATA 1 :REM level 1 DATA 7 DATA 1,1,8, 3,15,2, 4,14,6 DATA 1,2,16, 4,9,0 DATA 1,3,16, 4,9,0 DATA 7 DATA 1,1,8, 2,15,6, 4,14,6 DATA 1,2,16, 4,9,0 DATA 1,3,16, 4,9,0 DATA 6 DATA 1,1,17, 2,49,6 DATA 1,2,17, 2,49,0 DATA 1,3,17, 2,49,0 DATA 2 :REM level 2 DATA 8 DATA 1,1,17, 3,21,11 DATA 1,2,17, 2,21,0, 3,23,0, 4,10,2 DATA 1,3,24, 4,9,0 DATA 7 DATA 1,1,17, 2,21,0 DATA 1,2,17, 2,23,2, 4,10,2 DATA 1,3,24, 4,9,0 DATA 4 DATA 1,2,25, 2,49,2 DATA 1,3,25, 2,49,0 DATA 3 :REM level 3 DATA 5 DATA 1,2,25, 3,30,3, 4,13,5 DATA 1,3,31, 4,9,0 DATA 5 DATA 1,2,25, 2,30,5, 4,13,5 DATA 1,3,31, 4,9,0 DATA 4 DATA 1,2,32, 2,49,5 DATA 1,3,32, 2,49,0 DATA 4 :REM level 4 DATA 5 DATA 1,2,32, 3,36,10, 4,8,8 DATA 1,3,37, 4,9,0 DATA 5 DATA 1,2,32, 2,36,8, 4,8,8 DATA 1,3,37, 4,9,0 DATA 4 DATA 1,2,38, 2,49,8 DATA 1,3,38, 2,49,0 DATA 5 :REM level 5 DATA 3 DATA 1,3,38, 3,41,0, 4,11,3 DATA 3 DATA 1,3,38, 2,41,3, 4,11,3 DATA 2 DATA 1,3,43, 2,49,3 DATA 6 :REM level 6 DATA 3 DATA 1,3,43, 3,45,5, 4,13,5 DATA 3 DATA 1,3,43, 2,45,5, 4,13,5 DATA 2 DATA 1,3,47, 2,49,5 DATA 7 :REM level 7 DATA 3 DATA 1,3,47, 3,48,9, 4,14,14 DATA 3 DATA 1,3,47, 2,48,6, 4,14,14 DATA 2 DATA 1,3,49, 2,49,14 DATA -1 :REM end of definition
A Simple Just-In-Time Compiler
Now, we're facing the classical time vs. space paradigm: Are we going to interpet this in realtime, or should we compile this into a form that is instantly of use for drawing any of the 4 quadrants? — You bet, we're opting for the fastest runtime solution!
For this purpose, we implement a tiny compiler to assemble some 3-byte codes for any of these commands, structured by quadrant, distance-level, and mode. We're going to store this in two arrays, one containing a continuous stream of commands, and a second one, providing the individual start and end positions for any state of the 4 quadrants.
This will produce byte code instructions of the following format:
- Set LCD address counter to byte [page/offset] (third byte always zero).
- Send n times the given byte to the LCD-controller.
- Iterate over the line-pixels array from start-index to stop-index while sending it to the LCD.
- Depending on the edge-state, send either the first byte or the second one to the display.
Say, we'd want to draw the third quadrant (q), for the nearest level (l), in mode 1 (m), so we'll look up array VI to get the start-index and stop-index of the sequence of drawing commands required to display this specific part of the view.
q = 2 : l = 0 : m = 1 VI(q,l,m,0) -> start, VI(q,l,m,1) -> end FOR I = start TO end STEP 3 ON VK(I) GOSUB <instr-1>, <instr-2>, <instr-3>, <instr-4> NEXT REM first argument in VK(I+1), second argument in VK(I+2).
And this is how we compile these instructions (using rather labels than line numbers):
REM segment addresses (port A) DATA 1,32,64,2 DIM PV(3):FOR I=0 TO 3:READ B:PV(I)=B:NEXT :REM segement codes (for port A) DIM BL(29):FOR I=0 TO 29:READ B:BL(I)=B:NEXT :REM bytes for lines DIM BP(33):FOR I=0 TO 33:READ B:BP(I)=B:NEXT :REM bytes for repeats DIM VI(3,7,2,1) :REM segment,level,mode,idx-from/idx-to DIM VK(1392) :REM compiled code REM compile viewport instructions I4=0 <loop>: READ B :REM readlevel IF B<0 THEN RETURN :REM -1: end FOR J=0 TO 2 :REM mode 0..2 READ B0 :REM get length JL=B0*3 I0=I4:I1=I0+JL:I2=I1+JL:I3=I2+JL:I4=I3+JL :REM code offsets VI(0,B,J,0)=I0:VI(0,B,J,1)=I1-3 :REM store them in VI VI(1,B,J,0)=I1:VI(1,B,J,1)=I2-3 VI(2,B,J,0)=I2:VI(2,B,J,1)=I3-3 VI(3,B,J,0)=I3:VI(3,B,J,1)=I4-3 FOR K=1 TO B0 :REM process 3 bytes READ B1,B2,B3 VK(I0)=B1:VK(I1)=B1:VK(I2)=B1:VK(I3)=B1 :REM reuse 1st opcode as-is ON B1 GOTO <instr1>,<instr2>,<instr3>,<instr4> :REM assemble args for each instr. <instr1>: :REM instr. 1: set row/offset VK(I0+1)=(B2*64) OR B3 :REM quadrant 0 (top left) VK(I1+1)=((3-B2)*64) OR B3 :REM quadrant 1 (bottom left) VK(I2+1)=((3-B2)*64) OR (CN-B3) :REM quadrant 2 (bottom right) VK(I3+1)=(B2*64) OR (CN-B3) :REM quadrant 3 (top right) P=B3 :REM update position GOTO <iterate> <instr2>: :REM instr 2: repeat byte L=B2-P:P=B2+1 VK(I0+1)=L:VK(I0+2)=BP(B3) VK(I1+1)=L:VK(I1+2)=BP(B3+17) VK(I2+1)=L:VK(I2+2)=BP(B3+17) VK(I3+1)=L:VK(I3+2)=BP(B3) GOTO <iterate> <instr3>: :REM instr 3: iterate from..to L=B2-P+B3:P=B2+1 VK(I0+1)=B3:VK(I0+2)=L VK(I1+1)=B3+14:VK(I1+2)=L+14 VK(I2+1)=B3+14:VK(I2+2)=L+14 VK(I3+1)=B3:VK(I3+2)=L GOTO <iterate> <instr4>: :REM instr 4: alternate bytes VK(I0+1)=BP(B2):VK(I0+2)=BP(B3) VK(I1+1)=BP(B2+17):VK(I1+2)=BP(B3+17) VK(I2+1)=BP(B2+17):VK(I2+2)=BP(B3+17) VK(I3+1)=BP(B2):VK(I3+2)=BP(B3) P=P+1 <iterate>: I0=I0+3:I1=I1+3:I2=I2+3:I3=I3+3 :REM increment subscripts NEXT K NEXT J GOTO <loop>
By assigning quadrants as in 0 (top left), 1 (bottom left), 2 (bottom right), 3 (top right), we may not only process the left side first and then the right one (reusing the path information for the two sides), we may also update our viewport in a nice fashion, sprialling in clockwise from the closest level painted at the outer regions of the display into the more distant levels towards the center.
Now we've to collect the data of what's lying in front of us first, using two arrays VL (left side) and VR (right side). A 1 represents a passage, a zero a wall, and if there's an edge to draw, we OR a 2:
REM assemble view path data REM CM = -1 REM M: maze data (2-dim, encodes directions as bit-vectors of powers of 2) REM MD: current viewing direction (0..3) REM DL: direction to the left relative to current direction (0..3) REM DR: direction to the right relative to current direction (0..3) REM DD: direction encodings as in M (powers of 2: 1,2,4,8) REM MX: current position x REM MY: current position y REM DX: delta x of current direction (of -1, 0, 1) REM DY: delta y of current direction (of -1, 0, 1) REM arrays DX, DY: delta x, delta y values per direction code REM W1: distance of last frontal wall, if any REM results in REM W0: distance of frontal wall to draw, if any REM arrays VL, VR: codes for drawing left and right sides Y=MY:X=MX:VL=DD(DL):VR=DD(DR):VF=DD(MD):W0=CM:FW=0 FOR I=0 TO 7 :REM depth levels B=M(Y,X) :REM maze code IF B AND VL THEN VL(I)=1 ELSE VL(I)=0 :REM passage/wall left IF B AND VR THEN VR(I)=1 ELSE VR(I)=0 :REM passage/wall right IF I=0 THEN <skip-1> IF VL(I)<>VL(I-1) THEN VL(I-1)=VL(I-1) OR 2 :REM edge left IF VR(I)<>VR(I-1) THEN VR(I-1)=VR(I-1) OR 2 :REM edge right <skip-1>: IF B AND VF THEN <skip-2> :REM no wall in front REM wall in front, fix up codes for sides: 4 = junction IF VL(I)=0 THEN VL(I)=2 ELSE IF M(Y+DY(DL),X+DX(DL)) AND VF THEN VL(I)=4 ELSE VL(I)=1 IF VR(I)=0 THEN VR(I)=2 ELSE IF M(Y+DY(DR),X+DX(DR)) AND VF THEN VR(I)=4 ELSE VR(I)=1 W0=I:FW=I+1:I=7:GOTO <skip-3> :REM break out <skip-2>: X=X+DX:Y=Y+DY :REM increment position <skip-3>: NEXT IF W0=W1 THEN W1=CM REM clear any side walls/paths behind a frontal wall IF (FW) AND (FW<8) THEN FOR I=FW TO 7:VL(I)=CM:VR(I)=CM:NEXT RETURN
Variable W0 is the distance of a blocking wall, if any, or -1 else. The code at label "
<skip-1>" is processing the blocking wall branch. Here we've actuall to cover an edge case: There are some Y-shaped junctions in the maze, and in this case, there will be no walled passage shown on the side(s), but merely the empty space of the passage way leading into the distance, sideways ahead. In this case, we've to draw the edge only, but none of the other definitions of the passage. The code for this is a 4 and it is to be handled specially by the code for drawing a frontal wall.
Conveying Bytes in RT
And this is the code for actually drawing the viewport, our little byte-code interpreter:
REM constants used to address the LCD PA=185:PB=186:PC=254:PD=255 :REM LCD ports (A,B,Cmd,Data) PL=33:PR=66 :REM LCD blocks (2^n), L:0+5, R:1+6 PU=59:PQ=58 :REM LCD setting: counter up/down DIM PV(3) :REM quadrant:block (port A: 1,32,64,2) REM subroutine: render-viewport ON G GOSUB <disable-interrupts> OUT PB,0 :REM deselect port B (segm. 8,9) OUT PA,PL:OUT PC,PU :REM select segm 0,5, set to increment OUT PA,PR:OUT PC,PQ :REM select segm 1,6, set to decrement FOR I=0 TO 7 :REM iterate over distance levels IF W0=I THEN GOSUB <draw-wall>:I=7:GOTO <cont> :REM we reached a wall IF W1=I THEN GOSUB <erase-wall> :REM erase wall drawn previously IF VL(I)=HL(I) THEN <right-side> :REM same as previous frame, skip VM=VL(I) AND 1:VE=VL(I) AND 2 :REM process left side FOR VS=0 TO 1:OUT PA,PV(VS) :REM quadrants/segm. 0, 1 FOR K=VI(VS,I,VM,0) TO VI(VS,I,VM,1) STEP 3 ON VK(K) GOSUB <i1>,<i2>,<i3>,<i4> :REM exec draw commands NEXT K NEXT VS <right-side>: IF VR(I)=HR(I) THEN <cont> :REM same as previous, skip VM=VR(I) AND 1:VE=VR(I) AND 2 :REM process right side FOR VS=2 TO 3:OUT PA,PV(VS) FOR K=VI(VS,I,VM,0) TO VI(VS,I,VM,1) STEP 3 ON VK(K) GOSUB <i1>,<i2>,<i3>,<i4> NEXT:NEXT <cont>: NEXT FOR I=0 TO 7:HL(I)=VL(I):HR(I)=VR(I):NEXT :REM copy current to history W1=W0 :REM same for wall distance OUT PA,PR:OUT PC,PU :REM reset drawing directions ON G GOSUB <enable-interrupts> RETURN REM viewport drawing commands <i1>: :REM set pos OUT PC,VK(K+1) RETURN <i2>: :REM repeat byte B=VK(K+C2):FOR J=0 TO VK(K+1):OUT PD,B:NEXT RETURN <i3>: :REM iterate pattern FOR J=VK(K+1) TO VK(K+2):OUT PD,BL(J):NEXT RETURN <i4>: :REM alternate bytes IF VE THEN OUT PD,VK(K+1) ELSE OUT PD,VK(K+2) RETURN
We'll skip the code for erasing and drawing frontal walls for shortness sake (this was a good one, wasn't it?). — Please refer to the complete program listed below.
I was expecting to hit some wall with some complex code like this, written up at once and loaded into the target machine hoping the best. But, apart from a few simple typos, no critical errors. So, if we didn't hit a wall in code, we expected to see some on the screen. Or any lines at least. Some pixels?
Hmm. Why not any? Can't see why, really. — Stares at code. Tries to inspect code (see below for more on this). Time passes. — Eventually I discovered, I got confused with my variables. Wrong, erroneous, undefined name for the array containing the segment/port assignments. So we were talking to none of the LCD controllers at all. (Since BASIC does auto-DIMs for small arrays of a length of up to 10, there's no runtime error. Aaargh!)
Then, some pixels at last, even some lines. And a big mess on the righthand side of the viewport.
Virtual T + German Keyboard = Punishment
Sloppy coding deserves some punishment, but not this one. Trying to debug with this setup is like playing a game of blind chess while trying to compete with Google's newest AI in Go. You really do not want to try this one. In average, I get about 70% of the non-anums wrong, trying to concentrate on the task. (And there are lots of non-anums in subscripted variables, and there are lots of subscripts used in the code.) If you risk a glance at the labeled keyboard, you're lost for sure. Given the tiny size of the display, just 8 lines less 2 for the "OK" prompt (or, more often, "SN") and a new-line, it's near to impossible to do some decent debugging with some lines of output to refer to and to compare.
On the other hand, using the real thing, the serial uploads are now in the minutes at 1200 bauds. And still, there's only a tiny display and very little of the code to be seen at once.
By this we're in retro-retro mode, comparable to punchcard programming: Submit your code — Get errors returned — Take them home — Stare at the listing in order to figure out, what the code might be actually doing — If sufficently confident, resubmit — Take errors home …
Still an awfull mess. Saturday goes by.
Note: Like Walter Benjamin's Angel of History (Angelus Novus) the true retro-hacker is racing blindly through time, facing the past behind him — a grim view ornamented by the unresolved issues of previous turns of the hack-debug-cycle.
Sunday — Ultimo
It's the last day of January and the final day of Rectrochallenge. — We really should come up with some, a viable dungeon crawler, at least.
With a fresh head and a quick test, we confirm a nagging suspicion: Our code for setting a LCD-controller to auto-decrement isn't working at all. Comparing our settings to some sample code and a few experiments later, there's certainty: The documentation, as quoted in Episode #4, is wrong.
And this is how it actually works:
LCD: Select Address Counter Mode 7 6 5 4 3 2 1 0 bit +---+---+---+---+---+---+---+---+ | 0 | 0 | 1 | 1 | 1 | 0 | 1 |D/U| OUT 0xFE +---+---+---+---+---+---+---+---+ D/U: 0 = count down, 1 = count up (0x3A: down, 0x3B: up)
At least, it does work on the real machine. Not so in Virtual T, no support for LCD-segement counter auto-decrement. (To be fair, it's not used once by the code in ROM and we actually have to include some code for resetting writing directions in order to clean up. Else, the display will be left in some quite quirky state, since the operating system isn't resetting the block writing direction at all.)
However, at least, we're getting some (if only the left half on Virtual T) and are able to identify some quirks in our drawing definitions:
Otherwise, it's still the same torture with the keyboard as before, even with some sleep and a somewhat fresh head. On the other hand, there are still those long uploads and setup periods. — Did we have this already?
But eventually, we arrive at a viable program, and it looks like this:
10 REM Maze War Dungeon Roamer 20 DEFSNG A:DEFINT B-Z:SCREEN 0,0:CLS:PRINT"Setting up...":GOTO 200 35 REM == time critical subroutines == 30 REM viewport draw commands 31 OUT PC,VK(K+C1):RETURN 32 B=VK(K+C2):FOR J=C0 TO VK(K+C1):OUT PD,B:NEXT:RETURN 33 FOR J=VK(K+C1) TO VK(K+C2):OUT PD,BL(J):NEXT:RETURN 34 IF VE THEN OUT PD,VK(K+C1) ELSE OUT PD,VK(K+C2) 35 RETURN 36 FOR J=C0 TO VK(K+C1):OUT PD,C0:NEXT:RETURN 37 FOR J=VK(K+C1) TO VK(K+C2):OUT PD,C0:NEXT:RETURN 39 REM subroutine: render-viewport 40 ON G GOSUB 910,920,930,930,940 42 OUT PB,C0 44 OUT PA,PL:OUT PC,PU:OUT PA,PR:OUT PC,PQ 46 FOR I=C0 TO C7 48 IF W0=I THEN GOSUB 100:I=C7:GOTO 76 50 IF W1=I THEN GOSUB 90 52 IF VL(I)=HL(I) THEN 64 54 VM=VL(I) AND C1:VE=VL(I) AND C2 56 FOR VS=C0 TO C1:OUT PA,PV(VS) 58 FOR K=VI(VS,I,VM,C0) TO VI(VS,I,VM,C1) STEP C3 60 ON VK(K) GOSUB 31,32,33,34 62 NEXT:NEXT 64 IF VR(I)=HR(I) THEN 76 66 VM=VR(I) AND C1:VE=VR(I) AND C2 68 FOR VS=C2 TO C3:OUT PA,PV(VS) 70 FOR K=VI(VS,I,VM,C0) TO VI(VS,I,VM,C1) STEP C3 72 ON VK(K) GOSUB 31,32,33,34 74 NEXT:NEXT 76 NEXT 78 FOR I=C0 TO C7:HL(I)=VL(I):HR(I)=VR(I):NEXT:W1=W0 80 OUT PA,PR:OUT PC,PU 82 ON G GOSUB 960,970,980,980,990 84 RETURN 89 REM erase a wall 90 IF W1=C7 THEN RETURN 92 FOR VS=C0 TO C3:OUT PA,PV(VS):K=VI(VS,W1,C2,C0):OUT PC,VK(K+C1) 94 FOR J=C0 TO VK(K+C4):OUT PD,C0:NEXT 96 NEXT:RETURN 98 REM draw blocking wall; handle left side 100 IF VL(W0)=C4 THEN 112 102 VM=VL(W0) AND C1:VE=VL(W0) AND C2 104 FOR VS=C0 TO C1:OUT PA,PV(VS) 106 FOR K=VI(VS,W0,VM,C0) TO VI(VS,W0,VM,C1) STEP C3 108 ON VK(K) GOSUB 31,32,33,34 110 NEXT:NEXT:GOTO 124 112 VE=C1:REM passage to the left and ahead 114 FOR VS=C0 TO C1:OUT PA,PV(VS) 116 FOR K=VI(VS,W0,C1,C0) TO VI(VS,W0,C1,C1) STEP C3 118 ON VK(K) GOSUB 31,36,37,34 120 NEXT:NEXT 122 REM handle right side 124 IF VR(W0)=C4 THEN 136 126 VM=VR(W0) AND C1:VE=VL(W0) AND C2 128 FOR VS=C2 TO C3:OUT PA,PV(VS) 130 FOR K=VI(VS,W0,VM,C0) TO VI(VS,W0,VM,C1) STEP C3 132 ON VK(K) GOSUB 31,32,33,34 134 NEXT:NEXT:GOTO 148 136 VE=C1:REM passage to the right and ahead 138 FOR VS=C2 TO C3:OUT PA,PV(VS) 140 FOR K=VI(VS,W0,C1,C0) TO VI(VS,W0,C1,C1) STEP C3 142 ON VK(K) GOSUB 31,36,37,34 144 NEXT:NEXT 146 REM finally draw the wall 148 IF W0=C7 THEN RETURN 150 FOR VS=C0 TO C3:OUT PA,PV(VS) 152 FOR K=VI(VS,W0,C2,C0) TO VI(VS,W0,C2,C1) STEP C3 154 ON VK(K) GOSUB 31,32,33,34 156 NEXT:NEXT 158 RETURN 160 REM == setup, execution starts here == 195 REM G: 1=PC-8201A, 2=M10 (w/o modem), 3=Model 100, 4=Model 102, 5=KC85 200 B=PEEK(1):G= -(B=148) -(B=35)*2 -(B=51)*3 -(B=167)*4 -(B=225)*5 205 IF G=0 THEN SCREEN 0,1:PRINT "Sorry, model not supported. Gestalt:";B:END 210 IF G=1 THEN AK=65128! ELSE IF G=2 THEN AK=65389! ELSE IF G=5 THEN AK=65387! ELSE AK=65450! 215 C0=0:C1=1:C2=2:C3=3:C4=4:C5=5:C6=6:C7=7:C8=8:C9=9 220 CA=10:CC=12:CE=14:CN=49:CL=50:CP=64:CM=-1 225 UC=223:LC=97:CK=27:ES=27:EB=32:E8=17 230 PA=185:PB=186:PC=254:PD=255:DIM SA(9),SB(9):SG=-1:SH=0 235 FOR I=C0 TO C9:READ B1,B2:SA(I)=B1:SB(I)=B2:NEXT 240 DX=0:DY=0:DIM DX(3),DY(3):FOR I=C0 TO C3:READ B1,B2:DX(I)=B1:DY(I)=B2:NEXT 245 DL=0:DR=0:DIM DL(3),DR(3) 250 FOR I=C0 TO C3:READ B:DL(I)=B:NEXT 255 FOR I=C0 TO C3:READ B:DR(I)=B:NEXT 260 DIM DD(3):FOR I=C0 TO C3:READ B:DD(I)=B:NEXT 269 REM setup the maze 270 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 275 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 280 MW=31:MH=15:ML=132:MT=5:DIM M(MH,MW),BM(C9) 285 FOR Y=C0 TO C9:READ B:BM(Y)=B:NEXT 290 FOR Y=C0 TO MH:FOR X=C0 TO MW:READ B:M(Y,X)=B:NEXT:NEXT 295 GOSUB 1010:GOSUB 1110:REM setup viewport 299 REM == main == 300 CLS:GOSUB 700:MX=11:MY=7:MD=0:DX=DX(MD):DY=DY(MD):DL=DL(MD):DR=DR(MD) 310 PRINT CHR$(ES);"Y";CHR$(EB+C7);CHR$(EB+22);"Crsr,IJKL,Q:quit"; 315 GOSUB 460:GOSUB 1770:GOSUB 1510:GOSUB 40 319 REM main loop 320 B=PEEK(AK):IF B=C0 THEN 320 330 K=PEEK(AK+B):POKE AK,C0:IF K>CK THEN ON K-CK GOTO 500,520,540,560 340 IF K>=LC THEN K=K AND UC 350 ON INSTR("JLKI Q",CHR$(K)) GOTO 520,500,560,540,580,590 360 GOTO 320 398 REM == subroutines == 399 REM segement/pos select 400 SH=SG:SG=SX\CL:IF SY>C3 THEN SG=SG+C5 410 IF (SG<>SH) THEN OUT PA,SA(SG):OUT PB,SB(SG) 420 OUT PC,(SY MOD C4)*CP OR SX MOD CL:RETURN 449 REM clear/draw marker 450 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 460 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 470 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 470:MD=DR:DX=DX(MD):DY=DY(MD):DL=DL(MD):DR=DR(MD):GOTO 610 520 MD=DL:T0=C0:T1=C1:GOSUB 470:DX=DX(MD):DY=DY(MD):DL=DL(MD):DR=DR(MD):GOTO 610 540 GOSUB 450:MX=MX+DX:MY=MY+DY:IF M(MY,MX)=C0 THEN MX=MX-DX:MY=MY-DY:GOSUB 460:GOTO 320 550 GOSUB 460:GOTO 610 560 GOSUB 450:MX=MX-DX:MY=MY-DY:IF M(MY,MX)=C0 THEN MX=MX+DX:MY=MY+DY:GOSUB 460:GOTO 320 570 GOSUB 460:GOTO 610 580 GOTO 320:REM shoot 590 SCREEN 0,1:END 600 REM view port: get path and display it 610 GOSUB 1510:GOSUB 40:GOTO 320 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 400 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 400 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 setup viewport 1010 PL=33:PR=66:REM LCD blocks 0+5, 1+6 1020 PU=59:PQ=58:REM LCD counter setting increment/decrement 1030 DIM PV(3):FOR I=0 TO 3:READ B:PV(I)=B:NEXT 1040 DIM BL(29):FOR I=0 TO 29:READ B:BL(I)=B:NEXT 1050 DIM BP(33):FOR I=0 TO 33:READ B:BP(I)=B:NEXT 1060 DIM VL(7),VR(7),HL(7),HR(7) 1070 DIM VI(3,7,2,1):REM segment,level,mode,idx-from/idx-to 1080 DIM VK(1392) 1090 RETURN 1100 REM compile-viewport-instr 1110 I4=C0 1120 READ B:IF B<C0 THEN RETURN:REM level 1130 FOR J=C0 TO C2:READ B0:REM mode, get length 1140 JL=B0*C3:I0=I4:I1=I0+JL:I2=I1+JL:I3=I2+JL:I4=I3+JL 1150 VI(C0,B,J,C0)=I0:VI(C0,B,J,C1)=I1-C3 1160 VI(C1,B,J,C0)=I1:VI(C1,B,J,C1)=I2-C3 1170 VI(C2,B,J,C0)=I2:VI(C2,B,J,C1)=I3-C3 1180 VI(C3,B,J,C0)=I3:VI(C3,B,J,C1)=I4-C3 1190 FOR K=C1 TO B0 1193 READ B1,B2,B3:VK(I0)=B1:VK(I1)=B1:VK(I2)=B1:VK(I3)=B1 1196 ON B1 GOTO 1200,1260,1320,1380 1200 VK(I0+C1)=(B2*CP) OR B3 1210 VK(I1+C1)=((C3-B2)*CP) OR B3 1220 VK(I2+C1)=((C3-B2)*CP) OR (CN-B3) 1230 VK(I3+C1)=(B2*CP) OR (CN-B3) 1240 P=B3 1250 GOTO 1430 1260 L=B2-P:P=B2+C1 1270 VK(I0+C1)=L:VK(I0+C2)=BP(B3) 1280 VK(I1+C1)=L:VK(I1+C2)=BP(B3+E8) 1290 VK(I2+C1)=L:VK(I2+C2)=BP(B3+E8) 1300 VK(I3+C1)=L:VK(I3+C2)=BP(B3) 1310 GOTO 1430 1320 L=B2-P+B3:P=B2+C1 1330 VK(I0+C1)=B3:VK(I0+C2)=L 1340 VK(I1+C1)=B3+CE:VK(I1+C2)=L+CE 1350 VK(I2+C1)=B3+CE:VK(I2+C2)=L+CE 1360 VK(I3+C1)=B3:VK(I3+C2)=L 1370 GOTO 1430 1380 VK(I0+C1)=BP(B2):VK(I0+C2)=BP(B3) 1390 VK(I1+C1)=BP(B2+E8):VK(I1+C2)=BP(B3+E8) 1400 VK(I2+C1)=BP(B2+E8):VK(I2+C2)=BP(B3+E8) 1410 VK(I3+C1)=BP(B2):VK(I3+C2)=BP(B3) 1420 P=P+C1 1430 I0=I0+C3:I1=I1+C3:I2=I2+C3:I3=I3+C3 1440 NEXT:NEXT:GOTO 1120 1500 REM assemble view path data 1510 Y=MY:X=MX:VL=DD(DL):VR=DD(DR):VF=DD(MD):W0=CM:FW=C0 1520 FOR I=C0 TO C7:B=M(Y,X) 1523 IF B AND VL THEN VL(I)=C1 ELSE VL(I)=C0 1526 IF B AND VR THEN VR(I)=C1 ELSE VR(I)=C0 1530 IF I=C0 THEN 1570 1540 IF VL(I)<>VL(I-C1) THEN VL(I-C1)=VL(I-C1) OR C2 1560 IF VR(I)<>VR(I-C1) THEN VR(I-C1)=VR(I-C1) OR C2 1570 IF B AND VF THEN 1610 1580 IF VL(I)=C0 THEN VL(I)=C2 ELSE IF M(Y+DY(DL),X+DX(DL)) AND VF THEN VL(I)=C4 ELSE VL(I)=C1 1590 IF VR(I)=C0 THEN VR(I)=C2 ELSE IF M(Y+DY(DR),X+DX(DR)) AND VF THEN VR(I)=C4 ELSE VR(I)=C1 1600 W0=I:FW=I+C1:I=C7:GOTO 1620 1610 X=X+DX:Y=Y+DY 1620 NEXT 1630 IF W0=W1 THEN W1=CM 1640 IF (FW) AND (FW<C8) THEN FOR I=FW TO C7:VL(I)=CM:VR(I)=CM:NEXT 1650 RETURN 1700 REM clear/init viewport 1710 ON G GOSUB 910,920,930,930,940 1720 OUT PA,PL OR PR:OUT PB,C0:REM enable blocks 0,1,5,6 at once 1730 FOR Y=C0 TO C3:OUT PC,Y*CP 1740 FOR X=C0 TO CN:OUT PD,C0:NEXT 1750 NEXT 1760 ON G GOSUB 960,970,980,980,990 1770 FOR I=0 TO 7:HL(I)=CM:HR(I)=CM:NEXT:W0=CM:W1=CM 1780 RETURN 2000 REM port patterns for segments (0..9: PA,PB) 2001 DATA 1,0,2,0,4,0,8,0,16,0,32,0,64,0,128,0,0,1,0,2 2002 REM directions (0..3:dx,dy) 2003 DATA 0,-1,-1,0,0,1,1,0 2004 REM turns (L: 0..3, R: 0..3) 2005 DATA 1,2,3,0, 3,0,1,2 2006 REM directional codes (2^0..3) 2007 DATA 1,2,4,8 2008 REM maze maker defs (0..3:Y,X) 2009 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 2010 REM maze marker turn pixel mods (Y,X) 2011 DATA 2,1,1,0, 1,2,2,1, 0,1,1,2, 1,0,0,1 2012 REM maze bit patterns 2013 DATA 1,3,7,14,28,56,112,224,192,128 2019 REM maze data 2020 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 2021 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 2022 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 2023 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 2024 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 2025 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 2026 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 2027 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 2028 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 2029 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 2030 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 2031 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 2032 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 2033 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 2034 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 2035 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 3000 REM segment addresses (port A) 3001 DATA 1,32,64,2 3005 REM line pixels 3006 DATA 1, 1, 2, 2, 4, 4, 8, 8, 16, 16 3007 DATA 32, 32, 64, 64, 128, 128, 64, 64, 32, 32 3008 DATA 16, 16, 8, 8, 4, 4, 2, 2, 1, 1 3010 REM pixels for repeats 3011 DATA 0,1,2,4,8,16,32,64,128 3012 DATA 255,254,252,248,240,224,192,128 3013 REM inverse direction (bottom up) 3014 DATA 0,128,64,32,16,8,4,2,1 3015 DATA 255,127,63,31,15,7,3,1 3020 REM viewport commands struct, 1st quadrant, per level and mode 3021 DATA 0:REM level 0 3022 DATA 10 3023 DATA 1,0,0, 3,5,10 3024 DATA 1,1,0, 2,5,0, 2,6,1, 4,9,1 3025 DATA 1,2,7, 4,9,0 3026 DATA 1,3,7, 4,9,0 3027 DATA 9 3028 DATA 1,0,0, 2,5,0 3029 DATA 1,1,0, 2,6,1, 4,9,1 3030 DATA 1,2,7, 4,9,0 3031 DATA 1,3,7, 4,9,0 3032 DATA 6 3033 DATA 1,1,8, 2,49,1 3034 DATA 1,2,8, 2,49,0 3035 DATA 1,3,8, 2,49,0 3036 DATA 1:REM level 1 3037 DATA 7 3038 DATA 1,1,8, 3,15,2, 4,14,6 3039 DATA 1,2,16, 4,9,0 3040 DATA 1,3,16, 4,9,0 3041 DATA 7 3042 DATA 1,1,8, 2,15,6, 4,14,6 3043 DATA 1,2,16, 4,9,0 3044 DATA 1,3,16, 4,9,0 3045 DATA 6 3046 DATA 1,1,17, 2,49,6 3047 DATA 1,2,17, 2,49,0 3048 DATA 1,3,17, 2,49,0 3049 DATA 2:REM level 2 3050 DATA 8 3051 DATA 1,1,17, 3,21,11 3052 DATA 1,2,17, 2,21,0, 3,23,0, 4,10,2 3053 DATA 1,3,24, 4,9,0 3054 DATA 7 3055 DATA 1,1,17, 2,21,0 3056 DATA 1,2,17, 2,23,2, 4,10,2 3057 DATA 1,3,24, 4,9,0 3058 DATA 4 3059 DATA 1,2,25, 2,49,2 3060 DATA 1,3,25, 2,49,0 3061 DATA 3:REM level 3 3062 DATA 5 3063 DATA 1,2,25, 3,30,3, 4,13,5 3064 DATA 1,3,31, 4,9,0 3065 DATA 5 3066 DATA 1,2,25, 2,30,5, 4,13,5 3067 DATA 1,3,31, 4,9,0 3068 DATA 4 3069 DATA 1,2,32, 2,49,5 3070 DATA 1,3,32, 2,49,0 3071 DATA 4:REM level 4 3072 DATA 5 3073 DATA 1,2,32, 3,36,10, 4,8,8 3074 DATA 1,3,37, 4,9,0 3075 DATA 5 3076 DATA 1,2,32, 2,36,8, 4,8,8 3077 DATA 1,3,37, 4,9,0 3078 DATA 4 3079 DATA 1,2,38, 2,49,8 3080 DATA 1,3,38, 2,49,0 3081 DATA 5:REM level 5 3082 DATA 3 3083 DATA 1,3,38, 3,41,0, 4,11,3 3084 DATA 3 3085 DATA 1,3,38, 2,41,3, 4,11,3 3086 DATA 2 3087 DATA 1,3,43, 2,49,3 3089 DATA 6:REM level 6 3090 DATA 3 3091 DATA 1,3,43, 3,45,5, 4,13,5 3092 DATA 3 3093 DATA 1,3,43, 2,45,5, 4,13,5 3094 DATA 2 3095 DATA 1,3,47, 2,49,5 3096 DATA 7:REM level 7 3097 DATA 3 3098 DATA 1,3,47, 3,48,9, 4,14,14 3099 DATA 3 3100 DATA 1,3,47, 2,48,6, 4,14,14 3101 DATA 2 3102 DATA 1,3,49, 2,49,14 3103 DATA -1
Mind the time critical subroutines placed at the beginning of the program, because MS BASIC.
△! Note: When loading this from a "*.DO"-file, you've to KILL the document before running the program in order to free the memory required by the JIT-compiled drawing code, or you'll get an "OM" error else. There is no need for any preparations, if loaded into BASIC directly (via the serial interface).
It's a real, fully working dungeon crawler with a pseudo-3D 1st person perspective into the maze. And it works, for real!
But, it's not as fast as expected. True, we could speed up the thing a little by removing all the spaces that are still in the code for legibility and by removing the few REMarks, but, still, this wouldn't make this an exciting, fast-paced realtime game.
Moreover, we haven't implemented the iconic eyeball representing the respective other player, yet, and we were to paint this upon the viewport by repeating quite the same procedures. By this, we would become even slower, when the game heats up. Not so good.
As for the networking part, this could be easy: Just open two ports, one outgoing, on incoming, setup a command "
ON COM GOSUB ..." and exchange a few bytes for positions. One computer would feature the host, managing player names, initial positions and syncronization, and the other one would connect to it as a client.
But in reality, we've turned off any of the means to receive an incoming message, while we're talking to the LCD-controllers with interrupts disabled. We would have to implement some handshaking protocol of sorts, to synchronize, and we'll loose some runtime for this, too.
So, it's actually a good point to put this project to a rest. We've come as far, as it does make any sense, for real.
Closing Considerations / Retrospect
In retrospect, this didn't work out too badly. We knew from the beginning that BASIC would be hard on the limits for this job (but I personally expected it to perform a little better, though). Ironically, BASIC wasn't of any specific help — how I longed for some kind of assignable objects at times! So we ended up doing it all by lookups and subscripts. On the plus side, we arrived at an algorithm that could be ported to assembler with ease and would be really performant — but this is another project.
Anyway, exploring the Kyocera Siblings was some fun, and revisiting BASIC was some fun, too, for sure. This was actually my first complex BASIC programs in 30 years, since I left my C64 behind. Under these circumstances, it turned out quite amazing. — Had we reached our final goal of a full implementation, I had already dreamed up some box art with a sticker on it, reading, 'FROM THE MAKER OF "TEST.BA".' (And, maybe, another one, 'ONE MONTH IN THE MAKING!', too.)
So, we close our humble tribute to Retrochallenge 2016/01 with two laughing eyes, as we enjoyed ourselves quite a bit. (Apart from this debugging cycle. — No, calm down, I won't include the image once again. Here is a better one:)
Cheers, thanks to all who followed these posts,
and, finally, — finis —
Please mind the final update, including the release code (compressed for faster runtime performance)!
P.S.: And both of the machines are still running on their first set of AA batteries! Amazing!
▶ Next: Addendum: A Final Fix
◀ Previous: Episode 11: A Path to the Maze
▲ Back to the index.
2016-01-31, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2016/01. —