Interfacing assembly with BASIC via DEFUSR, part 6

See also: Part 1, Part 2Part 3, Part 4 and Part 5.

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).

The Maze

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:

http://subethasoftware.com/2014/02/09/arduino-pac-man-project/

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:

Pac-Man!

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:

Arduino Pac-Man!

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:

Pac-Man maze tiles.

…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:

Pac-Man full maze.

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.

The Listing

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:

  1. Uncomment line 9. This protects memory beyond &H3F00 for the assembly language code.
  2. Uncomment line 21. This will GOSUB to the routine that reads in the assembly language and POKEs it in to memory starting at &H3F00.
  3. Comment line 910 (BASIC redraw/scroll up code).
  4. Uncomment line 915. This calls the assembly routine to scroll the screen up, then redraws a new line at the bottom.
  5. Comment line 960 (BASIC redraw/scroll down code).
  6. 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.”

Assembly!

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.

Dissection

  • 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.

Baby steps.

Next time, let’s improve this a bit.

Leave a Reply