Roguelike Tutorial: It’s Over!

Last time a funny thing happened. I had what I thought was a super hairy, not-realistically-going-to-happen-alongside-real-life-obligations problem that I then solved the next day over lunch. So now I can proudly say that I did what I set out to do.

Last time I had a couple of huge issues with the combat:

  • The player’s power level no longer increased well enough in the endgame
  • The player’s attack and defense stats were no longer similar even though the level up has you choosing between them

As it turns out, the second problem was fixed by discovering I had a bug; my new combat system had two offensive stats (one for hit-rate, and one for damage) and I was showing the wrong one. Fixing that meant that I could now have the player choosing to level up either hit-rate or armor class (in the d20 “gives a percentage chance monsters miss you” sense and not in the tutorial’s “broken damage mitigation that makes you invincible within a few experience levels” sense).

The first one was more interesting. It also gives me the last bit of useful code to share. I ended up deciding that the game needed better gear. Since this was literally the last minute, I decided that I would just keep the sword and shield from the regular game’s item table but tweak the bonuses. Before, the Sword class looked like this:

class Sword(Entity):
    def __init__(self, x, y):
        super().__init__(x, y, '/', libtcod.sky, 'Sword')
        Equippable(EquipmentSlots.MAIN_HAND, power_bonus=3).add_to_entity(self)

So first things first, we tweak it to allow an arbitrary power bonus:

class Sword(Entity):
    def __init__(self, x, y, bonus=3):
        super().__init__(x, y, '/', libtcod.sky, 'Sword (+{})'.format(bonus))
        Equippable(EquipmentSlots.MAIN_HAND, power_bonus=bonus).add_to_entity(self)

This is pretty straightforward, but it creates a non-obvious problem when you consider how my drop table previously worked.

One of the benefits of having all these item and monster classes is that it allowed the drop table logic to look like this:

# this gives us the probabilities
item_chances = {
    items.HealingPotion: 5,
    items.Sword: from_dungeon_level([[5, 4]], self.dungeon_level),
    items.Shield: from_dungeon_level([[15, 8]], self.dungeon_level),
    items.RejuvenationPotion: 35,
    items.LightningScroll: from_dungeon_level([[25, 4]], self.dungeon_level),
    items.FireballScroll: from_dungeon_level([[25, 6]], self.dungeon_level),
    items.ConfusionScroll: from_dungeon_level([[10, 2]], self.dungeon_level),
}

# this actually places the items
for _ in range(number_of_items):
    x = randint(room.x1 + 1, room.x2 - 1)
    y = randint(room.y1 + 1, room.y2 - 1)

    if not any([entity for entity in self.entities if entity.x == x and entity.y == y]):
        item_choice = random_choice_from_dict(item_chances)
        self.entities.append(item_choice(x, y))

Note that unlike the tutorial I’ve been working from, I’m using class names instead of strings as keys. This is because in Python, the class name can be used as a first-class value, and also called as a function to create the object in question. So my drop logic can just say “Whatever class we get from the randomizer, just drop one of those.”

The problem is that this doesn’t give us an easy way to create classes that need another parameter. It might look like I need to special-case equipment in the above loop somehow, but what I actually want is a way to create a function that says “make a sword where the bonus is some arbitrary value I decided earlier.” It turns out that Python gives a really easy way to do that:

from functools import partial

# intervening code omitted

 def sword_class(bonus):
    return partial(Sword, bonus=bonus)

If it’s not obvious what that does, these lines are basically equivalent:

partial(Sword, 3)(10, 30)

sword_class(3)(10, 30)

Sword(10, 30, bonus=3)

So this in turn means that I can just write sword_class(3) and get what used to be Sword, or even sword_class(x) where x is defined somewhere else, and then stuff that value in a drop table dictionary. After setting all that up, and nearly identical code for shields, I can now give the player a semi-random loot progression that allows for power gains on top of XP-levelling.

I’m not sure I love the balance but I do like it better than in the “oh god” state from last time. It shifts the game experience from hoarding consumables and trying to kill everything tactically to trying to find your way to the next floor while constantly keeping an eye out for better loot.

I was originally going to write about some of the ideas I had that never made it in, but then I realized that the effort needed to do that would be only slightly less than the effort needed to actually implement those ideas. Part of me really does want to try implementing some of the ideas I’ve had for the AI, but there’s another project I’ve had on the backburner for awhile and all of this coding for fun has made me really want to go work on that.

Thanks to everyone who read this far. It’s been a fun couple of months. If you’d like to see the Final(?) Version and an archive of previous posts about this game, you can now find the full archive here.

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