posts | links

Code review: POWDER

2013-12-10 - 2024-09-05 | roguelike, code-review

This is migrated from the pre-blog roguereview project.

Notes of Jeff Lait's POWDER, version 117.

POWDER is a somewhat NetHack influenced game designed to be played on a GameBoy Advance.

Build system

POWDER has some data-driven architecture, and generates sources from text data files.

GameBoy Advance as a virtual machine architecture

Jeff Lait did a talk about porting POWDER in IRDC 2012. His basic approach is picking a sufficiently low-level and simple abstraction layer (a 'bedrock') to build the game on, and then port the bedrock to other platforms. The GBA architecture he originally build POWDER on ended up working as a passable general bottom layer he just wrote an adapter on for other platforms.

The features in GBA, graphics, the save game memory, the input buttons, are just memory mapped instead of being defined by an API. Lait thinks this worked well, from the presentation slides:

Bedrock is

  • Easy to write
  • Highly fault resistant – garbage in doesn't crash

Think of it as a hardware problem

  • Very simple interface
  • Memory model rather than API Allow partial updates!
  • Should not be affected by new game features! Should not occur to you to dive lower!

The GBA probably isn't the last word on a roguelike VM, but the more hardware design oriented approach does seem promising. The interactive fiction folk have seen massive success with virtual machine game interpreters starting from Infocom's offerings from the late 1970s.

Source code overview

Some source files are generated from data files.

  8688   23953  200077 powder117_src/action.cpp
  2491    7238   55792 powder117_src/ai.cpp
   462    1339   11293 powder117_src/artifact.cpp
    80     256    1751 powder117_src/assert.cpp
   617    1877   13496 powder117_src/bmp.cpp
   491    1020    8096 powder117_src/buf.cpp
  3479   10543   72977 powder117_src/build.cpp
   312     757    5860 powder117_src/control.cpp
  9915   27518  229938 powder117_src/creature.cpp
   167     848    6321 powder117_src/credits.cpp
    15     330    2000 powder117_src/dpdf_table.cpp
  5412   25591  212248 powder117_src/encyclopedia.cpp
   224     422    4331 powder117_src/encyc_support.cpp
  4379   12607   93379 powder117_src/gfxengine.cpp
 19748   25860  265630 powder117_src/glbdef.cpp
   890    2750   18963 powder117_src/grammar.cpp
   494    1185   10005 powder117_src/hiscore.cpp
   458    1269    9299 powder117_src/input.cpp
   187     404    3055 powder117_src/intrinsic.cpp
  4802   11225  105924 powder117_src/item.cpp
   166     385    2819 powder117_src/itemstack.cpp
    92     578    3912 powder117_src/license.cpp
  6687   16919  142522 powder117_src/main.cpp
  6250   17630  135402 powder117_src/map.cpp
   527    1355    9759 powder117_src/mobref.cpp
   688    2038   14374 powder117_src/msg.cpp
   253     522    4037 powder117_src/name.cpp
  1609    4037   34310 powder117_src/piety.cpp
   352     809    5432 powder117_src/ptrlist.cpp
   702    2796   15683 powder117_src/rand.cpp
   126     285    1996 powder117_src/signpost.cpp
   264     556    4169 powder117_src/smokestack.cpp
   165     449    3117 powder117_src/speed.cpp
   889    2302   16276 powder117_src/sramstream.cpp
  1083    3027   20932 powder117_src/stylus.cpp
   199     323    3061 powder117_src/thread.cpp
   102     156    1728 powder117_src/thread_linux.cpp
    96     166    1654 powder117_src/thread_win.cpp
   833    2350   17603 powder117_src/victory.cpp
    69     223    1585 powder117_src/artifact.h
    52     159    1122 powder117_src/assert.h
    32     130     777 powder117_src/bmp.h
   107     396    2789 powder117_src/buf.h
    73     270    1828 powder117_src/control.h
  1088    5518   39792 powder117_src/creature.h
    11      48     340 powder117_src/dpdf_table.h
    24      52     538 powder117_src/encyclopedia.h
    41     147    1027 powder117_src/encyc_support.h
   437    2047   14103 powder117_src/gfxengine.h
  3054    4832   62620 powder117_src/glbdef.h
   145     476    3430 powder117_src/grammar.h
    82     258    1696 powder117_src/hiscore.h
    31     158     896 powder117_src/input.h
    52     147    1248 powder117_src/intrinsic.h
   437    2026   15532 powder117_src/item.h
    66     184    1361 powder117_src/itemstack.h
   777    3960   27202 powder117_src/map.h
    83     345    2242 powder117_src/mobref.h
    70     322    2076 powder117_src/msg.h
    59     193    1342 powder117_src/name.h
    38     129     853 powder117_src/piety.h
    96     309    2188 powder117_src/ptrlist.h
    95     238    1743 powder117_src/queue.h
   124     549    3451 powder117_src/rand.h
    65     175    1316 powder117_src/signpost.h
    89     322    2253 powder117_src/smokestack.h
    65     250    1668 powder117_src/speed.h
   100     355    2790 powder117_src/sramstream.h
   132     576    3946 powder117_src/stylus.h
   248     623    4625 powder117_src/thread.h
    55     130    1095 powder117_src/thread_linux.h
    58     133    1057 powder117_src/thread_win.h
    45     169    1193 powder117_src/victory.h
 92394  239524 1980945 total

