Rogue! If you're reading this, then you know the significance. Rogue kicked off the 'Roguelike' genre that continues to inspire game designers today, with dreams of infinite adventure in a computer designed world. Procedural generation of game worlds started here!
My first run through Rogue was in the late 1980s, years after its release. I was using the DOS port since I didn't have access to a BSD system. I wasn't a dominant player - I think I won maybe three times. Anyway, it's this DOS version that we're going to dig in to in the name of code archaeology. Here are some of the learning highlights:
Rogue Source Code
I'm using the version 1.1 DOS port by Jon Lane from circa 1983/84. Most of the annotations appear to be from 1983 with a few mentions of 1984, particularly in the assembly code. All but four files are used in the final build. The key statistics:
Lines of C Code: 9,000
Lines of Assembly: 800
Number of functions: 300
Here are the 45 source files with links to my line-by-line code walkthroughs. If you're really interested in reading the entire walkthrough then please help me save bandwidth by downloading it compressed.
Bonus: A master list of all the functions used in Rogue
Source File | Purpose | Code |
---|---|---|
ARMOR.C | Armor equipping functions | (Code w/lines) (Code Walkthrough) |
BEGIN.ASM | Aztec C EXE loader. Sets up segments, environment, and arguments. | (Code w/lines) (Code Walkthrough) |
CHASE.C | Monster pursuit functions | (Code w/lines) (Code Walkthrough) |
COMMAND.C | Game parser and user input handler | (Code w/lines) (Code Walkthrough) |
CROOT.C | Load time argument handler and game launcher | (Code w/lines) (Code Walkthrough) |
CSAV.ASM | Checks for stack overflows | -- not used -- |
CURSES.C | Modifies curses library for Rogue | (Code w/lines) (Code Walkthrough) |
DAEMON.C | Background processes for timer-based events | (Code w/lines) (Code Walkthrough) |
DAEMONS.C | Specific effects and handlers used in Rogue | (Code w/lines) (Code Walkthrough) |
DOS.ASM | DOS interface routines | (Code w/lines) (Code Walkthrough) |
ENV.C | Runtime environment functions | -- not used -- |
EXTERN.C | Game global definitions, including monsters, player stats, etc | (Code w/lines) (Code Walkthrough) |
FAKEDOS.C | Boss key support! | (Code w/lines) (Code Walkthrough) |
FIGHT.C | Fighting routines | (Code w/lines) (Code Walkthrough) |
FIO.ASM | File I/O procedures for DOS | (Code w/lines) (Code Walkthrough) |
INIT.C | More global initialization | (Code w/lines) (Code Walkthrough) |
IO.C | Game I/O functions. Top bar parser and input handler | (Code w/lines) (Code Walkthrough) |
LIST.C | Linked list management | (Code w/lines) (Code Walkthrough) |
LOAD.C | Loads and displays pictures | (Code w/lines) (Code Walkthrough) |
MACH_DEP.C | Hardware-specific routines (#ifdefs yay!) | (Code w/lines) (Code Walkthrough) |
MAIN.C | The main entry, game loop, and exit points | (Code w/lines) (Code Walkthrough) |
MAZE.C | Maze creation routines (David Matuszek) | (Code w/lines) (Code Walkthrough) |
MISC.C | Kitchen sink game procedures. Lots of random stuff here | (Code w/lines) (Code Walkthrough) |
MONSTERS.C | Monster procedures | (Code w/lines) (Code Walkthrough) |
MOVE.C | Player movement procedures | (Code w/lines) (Code Walkthrough) |
NEW_LEVE.C | Procedural generation of levels! | (Code w/lines) (Code Walkthrough) |
OPROTEC.ASM | Low-level copyright protection functions? | -- not used -- |
PACK.C | Inventory management procedures | (Code w/lines) (Code Walkthrough) |
PASSAGES.C | Creating and drawing passages/hallways between rooms | (Code w/lines) (Code Walkthrough) |
POTIONS.C | Potion management procedures | (Code w/lines) (Code Walkthrough) |
PROTECT.C | Copy protection procedures | (Code w/lines) (Code Walkthrough) |
RINGS.C | Ring management procedures | (Code w/lines) (Code Walkthrough) |
RIP.C | End game stuff, hall of fame, etc | (Code w/lines) (Code Walkthrough) |
ROOMS.C | Room creation and runtime drawing procedures | (Code w/lines) (Code Walkthrough) |
SAVE.C | Saving and loading games | (Code w/lines) (Code Walkthrough) |
SBRK.ASM | Heap management procedures | (Code w/lines) (Code Walkthrough) |
SCROLLS.C | Scroll reading and effects | (Code w/lines) (Code Walkthrough) |
SLIME.C | Unique slime functions (dividing, etc) | (Code w/lines) (Code Walkthrough) |
STICKS.C | Wands and effects | (Code w/lines) (Code Walkthrough) |
STRINGS.C | Custom string functions | (Code w/lines) (Code Walkthrough) |
TEST.C | Unused entry point test | -- not used -- |
THINGS.C | Miscellaneous items management procedures | (Code w/lines) (Code Walkthrough) |
WEAPONS.C | Weapon usage procedures | (Code w/lines) (Code Walkthrough) |
WIZARD.C | Functions for wizard mode (debug/cheat mode) | (Code w/lines) (Code Walkthrough) |
ZOOM.ASM | Fast screen update procedures | (Code w/lines) (Code Walkthrough) |
Procedural Generation
The idea of endless adventure has kept Rogue alive for generations. But this is what 'endless adventure' meant in 1980.
Levels
The code for generating a new level lives in procedures from 5 files: NEW_LEVE.C, ROOMS.C, PASSAGES.C, MONSTERS.C and THINGS.C. Creating a level kicks off with a call from main() in to new_level(). Each new level follows this pattern:Putting it all together, the result is a (nearly) limitless combination of level designs that look something like this:
Rules for rooms
Rules for passages
Rules for monsters
Code Quirks
This code is old. There's a lot of preprocessor usage, especially for implementing basic data structures such as lists. Be ready to dig in to x86 assembly to understand how Rogue uses the BIOS and DOS services. The Manx Aztec C compiler provides basic functions but are no where near as robust as the standard library. It was still the programmer's job to get command line arguments in to the game. Let's take a closer look at all of this.
K&R C
Most of the code was written in the early 1980s, years before the ANSI C standard. So we see many things considered unusual today, such as function declarations that don't specify all arguments (they are assumed to be int). Then there are the definitions don't include type in the first line as shown below. The style isn't difficult, code reading is slower since programs just aren't written in this style today. A classic example is the difference between function definitions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* Standard C function */ int copy_data(char *src, char *dest, int len) { ...do work... return final_len; } /* K&R style as used in Rogue */ copy_data(src, dest, len) char *src; char *dest; int len; { ...do work... return final_len; } |
Both accomplish the same thing, but the K&R style might take a minute to adjust.
BIOS and DOS services using x86 assembly
Playing with interrupts directly in game code is almost unheard of today, but once upon a time it was the only standard interface available! DOS Rogue runs in real mode and makes full use of interrupts for screen, memory, and keyboard management. My guess is that the target OS was at least DOS 2.0, although 3.0 was available at the time.
All interrupts are invoked within the assembly files, often with a thin wrapper exported to C. The assembly in Rogue is clean and easy to understand (in my opinion). If you need a primer, the best way to pick up x86 assembly is to understand the thought process, read a lot of examples (like Rogue) and test if possible. Most assembly in Rogue focuses on reading arguments from the stack, setting up and invoking an interrupt, then shuffling the results to where they need to go. Below is a summary table of the interrupts used. For a full description of each, check out Ralf Brown's excellent interrupt reference library.
Interrupt | Number | Setup | Purpose |
---|---|---|---|
Set cursor position | 0x10 (BIOS) | AX=0x0200, DX=Row/Col | Moves the cursor to the position in DX |
Read character | 0x10 (BIOS) | AX=0x0800, BL=attribute | Reads character at the cursor position |
Write character | 0x10 (BIOS) | AX=0x09yy | Writes character code yy to cursor position |
Read y sectors | 0x13 (BIOS) | AX=0x02yy | Reads yy sectors from disk in to memory (ES) |
Get keyboard input | 0x16 (BIOS) | AX=0x0000 | Gets BIOS keycode in AH and ASCII code in AL |
Check for input | 0x16 (BIOS) | AX=0x01yy | Checks if there is keyboard input waiting. |
Check special keys | 0x16 (BIOS) | AX=0x02yy | Gets state of special keys (num/caps/alt) |
Output to STDIO | 0x21 (DOS) | AX=0x09yy, DS:DX=String | Prints string from DS:DX to standard out |
User-defined | 0x21 (DOS) | AX=0x2523 | Assigns interrupt 25. Handler in DS:DX |
Get system date | 0x21 (DOS) | AX=0x2ayy | Returns date. Year in CX. Month/Day in DX |
Get system time | 0x21 (DOS) | AX=0x2c00 | Hour/Min in CX, Second/Fraction in DX |
CTRL-BREAK State | 0x21 (DOS) | AX=0x3300 | Reads/changes extended break checking |
Open file | 0x21 (DOS) | AX=0x3dyy, DS:DX=Name | Opens a file with handle in AX |
Close file | 0x21 (DOS) | AX=0x3eyy, BX=handle | Closes a file |
Read file | 0x21 (DOS) | AX=0x3fyy, BX=handle | Reads from a file. Size in CX |
Write file | 0x21 (DOS) | AX=0x40yy | Writes to a file. Data in DS:DX |
Delete file | 0x21 (DOS) | AX=0x41yy, DS:DX=Name | Deletes a file |
Seek in file | 0x21 (DOS) | AX=0x42yy, BX=handle | Seeks to a point in a file |
Resize memory | 0x21 (DOS) | AX=0x4ayy | Reserves more memory -- used in loader |
Exit Application | 0x21 (DOS) | AX=0x4cyy | Exits Rogue with exit code in AL |
Preprocessor-fu
Today, preprocessor usage is light and predictable. But the 1980s was the wild west and the Rogue pp is no exception! We have almost 300 lines worth of #* and none of it includes header guards. This isn't a problem - all code in this program is self-contained and all includes go 'one way' to another header. No standard library or other external libraries means we're mostly safe from multiple inclusion. Not good for building scalable software...but good enough for making Rogue!
Not all is lost. Rogue also includes preprocessor macro functions that survive today, such as the canonical two-input MAX macro. The rule in Rogue seems to be: "If you can express it in a single line, it must be a macro". Apparently that also applies to the four-input variant:
#define MAX(a,b,c,d) (a>b?(a>c?(a>d?a:d):(c>d?c:d)):(b>c?(b>d?b:d):(c>d?c:d)))
Got that? But wait, there's more!
Changing keywords because...they look better?
Ever use switch? Usually there's some case and maybe a default to go with it. Not in Rogue - someone preferred using 'when' and 'otherwise' instead. Even worse: the usage isn't consistent. Sometimes there are 'cases' and 'whens' mixed together in the same block. I'm not sure if this was more readable in 1983, but my 2018 IDE is not amused.
Linked-lists as macros
Rogue includes a small linked-list implementation with the minimal set of functions you'd expect, such as attach and detach for adding elements to a list. Unfortunately, the functions aren't meant to be used directly - instead, we have a macro for attach() that uses the internal list function _attach. This is meant to hide the fact that the list is really a reference...I think? The bottom line is that all list interaction is done through macros. I'm glad that practice died on the vine.
Overriding functions with macros
This idea isn't completely useless, but it's certainly unnecessary in Rogue. For example...some code uses good old printf...the problem was that the compiler didn't include a printf so rather than change the code to 'printw' like the rest of the program, we get a macro for printf and the dead code was patched. I admit that I've used this trick when updating legacy code, when sometimes arguments change order over the span of decades. Not necessary in a program this small.
Aztec C
Aztec C proved very popular with DOS developers and it remained independent from the major corporate owned alternatives. By today's standards these tools are a novelty, but still worthy of attention for code archaeologist. Keep these issues in mind when reading the Rogue source:
No standard library
Enjoy programming in C without a library? If so, you must be a kernel development! But yes, Aztec C didn't bring many functions to the table. Rogue had to implement its very own 'sprintf' function! (IO.C). Although it's possibly adapted from compiler included code. Some familiar functions like 'memset' are actually called 'setmem'. Serious standardization was still 5 years away.
Manual program loading
Today we take for granted that C programs start running from main(). This isn't technically true but these details aren't important for application programmers. Setting up segments (in DOS), clear .bss, allocating stack, and putting command line arguments on the stack for main are some examples. Compilers and the OS kernel take care of that. Not true in the Aztec C days. The compiler comes with a basic loader but it's not full-featured and Rogue developers customized it in both CROOT.C and BEGIN.ASM.
Register keyword actually means something
Optimizing compilers were in their infancy in the early 1980s. (Dragon book, 1st ed!). Programmers were expected to police their code performance and keywords like 'register' were useful for telling compilers which values should stay alive for the duration of a stack frame. Rogue is full of this, especially in loop-intensive code blocks. The register keyword is still usable today, but default compiler optimizations take precedence.
Everything Else
Most of the heavy lifting on this project is buried in the code walkthrough at the top of this page. Here are some other useful tidbits to know before digging in: