Down in the Dungeons, Part 2

Bandits to the left of me, Goblins to the right. My Python CRPG engine can now randomly populate its dungeon rooms – but I may have gotten a little carried away!

After talking about my “Roguesque” game engine last week, I meant to move on to some other projects, but it’s been hard to stop tinkering with it. One of the advantages of using a language as expressive as Python, and working on the early prototype stages of a project like this, is that it’s really easy to pick small chunks of work to do. I’ve been picking away at it in half-hour or hour free spots in my schedule, and really enjoying the process.

For example, I’ve tried a couple different variations of implementing the “Fog of War” – in this case, hiding the parts of the dungeon that the character hasn’t visited yet. In NetHack, as you wander through passages, only the square you move into is revealed. When you enter a room, the entire rectangular room becomes visible at once. I started with a similar method, except I revealed a little more of the surrounding area in the passages. I implemented the fog of war using bitwise logic, since I hadn’t done any of that in Python. My map is a 2-dimensional list of integers, so I figured I could use the bottom 8 bits as a tile identifier (e.g. wall, floor, dirt, water) and the top 8 bits for extra information (1 bit for fog of war, maybe a few more to store the room number a square is in, and so on). I’m not limited to 16-bit numbers, of course, but I’m playing with ideas that I could transplant to other (8-bit?) systems.

I also tried a Fog of War technique that cleared a circular area with a 2-square radius around the character, because it gave me a chance to play with Python’s generators. Generators are a kind of coroutine (if you’re familiar with the CS jargon). A generator is a little bit like an iterator – it’s a function that, once called, can “yield” a series of values. For example, this generator returns the coordinates of squares in a 2-square circle around the (x,y) position passed to it, while filtering out values that are off the edge of the map:

Coroutines are an interesting kind of construct that don’t get a lot of attention because they’re hard to implement well in languages like C. This one can be used in a for loop like “for (x,y) in map.Adjacent2Squares(player.x,player.y):”. When Adjacent2Squares() is called, it runs until it hits the “yield” statement, and then it’s suspended. The next time through the for loop, execution of Adjacent2Squares() picks up exactly where it left off until the next yield, and so on.

While I was working on the Fog of War, and starting to think about how to automatically populate my dungeon with monsters and traps, I realized that my concept of rooms needed some refinement. My map-generation routine allows rectangular rooms to overlap, creating interesting shapes, but doesn’t keep track of that in a useful way.

Rooms are made of overlapping rectangles. This room has unexplored exits on the left and right.

I decided to redefine my rooms to be lists of rectangular areas. When building the map, each time I create a rectangle I check if it overlaps with any rectangle of any existing room. If it does, I add it to that room, and if not, I create a new room. With this data structure, I can quickly check if an (x,y) map coordinate is inside a room by checking if it’s inside any individual rectangle. I can select a reasonably random point within a room by randomly picking x and y coordinates inside a random rectangular area (yeah, that favors squares where areas overlap, but that’s not necessarily a bad thing).

To implement this stuff, I had to write a set of functions for “contains” and “overlaps”. I only realized afterwards that I could have saved a few lines of code by using PyGame’s Rect class – oops! I broke one of the cardinal rules of lazy programmers: always let your libraries do the heavy lifting!

Now that my map structure had a better understanding of how its rooms were shaped, I was able to look into randomly populating those rooms. I wanted to do this in a “data-driven” way. For example, all the information about different kinds of monsters is currently stored in a Python dictionary:

You can read that as “a goblin room contains between 1 and 5 goblins and sometimes 1 troll, while a bandit room contains between 2 and 6 bandits”. To populate a dungeon level, we just select a type for each room, and then fill in the contents according to the list. This could be expanded to include furnishings, traps, and so on. This is a great example of how easy Python makes it to express these kinds of ideas.

Of course, in a “real” game engine, those tables wouldn’t be hardcoded – they’d be read out of a file or database. Well, maybe I’ll get to that later.

Speaking of objects, I’ve been thinking about how to represent all the stuff you might find down in a dungeon (monsters being just one example). There are lots of ways to do this. I’ve tried big class hierarchies and different kinds of object composition in C++ before. Python gurus seem to be fond of duck-typing solutions, so that’s where I’m going. For example, anything with hit points and an armor class can be attacked, anything with a “climb” method can be climbed, and so on. I’m playing with ways of implementing that, and using Python’s reflection capabilities to keep it all as data-driven as possible. I may write another post on that topic when it’s more fully developed.

Right now, though, it’s time to take a break from writing games to play some games. I’ve been slowly trying to rebuild the collection of computer games I had as a kid, and the latest one just arrived courtesy of eBay. Questron II wasn’t exactly ground-breaking, but I remember it being fun. We’ll have to see how it holds up.

Amiga version. Hmm….

After a hard day’s coding, it’s time to kick back with some retro-gaming. I just need to figure out how to run this thing…