Skip to content

patricksurry/advent-forth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Colossal Cave Adventure for the 65C02

YOU ARE IN A MAZE OF TWISTY LITTLE PASSAGES, ALL ALIKE.

This is a TaliForth port of Crowther & Wood's original Colossal Cave Adventure (350 point version) in only 64K!

You can play on real hardware like my µ65c02 setup (pictured), adapt the code for your own hardware, or play using a 6502 simulator like c65 or py65mon.

How to play

Skip the hardware and play on a simulator. Two good options are py65mon which runs on python, and the C-based c65 which is much faster and supports external storage. The only I/O requirement is for memory-mapped putc and getc hooks.

First download the prebuilt 64K memory image from data/advent.rom and place it in an empty folder called adventure. Make sure you have the raw file which is exactly 65,536 bytes.

To play using c65 with game save and load follow these steps:

cd adventure        # wherever you put advent.rom

# (once) build the simulator using gmake and gcc
git clone https://github.com/patricksurry/c65.git
make -C c65

touch advent.blk    # create an empty block file for save/load

# play the game (omit -b advent.blk if you don't care to save)
c65/c65 -r advent.rom -m 0xffe0 -b advent.blk

To play using py65mon follow these steps:

cd adventure        # wherever you put advent.rom

# optionally create and activate a virtual environment
python3 -m venv venv
. ./venv/bin/activate

# (once) install the simulator
pip install py65

# play the game
py65mon -m 65c02 -r advent.rom -i ffe4 -o ffe1

Either way you should see the startup text rendered for a 40x16 screen. Note that text is paged if it would exceed the 16 row screen height. Just press a key to continue to the next page. There are many resources online to get started or get help if you're stuck.

               Welcome to                                               
                                                                        
        Colossal Cave Adventure!                                        
                                                                        
                                                                        
Original development by Willie Crowther.                                
   Major features added by Don Woods.                                   
  Conversion to BDS C by J. R. Jaeger.                                  
 Unix standardization by Jerry D. Pohl.                                 
 QNX 4 port, bug fixes by James Lummel.                                 
  Taliforth 6502 port by P. Surry from                                  
    code by J. Wiberg, J. Gillogly.                                     
                                                                        
Would you like instructions?                                            
> 

If you save or quit the game, you'll drop back into Forth where you can run Forth code, explore or modify the game itself, or restart with replay. Saving simply writes the game state to a fixed location on the current block device. To restore your game just start with the same block device connected and give the load command. If you want to keep several game states just make backup copies of your block file.

> quit                      
Do you really want to quit now?         
> y                                     
OK                                      
      Treasures: 0                      
       Survival: 30                     
          Score: 32                     
                                    
3 2 + . 5 ok
words
...
replay

About the port

I started from Wiberg's C-port and made some adaptations and fixes based on Gillogly's earlier C-port which contains more direct translations of the original(?) FORTRAN code. Other useful references were Raymond's Open Adventure and the Universal Adventure 350 Walkthrough.

The main challenge was hitting the 64K target: the original glorkz data file that drives the game is already 56K before adding any logic! Wiberg's port extracts most string data to advent*.txt which together weigh in at 47K but also leaves some data in the source files.

I created some scripts to extract and reorganize the cave description, connectivity and object data. This is compressed using some simple preprocessing followed by a recursive digram coding scheme. The compression ratio is about 50% which results in the 27K binary data file data/advent.dat. The two stage decompression was straightforward to implement in assembly, though I used a streaming approach to avoid the need for fixed size buffers in memory. See scripts/README.md for details.

Compression left only 37K for the Forth kernel plus the game's source code. I used the awesome TaliForth2 project for my kernel, but the vanilla build wants almost 24K of ROM. I configured a minimal build which is stripped down to just the basics using only 12K of my 16K ROM. This left just enough space for my kernel hardware drivers plus a few native routines to support the game (e.g. decompression) with only a couple of ROM pages to spare.

The remaining 21K RAM budget (48K less 27K of data) was tight but doable for the game code. The raw Forth source is nearly 64K of ascii text, but compacts to about 27K with some pre-processing to inline constants and strip comments and excess whitespace (see scripts/fpp.py). Unfortunately 27K is still too large to load both the source code and the game data at the same time. A little dance thus ensues to build the game image.

First the minified source is written to a block device image along with the binary data file. A tiny Forth bootloader reads the source into high memory and compiles it—overwriting the source as it goes—eventually using about 19K. The data file is then loaded above the code aligned to the end of RAM. This leaves the game ready to play with about 1K of free RAM. Finally the loader dumps a snapshot of the entire 64K memory image back to the block device to support standalone simulator play.

Hacking

There are a lot of moving parts here. We currently assume the hardware has 48K of RAM from $0-BFFF, one page of memory-mapped hardware IO from $C000-C0FF and just under 16K of ROM from $C100-FFFF. Both hardware and simulator support an external block device that reads and writes 1K blocks. This is required for building from source, but a prebuilt 64K memory image is enough to play in a simulator.

The micro-colossus repo implements a TaliForth2 platform called uc. To build it, download TaliForth2, then pull the platform code and build it:

# download Tali
git clone https://github.com/SamCoVT/TaliForth2.git
# download the optional uc platform
git submodule update platform/uc
# make the platform configured for the simulator
make uc CONFIG=c65

This generates a simulator-ready version of the Forth ROM in taliforth-uc.bin along with symbol files for debugging. The ROM kernel initializes the hardware, displays a splash message and starts Forth. The simulator code has slightly different I/O hooks but is otherwise almost identical to the hardware version for debugging purposes.

The uc platform's platform_forth.fs runs a turnkey routine on startup. This initially points to the block-boot word which attempts to boot from block zero of an SD card (or binary block file in the simulator). On successful read it verifies a magic byte pair and evaluates the remaining block content as Forth code. The boot code leaves a bootstrap word on the data stack that will continue the loading process after we discard the block content.

The loader for Colossal Cave is implemented in src/boot.fs. It loads and compiles the game source and data, and updates the turnkey word to the game entrypoint play so that a memory snapshot will boot directly into the prebuilt game. The loader depends on various constants calculated while preparing the game data and injected by scripts/advblk.py. The data itself is extracted and compressed by scripts/advextract.py and scripts/advpack.py using the compression routines in scripts/dizzy.py.

The word play is defined in src/advent.fs, built on words in the other files src/*.fs. These correspond closely to the original C files with as noted in the code. All of the Forth source is combined and minified to a single file data/advent_fpp.fs by scripts/fpp.py. This ensures that the source code is small enough to load and compile, and that the compiled code is compact enough to leave space for the compressed game data in data/advent.dat.

Almost all of the game is standard Forth with only a couple of words written in assembler to save space (see adventure.asm). The decode-link word could easily be recast in Forth. The main output word, typez, displays compressed game strings. It does decompression and word-wrapping on the fly to avoid large buffers (some game strings are very long). This is currently tangled up with the kernel's text display code. With more memory a much simpler implementation in Forth would be possible.

The game has been reasonably well tested, including automated playthroughs in tests/excursion*.txt.
These compare output between the C and Forth versions which identified a few bugs and omissions in the C code. For example the hint system was missing in C but added back in Forth. A couple of remaining issues are noted in TODO.md: pull requests are welcome.

Enjoy!

About

The complete Colossal Cave Adventure in 64K! (A Forth port for 65c02)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published