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:

Procedural generation as we knew it in 1980

x86 assembly using DOS and BIOS for low-level hardware services

A custom curses implementation for screen control

Ancient Aztec C compiler tools (no standard library!)

Early C cruft: (K&R style, preprocessor hell, no malloc ... sbrk!)

No best practices. Globals everywhere! No standard functions or header guards

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

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:

Remove last level data (layout, rooms, monsters, items, etc)

Create up to 9 rooms of sizes from 4x4 to 25x7 including border wall

Add possible gold and monsters to each room during creation

Connect rooms with passages

Add items to random rooms (and Amulet of Yendor if level 26)

Place the level exit stairs

Place traps with increasing probability based on level number

Add the hero to a random room at a random position

Putting it all together, the result is a (nearly) limitless combination of level designs that look something like this:

Rules for rooms

Cut screen in to thirds horizonally and vertically to define 9 areas

Each area may contain a single room sized from 4x4 to 25x7

The room may be positioned anywhere within its assigned area

Up to four rooms may be skipped. Could instead be passages or a maze

A room may be 'dark' with 10%/level probability starting at level 2

Half of rooms will have gold piles up to 50 coins plus 10 per level

A room may have a monster. 80% chance if there's gold, otherwise 20%

A 5% chance for a treasure room full of goodies...and baddies

Rules for passages

Passages connect rooms only to their horizonal or vertical neighbors

Doorways may be secret, increasing by level with max of 20%

Passages between rooms will turn at most twice to align doorways

Maze passageways may be drawn when no actual room exists in a space

Rules for monsters

Twenty-six monster types, one for each English letter

Each dungeon level expands potential monsters that could generate

Monsters may be in wander mode or static. Wanderers chase the player

Monsters have traits greedy (guard gold), mean (chases player)

Some monsters (Medusa, Xeroc) have unique and dangerous abilities

Monsters randomly start wandering as the player stays on the level

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:

/* 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; } 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:

All game data is stored as global and is accessible from any function

One level exists at a time as two arrays: The map and the map flags

Monsters are a global list, mlist. Objects are in the list lvl_obj

All objects on the map cache and replace the map tile as they move

Rogue has a boss key! Did those ever work?

This version is playable in a web browser courtesy of myabandonware