Skipping some utility and portability source in subdirectories.

Source code mixes tabs and spaces. Boo.

The blob classes

A big part of the roguelike logic in POWDER is stored in the methods of the ITEM, MAP and MOB classes. The class definitions are massive blobs of methods, and for maps and mobs, the implementation is spread to several files.

Having classes with a massive collection of methods is a recognized anti-pattern. However, roguelikes just plain do have creatures, items and maps that have a whole bunch of stuff going on for them.

Some alternative approaches that could be used would be to make the classes into much more like dumb objects, and just have the complex game rules in independent functions that take the objects as arguments. This is the suggested best practice in The C++ Programming Language 4ed: "Make a function a member only if it needs direct access to the representation of a class."

The D language has the uniform function call syntax for cases like this. It would do away with the problem in C++, where the representation-accessing member functions will have syntax a.foo(...) and the other functions would be bar(a, ...). UFCS just lets you say a.bar(...) if there's any function bar that takes a first parameter of a's type.

You could also use an entity component system and treat the game objects as collections of components handling different areas of responsibility (having a grammatical name, being an object on the game map, having an AI), but this can bring in its own architectural weight. Might not be a good idea on a GBA.

gfxengine.h, gfxengine.cpp

Because of its GBA target, POWDER uses a fixed 240x160 resolution, the same as the GBA display. This is quite different from most roguelikes, which are expected to run on desktop computers which used to have at least a 640x480 resolution for limited color modes even back in the late 1980s.

The gfx engine seems to also have some cruft informed by GBA's hardware limitations, such as the amount of in-memory tiles.

Not sure how much stuff there is here that a simpler SDL based graphics system wouldn't handle trivially. Printing colored text is one thing.

There are also GBA key defines here, so this thing might be some sort of more general GBA engine layer for the whole thing.

glbdef.h, glbdef.cpp

Generated C++ source with data for items, creatures etc. generated from the text data files with POWDER's custom preprocessor tools.

rand.h, rand.cpp

Random number generation management, and several utility functions.

There is some direction handling code here. First the random direction code for four and eight-directional compasses (POWDER has four-directional movement), but then there are also functions for changing a 4-direction or an 8-direction value (POWDER calls the directions in the 8-way compass 'angles' here as opposed to directions) into a dx, dy vector. This doesn't have anything to do with RNG anymore, but I guess it was being used with the random code.

There are functions for shuffling an array or picking from an array which are hardcoded to types instead of being templated, even though the source does use templates elsewhere.

There's also string hashing (again not RNG related) and generating random player name strings:

// Written in a beautiful provincial park in a nice cool June day.
// If this were traditional manuscript, it would smell of woodsmoke,
// but the curse of digital is the destruction of all sidebands
// of history.  Which I guess makes comments like this all the
// more important.
void
rand_name(char *text, int len)

Then a state machine between sets of letters classified as vowels, fricatives, plosives and 'weird' ("qwjx") follows.

The internal implementation is Mersenne Twister, the source includes the free software MT C file.

There's a global variable for the RNG state. There can be a stack of the states via an intrusive linked list in the RAND_STATE type for the state. (Class names are ALLCAPS in POWDER.) So you could have a bottom RNG that is used to create all the levels deterministically from the seed, and a higher-level RNG that can be called during gameplay and which won't mess the generation of the next level no matter how much entropy the player's actions consume.

There's a strangely complex rand_choice(num), which just samples [0, num[. Apparently the modulo operation is slow enough on GBA that common cases like 2, 8 and 100 need special optimization to make them work with bitwise operations and a static 1024-element look-up table in case of 100.

buf.h, buf.cpp

Ooh, here's the "oh no, we absolutely can't use std::vector or std::string, those filthy things" bit. Maybe std::containers really weren't an option on the GBA, but still. Those things are standard, they're good enough, and they have the standard interface. Don't make me worry about code and API that would just go away if you grabbed the standard containers.

(Okay, granted, there's sprintf stuff here, and C++ std has that horrible mess of idioms for catenating strings or printing to streams which nobody wants to use if they can have printf.)

ptrlist.h, ptrlist.cpp

Some code that's actually templated. Seems to be another container that could be pretty much replaced with a STL container.

queue.h

Another templated thing. This one has some thread-locking support too.

stylus.h, stylus.cpp

GUI code for a touch screen. Implements a STYLUSLOCK class that has lots of draggy and select-y logic in it. I guess this is a more recent addition to the game, the GBA didn't have a touchscreen, but Nintendo DS did. And touchscreen is all modern smartphones have.

grammar.h, grammar.cpp

Utilities for procedural messages. Stuff that makes non-English language localizers want to kill the developer, aka cool shit. I'm guessing POWDER will be able to derive messages like "You zap the orc with the wand." and "Siegfried zaps himself with the wand." from a single message template.

There's an enumeration for the different cases:

enum VERB_PERSON
{
  VERB_I,
  VERB_YOU,
  VERB_HE,
  VERB_SHE,
  VERB_IT,
  VERB_WE,
  VERB_YALL,
  VERB_HES,
  VERB_SHES,
  VERB_THEY,
  NUM_VERBS
};

Does the game ever actually use first person? The message narration is second person. Also, does it ever matter for the plural part which of the three 3rd person pronouns is used? I guess you can just shift the he/she/it value up by 5 instead of collapsing the three into one value...

There's a very hairy function for handling pluralizations. Might have been simpler to just do the '-s'/'-es' default thing and make it possible to specify more unusual plural forms in the name data. Also the suffix checking code uses reverse strings, probably in some GBA optimization again, but making things more unfun for the code maintainer.

There's also a function for determining if a string is a plural noun, which smells like something that would backfire on you horribly in actual use. (Seems to be used to check that things aren't being pluralized twice.)

