Wiki

Clone wiki

pyrel / CodeOverview

This page gives a high-level overview of Pyrel's codebase -- what goes where, how things generally interrelate.

First off, we have the following important items at the root level:

  • pyrel.py: The main script file; this is the entry point for the program. It parses commandline arguments, sets up the game, and initializes the UI.
  • data: This directory holds all of the content of the game -- definitions for items, monsters, etc. It serves a similar purpose as the "edit" directory in Angband.
  • things: This directory contains all of the engine logic for dealing with "stuff that exists in the game world", more or less. A Thing is simply an entity that can be placed into a Container. To be more specific, code for dealing with creatures, items, terrain, and the like all goes in here.
  • container.py: This file contains the specifications for the Container and ContainerMap classes, which hold Things. It also has a list of "types" of Things (Things that block movement, that can carry other Things, that are the player, etc.). Arguably this should be in the things directory.
  • mapgen: Naturally enough, code for generating levels is located here. This directory also contains the GameMap class, which is where every Thing in the game is stored.
  • gui: This directory contains all of the logic for displaying things to the user and processing input from them.
  • userCommand.py: This script is for handling interactions with the user -- each interaction is coded as a Command (e.g. "cast a spell" or "get an item") which specifies information Prompts that the user must fill out ("cast which spell?", "pick up which item?"). Once the Command has all the information it needs, it performs the appropriate mutations on the game state, if any.
  • effects: This directory is for bits of code that either mutate the game state, or modify logic flow. All of the code in this directory is referred to by something in the data directory -- thus, any "content code" belongs here. For example, the code that prevents a unique monster from being allocated twice is referred to in the "unique" creature template in data/creature_template.txt.
  • events.py: This module implements a simple publish/subscribe system. In brief, an object can subscribe to an event, which means it will be notified when that event occurs; other objects can publish events. For example, the GameMap publishes an event whenever a new level is generated, and the UI layers listen to that event so they know they have to redraw the entire screen.
  • util: Various miscellaneous functionality that doesn't belong anywhere else goes into this directory.
  • meta: This is for code that isn't part of normal program execution, but may be useful for development purposes (e.g. for parsing profiler output or generating code stats).

Things

Broadly speaking, a Thing is any object that has any degree of permanence in the game world. This includes creatures, items, terrain, traps, stores, et cetera. Each of these gets its own sub-directory in the things directory (e.g. things/creatures, things/items).

Things and Containers are heavily inter-related -- Things are placed in Containers and often have other Containers in them as well. In particular, there are a number of "fundamental" Containers that we use to track important attributes of Things. For example, there is a Container that all Item-type Things are placed into, another for all Things that can obstruct movement, a specific Container for each tile in the map, etc. All of these special Containers are stored in the GameMap. All of them except for the location-based Containers are named in container.py (e.g. container.ITEMS, container.TERRAIN, etc.).

Loading Things

Things have a common structure for how they get loaded from data. Each type of Thing has one or two files in the data directory for it: a base file and an optional template file (e.g. data/object.txt and data/object_template.txt). These files use the JSON data-storage format, which is a nice compromise between machine- and human-readability.

Each Thing has a corresponding Loader module (e.g. things/items/itemLoader.py) and Factory (things/items/itemFactory.py). The Loader's job is to parse the data file, and convert each entry in it into a Factory. The Factory can be thought of as a "master record" for that type of Thing, and is responsible for creating the actual Things that are used in the game. For example, itemLoader.py loads the object.txt file, which contains an entry for every item type in the game. It creates an ItemFactory for the Dagger item type. Then later, when we want to create a Dagger, we ask itemLoader.py for the relevant ItemFactory, and tell it to make a Dagger for us.

Mixins

We have a number of mixin classes that we can attach to other Things. These are located in the things/mixins directory. If you aren't familiar with the concept of mixins, it may help to think of them as being like multiple inheritance (when a class derives from multiple parent classes), except that the classes it inherits from are not necessarily complete objects in their own right. For example, the Carrier mixin (things/mixins/carrier.py) allows a Thing to contain other Things -- it adds the "inventory" attribute, the addItemToInventory function, et cetera. If we want to make Terrain able to carry Things, then, we just modify the Terrain class definition to have it inherit from Carrier; no further modifications to the Terrain class itself are needed.

The GameMap

This class is defined in mapgen/gameMap.py. This class might be better-defined as "Game state storage" -- it holds every Thing in the game and is responsible for figuring out how they relate to each other. It holds the game map, naturally -- a large 2D array of cells, each of which is a Container that holds all the things at that location. It also holds all of the fundamental Containers.

Map generation

Naturally enough, mapgen/generator.py currently handles most of our level-generation code; the rest is in GameMap.makeLevel(). This is likely to be refactored at some point, since we'll want to allow for files in the data directory to influence level generation.

The GUI

Pyrel strives to maintain a good separation between the user interface and the game engine. Ideally, the engine has no idea how the game is actually being played -- in a terminal with a curses display, windowed with tiles, full-screen 3D with beautiful hi-poly models, or even over a network, it doesn't care.

Because the engine doesn't know how the UI is set up, it operates on "abstract" input in the form of Commands, which are defined in userCommand.py. Commands may require information from the user (for example, if you choose to drink a potion, then the game needs to know which potion you want to drink); this information is obtained in the form of Prompts. Once the Command has all the information it needs, it executes.

The actual command flow looks something like this:

  1. UI layer receives input from the player (e.g. a key is pressed).
  2. UI layer translates that input into a Command (see the various keymap.py modules for a mapping of keystrokes to Commands)
  3. The Command creates a Prompt, which it hands to the UI.
    1. UI displays Prompt to the user
    2. Input is sent to the UI, which uses the Prompt to process it (and make a decision)
    3. The result of the Prompt is handed back to the Command.
    4. Repeat until the Command has all of the information it needs.
  4. The Command is executed.

Most of the Prompts are defined in gui/base/prompt.py, which serves as a "basic user interface" that should be adaptable to any 2D tiled display.

Updated