The Long Road Home: Porting a 1983 Dungeon Game to the Commodore 128
Sean Lewis 30 September 2026 0How a 40-year-old BASIC game made it from the C64 to the C128 — and what it taught me about the C128’s surprisingly different personality on the way,
“The Dungeons” was written in 1983 by Brian and Marian Clark for the Commodore 64. It is a text-and-graphics dungeon RPG in pure BASIC: 471 lines of code, complete with animated monster sprites, SID sound and a save-to-tape mechanic. Recently I reconstructed it to a C64 disk image using the ACME compiler with VS64 using VS Code, with binary assets extracted and documented. That reconstruction — a complete, working C64 build available on GitHub here became my starting point for this project.
My goal is now to port The Dungeons to run natively on the Commodore 128 in C128 mode, not in the C64 compatibility mode the 128 also offers. This is intended to be an interim step before the next stage, which will be to develop from this code base and enable the program to be able to use the extended BASIC 7 commands and feature set of the C128.
What followed was a journey through approximately every subtle difference between the C64 and C128 that exists. Full disclaimer for some parts of this work I used by CoPilot and Claude as in many cases it was able to make the necessary adjustments far quicker than I could by typing alone.
Why Native C128 Mode was Harder Than It Sounds
BASIC 7.0 on the C128 is a superset of C64 BASIC V2, but it’s not just BASIC V2 with extras. It’s a substantially larger ROM that shares the low-level interpreter structure but diverges in its startup sequence, its memory management, and its IRQ handling. Many zero-page addresses shift by exactly two bytes from their C64 positions — not all, not randomly, just two bytes further up. TXTTAB (the pointer to the start of the BASIC program) lives at $2D/$2E instead of $2B/$2C. VARTAB sits at $2F/$30 instead of $2D/$2E.
More importantly: on the C128, VARTAB doesn’t point into the same address space as the BASIC program. It points into bank 1 — a completely separate 64K of RAM used for variable storage. The BASIC program text lives in bank 0. They’re not just in different memory regions; they’re in different banks that require MMU reconfiguration to cross. This distinction, apparently minor, would take several sessions to fully understand and one area where some assistance from Claude became extremely useful.
The C128 also has a much more complex memory map overall. The BASIC runtime stack — GOSUB/FOR/DO return addresses — occupies $0800–$09FF. On the C64, that address range holds the sprite pointer blocks. Changing the game’s sprite pool address, and remapping every sprite pointer in the BASIC source accordingly, was one of the first things on the list that was tackled once I realised this was one significant difference between the two BASIC configuraitons.
The Toolkit
Three tools carried the whole project:
- ACME — a cross-assembler that handles the `.asm` source files, tokenized binary output, and the multi-segment PRG structure that lets a single loadable file populate into different memory regions.
- A custom Python tokenizer — built during the original C64 reconstruction with some help from CoPilot, this converted the human-readable `.bas` source file into a properly linked, correctly addressed BASIC binary that the C128’s interpreter could list and run. One early fix: the `–base` command-line flag, which specifies where the program loads in memory, had been silently ignored and hardcoded to the C64 address. At this point as a side bar I printed out the BASIC source using a recently acquired dot-matrix printer. A very nostalgic 10 minutes or so was then spent listing to the background brrrrr noise of the printer doing its thing.
- VICE x128 — the VICE emulator’s Commodore 128 implementation, running the real C128 ROMs. Crucially, VICE’s built-in monitor was the primary verification tool throughout: `d` for disassembly, `m` for memory dumps, `z` for single-step, `break` and `watch` for breakpoints and watchpoints, and `h` (hunt) for searching memory for known byte sequences. Almost every theory in this project was settled by using the monitor functions directly, rather than by inference, I learnt a lot more about how this works which is what a large part of doing this port for fun entails.
A key reference source was a copy of Mapping the Commodore 128 by Sheldon Cowper (COMPUTE!, 1986) its covers a thorough documentation of the C128’s memory map, zero-page layout, and ROM structure that was invaluable for planning and quickly learnt to cross-check with frequently and often.
The Hardest Problem: Getting the Game to Start
The C64 original used a small ML stub (`ml_entry.asm`) that set TXTTAB to point at the game’s BASIC program, ran BASIC’s CLR routine to reset the interpreter state, and then jumped into BASIC’s main dispatch loop. Simple enough on C64. On C128, the equivalent involved nine distinct iterations before anything worked.
The failures were instructive. The first CLR address tried ($51F8, cited in documentation) turned out to be in the middle of a BNE instruction inside a different routine — not a valid entry point at all resulting in a `CPU JAM.` The correct address ($51F3) was found through disassembling the actual ROM. A `LINKPRG` call that seemed necessary for relinking the program’s internal line pointers turned out to be unnecessary — the tokenizer was already computing the correct absolute links at build time for me.
Memory: A Map Full of Landmines
The C128 reserves a number of memory regions that the C64 had available. $0800–$09FF is the BASIC runtime stack, which meant the C64’s sprite pool at $0840 had to move. Choosing $1300, as a properly 64-byte aligned as the VIC requires, shifted all sprite block numbers from C64’s 33–68 range to C128’s 76–111 range. A quick script applied the required +43 offset to every POKE 2040–2047 and PEEK(2040)–(2047) reference across all of the 38 affected lines in the BASIC source.
The region $1200–$121F is permanently off-limits as it holds the CONT/error-state bookkeeping, the current line number, the `TEXT_TOP` and `MAX_MEM_0` pointers, and the RND seed. While never intentionality writing to these locations deliberately the PRG file, being a single contiguous block when loaded, spanning from the game state located at $0B00 all the way up to $2D60 at this point, passed through that region. ACME appears to zero-fill the gaps in a contiguous build. So every time the file loaded, the C128’s LOAD command wrote zeros across $1200–$121F, including `MAX_MEM_0` ($1212/$1213), which BASIC uses to check whether there’s room to edit programs.
The symptom was a `OUT OF MEMORY ERROR` with no line number, happening on any attempt to edit the BASIC source after loading. The root cause took several sessions to find — `m 1212 1213` immediately after a fresh load showed `00 00`, even before the SYS 11488 to run the program was called. The fix was to restore `MAX_MEM_0` to its documented cold-start value ($FF00) explicitly in `ml_entry_c128.asm`, which runs now before the game starts.
The 40-Character String That Moved the Walls
Once the game was running, most of it worked remarkably well. But there was a persistent visual glitch: the room graphic appeared three rows too low on C128 compared to C64, you could actually see the room drop down the screen. Not in some rooms — in all rooms, consistently.
The culprit was found at line 490. It printed three copies of `L$(0)` — a 40-character string of spaces — side by side with semicolons, relying on the C128’s screen editor not to automatically wrap to the next row each time a line filled exactly. The C64’s screen editor appeared to do this correctly, the C128 did not. A test using use C128 `SPC(40)` call instead worked fine. Direct POKEs to screen memory worked fine.
In the end I went for a pragmatic fix: replace the PRINT loop using Cursor Home, Cursor Down and these three blank L$(0) arrays of spaces, was replaced with direct screen-memory POKEs at line 490. No cursor involved. No screen editor to disagree with. This is the one code change I adopted to at this stage that was different from the C64 BASIC code. As we will come onto in a moment, I made some changes to accommodate a more convenient Save-To-Disk function rather than the Save-to-Tape function, but this was the only area where the code was changed to accommodate unexpected behavior.
Disk Save and Load: Six Weeks of Edge Cases
The save/load implementation took longer than any other single feature. The C64 original used tape — a small ML routine that called the KERNAL’s tape write and read functions directly. The C128 port I decided needed a more convinient Save-To-Disk.
The first approach was to try a machine-code routine calling KERNAL_SETLFS, KERNAL_SETNAM, and KERNAL_SAVE directly from ML. It assembled correctly. The parameters were traced through every register, confirmed correct all the way into the KERNAL’s own internal copies. And yet, every time it ran, the filename appeared corrupted in the disk directory. Not the data — the filename. `DUNGEON.SAV` became an illegible garble of characters, and the subsequent LOAD routine couldn’t find it.
The probable explanation, reached after exhausting other possibilities, involves the C128’s MMU and how the KERNAL’s IEC bit-banging routines access the filename buffer. When BASIC calls SETNAM internally, it passes a filename that lives in bank 1 (the variable/string storage bank). When the KERNAL’s IEC routines run in their own MMU context, they can correctly access that memory. When the ML code called SETNAM with a filename at $1134 in bank 0, the IEC routines read that address in their context and got something different. The filename was corrupted at the hardware serial transmission level, not at code level.
The solution after a bit of research was simply to re-use BASIC’s own `SAVE` command, which already knows how to do this correctly. On A C64, BASIC saves from TXTTAB to VARTAB. On C128, VARTAB points into bank 1, which has nothing to do with the program text. The actual pointer BASIC uses for the end of the save range is `TEXT_TOP` at $1210/$1211 (decimal addresses 4624/4625). Setting POKE 47/48 (VARTAB) did nothing useful for the save range, and actively broke the variable lookup. Setting POKE 4624/4625 (TEXT_TOP) worked correctly and this became the routine that made it to the final build.
712 T1=PEEK(45)+PEEK(46)*256:T3=PEEK(4624)+PEEK(4625)*256
713 POKE45,0:POKE46,11:POKE4624,0:POKE4625,15
714 SAVE"DUNGEON.SAV",8,1
715 POKE46,INT(T1/256):POKE45,T1 AND 255:POKE4625,INT(T3/256):POKE4624,T3 AND 255:PRINT
Loading the saved file then had its own surprise. BASIC’s `LOAD “file”,8,1` (secondary address 1, the binary-data form) — when called from within a running BASIC program — causes the C128 to restart the program from line 1.
The solution was to use a flag stored in absolute memory at address $1125 (outside the save/load data range at $0B00–$0EFF, and outside what any LOAD or CLR would touch). Set before the LOAD call, detected in lines 1001/1002 at the very start of the initialization subroutine:
1001 IF PEEK(4389)=1 THEN V=54296:AD=54277:...
1002 IF PEEK(4389)=1 THEN POKE4389,0:...:GOTO1150
If the flag is set, it means we’re in a post-LOAD restart, the game data is already in memory, and the initialization should be skipped in favour of going directly to state restoration. The title screen will re-appear after the load completes as GOSUB 971 runs again before lines 1001/1002 catch the flag, but the game resumes correctly, so this seemed a small price to pay,
The BASIC Loader Stub
With everything working, the final user experience was still two commands: `LOAD”GAME”,8,1` followed by `SYS 11488`. A small improvement to the code now was to add a final BASIC stub to enable to program to be simply RUN following load.
The C128’s default TXTTAB address on a cold boot is $1C01. The PRG file when loaded was spannig from $0B00 to the end of the BASIC program, which means it already covers $1C01 in its address range — that location had been zero-filled gap throughout the project up to this point. By placing a tiny, valid BASIC program there:
$1C01: 0D 1C 0A 00 9E 20 31 31 34 38 38 00 00 00
…which tokenizes to `10 SYS 11488`, the user experience becomes: `LOAD”GAME”,8,1` followed by `RUN`. BASIC finds the stub at its default TXTTAB, executes SYS 11488, and the ML entry stub takes it from there.
This also resolved a subtle bug that had been present throughout the project: BASIC’s own RELINK pass, which runs automatically as part of every LOAD, starts from TXTTAB ($1C01) and walks the program’s line structure looking for links to rewrite. Previously it found zeros and stopped immediately. When placing an ML routine at $1C00 (the original save/load location), RELINK walked into that machine code and corrupted it, which was the cause of a very confusing `SYNTAX ERROR` early in the save/load implementation. The stub gives RELINK a valid 1-line program to process, a correct link to the end-of-program marker, and a clean stop.
What I learned as part of this journey
- Verify everything empirically. Documentation for 40-year-old hardware is incomplete and sometimes wrong. The CLR address cited in one reference source was in the middle of a different instruction. The assumption that VARTAB defines the SAVE range was correct for C64 and wrong for C128. Every theory that mattered was settled by a memory dump or a single-step trace.
- ROM internals reconstruction is brittle. When replicating BASIC’s internal start-up sequence — calling CLR, RELINK, setting CURLIN, jumping into GONE’s dispatcher — nine distinct failure modes where found before abandoning this approach. Each fix revealed another gap. The keyboard-buffer injection approach bypassed all of it by using the one code path that provably worked: the same one the user used when they typed RUN.
- The difference between platforms is often in the edge cases. BASIC SAVE on C128 uses TEXT_TOP, not VARTAB. LOAD SA=1 from within a program restarts it. String variable auto-wrap at exactly 40 characters behaves differently from SPC(40). None of these differences are advertised. Each was found by observing unexpected behaviour and tracing it to its source.
- The tools are the project. VICE’s monitor — particularly the watchpoint and single-step commands — was the single most valuable tool in the whole project. `watch 1c0d` catching the RELINK corruption. `watch 1212` finding the MAX_MEM_0 zeroing. `d 4f4f 4f90` revealing what RELINK actually does. Almost nothing in this project would have been found without the ability to stop the CPU mid-instruction and read the state.
Doing this project on a modern development platform with lots of modern development tools while often frustrating in hitting some of the problems above, could only have been a real nightmare of a journey if I had attempted this back in the early 1990’s when I had my original C128 hardware. It gives me a great appreciation for how far technology has developed over the years to make these things possible today.
What next?
The port is done. The Dungeons now runs natively on the Commodore 128 for maybe the first time: title screen, character creation, dungeon navigation, combat, SID sound, sprites, custom character set, and disk save/load across sessions. Almost 100% of the same 471 lines of BASIC that ran on a C64 in 1983 now run on its emulated successor, some forty-odd years later.
Now to tidy up the build folders and upload this to GitHub and prepare for the next phase.
It turns out porting a 1983 BASIC game to a 1985 computer was not a simple task. But it is a satisfying one to have completed and now enables me to delve back into BASIC 7 and (re)learn how the additional features and functions in this improved BASIC can help modernise this fondly remembered early D&D influenced game.
Resources
Mapping the Commodore 128 by Sheldon Cowper (COMPUTE! Publications, 1986) — the foundational reference for the C128’s memory map, zero-page layout, ROM entry points, and hardware register shadows. Cross-checked empirically throughout but essential as a starting point.
x128 and x64sc from the VICE (Versatile Commodore Emulator) — open-source emulator providing cycle-accurate C128 emulation including the full built-in machine code monitor [vice-emu.sourceforge.io](https://vice-emu.sourceforge.io/)
ACME cross-assembler the assembler used throughout the project.
VS64 – The C64 Development Environment by Roland Shacks, a truly wonderful extension to make development far more of a joy.
Commodore 128 Programmer’s Reference Guide** (Commodore, 1986) — supplementary reference for BASIC 7.0 commands and the KERNAL jump table.
The Commodore 64 Programmer’s Reference Guide** (Commodore, 1982) — the equivalent C64 reference, useful for understanding which behaviours are shared and which have changed.
And of course with acknowledgements and thanks to the creators of The Dungeons (1983) – Brian and Marian Clark as it was the inspiration for the undertaking this project based on my fond memories of this game from back in the 80’s.