The string handling code is quite C-like here, even though it does use the BUF objects.

Verbs are conjugated by person. This thing's just wired to special case all the weird stuff that occurs. I guess if you can control the text of your input data, it's neatest to just push the complexity inside the grammar functions, keep the data simple, and keep adding new exception cases as you get new strange words in the data the system doesn't handle.

artifact.h, artifact.

Random artifacts, where the artifact properties are derived from its name string.

Class is ARTIFACT, it doesn't inherit from anything.

The long artifact_buildartifact function (260 lines) creates the random artifact. There's determination of intrinsics, a name modifier and stat values. Artifacts have attack, thrown attack, armor and light radius stats. Intrinsics are stored in a char array. I guess each char corresponds to an intrinsic and the array is null-terminated. So this is something you'd want to use std::vector for? There's also "carry intrinsic", which I guess means that the artifact works just by being in your inventory instead of being equipped.

encyclopedia.h, encyclopedia.cpp, encyc_support.h, encyc_support.cpp

Some kind of help system? The encyclopedia.cpp is a generated source file of text descriptions of the various game elements.

Nice descriptions of the items. Though it's maybe a bit odd to have descriptions of game object separate from the main object data spec in the data files. Duplicating the item lists violates Don't Repeat Yourself.

There's a pager for the encyclopedia in the code. Nothing particularly interesting here otherwise, this is basically just another one of the ultra-lightweight hypertexty in-game help systems that are mostly decoupled from the rest of the game logic.

control.h, control.cpp

Mapping low-level input routines into queries of logical keypresses.

assert.h, assert.cpp

Assert macro. With an option to display the assert error graphically on screen if a printf isn't available.

input.h, input.cpp

String input. 500 lines of pain you could mostly just forget all about if you limited yourself to platforms that come with a text input device provided by the hardware or the operating system.

bmp.h, bmp.cpp

Parsing the BMP files the assets come in. There's already the bmp2c preprocessing tool for converting images directly into memory-mapped static data, so why is this even here?

msg.h, msg.cpp

API functions for the on-screen messages. Do the wait-for-more bit, a pager for the message history, and so on.

The raw text printing function is gfx_printtext(int x, int y, const char *text), which prints lines of text on the screen. The text buffer is persistent, you need to clear the text by printing blanks instead of just not drawing it on this frame.

There's code for splitting lines at spaces. This can get hairy, since the strings can also be built piecemeal from short segments.

There's also msg_askdir, which waits for user input on direction.

intrinsic.h, intrinsic.cpp

The intrinsics here, defined by bytes with the intrinsic sets being strings. The class is INTRINSIC, while the list is the generated enum INTRINSIC_NAMES. There's also a bit vector for the intrinsics, the member myData in INTRINSIC. This is the only data the class holds, so I guess it's what gets used during runtime. Some duplication of the bit vector addressing code in intrinsic.cpp.

mobref.h, mobref.cpp

Some kind of reference counting handle thing for monsters.

name.h, name.cpp

Some trickery to contain the few custom names in the game that mostly uses static names. Seems to be mostly memory use optimization, not seeing much here that you couldn't just do with std::string and lose some lines of code you need to maintain.

speed.h, speed.cpp

Here's Lait's discrete speed system. From the documenting comment:

*      Speed is INTENTIONALLY round based.  The goal is to allow
*      predictable movement, rather than the tendency to floating
*      point speed systems that have confused (IMO) later roguelikes.
*
*      As such, there are three speed modifiers: Fast (F), Quick (Q),
*      and Slow (S).  There are also 4 types of phases: Fast (F),
*      Quick (Q), Slow (S), and Normal (N).
*
*      doHeartbeat style functions execute only on N & S phases.
*
*      doAI movement operates on the phases according to the following
*      chart:
*
*          F N S Q N Speed
*      N     x x   x  100%
*      F   x x x   x  133%
*      Q     x x x x  133%
*      FQ  x x x x x  167%
*      S     x     x   67%
*      FS  x x     x  100%
*      QS    x   x x  100%
*      FQS x x   x x  133%

So you can have separate and (barring intricacies from turn order) equally good Fast and Quick modifiers, a combination of the two for twice the effect, and a separate slow modifier that slows you down whatever your other modifiers.

Speed uses some global variables for the ticks. Gamestate-relevant stuff in loose global variables instead of tucked within a single gamestate structure makes me uneasy. You'll need to remember to store all those individual bits in your save game code. There are indeed speed_save and speed_load functions here, using SRAMSTREAM class. With code mostly identical, this could have used the symmetric serialization code idiom where a single function can do both saving and loading depending on the parameters you call it with.

