Apple programmer, François Vander Linden , has created a version of the Synergistic Software game Bolo, completely in Applesoft. The original Apple II game was produced in 1982 and has remained one of the favorite games ever played on the venerable platform.
You can download the Disk Image of Bolo in Applesoft from here:
The Disk Image for Bolo in Applesoft includes:
- a BOLO MAZE GENERATOR in 2 lines
- a full screen BOLO in 3 lines with tank display
- a full screen BOLO in 2 lines with no tank display
- a “FAST” full screen BOLO using a 6502 routine to handle the keyboard, the drawing of the maze and the drawing of the tank … it’s not meant to be a “few-liner” … but it demonstrates that 6502 does not solve everything … the XDRAW routine is the bottleneck in these programs …
A complete breakdown of the program follows:
Technical notes for “AppleSoft BOLO” (3 lines)
Here’s an “expanded” version of the program, from 3 lines to 44.
You certainly know that Applesoft HPLOTs do not support screen-clipping: if you try to HPLOT outside the screen limits, you’ll generate an error.
The XDRAW/DRAW statements do not have this limitation (as long as your first point is within screen limits): plotted lines are wrapping around the screen.
The idea behind this 3-liner is that this feature could be used to simulate an “infinite” BOLO maze.
Lines 0 through 19 will dynamically generate a maze using 5 different instructions: move left/right without plotting (that’s 2), plot and move left/right (that’s 2 more), move down AND plot up (that’s the last).
Each screen is “divided” in 7 columns and 5 rows. Starting from the upper left corner, we randomly select, 7 times, an instruction that either plots left (1), moves left (2) or moves down AND THEN plots up (3). Notice that the last instruction has no movement to the left. meaning that we might end or 7 instructions without being on the right side of the screen. This will allow more “corners” …
Then we move down, and now select 7 instructions that move/plot RIGHT or, as before, move down AND THEN plot up. After 5 “lines” the shape is considered complete.
Lines 0 through 19 will also generate 4 bitmaps for the tank. We take advantage of the fact that we have several loops for the shape table. The innermost loop has 7 iterations and, miracle, our 4 bitmaps are all 7 lines high !
Also, the bitmaps are quite similar. For example, the bytes to draw the tank facing up is the same as the ones for the tank facing down but in reverse.
The 2 bitmaps for the tank facing right/left are almost identical except for middle byte that is inversed. It also means that, except for the middle byte, the tank facing left bitmap is the tank facing right bitmap in reverse AS WELL !
I use these similarities to reduce the bytes actually encoded in the program: only the bitmap for the tank facing up and the tank facing left are encoded. The other bitmaps are generated by code.
Let’s review how this works:
0 HOME:INVERSE : REM We are going to use the PRINT/INVERSE technique for shape tables. But also, as this prints characters with ASCII value < 64, it can be used for most of the bytes of the bitmaps as well.
1 ?”H*77?”CHR$(34)”@>\S>\A@D@”; : FLASH : HTAB3 : ?”77?”: REM those garbled characters are the first 2 bitmaps. The last four characters of the first PRINT (“A@D@”) are the start of the shape table. As you can see, I was unlucky, in that, one of the bytes in the bitmaps is actually a double-quote. This forced me to use CHR$(34). Also, I have 3 characters at the end of the line that had to be printed as FLASH because they were above 64 (but below 128).
2 I=1042 : REM We will build the shape table from this byte (1042=$400 + $12 = 1024 + 18 characters)
3 FOR K = 0 TO 4 : REM the maze is a grid of 5 rows
4 FOR J = 0 TO 6 : REM and 7 columns
5 M% = RND(1)*3 + 1 : REM pick a number between 1 and 3
6 Z = M%<>3 : REM Z indicates we have not picked 3
7 M = M% * Z + 3 * (M%=2) + E*Z + 34 * NOTZ: REM this line means “if we have not picked 3, add 3 to M if we picked 2 and add E. If we have picked 3, then use 34 insted of 3”. E is either 0 or 2 and is a constant to add on “odd” rows of our maze grid to allow movement/plot to the right. So the possible values for M are either: 1 (move left), 2+3=5 (plot left), 1+2=3 (move right), 2+3+2=7 (plot right) or 34 (move down AND THEN plot up)
8 POKE I, M : REM we add the shape table instructions to what we printed in INVERSE a few statements above.
9 Z = 4*(K>1) + K : REM now we deal with our bitmaps. Z will hold the value of K if K is <=1. Otherwise it will be K + 4 (4 is used to “skip over” 4 bitmaps — its purpose is to avoid destroying previous bitmaps — see next lines)
10 A(Z,J)=PEEK(I-18-K) : REM here we stored in an array A(tank, bytes) the characters that we printed above. Notice we don’t have to DIM arrays as long as their dimensions are below 11. Notice that if K is above 1, Z will be above 4, “skipping” the bitmaps in 0,1,2 and 3.
11 A(2+Z,6-J)=A(K,J) : REM and because tank up and tank down are similar but reversed and tank left/right too, we use this statement to store the bytes of the next-next tank. We will do specifics later.
12 I=I+1 : REM increment out pointer
13 NEXT : REM loop
14 POKE I,2 : REM this instruction is “move down”
15 I=I+1 : REM increment the pointer
16 E=2-E : REM flip/flop E from 0 to 2 and vice versa (allowing us to move/plot left and then right)
17 NEXT : REM 2nd loop
18 POKE I,0 : REM final byte in our shape table (always zero)
19 A(3,3) = 100 : REM this handles the fact that the cannon of the tank facing right must face …right !
20 POKE232,14 : POKE233,4 : REM address of the shape table
21 SCALE=38 : ROT=0 : REM the screen does not divide in 38 equal blocks per line or per column but you won’t really see the difference.
22 DEF FN Q(N) = Q(Q>=0 AND Q(Q<0) + (Q-N)*(Q>=N) : REM this function will be used to “wrap-around” the X/Y coordinates. N is the maximum value for the coordinate.
23 HGR : XDRAW 1 AT X,Y: T=PEEK(234) : REM We clear HGR first so that we don’t have to POKE a soft-switch to be fullscreen after. We draw the maze once. And we hold in T the value of the collision counter when there’s no collision.
24 HGR2 : XDRAW 1 AT X,Y : REM now we clear HGR2 and draw the maze there as well …
25 P=1 : S=2 : REM P is the page we’ll be working on, while S is the “speed” of the tank: 2 pixels.
26 FOR I = 0 TO 2 : REM this will be an infinite loop … with a twist !
27 POKE 230, 32*P : REM we work on page P
28 XDRAW 1 AT X(P),Y(P) : REM we erase previously stored position of the maze for page P
29 K = PEEK(49152)-201 : REM keys I/J/K/L are ASCII 201-202-203-204 !
30 X = X + S(K=1) – S(K=3) : REM update X according to the key pressed
31 Y = Y + SNOTK – S(K=2) : REM same for Y
32 Q=X: X=FNQ(280) : REM make sure X wraps around
33 Q=Y: Y=FNQ(192) : REM same for Y
34 X(P)=X: Y(P)=Y : REM save X and Y for page P so we can erase the maze later
35 Z = K>=0 AND K<4 : REM we are now going to draw the tank. The original code had an IF/THEN here. I hoped I could use a POKE 184 to “emulate” it but unfortunately, the rest of the code was on another page (if you didn’t get that part, read the posts where we discuss the use of POKE 184). Z indicates that we have a valid key.
36 FOR J = 0 TO 6*((L(P)<> K AND Z) OR I<2) : REM we are going to loop through the 7 bytes of each bitmap. Except if we have not changed direction and in this case we will only loop one iteration. Also the test I<2 will force the drawing of the tank for the first two iterations of the “I” loop (otherwise the tank would not be drawn before you move)
37 L(P)=K*Z : REM Remember last direction
38 POKE 8192P + 316 + J1024, A(K*(I=2 AND Z), J) : REM poke the appropriate byte
39 NEXT : REM loop
40 XDRAW 1 AT X,Y : REM finally draw the maze
41 POKE 49235+P, PEEK(49200*(T<>PEEK(234))) : REM flip page AND if a collision occurred, click the speaker
42 P=3-P : REM change page
43 I=I-(I=2) : REM If I=2 then I = I – 1. Otherwise I is left unchanged. It means that after the first two iterations (used to draw the tank on page 1 and 2), we have an infinite loop.
44 NEXT : REM infinite loop !