See also: part 1, part 2, part 3, part 4, part 5 and part 6.
Previously, we finally got to do something semi-useful with assembly: we replaced a slow full-screen scrolling routine in BASIC with a turbo-charged assembly routine, all called via the DEFUSR command.
Today, let’s apply this concept a bit further with the shell of a Pac-Man style video game written in BASIC, but enhanced with assembly.
In my Optimizing Color BASIC, part 3 article, I set the groundwork for writing a game in BASIC that involved moving a character around the screen and detecting collisions with enemy characters. Today I will combine that with the previous maze demo and create the world’s easiest Pac-Man game (no enemies, and no bothersome dots to eat).
A few years ago, I started toying with a video output project for the Arduino computers. I began by simply bouncing a circle around the screen and then, for some reason, turned that in to an animated Pac-Man. This led me to digging in to some wonderful websites that had reverse engineered the original Pac-Man source code to explain how everything worked. You can find the series here:
Although I have yet to finish the game, I learned quite a bit about how Pac-Man works, including how the ghosts behave. I don’t know if BASIC would be fast enough to handle the logic of four ghosts and all the other stuff, but it sure would be fun to try — it would be much easier to write it in BASIC than C, I think.
But I digress.
The reason I mention this series is so I can show this picture:
For the Arduino project, I started with a screen shot of the original game and downsized it to fit the low resolution, black and white Arduino TVOut graphics library. It ended up looking like this:
Pac-Man was designed on a tile system. The original game resolution was 224×288. The screen was made up of 8×8 tiles, 28 across and 36 down. Without the score lines at the top and the players left lines at the bottom, the playfield itself was 28×31. The maze tiles looked like this:
…and since the CoCo’s screen is 32×16, if we used one character per tile, we could replicate the same horizontal dimensions, but we’d need to scroll up and down to get to all 31 lines of the maze.
I was initially working on this for a 4K programming challenge I started (and have yet to complete). Using ASCII, the make looks like this:
XXXXXXXXXXXXXXXXXXXXXXXXXXXX X XX X X XXXX XXXXX XX XXXXX XXXX X X X X X X XX X X X X X X XXXX XXXXX XX XXXXX XXXX X X X X XXXX XX XXXXXXXX XX XXXX X X XXXX XX XXXXXXXX XX XXXX X X XX XX XX X XXXXXX XXXXX XX XXXXX XXXXXX X XXXXX XX XXXXX X X XX XX X X XX XXX--XXX XX X XXXXXX XX X X XX XXXXXX X X XXXXXX XX X X XX XXXXXX X XX XXXXXXXX XX X X XX XX X X XX XXXXXXXX XX X XXXXXX XX XXXXXXXX XX XXXXXX X XX X X XXXX XXXXX XX XXXXX XXXX X X XXXX XXXXX XX XXXXX XXXX X X XX XX X XXX XX XX XXXXXXXX XX XX XXX XXX XX XX XXXXXXXX XX XX XXX X XX XX XX X X XXXXXXXXXX XX XXXXXXXXXX X X XXXXXXXXXX XX XXXXXXXXXX X X X XXXXXXXXXXXXXXXXXXXXXXXXXXXX
It may look odd presented as Xs. and the aspect ratio is different, but it’s the exact Pac-Man layout used in the arcade. Here is the full play field that will scroll on the CoCo’s 32×16 screen:
Since the original Pac-Man played on a monitor that was turned sideways, it was taller than it was wider. Most home ports either shrink the screen down, or flatten it out. By scrolling, maybe we can keep the aspect ratio similar.
And this is how my ASCII Pac-Man maze came to be.
As I referenced at the top of this article, I have been covering ways to Optimize Color BASIC in another article series. A recent part discussed reading the keyboard and moving a character around the screen. I took some of this code and used it to place a character in the Pac-Man maze and move it around. I also added collision detection making sure the player could not run through any of the walls.
Today I would like to present my work-in-progress Pac-Man maze, entirely in BASIC, and the changes I made to integrate the screen moving assembly routines. The assembly calls (and all the DATA statements) are in this listing, but are commented out. The ‘commented-out ines in red are what lines to uncomment to see the assembly-enhanced version, and any line just in red is the BASIC version that would need to be commented out.
Here is the current listing, with comments to follow explaining how it works. I have been writing this on my Mac in a text editor, then loading it in to the XRoar emulator for testing. Because of this, you will notice I put spaces between program sections to make them easier to see. When this loads in to an emulator as an ASCII program, those empty lines are ignored. It works out nice.
0 REM 1 REM PAC-MAZE 1.00 2 REM BY ALLEN C. HUFFMAN 3 REM WWW.SUBETHASOFTWARE.COM 4 REM 6 REM 7 REM 8 REM 9 'CLEAR200,&H3F00 10 DIM MZ$(30) 15 REM 16 REM READ MAZE IN TO ARRAY 17 REM 20 FOR A=0 TO 30:READ MZ$(A):NEXT 21 'GOSUB2000:DEFUSR0=&H3F00 25 REM 26 REM UP+DOWN+LEFT+RGHT CHARS 27 REM 30 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 35 REM 36 REM PLAYER/WALL/BG CHARS 37 REM 40 PC=159 'PAC-MAN CHAR 41 WC=ASC("X") 'WALL CHAR 42 BG=96 'BACKGRND CHAR 50 REM 51 REM INITIALIZATION 52 REM 60 ST=7 'SCRN START LINE 61 PM=1360 'PAC-MAN START LOC 62 DR=0 'CURRENT DIRECTION 63 DN=0 'NEXT DIRECTION 80 REM 81 REM DRAW INITIAL MAZE 82 REM 90 CLS:FOR A=0 TO 15:PRINT @A*32+2,MZ$(A+ST);:NEXT 100 REM 101 REM MAIN LOOP 102 REM 110 POKE PM,PC 120 A$=INKEY$:IF A$="" THEN 140 130 KB=INSTR(KB$,A$):IF KB=0 THEN 140 ELSE DN=KB 135 REM TRY NEXT DIRECTION 140 ON DN GOSUB 500,600,700,800 145 REM THEN TRY CURRENT DIR 150 IF DR<>DN THEN ON DR GOSUB 500,600,700,800 160 GOTO 100 500 REM 501 REM UP 502 REM 510 IF PEEK(PM-32)<>BG THEN RETURN 520 POKE PM,BG:DR=1 530 IF PM<1183 AND ST>0 THEN ST=ST-1:GOSUB 950 ELSE PM=PM-32 540 RETURN 600 REM 601 REM DOWN 602 REM 610 IF PEEK(PM+32)<>BG THEN RETURN 620 POKE PM,BG:DR=2 630 IF PM>1376 AND ST<15 THEN ST=ST+1:GOSUB 900 ELSE PM=PM+32 640 RETURN 700 REM 701 REM LEFT 702 REM 710 IF PEEK(PM-1)<>BG THEN RETURN 720 POKE PM,BG:DR=3 730 PM=PM-1:RETURN 800 REM 801 REM RIGHT 802 REM 810 IF PEEK(PM+1)<>BG THEN RETURN 820 POKE PM,BG:DR=4 830 PM=PM+1:RETURN 900 REM 901 REM SCROLL SCREEN UP 902 REM 910 FOR A=0 TO 15:PRINT @A*32+2,MZ$(A+ST);:NEXT 915 'Z=USR0(1):PRINT@482,MZ$(ST+15); 920 RETURN 950 REM 951 REM SCROLL SCREEN DOWN 952 REM 960 FOR A=0 TO 15:PRINT @A*32+2,MZ$(A+ST);:NEXT 965 'Z=USR0(2):PRINT@2,MZ$(ST); 970 RETURN 999 GOTO 999 1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX" 1001 DATA "X XX X" 1002 DATA "X XXXX XXXXX XX XXXXX XXXX X" 1003 DATA "X X X X X XX X X X X X" 1004 DATA "X XXXX XXXXX XX XXXXX XXXX X" 1005 DATA "X X" 1006 DATA "X XXXX XX XXXXXXXX XX XXXX X" 1007 DATA "X XXXX XX XXXXXXXX XX XXXX X" 1008 DATA "X XX XX XX X" 1009 DATA "XXXXXX XXXXX XX XXXXX XXXXXX" 1010 DATA " X XXXXX XX XXXXX X " 1011 DATA " X XX XX X " 1012 DATA " X XX XXX--XXX XX X " 1013 DATA "XXXXXX XX X X XX XXXXXX" 1014 DATA "< X X >" 1015 DATA "XXXXXX XX X X XX XXXXXX" 1016 DATA " X XX XXXXXXXX XX X " 1017 DATA " X XX XX X " 1018 DATA " X XX XXXXXXXX XX X " 1019 DATA "XXXXXX XX XXXXXXXX XX XXXXXX" 1020 DATA "X XX X" 1021 DATA "X XXXX XXXXX XX XXXXX XXXX X" 1022 DATA "X XXXX XXXXX XX XXXXX XXXX X" 1023 DATA "X XX XX X" 1024 DATA "XXX XX XX XXXXXXXX XX XX XXX" 1025 DATA "XXX XX XX XXXXXXXX XX XX XXX" 1026 DATA "X XX XX XX X" 1027 DATA "X XXXXXXXXXX XX XXXXXXXXXX X" 1028 DATA "X XXXXXXXXXX XX XXXXXXXXXX X" 1029 DATA "X X" 1030 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX" 1100 REM 1101 REM MAZE ARRAY TO GRAPHICS 1102 REM 1110 FOR R=0 TO 30 1120 DIM P,PL,PS,C:P=VARPTR(MZ$(R)) 1130 PL=PEEK(P):PS=PEEK(P+2)*256+PEEK(P+3) 1140 FOR C=PS TO PS+PL-1 1150 PRINT CHR$(PEEK(C)); 1155 IF PEEK(C)=ASC("X") THEN POKEC,175 1160 NEXT:PRINT 1170 NEXT 2000 REM 2001 REM LOAD ASSEMBLY ROUTINE 2002 2010 READ A,B 2020 IF A=-1 THEN 2070 2030 FOR C = A TO B 2040 READ D:POKE C,D 2050 NEXT C 2060 GOTO 2010 2070 RETURN 'END 2080 DATA 16128,16217,189,179,237,90,39,14,90,39,28,90,39,42,90,39,55,204,255,255,32,67,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,47,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,30,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,14 2090 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,204,0,0,126,180,244,-1,-1
As listed, this will do the game entirely in BASIC. Using the arrow keys, you can move around the yellow PAC-BLOCK and explore the maze. When you get near the top or bottom of the maze, the screen will sluggishly scroll so you can access the rest of the maze.
Give that a try and explore the top and bottom of the maze so you can get an idea of the speed BASIC scrolls at.
Then, to make it use the assembly language routines:
- Uncomment line 9. This protects memory beyond &H3F00 for the assembly language code.
- Uncomment line 21. This will GOSUB to the routine that reads in the assembly language and POKEs it in to memory starting at &H3F00.
- Comment line 910 (BASIC redraw/scroll up code).
- Uncomment line 915. This calls the assembly routine to scroll the screen up, then redraws a new line at the bottom.
- Comment line 960 (BASIC redraw/scroll down code).
- Uncomment line 965. This calls the assembly routine to scroll the screen down, then redraws a new line at the top.
Make those changes and re-run the program then move from top to bottom and see how much faster the scree “scrolls.”
And, the assembly could be made almost twice as fast, and the BASIC code could be optimized to be faster, too.
But before we do that, let’s dig in to how the code actually works.
- 20-21 read in all the maze lines in to an array called MZ$. The maze strings are in the DATA statements starting at line 1000.
- 30 builds a string that contains the ASCII characters for Up, Down, Left and Right. It is much faster to use INSTR and parse through a string rather than have to build one with CHR$() inside the INSTR call every time.
- 40-42 sets some default variables:
- PC is the character of Pac-Man to POKE to the screen (159 is a yellow block).
- WC is what character to use for wall detection (an ASCII “X” letter). The move code will PEEK screen memory, and not let you move in any direction that contains an “X”.
- BG is the background character (a space) that will be used to erase Pac-Man before moving him.
- 60-63 initialize some game play variables:
- ST is which of the 31 lines of the maze should be the first line to display. Thus, ST=7 means we will initially draw lines 7-22 on the screen to display that middle section of the maze.
- PM is the memory location where Pac-Man will be POKEd. The screen memory starts at 1024, so this default is somewhere in the middle of the screen under the ghost house.
- DR is the direction Pac-Man is currently moving.
- DN is the next direction the Pac-Man will try to move at an intersection. Like the arcade, this version will let you press UP while Pac-Man is moving left, and as soon as there is an opening in the wall, the direction will turn UP.
- 90 draws the initial 16 maze lines that will fit on the screen.
- 110 POKEs the Pac-Man character on to the screen (showing the yellow block).
- 120-130 wait for one of the four keys in KB$ (up, down, left or right) to be pressed. If no key is pressed, it skips to line 140, else it sets DN (direction next) to match the key that was pressed.
- 140 uses DN (next direction) to call a routine to try to move Pac-Man up, down, left or right.
- 150 assumes that if DN and DR don’t match, a new direction has been pressed, so it will use DR (current direction) to call the up, down, left or right routine.
- 160 goes back to 100 to keep doing this forever.
- 510 is the UP routine. It will PEEK the memory location 32 bytes higher in memory (one line up from the current Pac-Man PM location) and if it is NOT the background character (ie, not some place we can move), it returns.
- 520 POKEs the background character where Pac-Man is, erasing him, then sets DR (direction) to 1 for up.
- 530 checks to see if the Pac-Man location is before a certain spot on the screen and that the screen is starting at a line later than the first one. If so, then the screen is allowed to scroll up (start line ST is decremented). A GOSUB to 950 will handle scrolling the screen. Otherwise, we don’t need to scroll and can just subtract 32 from the Pac-Man location, moving him up one line.
- 540 returns us back to the main loop.
- 610-640 is the same process for moving Pac-Man down, but we check for locations at the bottom of the screen and memory +32 from Pac-Man.
- 710-730 is the same code for moving Pac-Man left. We never scroll left or right so we don’t have to do as much here.
- 810-830 is the same code for moving Pac-Man right.
- 910-920 is the routine to scroll the screen up:
- 910 scrolls the screen up in BASIC by redrawing all 16 lines of the maze.
- 915 uses the assembly language routine to move the screen up, then PRINTs the next line at the bottom that would be displayed.
- 960-970 are the same thing for scrolling down.
- 1000-1030 is the 31 line maze.
- 2000-2090 is the assembly language loader generated by lwasm and renumbered to fix. It READs in the assembly from DATA statements and POKEs it in to memory.
Next time, let’s improve this a bit.