The speed module doesn't do anything interesting beyond implementing the phase logic, like maintaining a list of live game entities and calling their update methods whenever the tick phase matching their speed profile comes up.

sramstream.h, sramstream.cpp

Streaming save files. GBA implements save memory as memory-mapped non-volatile RAM. Basically just filestream style read and write operations here, plus some compression code.

smokestack.h, smokestack.cpp

Data structures for gas clouds on the map. Gases get a mob that owns them, possibly so that the game can track kills by the gas to whoever caused the gas to be there. I think this basically comes down to a function from map positions to (gas type, owner mob) pairs.

signpost.h, signpost.cpp

Signposts are things on the map that print text, I guess. Repeated code from smokestack's SMOKEITEM in SIGNPOSTDATA for handling its positions on the map. The whole thing sorta looks like just a variant of the smokestack thing structurally, but with a slightly different payload type.

Of course there might be weird micro-optimizations at play here for the GBA-happiness of the whole.

itemstack.h, itemstack.cpp

Typedefs the PTRSTACK template container for items and mobs. Seems to be just a std::vector-ish container.

item.h, item.cpp

The ITEM class is the first of the three blob classes in POWDER. The data is minimizied, and items are stored in an invasive linked list. A big switching point is the myDefinition value, which points to the item type in the static item data. It's 8-bit only, so we're limited to 256 standard item types.

Items are created using the static create function, not the class constructor. The function gets the item type, and then generates lots of random properties appropriate for the type, like stack size for stacking items, possible curses, possible artifact status etc. Then there are helper functions like creating any random item, or creating a random item of a specific type, or an item from a table.

This place might be able to use some sort of distribution abstraction, and the whole system might be split off into some sort of factory system to make the actual item class have less fat.

The static data for items can get complex. For example there's the attack spec, which has the standard description verb, damage as an 3d6 style thing, to-hit bonus. Then there's ray behavior like whether it reflects or penetrates targets, intrinsic infliction and the chance to create explosions.

The data file source.txt is a version using nicer formatting that gets converted into static C++ code. You probably could set up a nice and type-safe data entry system even in actual C++, but it'd probably be something that builds up the data structures at runtime. This would be a problem on the GBA which wants you to store as much as possible in a read-only memory.

The cycling method for game objects is doHeartBeat. It mostly does corpse decay for items.

Items can have transient statuses like enchantments or poison coating, and they can be canceled back into their neutral state with makeVanilla.

Corpse items can be resurrected into living mobs or raised as undead. Items can be petrified or unpetrified. Apparently there's the NetHack stone-to-flesh thing in play here that can be also used on stone that wasn't petrified flesh to begin with. Items can also be polymorphed into a different item of the same type, another thing lifted from NetHack.

There's logic for launchers. The thing with bows and arrows with one being mostly useless without the other and requiring equip juggling to use has been a thing since Hack at least, and Brogue did away with it by just having throwable projectiles like javelins, so you don't need to equip launchers so use them and you don't need to find pairs of items before you can fight.

Function calcAverageDamage is interesting. It gets the average from the damage dice, then basically just fudges bonuses from the extra features like elemental damage and poison. Looks like this is used by AI code, so it doesn't really matter if the numbers won't quite match with the actual average damages, as long as they form an ordering relation.

Then there's a battery of identification functions. You can know the type of an item (eg. healing potion instead of swirly potion), that the item isn't cursed (you have managed to unequip it), the full curse/bless status of the item, whether the item is enchanted, how many charges the item has left and whether the item is poisonous.

This is the sort of stuff people in the 70s wrote predicate languages for trying to build artificial intelligences.

The general pattern is that all query functions about the state of an item, and all functions that do stuff in game that mostly revolve around items are methods in the item class. This results the ugliness of the massive class definition, but works quite well otherwise. And there's good factorization of query and action concepts into named methods, so the code isn't full of repeated cryptic data structure jugglings.

Functions for changing item state, such as electrify, have some interesting bits in the signature:

bool electrify(int points, MOB *shocker, bool *interesting);

If the return value is true, the convention is to destroy the item. The interesting out parameter seems to be for generally signaling that some sort of notable effect took place.

Items define a stacking order with getStackOrder, which is used to determine what shows up on top of item stacks. Boulders on top of non-blocking items and so on.

Item stacks get their own cluster of methods. Again creating the feeling that ITEM is a large number of classes cobbled together.

Another long set for name formatting and generating event messages.

Then there are action methods for dipping and zapping items, whatever action as a specific thing means in this architecture. Action seems to be the way to call mob, or at least player to do stuff. I guess item actions are then item methods which use the same API convention. The dip has a switch for different potions you can dip a thing in. Likewise zap for wand types. Zap also has the giant switch statement pattern where the complex logic for each of the disjoint types of magical effect is written out in full under the branch, producing a huge function. Should just turn the branches into functions and keep the switch compact if possible.

Then there are callback functions for grenades and zapping. These are about the control returning to deal with a thrown thing or a zap hitting something after exiting from the initial throw/zap action method?

And that's the item. Data contents are a bunch of simple variables and an invasive list pointer to the next item. And a static mob reference variable ourZapper, which seems to be a kludge for keeping the original actor around over the call chasm between the action and callback methods.

