Roguelike Tutorial Week 2: Sanding Some Edges

(If you’re looking through the blog archives: no, there’s no post about Week 1. I didn’t blog about it because I was mostly just copying someone else’s code.)

Recently I discovered the roguelikedev subreddit was collectively inviting people to start a roguelike tutorial. I originally planned to follow along at the same pace, but then I ended up having enough fun that I blew through the entire thing in the first week. Things mostly went off without a hitch.

The part where I wrote a bug that let me pick myself up and put myself inside my own inventory was a little weird, but OTHER THAN THAT

The original tutorial is available online here. Much as I didn’t see much merit in copying all of that, I won’t be making much effort to explain things that were done over there. This post is also going to be scattershot in general as I didn’t really record changes as I made them (something I’ll likely change starting next week for as long as I keep doing this)

This week I haven’t really added any game mechanics as I’ve instead been busy polishing things, both the mechanics and the code.

First, Some Hackery

if show_load_error_message:
    message_box(con, 'No save game to load', 20, constants['screen_width'], constants['screen_height'] // 2)

Changed from:

message_box(con, 'No save game to load', 50, constants['screen_width'], constants['screen_height'])

I wanted to put this first to remind myself that the goal is to tweak things only to the degree that there is a concrete benefit as befitting a long-term game jam. As originally given, the tutorial code places this error message on top of another menu with an excessive trail of empty space. Fixing the empty space was just a matter of changing one parameter. Fixing the placement required either editing the renderer, or just lying to the renderer about how tall the screen is, which is ugly but does what I need it to do for now. I suitably admonished myself in the comments. Which is good because as I wrote this I realized that it’s now not obvious how to dismiss the message or that it needs to be dismissed (updates TODO list)

Title Screen Image

The tutorial linked at the beginning has some code that refers to a title background image. The code adding that image to the window is in there, but the image itself is not. It’s available here.

It occurs to me as I’m typing this how appropriate it would be to use a fantasy word dictionary to randomly generate the title.

Numpad Movement

The tutorial’s code includes arrow keys. But then it introduces diagonal movement via vim keys because not everyone has a numpad. That’s fine and all, but I have a numpad and I want to use it. Adding this was straightforward after consulting the key code list. In addition to using the eight directional keys, I mapped 5 to “wait” (as conceptually waiting can be a move to the place where you already are) while also mapping 0 to pickup an item, Enter to take stairs (since the other Enter key already does that), and . to open the inventory. I don’t think any of the other actions are common enough to warrant space on the numpad right now (although with 5 buttons remaining to work with I’ll at least think about adding some later)

Levels are for XP, not Dungeons

Order of the Stick made fun of this one in 2003. My preferred approach to this problem is to use level for the XP/levelling system and avoid it in all other contexts unless those other contexts are keyed to character levels somehow (e.g. if the player gained a level every time they descended a floor, suddenly “dungeon level” makes perfect sense). The most common synonyms for the other meaning of “level” are “stage”, or, in this case, “floor.” I’ve only changed it on the player-visible message for now as I’m not sure how much time I’ll want to spend renaming this stuff in code, especially when I’m also considering tweaks to monster generation.

Deprecation and Linting

It turns out that some deprecation warnings can’t be reasonably avoided here; tcod.event would require overhauling the entire input model all to use a model that is, in the documentation’s own words, “partially incomplete.” That didn’t stop me from trying, but the resulting dead end did make me glad I’m using version control. It turns out that the other warnings can generally be avoided by either using the Console class or adding some default parameters to a few function calls, which I did.

I also made a few small changes to get pylint off my case where possible. The most notable was turning dummy loop variables into _, which is an accepted convention for “there’s a variable here because there had to be one here but it’s not used for anything” recognized by pylint itself.

Character Sheet

character_sheet = ('Character Information',
    'Experience: {0}'.format(player.level.current_xp),
    'Experience to Level:{0}'.format(player.level.experience_to_next_level),
    'Maximum HP: {0}'.format(player.fighter.max_hp),
    'Attack: {0}'.format(player.fighter.power),
    'Defense: {0}'.format(player.fighter.defense))

for i, s in enumerate(character_sheet):
    libtcod.console_print_rect_ex(window, 0, i, character_screen_width, character_screen_height, libtcod.BKGND_NONE, libtcod.LEFT, s)

The character_screen function from the original tutorial is one of the few places where I changed the code solely to make it easier to write. This may form the backbone of a general “make menu from a list” type function that I may or may not need to write.

A Different Component Class

So above I wrote all that stuff about how I’d only change things for functional reasons. Then, over a lunch break, I end up rewriting the Component class just in case I add more components later even though I don’t currently have any feature ideas that would require a new component. Oops.

class Component:
    def __init__(self, name): = name

    def add_to_entity(self, entity):
        setattr(entity,, self)
        self.owner = entity

It’s probably not obvious why I did this. This is why:

    def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE):
        self.x = x
        self.y = y
        self.char = char
        self.color = color = name
        self.blocks = blocks
        self.render_order = render_order

That’s the entire __init__ method. Compare to the final tutorial version. It bothered me that adding any component to the system required going back in and adding more parameters to Entity. Instead of having to pass all the components in the constructor, you just use the add_to_entity method like so:

stairs_component = Stairs(self.dungeon_level + 1)
down_stairs = Entity(center_of_last_room_x, center_of_last_room_y, '>', libtcod.white, 'Stairs', render_order=RenderOrder.STAIRS)

The components themselves now inherit from the Component class like so:

class Stairs(Component):
    def __init__(self, floor):
        self.floor = floor

Most of the original Entity initialization code is now part of the components like this. Only Equippable required any real logic on top of that, and not by much. One problem I needed to address is that the tutorial code uses if statements to check if a component exists. With the classes written as above, such a check will crash the program. So I created one more Entity method that looks for a component and returns None if there isn’t one:

def try_component(self, name):
    return getattr(self, name, None)

The idea here being that if the original tutorial tested with if then we can now write if entity.try_component('ai'): to accomplish the same thing.

Maybe I’ll add races or (character) classes to the game to retroactively justify all this.

Public Repo

I think those are all my significant deviations from the tutorial code. If anyone reads this and finds any others I would be happy to explain what I did. I’ve posted the repo publicly on Github.

Leave a Reply

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

You are commenting using your 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