A brief note about the code samples: My goal is to make it possible to get the gist of what I did, but not to provide a full tutorial. If something makes no sense, please tell me, but if something doesn’t run exactly as presented it’s probably because it uses other code that’s in the Github repo but that I didn’t find interesting or important enough to reproduce here.
Last week we looked at some tweaking and barely any functional changes. This week I looked at my giant list of features I was considering and implemented the most complicated one. It all started with an idea for a monster: the wraith. The overall “spec” is:
- It doesn’t attack normally; it attempts to “haunt” the player, destroying itself if it succeeds.
- It is incorporeal and needs to move through walls.
- It can follow the player even if the player breaks line of sight.
- It should deal a lot of damage (to be a threat despite attacking only once), but it should also not suddenly blow a player up with little warning. So the damage will be dealt over time.
- Being an intangible ghost, it probably shouldn’t have a visible corpse.
A New Monster
First things first, I needed to create a new monster. This is when I ran into a disagreement with the way the tutorial does some things. In the tutorial, the code that creates and defines a monster is all stuffed into the place_entities
function. Personally I prefer to have the monster definitions together, ideally separate from the level generation, so it’s easy to call on them whenever needed (imagine a scroll or monster ability that summons other monsters, for instance). So I ended up putting this together:
import tcod as libtcod import components.ai as ai from components.fighter import Fighter from entity import Entity from render_functions import RenderOrder class Monster(Entity): def __init__(self, x, y, char, color, name, blocks=True, render_order=RenderOrder.ACTOR): super().__init__(x, y, char, color, name, blocks, render_order) class Orc(Monster): def __init__(self, x, y): super().__init__(x, y, 'o', libtcod.desaturated_green, 'Orc', blocks=True, render_order=RenderOrder.ACTOR) Fighter(hp=20, defense=0, power=4, xp=35).add_to_entity(self) ai.BasicMonster().add_to_entity(self) class Troll(Monster): def __init__(self, x, y): super().__init__(x, y, 'T', libtcod.darker_green, 'Troll', blocks=True, render_order=RenderOrder.ACTOR) Fighter(hp=30, defense=2, power=8, xp=100).add_to_entity(self) ai.BasicMonster().add_to_entity(self)
(I think it’s possible I’m going a bit overboard with the OOP stuff, which is why I’m careful to state this as a preference rather than The Right Way)
With this in mind, our monster generation code now looks like this:
# imports now include "import map_objects.monsters as monsters" if not any([entity for entity in entities if entity.x == x and entity.y == y]): monster_choice = random_choice_from_dict(monster_chances) if monster_choice == 'orc': monster = monsters.Orc(x, y) else: monster = monsters.Troll(x, y) entities.append(monster)
Now let’s add the most trivial possible monster type: the same thing but with beefier stats:
class Balrog(Monster): def __init__(self, x, y): super().__init__(x, y, 'B', libtcod.dark_flame, 'Balrog', blocks=True, render_order=RenderOrder.ACTOR) Fighter(hp=45, defense=4, power=12, xp=250).add_to_entity(self) ai.BasicMonster().add_to_entity(self)
From there it’s a simple matter of updating the monster_chances
and above if
statement to add a third option in game_map.py
, which is easy enough.