map.h, map.cpp

Moving on to the even bigger blob classes. Class MAP is divided into two implementation files. map.cpp has the methods for a runtime map, and build.cpp has the map generation methods.

POWDER has persistent maps, so the individual map objects have an invasive doubly linked list. Maps also store a branch in addition to depth. Besides main, there seems to be only a "tridude" branch, so I guess this feature isn't used that much.

Maps have a bunch of static members for efficient structures for searching the current map. I guess these can be regenerated on the fly from the more persistent map data.

There can be gases on the map, and there is wind blowing in random direction and occasionally changing that blows the smoke around.

There's the pattern with the intrusive linked lists, where map methods deal with both individual maps and the collection of maps. There's a deleteAllMaps method for example.

Pathfinding is in findPath. POWDER has 32 tile map width and height. The 32 shows up as a magic number everywhere. Though it looks like the code's making use of the fact that you can use 32-bit ints as bit vectors when your map width is 32, so I guess changing the dimensions might be tricky.

And there are global bit vector arrays of 32 ints, so they'll span the whole 32x32 map. glbBlockingTiles, glbWalkTiles1, glbWalkTiles2. (These don't get a nice class wrapper, you access them with array access for the row and bit shift for the column. There's some bitwise magic being done on them in the findPath code.) The pathfinding algorithm is a 4-directional floodfill in a 32x32 bit matrix representing the path, masked with the glbBlockingTiles bit matrix representing unpathable tiles. The result is a single step in which to move. No support for storing the whole path in the API, so you'll end up recalculating the path for every step with naive use? (The two glbWalkTiles tables are used to tell if there was no path progress during an iteration and you should give up.)

The toplevel function for creating mobs and items for a level seems to be populate. Also creates the end boss for the bottom level. This uses the createAndPlaceNPC method that creates random level-appropriate mobs. A nice touch is the avoidfov parameter, which lets you make them spawn outside the player's immediate vision, so that you can surreptitiously repopulate a level while the player explores around.

There's stuff with a "guilty mob map" from tiles to mobs. I think this is for when a mob drops down hazardous environment like traps, so that the game can trace the claim from things killed by the traps to whoever laid them. Kill claiming logic gets quite interesting when you need to start chasing logic tails with it.

The problem with 2x2 mobs and 1 tile wide corridors is solved with wallCrush, where the big mob crushes destructible walls around wherever it wants to move. It looks like the mob is expected to already be in its new position here, tracked by its top left point, and the crushing just happens as an afterthought. Also I guess the mob will just squeeze into a tighter space if some of the walls are indestructible by some reason. The function doesn't give any return value for whether the crushing was successful.

Functions registerMob and unregisterMob work with the quick lookup tables for on-level mobs. Does the game actually need these for speed? Would be simpler just to iterate the mob list every time you need to see if you hit something. Logic in unregisterMob seems rather complex. The next getMob function even implements both methods, looking in the quick lookup table for the current level and scanning the mob list if the level is not current. And the difference must be checked from a global variable, brittle...

Interesting kitchen sink API in the random location finder:

bool MAP::findRandomLoc(
  int &ox, int &oy, int movetype, bool allowmob,
  bool doublesize, bool avoidfov, bool avoidfire,
  bool avoidacid, bool avoidnomob)

So you can have all sorts of clever conditions for placing the mob. Though at this level you'd probably just want to throw in some sort of predicate function as the filter instead of enumerating every condition separately. Also, there's support for exactly the 2x2 size big mobs, but no architecture for general multi-tile spanning things.

Then another "why's this in MAP again?", toplevel for running mob AI, moveNPCs. This walks the mob list and does a "false return aborts" condition chain of MOB::doHeartbeat, MOB::doMovePrequel and MOB::doAI on them. This chaining bit should probably be collapsed into a method on mob.

Map-level updates in MAP::doHeartbeat. This is where avatar smells, something that presumably lets monsters track the player around, is updated. Smells are 16-bit timestamps laid in the map, and if you know the current time, you can tell how old each trace is. Except when the variable overflows:

// We overflowed our watermark.  We clear our map and set it
// to the defualt distance of 64.  I wonder if anyone will notice
// that every 65k moves all creatures become confused?

Now this crippling exploit is revealed.

This method also does the occasional spawn of a new mob and runs heartbeats on items.

Complex method acquireItem. Took a moment to figure out just what this is about. Seems to be for the map tile "acquiring" an item from a mob that drops it, so basically just figuring out what happens with the cache structures and such when the item is dropped in the square. (Why not call it "drop" or something?) Also if there's some interesting liquid there, that will have an effect on the item. Also the item can merge in a pile, and the UI will treat valuable-looking items dropping with some extra messages. Boulders are items and they can fill pits, like in NetHack.

FOV in buildFOV. Nothing clever here. It just traces LOS to every cell to see if its visible. Also spreads avatar smell, diluted by taxicab distance, to every visible cell. Takes an awful many lines to do such simple logic.

There are several different LOS functions. hasLOS looks like something Bresenham-y, hasSlowLOS is your standard naive floating point op line tracing, hasDrunkLOS

// This LOS algorithm was written while rather drunk.

Lighting logic in updateLights that spreads light from emitting mobs and items.

