Decoded: Rogue

Rogue (1980), the original Roguelike

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

    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:

  • 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:

    Potential level layout in Rogue

    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:

    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
  • Aztec C ad from BYTE Magazine (August, 1983)