Roguelike Tutorial: The Beginning of the End

I’m mostly happy with where my little game has settled so far.

My favorite change since last time came when I finally got tired enough of accidentally fireballing myself to do something about it. Since the game already had a TARGETING game-state as well as a targeting_item variable in the main loop, it ended up being pretty easy to tell the renderer to change the color of things if a targeting item is active:

The fact that I’m the same red color means I’m going to burn myself if I don’t move the mouse.

The only real roadblock came when I thought to myself, “Eh, Fireball is the only targeting spell. I’ll special-case it for now,” only to be reminded that the ConfusionScroll is also targeted. I ended up just building targeting information into the items themselves.

Last week’s adventure with exits got me thinking about my end-goal for this project. I could tinker with this thing forever if I wanted to, but I don’t really want to. At the same time, the “group exercise/game jam” event has now passed its halfway point.

I decided it was time to add a win condition to the game. It needed to be something simple. So simple that it’s actually a minor homage to one of the first video games I ever played. So now the game looks like:

  • Starting chamber with an altar
  • Some number of randomized dungeon floors
  • Ending chamber with a chalice. “Use”ing the chalice while standing on the altar wins the game

The first step here is to code the new rooms themselves:

 class StartingFloor(DungeonFloor):
    def __init__(self, player, map_width, map_height):
        super().__init__(player, map_width, map_height, 0, name="Entry Chamber")

    def make_map(self, player):
        starting_room = Rect(self.width // 2, self.height // 2, 6, 10)
        center = starting_room.center()
        self.create_room(starting_room)

        player.x, player.y = center
        player.y += 3

        self.entities.append(exits.Altar(player.x, player.y))

        destination=(
            StandardFloor,
            (player, self.width, self.height, self.dungeon_level + 1),
            {'room_max_size': self.room_max_size, 'room_min_size': self.room_min_size, 'max_rooms': self.max_rooms, 'previous_floor': self})
        self.entities.append(exits.DownStairs(center[0], center[1]-3, destination))

class EndingFloor(DungeonFloor):
    def __init__(self, player, map_width, map_height, dungeon_level, **kwargs):
        super().__init__(player, map_width, map_height, dungeon_level, name="Hall of the Chalice", **kwargs)

    def make_map(self, player):
        ending_room = Rect(self.width // 2, self.height // 2, 6, 10)
        center = ending_room.center()
        self.create_room(ending_room)

        player.x, player.y = center
        player.y += 3

        self.entities.append(exits.UpStairs(player.x, player.y, self.previous_floor))
        self.entities.append(items.Chalice(center[0], center[1]-3))

Previously, the dungeon was just an infinite sequence of DungeonFloors. After renaming that class to StandardFloor, I now have StartingFloor -> some number of StandardFloor -> EndingFloor. This lays the groundwork for creating more variant floors later, but more importantly it gives me the fastest possible way to prove I can make this game end.

Amusingly enough, I found myself wanting to code the altar as an Exit, and then I realized that I don’t have any code that takes into account the possibility that a player can’t use an Exit (since stairs always work). Rather than overhaul half the game again, I just made the chalice item’s “use” ability manually check for the presence of the altar:

def rub_chalice(*args, **kwargs):
    results = []
    player = args[0]
    entities = kwargs.get('entities')
    for e in entities:
        if e.name == 'Altar of the Dark Magician' and e.x == player.x and e.y == player.y:
            results.append({'won_game': True})
            break
    else:
        results.append({'consumed': False, 'message': Message('You rub the chalice, but nothing happens. You need to return to the altar.')})

    return results

(EDIT 8/6/2019: The original posted version of this code was completely broken. At first I was going to explain all the bugs, but then I decided to just fix it.)

So, how do the StandardFloors know to stop replicating themselves and create the ending chamber? Through top-level constants and special casing:

if self.dungeon_level < DUNGEON_DEPTH:
    destination=(
        StandardFloor,
        (player, self.width, self.height, self.dungeon_level + 1),
        {'room_max_size': self.room_max_size, 'room_min_size': self.room_min_size, 'max_rooms': self.max_rooms, 'previous_floor': self})
else:
    destination=(
        EndingFloor,
        (player, self.width, self.height, self.dungeon_level + 1),
        {'previous_floor': self})
self.entities.append(exits.DownStairs(center_of_last_room_x, center_of_last_room_y, destination))

This works, although it is a bit clunky. I may revisit this in the coming weeks, but I have some other hurdles to overcome first.

How Do I Center Text?

This week has been a bit less productive than previously. It all started when I decided I needed a victory message; after all, what’s the point of winning a game if it can’t tell you that A Winner is You?

Aligning the text to the left makes it appear in this box correctly. Changing the alignment to centered creates this nonsense. Several days later, I still have no idea why.

The results have been disappointing, not just in terms of “I couldn’t get it to work” but I learned very little in the process. This may partly be because I haven’t found the answer yet; false starts often become a lot more obvious after finding the real answer. I still don’t know how to make tcod do what I want here.

Part of the problem is that nearly everything used in the tutorial is deprecated. I tried digging through documentation and even source code but the process often went:

  • I can’t figure out how to make X do something I want
  • The documentation or source says X is deprecated and to use Y or Z
  • Y is missing one critical feature or option from X
  • Z is missing a different yet also critical feature or option from X

It’s gotten to the point that my message popups just run the tutorial’s “menu” code but use the message as the header and includes no options. I even gave that its own convenience function:

 def message_box(con, header, width, screen_width, screen_height):
    menu(con, header, [], width, screen_width, screen_height) 

It’s always aligned left, even though the tcod functions involved include an alignment parameter, because centering it makes the text print half outside the box for some reason.

The rendering and menu code are probably the only parts of this game I don’t fully understand at this point. Oddly enough, those parts are also the parts that use tcod. I’m not about to swap libraries in the middle of a challenge run like this, but if I extend this project past the tutorial I’m giving serious thoughts to trying it with a different framework.

How Am I Going to Overhaul Combat?

I have a decent number of monsters, and I’m considering more. Right now the list looks like:

  • Orc (from the tutorial)
  • Troll (from the tutorial)
  • Balrog (giant block of stats that behaves exactly like the Orc and Troll)
  • Wraith (self-destructing monster that damages over time)
  • Snake (monster that attempts to poison the player and then flee)
  • Archer (attacks from a distance only, flees if the player gets too close)

The Archer turned out to be final straw with me and the current combat/leveling system. The first iteration of the archer had somewhat similar stats to the Orc. I found that it was interesting…at first. The problem was that as the player leveled up, they could just put points in Agility at which point the archers became free bags of XP right along with the orcs.

I actually added armor piercing as an ability just so Archers could have it before noticing that every single monster I’ve made has either ignored armor or caused problems with it:

  • The Balrog has an armor rating so high that a low level player outright can’t harm it. This fact is not obvious and in playtest runs I often can’t remember if I can hurt it without consulting my own source code.
  • The Wraith and Snake both have 0 power and use damage over time effects that bypass Armor entirely
  • The Archer was hard enough to balance around it that I gave him an ability to bypass it.

So, I’m tempted to overhaul the entire leveling and combat system. Which means that now I almost have to do that, because otherwise any monster, item, or mechanic I consider creating will have the cloud of this hypothetical system hanging over it.

What Am I Going to Do with My Floors?

Now that I have starting and ending chambers, it feels silly to have N identically randomly generated rooms in between. I have some ideas on how to mix things up, but the other issues above are holding me back.

One obvious way to differentiate floors is by changing the graphics. Even changing the color can make a difference in feel for a game this simple.

I should really figure out the UI code so I can change the look of individual floors. Or maybe I should overhaul combat so I can use enemy spawn rates to change the feel of individual floors.

How Do I Challenge the Player on the Way Up?

Currently, once the player gets the chalice the game is almost always boring. There are a few exceptions (every once in awhile, it may make sense to just dive down a staircase while monsters are chasing you instead of finishing them off, leaving yourself a fight on the way back up at a higher level) but for the most part it’s just an exercise in backtracking with no real danger. Unfortunately, once the character has leveled up on the way down the dungeon we’re back at the point where the leveling system breaks. We’re back to the point where I have to fix the base mechanics before I can reasonably balance this.


It’s really put me at an impasse for the time being. I have the UI changes I want but don’t know how to implement, and the combat changes I could probably implement if only I knew what they wanted to be. I guess I’ll see which breakthrough comes first.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s