C64 BASIC Dungeon Crawler: Goblin Attack! (C64 BASIC Part 8)
Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,833 words · 6 segments analyzed
Redefinable keys, goblins that chase you, d20 combat, and the memory bug that had the game silently overwriting itself.In part seven the dungeon was full of goblins that were about as threatening as garden gnomes. They stayed exactly where the map generator dropped them. You could walk up, poke them, take five points of damage and stroll away, and they’d still be standing there in the same spot when you came back.This time they chase you. A small change in code but big step forward in feel, and getting there dragged me into a weird and nasty memory bug that corrupted my character set while I watched. The bug is probably the most useful lesson in this whole part, so if you skim, don’t miss the memory section.Open the full program and edit it in your browser here: part8d.bas in the RGC online IDE.Where we got to last timeQuick recap of where part seven left off. We had a custom character set loading from disk, a map built out of three by three meta-tiles which randomly generated on every run, a player rolled up with proper Dungeons and Dragons style stats, collision detection that read the screen back with PEEK, and a teleport key for when the random map boxed you into a corner.What it didn’t have was a game. Nothing threatened you, so there were no stakes.Four things change in this part:You can redefine the keys, because QAOP is not to everybody’s taste.Goblins get a position, a health value, and a brain (albeit, a very small one).Combat happens with a real d20 roll and a strength modifier.And the game stops eating itself, which is a longer story than it sounds.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!Choose your own keysSomebody called me out on the controls, and for good reason. QAOP was one of the standards growing up so it’s burned into my fingers, but if you didn’t grow up with it, it’s a weird cluster of keys. I could have switched to WASD, but that annoys a different set of people just as much. The answer is that both sides are right, and the fix is to stop picking for them.
The trick to making this work is that the game must never compare against a literal key again. Instead of asking “did they press Q“, it should ask “did they press whatever is currently stored as the up key“. So first we need to store the defaults:REM DEFAULT KEYS QKEY$="Q" AKEY$="A" OKEY$="O" PKEY$="P" FKEY$="SPACE" Those variable names look like they were chosen carelessly. They weren’t, and this is one of the common traps of Commodore BASIC for modern coders.Only the first two letters actually matterCommodore BASIC stores a variable name in exactly two bytes. You can type PLAYERHEALTH and it will happily accept it, but internally it only ever takes note of PL, so the other ten characters are read and thrown in the bin. Which means PLAYERHEALTH and PLAYERX are the same variable, which makes your game do something baffling and difficult to debug that has nothing to do with the logic you wrote.That means my five key variables are deliberately unique in their first two characters: QK, AK, OK, PK, FK.There’s a second, sneakier version of the same trap, and part seven’s code has a comment about it that people asked me to explain:REM GL NOT GOLD - GO CLASHES WITH GOTO (2-LETTER NAMES) GL=0 A variable name shouldn’t really contain a BASIC keyword anywhere inside it because BASIC tokenisers can get very unhappy when you do. The C64 doesn’t read your original code listing, it first needs to be tokenised, scanning left to right and replacing every keyword recognised with a token byte.It doesn’t know or care that you meant GOLD as a name. It sees GO, which is a keyword ( GO TO with a space), swaps it for a token, and leaves you with a token followed by LD. If it tries to run that it will throwsa syntax error at you from a line that looks perfectly OK when viewed on screen.The classic ones that get people are TO, IF, ON, OR and AND. So SCORE contains OR, COUNT (ON), TOTAL (TO). If a program refuses to work and you can’t see why, look at your variable names before you hack away at your logic.
Reading a key that you can’t predictWith the defaults in variables, the input routine changes from testing letters to testing variables:IF P$=OKEY$ THEN PX=PX-1 IF P$=PKEY$ THEN PX=PX+1 IF P$=QKEY$ THEN PY=PY-1 IF P$=AKEY$ THEN PY=PY+1 IF P$=FKEY$ THEN GOSUB FIRE Now the redefine screen just has to write new values into those five variables. The interesting constraint is that it has to use GET, not INPUT. INPUT waits for you to type something and press RETURN, it echoes what you type, and it has strong opinions about commas.Someone remapping their controls might want to press a key that INPUT would freak out with or mangle, for example someone using an emulator might want to use the modern cursor keys. Asking them to press their chosen key and then hit RETURN is a sure way to get the wrong result, besides it’s one keypress too many for something that should feel immediate.GET reads a single character from the keyboard buffer and returns immediately, whether or not anything is there. That “whether or not” needs to be tested. If the buffer is empty and you get an empty string, the program carries straight on, so GET on its own is not “wait for a key“, it’s “check for a key“. To make it wait, you must loop it until it gives you something useful:CHANGETHISKEY: GET K$: IF K$="" THEN GOTO CHANGETHISKEY RETURN Everything else is just asking the questions in order and recording the answers:CHANGEKEYS: PRINT "{CLR}{GREY}" PRINT "CHANGE KEYS" PRINT "===========" PRINT "" PRINT "DEFAULT: QAOP AND SPACE"
PRINT "PRESS THE KEY FOR UP" GOSUB CHANGETHISKEY QKEY$=K$ PRINT "PRESS THE KEY FOR DOWN" GOSUB CHANGETHISKEY AKEY$=K$ PRINT "PRESS THE KEY FOR LEFT" GOSUB CHANGETHISKEY OKEY$=K$ PRINT "PRESS THE KEY FOR RIGHT" GOSUB CHANGETHISKEY PKEY$=K$ PRINT "PRESS THE KEY FOR FIRE" GOSUB CHANGETHISKEY FKEY$=K$ PRINT "HAPPY WITH YOUR CHOICES? (Y/N)" HOLDFORKEY: GET A$ IF A$="" THEN GOTO HOLDFORKEY IF A$="Y" THEN RETURN IF A$="N" THEN GOTO CHANGEKEYS RETURN The confirmation at the end matters more than it looks. It’s very easy to fat-finger the third key of five, and without a way out you’d be stuck with a control scheme you didn’t really choose. Answering N jumps back to the label at the top and starts the whole thing again, which is about the easiest undo you’ll ever write.One note if you’re typing this into a real machine rather than the IDE: those labels (CHANGEKEYS, CHANGETHISKEY) are a convenience of the RGC online IDE, which resolves them to line numbers when it tokenises. On a stock C64 they’d be GOSUB 4000 and friends. Same code, but uglier to read IMO. Download and open the .PRG if you want to see the line numbers in all their glory!If you prefer line numbers, you do you!Goblins need to be tracked before they can moveHere’s something I glossed over in the video. Up until now, the goblins didn’t really exist as far as the program was concerned. They were drawn, sure, and by checking RAM the collision code could tell it had bumped into screen code 38, but the program had no way to track them, no idea how many there were, and no way to refer to any specific one.
That’s fine when a goblin is decoration, but the moment it needs to move, we need to know which one moved, from where, to where, and whether it’s still alive. So here the goblins get promoted from screen pixels to actual game objects:REM DECLARE ENEMY VARIABLES GC=0: REM GOBBO COUNT DIM GH(10) : REM GOBBO HEALTH ARRAY FOR GC=1 TO 10: GH(GC)=10: NEXT GC GC=0 DIM GX(10) : DIM GY(10) : REM GOBBO LIST Three parallel arrays: GX and GY hold each goblin’s column and row, GH holds its health, and the variable GC counts how many we’ve actually placed. Goblin number 3 is GX(3), GY(3), GH(3).Parallel arrays are how you represent a record or a struct in a language that has no such thing, and on an 8-bit machine they’re faster than the alternative anyway.The FOR GC=1 TO 10 loop is quietly reusing GC as its counter to fill every health slot with 10, then resetting it to zero. It saves a variable, and every variable you declare in BASIC costs you bytes plus a slot to search past on every single lookup. Small, this is the sort of thing that adds up.Our goblins get added to the list right where they get drawn in the meta-tile routine. If the tile we’re about to stamp down is number 17, that’s the tile with a goblin in the middle:DRAWMT: PRINT MT$(MT,0);"{DOWN}{LEFT}{LEFT}{LEFT}"; PRINT MT$(MT,1);"{DOWN}{LEFT}{LEFT}{LEFT}"; PRINT MT$(MT,2); REM IF MT=17 THEN IT CONTAINS A GOBBO IF MT=17 THEN GOSUB ADDGOBBO RETURN ADDGOBBO: REM ADD TO THE GOBBO LIST - GX=COL (X), GY=ROW (Y), MATCHES PX/PY REM GUARD: STOP AT 10 SO GC NEVER EXCEEDS DIM GX(10) = ?
BAD SUBSCRIPT IF GC>=10 THEN RETURN GC=GC+1 GX(GC)=COL+1 GY(GC)=ROW+1 RETURN The +1 on each coordinate is the bit worth noting. ROW and COL are where the metatile starts, its top left corner, but the goblin isn’t at the top left, it’s in the middle of the three by three block. So the goblin’s real screen position is COL+1, ROW+1. Get this wrong and your goblins are all standing in a wall, one square up and to the left of where you can see them.Ten metatile 17s could theoretically come up on a random map, so once GC hits 11 you’re writing to GX(11) in an array you dimensioned to 10, and BASIC stops your game dead with ?BAD SUBSCRIPT ERROR. Proper, fair enemy generation is on the list to do but for now this stops a crash.Dumb enemy ‘AI’ that worksThe game is turn based, which means the player moves, then the enemies move, then we check whether the game is over.GAMELOOP: OX=PX : OY=PY REM PLAYER INPUT AND DRAWING GOSUB KEYS IF PX<>OX OR PY<>OY THEN GOSUB ERASEPLAYER GOSUB DRAWPLAYER REM HERE IS WHERE YOU PUT THE ENEMY LOGIC GOSUB ENEMYLOGIC REM CHECK GAME OVER CONDITIONS HERE REM -------------------------------- REM RETURN TO GAMELOOP GOTO GAMELOOP Turn based offers some flexibility because the enemies only get to think after you press a key. There’s no timing, and no danger of the goblins running away with the CPU while you’re deciding what to do next. On a machine this slow, taking turns is the thing that makes enemy AI in BASIC viable.The simplest enemy that is actually a threat is one that always moves towards you.