There's a function isSquareInLockedRoom which is used in the NetHack situation where a map is generated where the only approach is through a secret door and a newbie who doesn't know about secret doors yet gets thoroughly frustrated to pop up a help message about a secret door being around. Instead of, y'know, just not generating maps where you need to find an unhinted secret door to progress to the next level.

throwItem is in map, because you throw the items over map I guess. There's graphics stuff mixed in here. Items can ricochet. Rays have a similar fireRay method.

There's knockbackMob doing what you'd expect. You don't see that much knockback in roguelikes.

Dig logic in digHole, digPit, digSquare. Dig code was pretty hairy in Hack already. You can dig sideways or down. Pits cause guilt markings on the map, so that the digger gets a kill if something dies falling in one. There doesn't seem to be any logic for the digging taking multiple turns. Can you just chop stuff up as you go? More terrain modification stuff for growing trees and messing with fluids.

Now there's a dropItems method. I guess this is why there's the different acquireItems. And fillSquare for boulders filling things. Another mechanic from NetHack. dropMobs is the logic for creatures falling in pits or through holes. Also seems to handle falling into liquids.

Fluid dynamics goes on in moveFluids, which calls applySuction.

// Update all map tiles with the following rules:
// 1) If a pit, and posseses neighbouring liquid, fill in.
// 2) If lava, and possesses neighbouring water, steam & dry out.
// 3) If ice, and neighbour is lava, turn to water.
// We don't want instant chain reactions, so the order of
// operation is important.

Lava can also start forest fires, which makes the tree grower guilty of the deaths caused.

// Because we propagated guilt already, we can use our own guilt
// for source.
// This system built at very high altitude likely over the Pacific
// (the map feature is broken on this plane, the only
// entertainment I like :<)
// Hopefully it holds together at sea level.

Not seeing a more general, Dwarf Fortress style fluid depth and spreading logic here. Broken viewport tiles also repair themselves here for some reason.

changeCurrentLevel has new level logics. Levels from 20 up have special messages.

And that's it for map basic logic. 6250 line file. Way big.

build.cpp

Map generation methods here. Starting point is MAP::build, which decides what kind of terrain to build based on depth.

// Decide what sort of map to build.  If the current level is 10
// we do the big room.
// Rough map:
// 0: Surface World.
// 1-5: Roguelikes.
// 6-7: Lit QIX.
// 8-9: Unlit QIX.
// 10: Big Room
// 11-14: Mazes
// 15: Cretan Minotaur
// 16-17: Lit cavern
// 18: Dark cavern
// 19-20: Dark water cavern
// 21: Dark cavern, Belweir
// 22: Quizar
// 23: Hruth
// 24: Klaskov
// 25: Circles of Hell.

There's some sort of stress test code under an if (0) here. Weird. After that, it's dispatch on depth to different map builder functions. Last thing the function does is place some random traps over the place for every level.

Rooms as a separate type show up here. They're rectangle-shaped areas, but have an explicit list of tiles, so I guess these are vault-type prefab things more than the classic roguelike mapgen where rectangular rooms just are a first-class map element instead of an emergent structure from the tile soup.

An example first generation function you call from build is MAP::buildRoguelike.

The first step is building random rooms until you've built 20 or can't fit any more (not fitting is defined as having failed to fit a random room 40 times). The rooms are kept in ROOM structs that are filled by buildRandomRoom function.

Also there's this bit (numroom is initially 0):

if (!numroom)
{
  buildRandomRoomFromDefinition(rooms[numroom],
    chooseRandomRoom(myDepth));
}
else
{
  buildRandomRoom(rooms[numroom]);
}

It doesn't seem like you'd need any special treatment when numroom is 0, so is it just "the first room built will be some sort of prefab"?

buildRandomRoom just sets up the room data structure into a rectangle that has a random size and position that make sense for the map bounds and the valid room sizes.

buildRandomRoomFromDefinition takes a prefab definition ROOM_DEF as an extra parameter, which locks the room dimensions. It gives it a rotation out of 8 possibilities, four directions and two mirrorings for each? Prefab rooms also get some extra border space compared to regular ones.

Each room is then checked with checkRoomFits. Factoring stuff like this into its own functions is pretty good in POWDER, a lot of the older code I've seen just smashes all logic together. The fitter checks that the room area is empty and that the room border doesn't have walls, as that would mean two rooms touching and you couldn't build a path between them.

