A while ago I wrote about the new version 3 of XC-BASIC which was a breaking update making my v2 code no longer compile. Though v2 is still available, I sat on it not wanting to keep trying updating my code to the new syntax.
That was until I came across an old school 3D maze listing that was written in Commodore BASIC for the C64. Could XC-BASIC speed it up enough to be more enjoyable, while giving the processor enough elbow room to add features?
Spoiler alert, it does make things a lot more speedy, but I decided to write my own 3D maze game from scratch. Read on to discover how the process went …
Original 3D Maze Code

The original game was called Labyrinth and it was a type-in game published in a German computer magazine from the 1980s.
It plays, but requires patience. You can try it in my C64 emulator here or look at the slightly improved code in my retro programming IDE.
As you can see, the screen is drawn using PETSCII, symbol by symbol, and we know from past experience this is slow to do in C64 BASIC.

I did attempt to optimise it, at least in terms of rendering the pseudo-3d view, but I failed to make a noticeable dent. Of course there are well-known limits to what interpreted BASIC can do, but I wonder if someone like Robin could make more headway.

You can now follow the tutorials and edit the code right in your web browser with the Online Retro IDE
– No downloads, configuration, etc necessary, and it is free!
Enter the XC-BASIC Compiler
While XC-BASIC will not accept a C64 BASIC program as-is and compile it right away, I did wonder how much speed improvement there would be if I modified it enough that it compiled to a PRG.