A New Component
Given the “damages over time” aspect of the wraith, I wanted a way to represent a temporary status effect on the player. The existing inventory gives a decent template for it. Once I opened that door, I realized that I already had a second status effect I wanted to add. The tutorial game as presented is super easy, and a big part of that is that there’s too much healing. A strategy of “drink potions at 10 or lower HP, always pick Agility as the level bonus, and only fight monsters one at a time in hallways” will generally result in the player becoming invincible without ever seriously being in danger.
A gradual heal stops at least part of this problem by making it so the player can’t jump from 10 health to 50 in one turn. So I created a HealOverTime
status effect to add it to a potion:
from components.component import Component from game_messages import Message class StatusEffect(Component): def __init__(self, status_name, effect, duration): super().__init__('status_effect') self.status_name = status_name self.effect = effect self.duration = duration class StatusEffects(Component): def __init__(self): super().__init__('status_effects') self.active_statuses = {} def add_status(self, status): self.active_statuses[status.status_name] = status def process_statuses(self): results = [] to_delete = set() for name, status in self.active_statuses.items(): if status.duration == 0: to_delete.add(name) else: status.duration -= 1 results.extend(status.effect(self.owner)) for name in to_delete: del self.active_statuses[name] results.append({'message': Message("{0} wore off.".format(name))}) return results class HealOverTime(StatusEffect): def __init__(self, status_name, amount, duration): def effect(target): target.fighter.heal(amount) return [] super().__init__(status_name, effect, duration)
I decided I wouldn’t allow multiple effects by the same name; if there’s more than one at a time, the newest one “wins” and ends the older one early. This means you can’t drink 4 potions to heal four times as fast, and also prevents getting wrecked out of nowhere by multiple simultaneous wraiths.
Putting these effects in the game was surprisingly hard. Currently, the main turn loop works by repeatedly requesting the player’s input and then changing the game state based on what it is (where the game state can include things like looking at the character sheet or taking other actions that don’t eat a turn).
I can’t run the status effects in that loop or we end up with an exploit where the player can just open their character sheet over and over until the healing potion wears off. I also can’t just shove it in the enemy turn logic because that includes a loop over every game entity; it would mean that if there are four monsters, the potion would heal four times as each monster took its turn. I also didn’t want to wire something up where the monsters go before potion effects as it feels janky to have the player drink a potion, get hit by a monster, then start healing. So instead I ended up putting status effects in their own for
loop at the beginning of the enemy turn. (There was a bug here but I didn’t notice until implementing the wraith. I’ll come back to this.)
Now to try applying one of those status effects. First we’ll create a use function similar to the potion function already in the game:
def regenerate(*args, **kwargs): entity = args[0] name = kwargs.get('name') amount = kwargs.get('amount') duration = kwargs.get('duration') results = [] results.append({'consumed': True, 'message': Message('You feel a warmth pass over you.', libtcod.green)}) entity.status_effects.add_status(status_effects.HealOverTime(name, amount, duration)) return results
From there we just change the code that generates the healing potions in game_map.py
:
if item_choice == 'rejuvenation_potion': item = Entity(x, y, '!', libtcod.desaturated_blue, "Potion of Rejuvenation", render_order=RenderOrder.ITEM) Item(use_function=regenerate, amount=10, duration=4).add_to_entity(item)
Everything works exactly as intended, except the game crashes if you try to save.
Wait, What?
AttributeError: Can't pickle local object 'HealOverTime.__init__.<locals>.effect'
Oh, I get it. I’m glad I knew quite a bit of python before starting this tutorial. For those who didn’t: pickle
is the module that shelve
uses to save data. It’s complaining that it can’t save effect
, a function I defined in HealOverTime
‘s __init__
method. pickle
isn’t up to the task of saving an arbitrary function that was only created at runtime (effect
is defined in __init__
and doesn’t exist until __init__
actually runs).
There’s another library called dill
that may be up to the task, but instead I found an even easier option. You can define custom __setstate__
and __getstate__
methods for pickle
‘s benefit. So, how could we represent the state of a partially-completed heal-over-time effect as a Python dictionary? That’s actually pretty easy. Say a player drinks a potion, heals the first 10 hp of it, then exits. Then we want to save and quit the game. When the player reloads, we can just give them a new 3-turn heal over time effect at 10 points/turn. This is almost as easy to write in Python as it is in English:
def __getstate__(self): return {'status_name': self.status_name, 'amount': self.amount, 'duration': self.duration} def __setstate__(self, state): self.__init__(state['status_name'], state['amount'], state['duration'])
It turns out we also have to add self.amount = amount
to the initializer to make that work, but otherwise everything goes without a hitch.
This also gave me the opportunity to fix something else that was bugging me. The tutorial’s saving code didn’t actually work on my machine. I suspect, though do not know for sure, that the difference is OS-specific. While poking around the shelve
documentation to fix my crashing bug, I also tweaked the saving/loading code slightly so that it no longer cares about the exact filename used for saved games. I hope this version (in the github repo) is more portable.
Damage Over Time
Given what we’ve already done at this point, this is barely any effort.
class DamageOverTime(StatusEffect): def __init__(self, status_name, amount, duration): self.amount = amount def effect(target): # unlike healing, take_damage returns results return target.fighter.take_damage(amount) super().__init__(status_name, effect, duration) def __getstate__(self): return {'status_name': self.status_name, 'amount': self.amount, 'duration': self.duration} def __setstate__(self, state): self.__init__(state['status_name'], state['amount'], state['duration'])
Finally, the Wraith
The Wraith class itself isn’t much:
class Wraith(Monster): def __init__(self, x, y): super().__init__(x, y, 'w', libtcod.han, 'Wraith', blocks=False, render_order=RenderOrder.ACTOR) Fighter(hp=1, defense=0, power=0, xp=50).add_to_entity(self) ai.WraithMonster().add_to_entity(self)
The real work is in the AI:
class WraithMonster(Component): def __init__(self): super().__init__('ai') self.player_spotted = False def take_turn(self, target, fov_map, game_map, entities): results = [] monster = self.owner # Return without doing anything until it spots the player for the first time if not self.player_spotted and not libtcod.map_is_in_fov(fov_map, monster.x, monster.y): return results self.player_spotted = True self.owner.move_towards(target.x, target.y, game_map, entities, ignore_blocking=True) if monster.distance_to(target) == 0: results.append({'message': Message("The wraith has haunted you!")}) target.status_effects.add_status(status_effects.DamageOverTime('Haunted by Wraith', 5, 10)) results.extend(monster.fighter.take_damage(1)) return results
I’ll mostly skip the silly bugs on this one, though it was pretty funny when I didn’t think to have the wraiths wait before following the player. You’d sometimes start a floor and have three or four previously-unseen wraiths pop into the room all at the same time.

