Somewhere along the way, “BASIC is too slow for games” morphed from a complaint into being universally received wisdom, and sadly that means a lot of people take it as a cue to skip the language entirely and go straight to assembly.
Obviously, I disagree. This dungeon crawler is my long-running argument with that idea. It runs on a stock Commodore 64, it is written entirely in BASIC, and by the end of this series you will know exactly how to make a similar game of your own.
The video above is deliberately show-more-than-tell, because that is what keeps a video watchable on the YouTubes. Here I want to actually walk through the code, because the most interesting parts of this build are the small decisions that keep plain BASIC fast enough to feel like a game rather than a slideshow.
Go ahead and open the full program in the browser and edit it yourself here: part7c.bas in the RGC online IDE.
Where we got to last time
This is part seven of the series, so a quick recap of what was already working before today. We had a custom character set loading from disk, we had keyboard control moving a player around the screen, and we had a map.
What we did not have was any sense of consequences. You could walk straight into a monster, and nothing happened. The map was also loaded from a file, which meant every play-through was identical. When you are doing a lot of testing, this even gets boring for the developer.
Three things change in this part.
The player gets real collision detection, so the world can finally react to being walked into.
Your map gets generated on the fly, so no two runs are the same.
And I fold in an earlier lesson, the Dungeons and Dragons style character generator, so you start each game as a freshly rolled adventurer rather than a default blob.
The screen is the map
Before any of the collision code makes sense, there is one thing I need to explain. On the C64, the screen itself can be your game map state. Screen memory starts at address 1024 (by default) and runs for 1000 bytes, one byte per character, 40 columns across and 25 rows down.
Whatever we draw at a given cell is readable straight back out of memory with PEEK. So instead of keeping a separate array that mirrors the map, I let the screen be the map. If I want to check what is in front of the player, I can just read the byte at that cell.
There is a catch that often trips people up, and in the video I gloss over it to keep things moving, so let me be explain here. When you PEEK screen memory you do not get the same PETSCII code (Commodore’s ASCII-style code) of the character as when you PRINT CHR$(). You get the screen code, which is a different numbering the video chip uses internally for memory reads/writes. That is why the collision checks compare against numbers that look weird at first glance.
Here is the map of what this game actually uses:
Screen codeCharacterRoleWhat happens32spaceemptywalk freely46.floorwalk freely38&goblinlose 5 health, bounce back8Hhealth potionheal, step onto it, remove it36$goldadd 10 gold, pick up47/swordadd 2 strength, pick up42*magicadd gold, pick up11Kkeyadd gold, pick up9Iidoladd gold, pick up
If you ever want to check a screen code yourself, the quickest way is to print the character on screen and PEEK the cell it landed in. The letters follow a tidy pattern, by the way. On the C64 the unshifted letters A to Z are screen codes 1 to 26, which is why H comes out as 8, I as 9 and K as 11. The punctuation codes line up with their positions too, so the ampersand is 38 and the slash is 47.
Collision detection without the slow maths
Reading a cell means turning a row and column into a single memory address, but the C64, like most 8-bit computers, sucks at multiplication.
The obvious formula is screen start address (1024) plus Player Row times 40 plus Player Column.
1024 + PY * 40 + PX
That works, but the row times 40 is a floating point multiply, and BASIC does a multiply the slow way. Doing it on every single move, inside the tightest loop in the program, is exactly the kind of thing that makes people conclude BASIC is hopeless.
The fix I went with is a lookup table in the form of a simple array. Every row offset is a fixed value so row 0 is 0, row 1 is 40, row 2 is 80 and so on. There are only 24 of them, so we work them all out once at start-up, and store them. After that the multiply never needs to happen again, we just read the answer back, aka look it up.
INITONCE:
FL=1: REM FIRST TIME ONLY
GOSUB METATILES
REM DECLARE VARIABLES
DIM LUT(23)
DIM STATS(5)
DIM STATS$(5)
DIM D(3)
REM PRECOMPUTE LOOKUP TABLE FOR SCREEN
FOR R=0 TO 23: LUT(R)=R*40: NEXT R
RETURN
With our LUT array filled in, reading the cell the player is standing on becomes a single addition with no multiply involved:
REM USES PRECOMPUTED LOOKUP TABLE - AVOIDS MULTIPLY EACH MOVE
CH=PEEK(SA+LUT(PY)+PX)
IF (CH=32 OR CH=46) AND MSG$<>"" THEN MSG$="": GOSUB ALERT
IF (CH=32 OR CH=46) THEN RETURN
Here SA is the screen address 1024, PY and PX are the player’s row and column, and CH ends up holding the screen code of whatever the player just tried to move onto. If that code is a space or a floor dot, there is nothing to react to, so we clear any leftover status message and return straight away. That early return matters because as the most common case (moving into empty space), it needs to do the least possible work.
Recognise, then react
Knowing what you walked into is only half the job. The other half is deciding what it does to the player, and crucially what it does to the map.
For example, a goblin should hurt you and make you stay in combat so you cannot just barge straight through it. A coin, though, should give you gold and then it should vanish, because a coin you can pick up forever is not much of a challenge. Those are two different behaviours, and we control them with two flag variables:
REM BK=BOUNCE PLAYER BACK RK=REMOVE TILE AT IX/IY
BK=1 : RK=0
REM GOBLINS HURT HP
IF CH=38 THEN HP=HP-5 : MSG$="GOBBO!":GOSUB ALERT
REM SWORDS ADD STRENGTH
IF CH=47 THEN STATS(0)=STATS(0)+2 : MSG$="SWORD!":GOSUB ALERT : RK=1 : IX=PX : IY=PY
REM HEALTH POTIONS BUFF VIA CONSTITUTION MATH
IF CH=8 THEN HP=HP+INT(STATS(2)/3)+1 : MSG$="HEALTH!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
REM CASH ADDS GOLD
IF CH=36 THEN GL=GL+10 : MSG$="CASH!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
BK is the bounce-back flag and RK is the remove flag. The default is bounce and do not remove, which is the safe behaviour for solid things. A goblin sets neither, so it just applies its damage and lets the default bounce leave you standing where you were. A coin or a potion sets BK=0 so you actually walk onto its square, and RK=1 so the item is then erased.
It also records the item’s position in IX and IY, which looks redundant right now, but it is deliberate. A moving enemy occupies a different cell from the one the player ends up on, so keeping the item’s own coordinates separate leaves room for that later.
At the end of the handler the two flags are brought in to play:
IF RK=1 THEN GOSUB ERASEITEM
IF BK=1 THEN PY=OY : PX=OX
If we are removing the item, we blank its cell back to a floor dot. If we are bouncing, we simply restore the player’s old position that was saved in OX and OY at the top of the game loop, and it is as if the player never moved.
The whole thing is efficient precisely because it does so little: read one byte, run it through a short list of comparisons, adjust a couple of variables, and maybe repaint one cell. That is well within what C64 BASIC can do without the game feeling sluggish.
Drawing the world with metatiles
The dungeon is not drawn a character at a time. It is built from metatiles, small three by three blocks of characters that slot together into a grid. Meta tiles means tiles made out of tiles.
Right now there are 23 of them, each stored as three strings of three characters in a two dimensional array, one string per row of the block.
REM ROOM (GOBBO)
MT$(17,0)="..."
MT$(17,1)=".{GREEN}&{GREY}."
MT$(17,2)="..."
Two things in that little block are worth focusing on. First, the tile is drawn with PRINT, not by poking bytes into screen memory. On the C64 a printed character sets both the screen code and its colour in one go, whereas poking means writing the character to screen memory and separately writing the colour to colour memory at a matching address. By letting PRINT do the work it is both less code and faster.
Second, those {GREEN} and {GREY} markers are colour control codes embedded right into the string, so the goblin comes out green and the surrounding floor stays grey, again without a single poke to colour memory. In my IDE and some desktop C64 editors these {tokens} are translated to the actual C64 character codes at tokenisation time if you are wondering why you can’t type them directly into a C64.
Printing a three line block has one wrinkle: after you print the first row the cursor is sitting at the end of what it just output, and you need it back at the start of the next row down. That is what the cursor control codes in the draw routine do.
DRAWMT:
PRINT MT$(MT,0);"{DOWN}{LEFT}{LEFT}{LEFT}";
PRINT MT$(MT,1);"{DOWN}{LEFT}{LEFT}{LEFT}";
PRINT MT$(MT,2);
RETURN
We print the top row, then step the cursor down one and back three to the left, which lands it exactly under the start of the row you just printed. Do it again for the middle row, print the bottom row, and the block is complete. It is the print equivalent of a carriage return, done by hand because we are creating a small grid rather than one line of text.
About that “procedural” generation
Now for the confession, because it is easy to feel mislead. The map is generated, and it is different every time, but I would not strictly call it procedural generation, at least yet. It is random tile placement, and there is a bit of a difference.
MAPROLL:
R=RND(-TI)
FOR ROW=3 TO 21 STEP 3
FOR COL=1 TO 19 STEP 3
GOSUB CURSORSET
MT= INT(RND(1)*23)+1:GOSUB DRAWMT
NEXT COL
NEXT ROW
RETURN
The first line seeds the random number generator from the system timer, TI, so you get a genuinely different sequence on each run rather than the same map every time (a negative value to RND is the C64’s way of re-seeding it). Then it steps across the map in a grid, three cells at a time because each metatile is three wide and three tall, and at each grid position it picks a tile from 1 to 23 at random and draws it.
What this does not do is guarantee the corridors line up into one connected walkable space. Real procedural generation would care about corridor connectivity, making sure every room can be reached from every other. That is why games such as the PET Dungeon took a whole minute to generate your game map every time you play.
Mine just drops random (but carefully curated) blocks down and hopes for the best. The pleasant surprise is that because the metatiles were designed with openings in sensible places, it works out walkable far more often than you would expect. But, of course, sometimes it does not, and you end up with a pocket of the map you simply cannot reach on foot. Which is where the sneaky feature comes in.
Teleporting, and a bit of history
When you get boxed into an unreachable section, you have two options. You can regenerate the whole map and start over, or give the player a way out. Some games back in the day let you walk through walls, sometimes at the cost of a hit to your health or the use of a spell. I went with a teleport instead, mapped to the T key, partly because it is genuinely useful when you get cornered and partly because it sidesteps the connectivity problem entirely for now.
TELEPORT:
PX=INT(RND(1)*21)+1
PY=INT(RND(1)*21)+1
IF PEEK(1024+(PY*40)+PX)<>46 THEN GOTO TELEPORT
RETURN
It picks a random cell, then checks whether that cell contains a space or floor dot (screen code 46). If it doesn’t, IE. the target spot is already taken, then it loops round and tries again. That single check prevents you from teleporting into a wall, onto an item, or on top of a monster.
Notice this one still does the multiply inline rather than using the lookup table. It runs so rarely, only when you press T, that optimising it wouldn’t be worth the effort. The lookup table is necessary in the movement loop that runs constantly, but wouldn’t add much here.
Rolling a new RPG character
You do not start as a generic blob any more. At the start of each game the character generator from the earlier lesson rolls your stats the way a tabletop role playing game would with four six sided dice per stat, drop the lowest, keep the best three:
DICEROLLS:
D(0) = INT(RND(1)*6)+1
D(1) = INT(RND(1)*6)+1
D(2) = INT(RND(1)*6)+1
D(3) = INT(RND(1)*6)+1
RETURN
PICKHIGHEST:
T = 0
GOSUB BUBBLESORT
FOR P=1 TO 3
T=T+D(P)
NEXT P
STATS(S)=T
PRINT "{CYN}"T"{GREY3}"
RETURN
The sort is a tiny bubble sort over four values, which is more than fast enough even on an 8-bit machine for four dice and is a nice self contained example to copy and paste if you have never written one.
BUBBLESORT:
FOR M=2 TO 0 STEP-1
FOR C=0 TO M
X=D(C):Y=D(C+1)
IF X>Y THEN D(C)=Y : D(C+1)=X
NEXT C: NEXT M
RETURN
Those stats will then feed back into the game. Your starting health is derived from your constitution, and the health potion you pick up in the dungeon heals more if your constitution is higher, so the character you rolled increasingly shapes how the game plays. This will especially come in handy for asking the player whether they want to buff their magic, wisdom, or strength as they level up.
A HUD that does not flicker
The HUD also contributes to how polished the game feels. Our heads up display shows your health and gold score at the top of the screen. A problem with printing numbers is that they change how much space they take up as the number of digits expands, 5 is one character, 100 is three, and if you just print them the display jumps left and right as the values change. Our fix is to pad every number to a fixed width before printing it.
SHOWHUD:
MV$=CHR$(19)+CHR$(17)+CHR$(159)
REM PAD STR$ TO 4 CHARS - STOPS HUD COLUMNS JUMPING AS VALUES CHANGE
H$=RIGHT$(" "+STR$(HP),4)
G$=RIGHT$(" "+STR$(GL),4)
PRINT MV$;"HEALTH:{YELLOW}";H$;" {CYAN}GOLD:{YELLOW}";G$;"{GREY3}"
RETURN
The main part is RIGHT$(" "+STR$(HP),4). Stick four spaces in front of the number, then keep only the rightmost four characters. Whatever the number’s width, the result is always exactly four characters wide (right aligned), so the HUD always takes up the same screen space. It is the difference between a display that looks deliberate and one that jumps about as you play.
One more supporting routine worth a mention, since it is used all over the code, is how the cursor gets positioned. Rather than poke the cursor coordinates, this calls a built in C64 Kernal routine.
CURSORSET:
X=ROW : Y=COL
POKE 780,0
POKE 781,X
POKE 782,Y
POKE 783,0
SYS 65520
RETURN
SYS 65520 calls the built in PLOT routine, which moves the text cursor to a given row and column. Load the registers through the addresses at 780 to 783, call PLOT, and the next thing you PRINT positions exactly where you want it. It is a nice way to precisely position text at an arbitrary location without a pile of cursor control characters.
What Next?
This is becoming a real game now. You can explore a brand new dungeon, pick things up, take damage, and watch your stats and gold change as you go. But it is still just one level, and the monsters just sit there waiting for you to run into them.
Two key things would turn it into a proper game: enemies that move around and chase after you, and actual combat rather than a goblin simply subtracting health when you bump into it.
That is what we will build next, and you can already see the foundations prepared for it, the separate item coordinates in the collision handler being the clearest one.
If you want to poke at it, the full program is below, and the live, editable version is in the RGC online IDE where you can run it in the browser and see how your edits change things. Have a read, change the numbers, break it, fix it. That is the best way to learn how any of this actually hangs together!
The full listing so far
REM ========================================
REM PART 7C - DUNGEON CRAWLER WITH CHARACTER
REM (REAL DISK VERSION FOR C64 & C128 IDE)
REM ----------------------------------------
REM LOADS CHARSET FROM CHARS.BIN ON DRIVE 8
REM MAP IS PROCEDURALLY GENERATED (METATILES)
REM THIS VERSION GENERATES A CHARACTER USING
REM D&D STYLE STAT ROLLS
REM ----------------------------------------
REM MOVE WITH Q (UP) A (DOWN) O (LEFT) P (RIGHT)
REM N = NEW CHARACTER (RE-ROLL + RELOAD MAP)
REM T = TELEPORT TO RANDOM LOCATION
REM ========================================
IF A=0 THEN PRINT CHR$(147)"LOADING, PLEASE WAIT "
A=A+1
IF A = 1 THEN GOTO CHARS
IF A = 2 THEN GOTO SETCHARS
CHARS:
LOAD "CHARS.BIN",8,1
SETCHARS:
POKE 53272,(PEEK(53272)AND240)+12
WELCOME:
POKE 53280,0 : POKE 53281,0
PRINT "{CLR}{GREY}"
PRINT "#### # # ### # ### #### ### ### #"
PRINT "# # # # # # # # # # # # # #"
PRINT "# # # # # # # # ## ### # # # # #"
PRINT "# # # # # # # # # # # # # # #"
PRINT "#### ## # ### ### #### ### # ###"
PRINT "======================================"
PRINT ""
PRINT "{WHITE}WELCOME TO THE DUNGEON"
PRINT ""
PRINT "{LIGHTBLUE}USE {RVSON}QAOP{RVSOFF} KEYS TO MOVE"
PRINT "{RVSON}T{RVSOFF} TELEPORT TO RANDOM LOCATION"
PRINT "{RVSON}N{RVSOFF} NEW CHARACTER (RE-ROLL + RELOAD MAP)"
PRINT ""
PRINT "{GREY}COLLECT THE {PURPLE}I{GREY}DOLS, AVOID THE {GREEN}G{GREY}OBBOS AND RECHARGE YOUR {PINK}H{GREY}EALTH AND {YELLOW}*{GREY}MAGIC{YELLOW}*{GREY}!"
PRINT "{GREEN}"
PRINT "{RVSON}PRESS A KEY TO START{RVSOFF}"
HOLDKEY:
GET K$: IF K$="" THEN GOTO HOLDKEY
DRAWMAP:
IF FL=0 THEN GOSUB INITONCE
GOSUB INITPLAYER
POKE 53280,0 : POKE 53281,0
GOSUB MAPBLANK
GOSUB MAPROLL
GOSUB DISPLAY
GOSUB DRAWPLAYER
GAMELOOP:
OX=PX : OY=PY
GOSUB KEYS
IF PX<>OX OR PY<>OY THEN GOSUB ERASEPLAYER
GOSUB DRAWPLAYER
GOTO GAMELOOP
REM GET KEYBOARD INPUT
KEYS:
REM RIGHT NOW ALL ACTIONS WAIT FOR PLAYER
GET P$: IF P$="" THEN GOTO KEYS
REM PLAYER INPUT
IF P$="N" THEN GOSUB INITPLAYER : GOSUB MAPBLANK : GOSUB MAPROLL : GOSUB DISPLAY : GOSUB DRAWPLAYER
IF P$="O" THEN PX=PX-1
IF P$="P" THEN PX=PX+1
IF P$="Q" THEN PY=PY-1
IF P$="A" THEN PY=PY+1
IF P$="T" THEN GOSUB TELEPORT
REM PLAYER BOUNDS (SHOULD HIT A WALL BUT GOOD TO CHECK)
IF PX < 0 THEN PX=0
IF PX > 39 THEN PX=39
IF PY < 0 THEN PY=0
IF PY > 23 THEN PY=23
REM CHECK FOR COLLISIONS (IE. MOVED TO NON-SPACE)
COLISSION:
IF PY=OY AND PX=OX THEN RETURN
REM USES PRECOMPUTED LOOKUP TABLE - AVOIDS MULTIPLY EACH MOVE
CH=PEEK(SA+LUT(PY)+PX)
IF (CH=32 OR CH=46) AND MSG$<>"" THEN MSG$="": GOSUB ALERT
IF (CH=32 OR CH=46) THEN RETURN
REM BK=BOUNCE PLAYER BACK RK=REMOVE TILE AT IX/IY (SET IX/IY IN HANDLER)
BK=1 : RK=0
REM GOBLINS HURT HP
REM - HP CAN GO BELOW 0, WE WILL FIX THIS WHEN WIN/LOSE STATES ARE HANDLED
REM - COMBAT WILL SET IX/IY + RK=1 WHEN THE GOBBo DIES
IF CH=38 THEN HP=HP-5 : MSG$="GOBBO!":GOSUB ALERT
REM SWORDS ADD STRENGTH
IF CH=47 THEN STATS(0)=STATS(0)+2 : MSG$="SWORD!":GOSUB ALERT : RK=1 : IX=PX : IY=PY
REM HEALTH POTIONS BUFF VIA CONSTITUTION MATH
IF CH=8 THEN HP=HP+INT(STATS(2)/3)+1 : MSG$="HEALTH!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
REM CASH ADDS GOLD
IF CH=36 THEN GL=GL+10 : MSG$="CASH!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
REM POWER UP
IF CH=42 THEN GL=GL+10 : MSG$="POWER!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
REM KEY
IF CH=11 THEN GL=GL+10 : MSG$="KEY!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
REM MACGUFFIN
IF CH=9 THEN GL=GL+10 : MSG$="IDOL FOUND!":GOSUB ALERT : BK=0 : RK=1 : IX=PX : IY=PY
REM IF CH<>32 AND CH<>46 THEN PRINT "{HOME}{DOWN}{DOWN}{DOWN}";STR$(CH):GOSUB ALERT
IF RK=1 THEN GOSUB ERASEITEM
IF BK=1 THEN PY=OY : PX=OX
REM REFRESH HUD AND RETURN TO GAMELOOP
GOSUB SHOWHUD
PRINT "{HOME}";
RETURN
ERASEPLAYER:
ROW=OY : COL=OX : GOSUB CURSORSET : PRINT "{GREY}.";
RETURN
ERASEITEM:
REM ERASE TILE AT IX/IY (NOT ALWAYS PX/PY — MOVING ITEMS USE THEIR OWN CELL)
ROW=IY : COL=IX : GOSUB CURSORSET : PRINT "{GREY}.";
RETURN
DRAWPLAYER:
ROW=PY : COL=PX : GOSUB CURSORSET : PRINT "{LIGHTBLUE}@";
RETURN
ALERT:
PRINT "{HOME} ";
PRINT "{HOME}{PINK}";LEFT$(MSG$+" ",37); "{WHT}";
RETURN
CURSORSET:
REM ROW/COL ARE CURSOR ONLY — SET BEFORE CALLING (PX/PY = PLAYER)
X=ROW : Y=COL
POKE 780,0
POKE 781,X
POKE 782,Y
POKE 783,0
SYS 65520
RETURN
DISPLAY:
SA=1024
POKE 53272,(PEEK(53272)AND240)+12
POKE 53280,0 : POKE 53281,0
PRINT "{HOME}{PINK}";MSG$; "{WHT} "
GOSUB SHOWHUD
RETURN
INITONCE:
FL=1: REM FIRST TIME ONLY
GOSUB METATILES
REM DECLARE VARIABLES
DIM LUT(23)
DIM STATS(5)
DIM STATS$(5)
DIM D(3)
REM PRECOMPUTE LOOKUP TABLE FOR SCREEN
FOR R=0 TO 23: LUT(R)=R*40: NEXT R
RETURN
INITPLAYER:
REM GL NOT GOLD - GO CLASHES WITH GOTO (2-LETTER NAMES)
GL=0
MSG$="USE QAOP KEYS"
GOSUB PLAYERSTATS
HP=STATS(2)*2
PX=10 : PY=10 : REM HARD CODED, WILL GLEAN FROM MAP LATER
OX=PX : OY=PY : REM STORE INITIAL 'OLD' POSITIONS
RETURN
MAPBLANK:
PRINT "{CLR}{DOWN}{DOWN}{GREY}#######################"
FOR ROW=3 TO 23
PRINT "#";SPC(21);"#"
NEXT ROW
PRINT "#######################";
PRINT "{HOME}";
RETURN
MAPROLL:
R=RND(-TI)
FOR ROW=3 TO 21 STEP 3
FOR COL=1 TO 19 STEP 3
GOSUB CURSORSET
MT= INT(RND(1)*23)+1:GOSUB DRAWMT
NEXT COL
NEXT ROW
RETURN
SHOWHUD:
MV$=CHR$(19)+CHR$(17)+CHR$(159)
REM PAD STR$ TO 4 CHARS — STOPS HUD COLUMNS JUMPING AS VALUES CHANGE
H$=RIGHT$(" "+STR$(HP),4)
G$=RIGHT$(" "+STR$(GL),4)
PRINT MV$;"HEALTH:{YELLOW}";H$;" {CYAN}GOLD:{YELLOW}";G$;"{GREY3}"
RETURN
PLAYERSTATS:
R=RND(-TI)
STATS$(0)="STRENGTH"
STATS$(1)="DEXTERITY"
STATS$(2)="CONSTITUTION"
STATS$(3)="INTELLIGENCE"
STATS$(4)="WISDOM"
STATS$(5)="CHARISMA"
REM CLEAR SCREEN AND SET CURSOR TO TOP LEFT
REM STATS SCREEN BLUE BORDER AND BACKGROUND
POKE 53280,6 : POKE 53281,6
PRINT "{CLR}{WHT}"
PRINT "{RVSON}CHARACTER GENERATOR{RVSOFF}"
PRINT "{13}{GREY3}"
FOR S = 0 TO 5
PRINT CHR$(13)STATS$(S);
GOSUB DICEROLLS
GOSUB PICKHIGHEST
NEXT S
PRINT "{13}{13}{CYAN}{RVSON}PRESS A KEY TO PLAY{RVSOFF}{WHITE}"
HOLDFORKEY:
GET A$
IF A$="" THEN GOTO HOLDFORKEY
RETURN
DICEROLLS:
D(0) = INT(RND(1)*6)+1
D(1) = INT(RND(1)*6)+1
D(2) = INT(RND(1)*6)+1
D(3) = INT(RND(1)*6)+1
RETURN
PICKHIGHEST:
T = 0
GOSUB BUBBLESORT
FOR P=1 TO 3
T=T+D(P)
NEXT P
STATS(S)=T
PRINT "{CYN}"T"{GREY3}"
RETURN
BUBBLESORT:
FOR M=2 TO 0 STEP-1
FOR C=0 TO M
X=D(C):Y=D(C+1)
IF X>Y THEN D(C)=Y : D(C+1)=X
NEXT C: NEXT M
RETURN
END
METATILES:
REM SHAPES 1,2,3,4,5
REM 1x2, 2x4, 3x4 FOR ROTATIONS
REM =13
DIM MT$(24,3)
REM -
MT$(1,0)="###"
MT$(1,1)="..."
MT$(1,2)="###"
REM |
MT$(2,0)="#.#"
MT$(2,1)="#.#"
MT$(2,2)="#.#"
REM ┐
MT$(3,0)="###"
MT$(3,1)="..#"
MT$(3,2)="#.#"
REM ┘
MT$(4,0)="#.#"
MT$(4,1)="..#"
MT$(4,2)="###"
REM └
MT$(5,0)="#.#"
MT$(5,1)="#.."
MT$(5,2)="###"
REM ┌
MT$(6,0)="###"
MT$(6,1)="#.."
MT$(6,2)="#.#"
REM ├
MT$(7,0)="#.#"
MT$(7,1)="#.."
MT$(7,2)="#.#"
REM ┤
MT$(8,0)="#.#"
MT$(8,1)="..#"
MT$(8,2)="#.#"
REM ┬
MT$(9,0)="###"
MT$(9,1)="..."
MT$(9,2)="#.#"
REM ┴
MT$(10,0)="#.#"
MT$(10,1)="..."
MT$(10,2)="###"
REM ┼
MT$(11,0)="#.#"
MT$(11,1)="..."
MT$(11,2)="#.#"
REM ROOM (EMPTY)
MT$(12,0)="..."
MT$(12,1)="..."
MT$(12,2)="..."
REM ROOM ()
MT$(13,0)="###"
MT$(13,1)="..."
MT$(13,2)="..."
REM ROOM ()
MT$(14,0)="..."
MT$(14,1)="..."
MT$(14,2)="###"
REM ROOM ()
MT$(15,0)="#.."
MT$(15,1)="#.."
MT$(15,2)="#.."
REM ROOM ()
MT$(16,0)="..#"
MT$(16,1)="..#"
MT$(16,2)="..#"
REM ROOM (GOBBO)
MT$(17,0)="..."
MT$(17,1)=".{GREEN}&{GREY}."
MT$(17,2)="..."
REM ROOM (MAGIC)
MT$(18,0)="..."
MT$(18,1)=".{YELLOW}*{GREY}."
MT$(18,2)="..."
REM ROOM (KEY)
MT$(19,0)="..."
MT$(19,1)=".{ORANGE}K{GREY}."
MT$(19,2)="..."
REM
MT$(20,0)="..."
MT$(20,1)="..."
MT$(20,2)="..#"
REM
MT$(21,0)="#.."
MT$(21,1)="..."
MT$(21,2)="..#"
REM
MT$(22,0)="..."
MT$(22,1)=".{PURPLE}I{GREY}."
MT$(22,2)="..."
REM
MT$(23,0)="..."
MT$(23,1)=".{PINK}H{GREY}."
MT$(23,2)="..."
RETURN
DRAWMT:
PRINT MT$(MT,0);"{DOWN}{LEFT}{LEFT}{LEFT}";
PRINT MT$(MT,1);"{DOWN}{LEFT}{LEFT}{LEFT}";
PRINT MT$(MT,2);
RETURN
TELEPORT:
PX=INT(RND(1)*21)+1
PY=INT(RND(1)*21)+1
IF PEEK(1024+(PY*40)+PX)<>46 THEN GOTO TELEPORT
RETURN
The post Commodore 64 BASIC Dungeon Crawler (C64 BASIC Part 7) appeared first on Retro Game Coders.