The answer was, after quite a fair bit of effort, it speeds things up by a lot. You can see how far I got on Github if you would like to finish the job!
My main problems at this point are using the correct screen codes in the correct places, plus I think there is some issue in the depth logic in the middle of the screen there.
Things are not helped by the constraints of C64 BASIC in terms of variable names, plus the old school “optimisations” that programmers for magazines would do such as writing to memory instead of using arrays, and things like that.
One of the major frustrations was as I added REM comments, the basic listing grew, and the direct memory writes started overwriting the top of the basic code!
It also did not help that all I had as source was the .PRG which didn’t de-tokenise perfectly.
Someone suggested using “AI” tools such as ChatGPT or Cursor. While I am not 100% against these tools, ecological, intellectual property, and employment concerns aside, they did not help one bit. It seems even 40+ year old programming languages are not surfaced in their training data very well.
In the end it stopped being fun, and I had learned what I could from the exercise, so I stopped. By all means take over though if you like!
Converting C64 BASIC to XC-BASIC3
What did I learn?
- XC-BASIC is type-safe, in that variables must be explicitly or implicitly specified as an
INT,String, and so on. C64 BASIC numbers are internally floating point. If you just assign a value, that initial value works as setting the variable type, even if it accidentally guesses wrong. This can cause a lot of issues converting over to XC unless you take care toDIMeverything beforehand. - C64 BASIC code often uses the same variable names (
A,A$,A(3)and so on) but that causes problems in XC. - XC-BASIC3 uses
CINTrather thanINTto convert a value to integer (INTis a variable type in XC). AlsoRND()takes no parameters in XC. Oh, andTI()is a function. - You must have the
GOTOcommand afterTHENif you want to jump to a program line, but in C64 BASIC you can sayTHEN 10. In XC you can not end a line with:either, or break up your code using colons (syntax error near ':' in file). - REM must be separated by
:but ‘ can go anywhere. You can’t use both : and ‘ or you getsyntax error near '' in fileerror. - There were quite a few situations where I think the logic broke after conversion because an
IFused multiple:statements afterTHENto do more than one thing. Instead useIF ... END IFfor safety. FORloop counter variables must be defined in-line or global. In C64 BASIC we will doFOR I = 0 TO 10which will throw an error in XC.- C64 arrays are defined with the highest required index, XC arrays are defined by how many cells you need. This would work on C64 but throw an error if you try to compile it:
DIM A(3)
A(0)=1
A(1)=1
A(2)=1
A(3)=1New XC-BASIC3 3D Maze from Scratch
I got quite close with it, but I wasn’t happy with the result. With all this energy going into a program, why not make my own? So I dropped the conversion effort and started over.
Here is just enough 3D Maze so you can wander around and show the 2D overhead view:
3D Maze XC-BASIC Code
' ==========================================
' 3D MAZE FOR XC-BASIC (C64)
' BY CHRIS GARRETT
' RETROGAMECODERS 2025
' ==========================================
BACKGROUND 0
BORDER 0
PRINT CHR$(155)
' ==========================================
' CONFIG
' ==========================================
CONST DIR_NORTH = 0
CONST DIR_EAST = 1
CONST DIR_SOUTH = 2
CONST DIR_WEST = 3
' ===== Global Arrays =====
DIM wall_height AS BYTE
DIM carve_dx(4) AS BYTE
DIM carve_dy(4) AS BYTE
DIM stack_x(400) AS BYTE
DIM stack_y(400) AS BYTE
DIM dirs(4) AS BYTE
DIM map_cx AS BYTE
DIM map_cy AS BYTE
DIM start_x AS BYTE
DIM start_y AS BYTE
DIM maze(20,20) AS BYTE
DIM player_x AS BYTE
DIM player_y AS BYTE
DIM player_dir AS BYTE
DIM forward_x(4) AS BYTE
DIM forward_y(4) AS BYTE
DIM left_x(4) AS BYTE
DIM left_y(4) AS BYTE
DIM right_x(4) AS BYTE
DIM right_y(4) AS BYTE
DIM depth AS BYTE
DIM key$ AS STRING * 1
DIM sp AS INT ' GLOBAL STACK POINTER
' ==========================================
' SETUP DIRECTION LOOKUP TABLES
' ==========================================
SUB init_direction_tables()
forward_x(DIR_NORTH)=0 : forward_y(DIR_NORTH)=-1
left_x(DIR_NORTH)=-1 : left_y(DIR_NORTH)=0
right_x(DIR_NORTH)=1 : right_y(DIR_NORTH)=0
forward_x(DIR_EAST)=1 : forward_y(DIR_EAST)=0
left_x(DIR_EAST)=0 : left_y(DIR_EAST)=-1
right_x(DIR_EAST)=0 : right_y(DIR_EAST)=1
forward_x(DIR_SOUTH)=0 : forward_y(DIR_SOUTH)=1
left_x(DIR_SOUTH)=1 : left_y(DIR_SOUTH)=0
right_x(DIR_SOUTH)=-1 : right_y(DIR_SOUTH)=0
forward_x(DIR_WEST)=-1 : forward_y(DIR_WEST)=0
left_x(DIR_WEST)=0 : left_y(DIR_WEST)=1
right_x(DIR_WEST)=0 : right_y(DIR_WEST)=-1
END SUB
' ==========================================
' DRAW WALL SEGMENTS (3D VIEW)
' ==========================================
SUB draw_full_wall(d AS BYTE)
if d < 5 then
wall_height = (d*2)
FOR x AS BYTE = d*2 TO 21-wall_height
CHARAT x, wall_height-1, 100
NEXT x
FOR y AS BYTE = d*2 TO 19-wall_height
FOR x AS BYTE = d*2 TO 21-wall_height
CHARAT x, y, 160
NEXT x
NEXT y
FOR x AS BYTE = d*2 TO 21-wall_height
CHARAT x, y, 232
NEXT x
else
if d = 5 then
charat 9,9,98
charat 10,9,98
charat 11,9,98
charat 12,9,98
charat 9,10,160
charat 10,10,160
charat 11,10,160
charat 12,10,160
else
charat 9,10,232
charat 10,10,232
charat 11,10,232
charat 12,10,232
end if
end if
END SUB
SUB draw_left_wall(d AS BYTE)
DIM x AS BYTE : x=d*2
wall_height = 19-(d*2)-1
CHARAT x,d*2,223
FOR y AS BYTE=d*2+1 TO wall_height: CHARAT x,y,160: NEXT y
CHARAT x,y,105
x=x+1
wall_height = wall_height-1
CHARAT x,d*2+1,223
FOR y AS BYTE=d*2+2 TO wall_height: CHARAT x,y,160: NEXT y
CHARAT x,y,105
END SUB
SUB draw_right_wall(d AS BYTE)
DIM x AS BYTE : x=21-(d*2)
wall_height = 19-(d*2)-1
CHARAT x,d*2,233
FOR y AS BYTE=d*2+1 TO wall_height: CHARAT x,y,160: NEXT y
CHARAT x,y,95
x=x-1
CHARAT x,d*2+1,233
wall_height = wall_height-1
FOR y AS BYTE=d*2+2 TO wall_height: CHARAT x,y,160: NEXT y
CHARAT x,y,95
END SUB
SUB draw_left_gap(d AS BYTE)
DIM x AS BYTE : x=d*2
CHARAT x,d*2+1,100
wall_height = 19-(d*2)-2
if wall_height > 9 then
FOR y AS BYTE=d*2+2 TO wall_height: CHARAT x,y,32: NEXT y
CHARAT x,y,104
x=x+1
CHARAT x,d*2+1,100
FOR y AS BYTE=d*2+2 TO wall_height: CHARAT x,y,32: NEXT y
CHARAT x,y,104
else
CHARAT x,wall_height,70
CHARAT x+1,wall_height,123
CHARAT x,wall_height+1,100
CHARAT x+1,wall_height+1,97
end if
END SUB
SUB draw_right_gap(d AS BYTE)
DIM x AS BYTE : x=21-(d*2)
CHARAT x,d*2+1,100
wall_height = 19-(d*2)-2
if wall_height > 9 then
FOR y AS BYTE=d*2+2 TO wall_height: CHARAT x,y,32: NEXT y
CHARAT x,y,104
x=x-1
CHARAT x,d*2+1,100
FOR y AS BYTE=d*2+2 TO wall_height: CHARAT x,y,32: NEXT y
CHARAT x,y,104
else
CHARAT x-1,wall_height,108
CHARAT x,wall_height,70
CHARAT x-1,wall_height+1,225
CHARAT x,wall_height+1,100
end if
END SUB
' ==========================================
' RENDER 3D
' ==========================================
SUB render_view()
' clear screen unrolled
MEMSET 1024, 400, 32
MEMSET 1424, 22, 230
MEMSET 1464, 22, 230
MEMSET 1504, 22, 230
MEMSET 1544, 22, 230
MEMSET 1584, 22, 230
MEMSET 1624, 22, 230
MEMSET 1664, 22, 230
MEMSET 1704, 22, 230
MEMSET 1744, 22, 230
MEMSET 1784, 22, 230
MEMSET 1824, 22, 32
MEMSET 1864, 26, 32
' render view by depth
FOR d AS BYTE = 0 TO 6
DIM ax AS BYTE
DIM ay AS BYTE
ax = player_x + forward_x(player_dir)*d
ay = player_y + forward_y(player_dir)*d
IF maze(ax,ay)=1 THEN
CALL draw_full_wall(d)
RETURN
END IF
if d < 5 then
DIM lx AS BYTE : lx = ax + left_x(player_dir)
DIM ly AS BYTE : ly = ay + left_y(player_dir)
DIM rx AS BYTE : rx = ax + right_x(player_dir)
DIM ry AS BYTE : ry = ay + right_y(player_dir)
IF maze(lx,ly)=1 THEN CALL draw_left_wall(d) ELSE CALL draw_left_gap(d)
IF maze(rx,ry)=1 THEN CALL draw_right_wall(d) ELSE CALL draw_right_gap(d)
end if
NEXT d
END SUB
' ==========================================
' MOVEMENT
' ==========================================
SUB move_forward()
DIM nx AS BYTE : nx = player_x + forward_x(player_dir)
DIM ny AS BYTE : ny = player_y + forward_y(player_dir)
IF maze(nx,ny)=0 THEN
player_x=nx: player_y=ny
END IF
END SUB
SUB turn_left()
player_dir = player_dir - 1
IF player_dir > 3 THEN player_dir = DIR_WEST
END SUB
SUB turn_right()
player_dir = player_dir + 1
IF player_dir > DIR_WEST THEN player_dir = DIR_NORTH
END SUB
' ==========================================
' SHOW MAP
' ==========================================
SUB show_map()
PRINT CHR$(147)
PRINT " overhead map view"
FOR map_cy = 0 TO 18
FOR map_cx = 0 TO 18
IF map_cx=player_x AND map_cy=player_y THEN
PRINT CHR$(146);"@";
ELSE
IF maze(map_cx,map_cy)=1 THEN PRINT CHR$(18);CHR$(186); ELSE PRINT CHR$(146);CHR$(166);
END IF
NEXT map_cx
PRINT ""
NEXT map_cy
PRINT "press a key to continue..."
key$=""
DO WHILE key$=""
GET key$
LOOP
END SUB
' ==========================================
' MAZE CARVING
' ==========================================
SUB carve_maze()
PRINT CHR$(147);"generating maze..."
' directional vectors
carve_dx(0)=0 : carve_dy(0)=-1 ' north
carve_dx(1)=1 : carve_dy(1)=0 ' east
carve_dx(2)=0 : carve_dy(2)=1 ' south
carve_dx(3)=-1 : carve_dy(3)=0 ' west
' working variables
DIM cell_x AS BYTE
DIM cell_y AS BYTE
DIM next_cell_x AS BYTE
DIM next_cell_y AS BYTE
DIM wall_x AS BYTE
DIM wall_y AS BYTE
DIM direction_index AS BYTE
DIM temp AS BYTE
' ------------------------------------
' 1. fill maze with walls (1)
' ------------------------------------
FOR map_cy = 0 TO 19
FOR map_cx = 0 TO 19
maze(map_cx,map_cy) = 1
NEXT map_cx
NEXT map_cy
' ------------------------------------
' 2. choose odd starting cell
' ------------------------------------
cell_x = ((CINT(RND()*8))*2)+1
cell_y = ((CINT(RND()*8))*2)+1
maze(cell_x,cell_y)=0
start_x = cell_x
start_y = cell_y
sp = 0
stack_x(0) = cell_x
stack_y(0) = cell_y
carve_loop:
' load current cell from stack
cell_x = stack_x(sp)
cell_y = stack_y(sp)
' ------------------------------------
' build shuffled direction list
' ------------------------------------
dirs(0)=0 : dirs(1)=1 : dirs(2)=2 : dirs(3)=3
FOR shuffle_i AS BYTE = 0 TO 3
DIM shuffle_j AS BYTE
shuffle_j = shuffle_i + CINT(RND()*(3-shuffle_i))
temp = dirs(shuffle_i)
dirs(shuffle_i)=dirs(shuffle_j)
dirs(shuffle_j)=temp
NEXT shuffle_i
' ------------------------------------
' attempt each direction
' ------------------------------------
FOR try_direction AS BYTE = 0 TO 3
direction_index = dirs(try_direction)
next_cell_x = cell_x + carve_dx(direction_index)*2
next_cell_y = cell_y + carve_dy(direction_index)*2
' boundary protection (keep 1-cell border)
IF next_cell_x<1 OR next_cell_y<1 OR next_cell_x>18 OR next_cell_y>18 THEN GOTO no_go
' can we carve?
IF maze(next_cell_x,next_cell_y)=1 THEN
' carve the wall between the cells
wall_x = cell_x + carve_dx(direction_index)
wall_y = cell_y + carve_dy(direction_index)
maze(wall_x,wall_y)=0
' carve the destination cell
maze(next_cell_x,next_cell_y)=0
' push next cell onto stack
sp = sp + 1
stack_x(sp)=next_cell_x
stack_y(sp)=next_cell_y
GOTO carve_loop
END IF
' --- optional side opening ---
IF RND() < 0.25 THEN
DIM side_dir AS BYTE
DIM side_x AS BYTE
DIM side_y AS BYTE
side_dir = (direction_index + 1 + CINT(RND()*1)*2) AND 3
side_x = cell_x + carve_dx(side_dir)
side_y = cell_y + carve_dy(side_dir)
IF side_x>1 AND side_x<18 AND side_y>1 AND side_y<18 THEN
maze(side_x,side_y)=0
END IF
END IF
no_go:
NEXT try_direction
' backtrack
sp = sp - 1
IF sp<0 THEN RETURN
GOTO carve_loop
END SUB
' ==========================================
' INITIALISE MAZE
' ==========================================
SUB init_maze()
CALL carve_maze()
END SUB
' ==========================================
' MAIN LOOP
' ==========================================
SUB main()
RANDOMIZE TI()
CALL init_direction_tables()
CALL init_maze()
player_x = start_x
player_y = start_y
player_dir = DIR_NORTH
CALL render_view()
game_loop:
GET key$
IF key$="" THEN GOTO game_loop
IF key$="m" THEN
CALL show_map()
CALL render_view()
GOTO game_loop
END IF
IF key$="f" THEN CALL move_forward()
IF key$="l" THEN CALL turn_left()
IF key$="r" THEN CALL turn_right()
CALL render_view()
GOTO game_loop
END SUB
CALL main()
You will notice it is not fully embracing the full XC-BASIC capabilities, I even use GOTO! There are a few nice benefits of XC syntax that I do take advantage of though:
- Call subroutines instead of
GOSUB, eg.CALL render_view()(there are also functions, the difference being functions return a value) - As mentioned above,
IF ... END IFso your logic can have several operations per condition, andELSEso you can provide operations for when the condition is not matched. DO WHILEloops, plus there is also aDO LOOPwhere the condition is checked after the operations too.MEMSETfor fast batch memory writes. Much more efficient than looping and setting one value at a time!CHARAT 9,9,98for writing a character to screen memory at a certain cursor location. This is the equivalent of severalPOKEs in C64 BASIC.BACKGROUNDandBORDERare more macros than commands but still handy, especially as XC can compile to multiple target computers.
That last point is an interesting one. Yes, this could in theory be compiled for multiple Commodore systems, even the Vic-20 if we demand RAM expansion …
Had I planned for that in advance I would have made any C64 hard coded values use constants instead, for example I use a lot of specific PETSCII screen codes. This is bad practice for readability anyway so getting out of the habit is just generally a good idea.
What’s Next?
There are a lot of improvements to be made, but I am pretty happy with it so far considering this is my first real XC-BASIC3 project. One of the next things I need to do, as well as finish prettying it up, is use the XC SCREEN feature and write to a buffer screen to reduce screen flicker.
You can try it out in your web browser here.
As in the original, F = Forward, R/L for Left/Right, and M for map



How to Modulo on the C64 and Why You Need It