There is one bug worth discussing because it was in the original tutorial code. It turns out that if move_towards
tries to check the distance between two objects in the same location, it throws a ZeroDivisionError
. It turns out this same function was the easiest way to implement creatures that can walk through walls by adding a new parameter:
def move_towards(self, target_x, target_y, game_map, entities, ignore_blocking=False): dx = target_x - self.x dy = target_y - self.y distance = math.sqrt(dx ** 2 + dy ** 2) dx = int(round(dx / distance)) if distance != 0 else 0 dy = int(round(dy / distance)) if distance != 0 else 0 if ignore_blocking or not (game_map.is_blocked(self.x + dx, self.y + dy) or get_blocking_entities_at_location(entities, self.x + dx, self.y + dy)): self.move(dx, dy)
Now would be a good time to go back to that bug I talked about in the main game loop. It turns out that the tutorial version of the game only checks for a dead player when a monster attacks. This meant that going to zero HP from wraith damage let the player run around with a zero or negative HP total and continue playing the game.

I dislike the code I wrote to handle this. Enough that I’m not going to post it here (it’s in the Github repo if you really want to see it). I’m still deciding on what less-kludgy way I would prefer to do this.
Ghosts Don’t Leave Corpses
So, now we have wraiths that mostly work except they’re leaving dead ghost bodies. It turns out that the function that kills monsters also has all the corpse logic built in. Even worse, it doesn’t actually delete the monster, but replace its attributes with the corpse attributes. I decided that for now I’d settle for making dead wraiths “invisible” so they don’t render.
So first step, factor that stuff out and put it in the Monster
class:
class Monster(Entity): def __init__(self, x, y, char, color, name, blocks=True, render_order=RenderOrder.ACTOR): super().__init__(x, y, char, color, name, blocks, render_order) def set_corpse(self): self.char = '%' self.color = libtcod.dark_red self.blocks = False self.fighter = None self.ai = None self.name = 'remains of ' + self.name self.render_order = RenderOrder.CORPSE
Next step, immediately go back to Wraith
and override it:
def set_corpse(self): self.blocks = False self.fighter = None self.ai = None self.name = '' self.render_order = RenderOrder.INVISIBLE
And making RenderOrder.INVISIBLE
a thing just requires a couple small tweaks to the renderer:
class RenderOrder(Enum): INVISIBLE = auto() STAIRS = auto() CORPSE = auto() ITEM = auto() ACTOR = auto() # intervening code omitted # Draw all entities in the list visible_entities = [e for e in entities if e.render_order != RenderOrder.INVISIBLE] entities_in_render_order = sorted(visible_entities, key=lambda x: x.render_order.value)
Whew!
That was a lot of work for (mostly) one monster, but I’m really happy at the groundwork it laid for later things. I may want other creatures that can ignore walls, or leave different/no corpses, or other temporary statuses I can throw around, or spawn monsters outside of the level generation function.
In case you somehow missed it, I’ve put the whole thing on GitHub.
2 thoughts on “Roguelike Tutorial Week 3: The Wraith”