The rooms are drawn on the map immediately after they are observed fitting. (It makes sense why you'd build the vault room first if you want to be sure it gets on the map, the first room should always get a pass on being valid since the rest of the map is empty.) Given that passing rooms are frozen in place, you can't do fancy search-algorithmy stuff like backtracking with this type of generation, where you could discard a whole branch of rooms-to-be before getting satisfied with the whole set and only then baking it in.

Next in the build function is logic for connecting the rooms. This part is neat. It tracks the connected rooms with just the single loop counter. Counting up i, it connects room i to a random room [0, i) at every cycle. So it grows the connected set upwards, and since the rooms are random, always going in the same direction doesn't matter.

The pathing is done by MAP::drawPath, which gets start and end positions and tries to connect them. A bit hard to follow the logic, but it's basically digging towards the end point and turning around if it hits map walls.

After the rooms and corridors are done, there's a tricky bit in closeUnusedExits. This only applies to prefab rooms. They have a fixed set of exits defined in the prefab, but only some of those are likely to have been connected in the previous phase. There's a verbose part in the function that basically walks through the edges of the room, recognizes exit tiles on the edge with no corridor beyond them, and replaces them with the type of the tile immediately preceding them along the edge. (It takes 50 lines of C++ to express what I just said in one sentence, some sort of clever geometric primitive vocabulary might help make the programs more concise here? Possibly also to make them utterly incomprehensible.) There's also option force to make the room have an exit if it has none. This looks like it should basically be a separate function.

Next bit is MAP::populateRoom. Early on there's this bit:

// If we are loading from disk, we've already populated the room
// from disk.
if (ourIsLoading)
  return;

Functions having to become different functions if global state is set some way seems a bit error-prone. Code after this creates mobs from a prefab room's mob specification. Rooms can have specific mobs in definition->moblist and a list of places to spawn generic mobs of a given level in definition->moblevellist. Also support for boss mobs using INTRINSIC_LEADER, and the other mobs are tamed to the leader if present. Not really sure what this entails, I suppose at least members of the same tame group won't infight. Then there's similar generation for items and signposts. This seems to only do the prefab room case, there's no spawning of completely random mobs for a generic room.

Interesting minor utility: MAP::smooth. This deletes turns floor tiles that have 3 non-floor neighbors into wall tiles, effectively wiping out single-tile dead ends from the map.

Some different mapgens then. There doesn't seem to be much abstraction for them, all are just writing the raw tile array basically.

Bit more interesting is MAP::buildSurfaceWorld, though I think the surface in POWDER isn't that big a part of the gameplay. The code seems to be doing some kind of heightmap generation that makes the map boundaries have max height so that they get mapped into impassable mountains, and then putting grass, water, grass, forest, hills, mountain based on ascending height on the map. Interesting trick having water between two grass layers, that gets you lakes I guess.

Some quest level stuff like a tridude lair. There's a quite complicated buildSpaceShip function. There's functions for drawing circle shapes on the map for this.

creature.h, creature.cpp

Class MOB is the last blob thing. These are the player avatar and the other creatures. The class definition in the header lists the huge number of methods these have. The implementation is split to three files, creature.cpp, action.cpp and ai.cpp. Around 1000 lines in creature.h and 10000 lines in creature.cpp.

A method that shows up a lot is isAvatar, which checks if the recipient is the player avatar mob.

Mobs are set up using MOB::create which takes a definition value as the parameter. The definition MOB_AVATAR causes the player to be created, and triggers player character specific setup. Starting classes get queried with piety_chosengod, which I guess refers to some global state. There's almost 400 lines of code inside the function specifying the starting inventories of the various player types.

Spawns are chosen by chooseNPC. The weighting describes some more complex weighting schemes, then settles to one where all mobs less than or equal to threat level have equal chance of spawning. This seems to pop up in several places as the nicely working version. Mobs one level up have .5 chance of spawning, two levels up have .1 chance and ones above have zero chance. Unless the RNG tells you to fuck the player 1 times out of 1000 for regular chars, 1 out of 100 for cultist chars.

MOB::makeUnique creates named uniques using a demon language syllable set and giving them random intrinsics.

Movement test for diagonal movement only allows a diagonal movement if the path is open along either orthogonal direction towards the diagonal.

Mobs can sense other mobs via sight, esp, hearing or "warning". Only mobs with a mind can be sensed with esp. Blind mobs sense themselves with esp even when they're not clairvoyant.

The reliably hypermassive main entity update method is MOB::doHeartBeat. This one's 1000 lines. All the status counters and changing statuses, tame pets going wild and so on are crammed into the single monster function.

I don't know what the solution to the giant entity update method antipattern is. I guess you'd want to write the processes that operate on the live entity as linear code, with some kind of way to annotate the turn breaks. Cooperative threads or something. Of course then you'd need a way to instantly clear all the threads when the mob dies and avoid any strange bugs from interactions of the several threads. The giant method does make execution order of the various logics explicit, which makes it easier to set up fixes for interactions.

Some of the logic is conditioned directly on mob's type, eg. MOB_FIREELEMENTAL instead of specific intrinsic flags.

MOB::validMovePhase implements Lait's slow - normal - quick - fast speed system. Method doMovePrequel calls it and tries to figure out various other reasons for a mob not to act on a given turn.

Big bunch of combat logic in MOB::receiveAttack. There's breaking autorun for the player, angering NPCs, a to-hit calculation that's full of pen&paper baroqueness.

Then there's reflex counterattacks, and checks against causing an infinite loop of counter-counterattacks.

A separate MOB::receiveDamage handles the actual hurting, damage reductions and the like. There's also death logic here. Creating corpses, messaging for death and so on.

Also there's message string format parsing here. Urgh, string parsing code in an application logic module always feels wrong to me.

There are strength/smarts checks where you autowin or autofail unless your relevant stat isn't within two points of the target level.

Equipment stuff is handled by MOB::rebuildAppearance which generates the composite player avatar sprite and MOB::rebuildWornIntrinsic which collects intrinsic bonuses from equipped inventory items. I guess these need to be called whenever equipment changes.

The zapping callback thing shows up again with MOB::zapCallbackStatic. This handles how mobs respond to spell effects. It also seems to have a general fireball explosion logic and similar environment effects that will trigger even if there wasn't a mob that got hit. Also, it's fun how there is forest fire logic everywhere around fire logic, even though as far as I understand the overworld with forests is basically some weird endgame Easter egg in POWDER.

action.cpp

While creature.cpp was about queries and effects done on a mob, action.cpp is about mobs doing stuff, also for the command functions which player input maps to.

MOB::actionBump does the smart movement. It attacks hostiles on the way, displaces friendlies on the way.

The general code is pretty straightforward here. Each verb gets a method, the method has logic for trying to execute the verb, generating messages from actions and so on. The player and the NPCs use the same code.

The NetHackiness is showing. There's actions for talking and jumping for example which are very marginal for the core gameplay. Also logic for falling down pits and climbing out of them, which isn't very interesting gameplay-wise, doesn't mesh that well with the 2D map vocabulary, but it was in Hack so it gets put here.

There's more pseudo-3D stuff, you can try to swim to surface if submerged in liquid. You can climb to trees in the endgame overworld. (Lait went utterly 2D in his later portaling engine used for 7DRLs, there weren't even stairs down anymore, just non-Euclidean paths winding down to different dungeon layers.)

MOB::actionAttack is a big piece of logic. An interesting piece is that the system needs to remember the previous movement direction of the mob so that it can trigger a charge ability if the mob is attacking in the same direction it was previously moving.

There's a loop for doing multiple attacks, and at loop start it needs to recheck whether the target mob is still alive and in place to receive blows, and whether the player has not died from counterattacks or other combat effects.

Artifact items can make extra attacks, head butts with helmets, shield slams, kicks with boots and so on.

A lot of this stuff could probably make do with NetHack style division into areas of concern like potions, food, movement that go into their own files.

Eating things has quite a complex logic. The effects for various potions seem to be here. Also, apparently it can be possible to eat spellbooks.

Reading has the logic for scrolls and spellbooks.

This is a bit iffy pattern, there's huge functions that have quite different actual behaviors under them, like eating foot, drinking a potion, or eating an item that grants intrinsics, or reading a scroll that produces temporary effects versus a spellbook that teaches a spell. The effects should probably be split up further into their own functions.

Biggest humongous method seems to be MOB::actionCast, which has the logic of every spell under a switch-case. The function's close to 3000 lines long. Refactoring this to functions for specific spells shouldn't be too complex. Also, might want to separate physical effects of a spell, like a bunch of acid flying about, from the casting of the spell, so that you could have other sources of acid explosion that produce the same end result in the game.

There aren't many purely data-driven spells though, like "fire blast", "ice blast" and "lightning blast" that would differ only on a damage type enum. Lait wrote somewhere about how each new spell should involve actual new nontrivial code to be worth adding. There are spell that seem to differ just in magnitude of effect though, like heal and major heal, which could be templatized with data. Maybe the system should just have been set up to support power levels for the spells to maintain the new spell - new logic rule.

Some more involved spells are force wall, which creates an expanding force field around the player avatar which pushes enemies away and rolling boulder which turns a wall into a boulder that can run over enemies and into pits.

There's polymorphing logic, this ought to be fun. Also there seems to be a Pokemon-style evolution mechanic here where creatures evolve into more powerful forms. There's also logic for a creature polymorphing into a multi-tile form and crushing surrounding walls.

In the petrifying mechanics, flesh golems turn to stone golems and regular trolls turn to rocky cave trolls, and vice versa for stone-to-flesh. (Shouldn't a stone golem just turn to inert meat since it was just a magically animated sculpted statue instead of a biological thing that was turned into stone? Not sure if you can try to stone-to-flesh walls and boulders like you can in NetHack here.)

ai.cpp

This is the last and shortest mob behavior module. Toplevel function is MOB::doAI. The basic logic is directed by a state machine state.

There's quite a bit of item-using cleverness here. Smart mobs with wands can use them both to buff themselves and to attack enemies. There's a long section where the AI searches for target points for a rolling boulder spell against whatever it wants to attack, which is quite neat.

This module is a bit better than the huge switch-cases elsewhere in code. Different AI actions like dousing self, stopping petrification and so on by and large do have separate methods.

victory.h, victory.cpp

Post-ending stuff. List of kills, victory screen.

piety.h, piety.cpp

Stuff that the gods do or don't like. Different behaviors (like zapping wands and eating corpses) and situations (like being surrounded) get piety methods called, and the piety is adjusted for all the gods all the time as far as I can tell.

There's also logic for gods granting boons or giving punishments here.

hiscore.h, hiscore.cpp

Score display and saving to permanent memory.

main.cpp

Another file that has probably seen a lot of accretion of stuff over time. There's some toplevel UI logic here. Also a wishing action function for whatever reason. Is this debug stuff or a thing that you can get in regular play? More UI stuff then, being a bit messy as UI is. Also there's some #ifdef cruft for SDL, Nintedo DS and iOS builds.

Toplevel game logic too, in processWorld.

The GUI seems to be pretty much hardcoded, without much any abstraction to it. Long, messy functions. processAction is a 1000-line function for processing ingame player input.