This is part of a series of tutorials; the main page can be found here.
The tutorial uses libtcod version 1.6.0 and above.
Finally, itâs time to bash some orc helmets into tiny metal pancakes! Combat is a big turning point in the development of any game: it separates the game from the tech demo. Although dungeon exploration is interesting, the simplest fight can be far more fun, and you may even find yourself playing your game more than you code it!
We have a big design decision ahead of us. Until now, the Object class was enough to hold the properties of both the player and the enemies. But as we develop further, many properties will make sense for one kind of Object, but not for another. How do we solve this?
Well, we could just pretend that this is not a problem, and use the data-driven approach, which is used by many of the older roguelikes. In this approach, all objects have the same set of properties, and only the values of these properties are modified from object to object. There isnât any feature variation between different objects. For example, there might be a property that determines whether a monster has a petrification attack, another for stealing items or gold, other properties for magical capabilitiesâŚ
As can be seen, it becomes quite tedious to implement a new property for all items every time you want to add a new feature to only one. The only way around this is to limit the number of properties, which in turn limits the number of special features that can be added. This is, of course, not good for us.
The other popular alternative is inheritance. You define a hierarchy of parent (a.k.a. base) and child (a.k.a. derived) classes. Child/derived classes (like Item or Monster), in addition to their own properties, receive the properties from their parent classes (such as Object). This reduces redundancy, and thereâs a seemingly clean separation between different classes.
However, the separation is not exactly clean, since the properties of parent classes are âpastedâ on the same space as the childâs properties; their properties can conflict if they share names. And thereâs also the temptation to define deep hierarchies of classes. As you develop further, your hierarchy will grow to extreme lengths (such as Object > Item > Equipment > Weapon > Melee weapon > Blunt weapon > Mace > Legendary Mace of Deep Hierarchies). Each level can add just a tiny bit of functionality over the last one.
The fact that a Mace canât be both a Weapon and a Magic Item due to the rigid hierarchy is a bummer. Shuffling classes and code around to achieve these simple tasks is common with inheritance. We want to be able to mix and match freely! Hence, we have inheritanceâs older, but often forgotten, cousin: composition. It has none of the disadvantages listed above.
Itâs dead simple: thereâs the Object class, and some component classes. A component class defines extra properties and methods for an Object that needs them. Then you just slap an instance of the component class as a property of the Object; it now âownsâ the component. It doesnât even require special functions or code! Letâs see how it works.
Our first component will be the Fighter. Any object that can fight or be attacked must have it. It holds hit points, maximum hit points (for healing), defense and attack power.
class Fighter:
#combat-related properties and methods (monster, player, NPC).
def __init__(self, hp, defense, power):
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
Itâll later be augmented with methods to attack and take damage.
Then thereâs the BasicMonster component, which holds basic AI routines. You can create other AI components (say, for ranged combat) and use them for some monsters. Weâll define a take_turn method; as long as a component defines this method, itâs a valid alternative to BasicMonster. For now it just prints a debug message:
class BasicMonster:
#AI for a basic monster.
def take_turn(self):
print 'The ' + self.owner.name + ' growls!'
Ignore the reference to self.owner â itâs just the Object instance that owns this component, and is initialized elsewhere. Weâll get to that in a moment. So how do we associate components with an Object? Itâs simple: create a Fighter instance, and/or a BasicMonster instance, and pass them as parameters when initializing the Object:
def __init__(self, x, y, char, name, color, blocks=False, fighter=None, ai=None):
Notice that all components are optional; they can be None if you donât want them. Then theyâre stored as properties of the object, for example with self.fighter = fighter. Also, since a component will often want to deal with its owner Object, it has to âknowâ who it is (for example, to get its position, or its name â as you noticed earlier, BasicMonsterâs take_turn method needs to know the objectâs name to display a proper message). So, in addition to holding the component, the Object will set the componentâs owner property to itself. The if lines just make sure this happens only if the component is actually defined.
self.fighter = fighter
if self.fighter: #let the fighter component know who owns it
self.fighter.owner = self
self.ai = ai
if self.ai: #let the AI component know who owns it
self.ai.owner = self
This may look a bit weird, but now we can follow these properties around to go from a component (self), to its owner object (self.owner), to a different one of its components (self.owner.ai), allowing us to do all sorts of funky stuff! Most other systems donât have this kind of flexibility for free. This is actually the most complicated code that composition needs; the rest will be pure game logic!
OK, now itâs time to decide on some stats for the monsters and the player! First up, the player. Just create a Fighter component with the stats you choose, and set it as the fighter parameter when creating the player object. Place it above the main loop:
#create object representing the player
fighter_component = Fighter(hp=30, defense=2, power=5)
player = Object(0, 0, '@', 'player', libtcod.white, blocks=True, fighter=fighter_component)
Here, I decided to use keyword arguments to make it clear what the different stats are (Fighter(30, 2, 5) is hard to interpret). Theyâre not necessary for the first few arguments of Object since you can easily deduce what they mean (name, color, etc). This is common sense for most people, but Iâll say it anyway: always try to strike a good balance between short and readable code; in places where you canât, pepper it with lots explanatory comments. It will make your code much easier to maintain!
Now, the monsters are defined in place_objects. Trolls will be obviously stronger than orcs. Monsters have two components: Fighter and BasicMonster.
#create an orc
fighter_component = Fighter(hp=10, defense=0, power=3)
ai_component = BasicMonster()
monster = Object(x, y, 'o', 'orc', libtcod.desaturated_green,
blocks=True, fighter=fighter_component, ai=ai_component)
else:
#create a troll
fighter_component = Fighter(hp=16, defense=1, power=4)
ai_component = BasicMonster()
monster = Object(x, y, 'T', 'troll', libtcod.darker_green,
blocks=True, fighter=fighter_component, ai=ai_component)
Keyword arguments come to the rescue again, since in the future most objects will have only a handful of all possible components. This way you can set only the ones you want, even if theyâre out-of-order!
Itâs time to make our monsters move and kick about! Itâs not really âartificial intelligenceâ, as these guys will be pretty thick. The rule for them is: if you see the player, chase him. Actually, weâll assume that the monster can see the player if its within the playerâs FOV.
Weâll create a chasing method (move_towards) in the Object class, which can be used to simplify all your AI functions. It has a bit of vector mathematics, but if youâre not into that you can use it without understanding how it works. Basically, we get a vector from the object to the target, then we normalize it so it has the same direction but has a length of exactly 1 tile, and then we round it so the resulting vector is integer (instead of fractional as usual - so dx and dy can only take the values 0, -1 or +1). The object then moves by this amount. Of course, you donât have to understand the math thoroughly in order to use it!
def move_towards(self, target_x, target_y):
#vector from this object to the target, and distance
dx = target_x - self.x
dy = target_y - self.y
distance = math.sqrt(dx ** 2 + dy ** 2)
#normalize it to length 1 (preserving direction), then round it and
#convert to integer so the movement is restricted to the map grid
dx = int(round(dx / distance))
dy = int(round(dy / distance))
self.move(dx, dy)
Another useful Object method returns the distance between two objects, using the common distance formula. You need import math at the top of the file in order to use the square root function.
def distance_to(self, other):
#return the distance to another object
dx = other.x - self.x
dy = other.y - self.y
return math.sqrt(dx ** 2 + dy ** 2)
As mentioned earlier, the behavior is simply âif you see the player, chase himâ. Hereâs the full code for the BasicMonster class that does it. The monster is only active if its within the playerâs FOV.
class BasicMonster:
#AI for a basic monster.
def take_turn(self):
#a basic monster takes its turn. If you can see it, it can see you
monster = self.owner
if libtcod.map_is_in_fov(fov_map, monster.x, monster.y):
#move towards player if far away
if monster.distance_to(player) >= 2:
monster.move_towards(player.x, player.y)
#close enough, attack! (if the player is still alive.)
elif player.fighter.hp > 0:
print 'The attack of the ' + monster.name + ' bounces off your shiny metal armor!'
Thatâs not terribly smart, but it gets the job done! You can, of course, improve it a lot; for now weâll just leave it like this and continue working on combat. The last thing is to call take_turn for any intelligent monsters from the main loop:
if game_state == 'playing' and player_action != 'didnt-take-turn':
for object in objects:
if object.ai:
object.ai.take_turn()
Ready to test! The annoying little buggers will now chase you and try to hit you.
The whole code is available here.
The quest for some epic medieval combat is coming to an end! We will now write the actual functions to attack and take damage, and replace those silly placeholders with the meaty stuff.
The âmeaty stuffâ is deliberately simple. This is so you can easily change it with your own damage system, whatever it may be. The Fighter class will have a method to take damage:
def take_damage(self, damage):
#apply damage if possible
if damage > 0:
self.hp -= damage
In the next section weâll modify it to also handle deaths. Then thereâs the method to attack another object:
def attack(self, target):
#a simple formula for attack damage
damage = self.power - target.fighter.defense
if damage > 0:
#make the target take some damage
print self.owner.name.capitalize() + ' attacks ' + target.name + ' for ' + str(damage) + ' hit points.'
target.fighter.take_damage(damage)
else:
print self.owner.name.capitalize() + ' attacks ' + target.name + ' but it has no effect!'
It calls the previous method in order to handle taking damage. We separated âattacksâ and âdamageâ because you might want an event, like poison or a trap, to directly damage an object by some amount, without going through the attack damage formula.
Now to give them some use. In the BasicMonsterâs take_turn method, replace the dummy print line for the monsterâs attack with:
monster.fighter.attack(player)
And the dummy print line for the playerâs attack, in player_move_or_attack, with:
player.fighter.attack(target)
Thatâs it, the player and the monsters can beat each other silly, but no-one will die. Weâll take this opportunity to print the playerâs HP so you can see it plummeting to negative values as the monsters attack you. This is how you make a simple GUI! At the end of the render_all function:
#show the player's stats
libtcod.console_set_default_foreground(con, libtcod.white)
libtcod.console_print_ex(con, 1, SCREEN_HEIGHT - 2, libtcod.BKGND_NONE, libtcod.LEFT,
'HP: ' + str(player.fighter.hp) + '/' + str(player.fighter.max_hp))
Of course, nobody can lose HP indefinitely. Weâll now code the inevitable demise of both the monsters and the player! This is handled by the Fighter class. Since different objects have different behaviors when killed, the Fighter class must know what function to call when the object dies. This is so that monsters leave corpses behind, the player loses the game, the end-level boss reveals the stairs to the next level, etc. This death_function is passed as a parameter when creating a Fighter instance.
def __init__(self, hp, defense, power, death_function=None):
self.death_function = death_function
It is then called by the take_damage method, in the event that the HP reaches 0:
#check for death. if there's a death function, call it
if self.hp <= 0:
function = self.death_function
if function is not None:
function(self.owner)
Now weâll define some fun death functions! They just change the object so it looks like a corpse, as well as printing some messages. The playerâs death also changes the game state, so he canât move or attack any more.
def player_death(player):
#the game ended!
global game_state
print 'You died!'
game_state = 'dead'
#for added effect, transform the player into a corpse!
player.char = '%'
player.color = libtcod.dark_red
def monster_death(monster):
#transform it into a nasty corpse! it doesn't block, can't be
#attacked and doesn't move
print monster.name.capitalize() + ' is dead!'
monster.char = '%'
monster.color = libtcod.dark_red
monster.blocks = False
monster.fighter = None
monster.ai = None
monster.name = 'remains of ' + monster.name
Notice that the monsterâs components were disabled, so it doesnât run any AI functions and can no longer be attacked.
To assign these behaviours to the player and monsters, pass the extra parameter death_function=monster_death when creating the monstersâ Fighter component, in place_objects; and also when creating the playerâs Fighter component before the main loop (death_function=player_death).
Weâll add a few details to polish it up. For the impatient, however: itâs ready to play now! You may notice some glitches though. In player_move_or_attack, we only want the player to attack objects that have a Fighter component. So change the if object.x == x ⌠line to:
if object.fighter and object.x == x and object.y == y:
Thereâs also currently the issue that, when the player walks over a corpse, sometimes itâs drawn over the player! Thereâs no guarantee that the player is the object that is drawn last. So we need to draw all other objects first, and only then the player. Just change the rendering loop in render_all to:
#draw all objects in the list, except the player. we want it to
#always appear over all other objects! so it's drawn later.
for object in objects:
if object != player:
object.draw()
player.draw()
The same thing also happens with monsters â a monster corpse being drawn over another monster. To fix it, create a method in the Object class that moves it to the start of the list, so itâs drawn first:
def send_to_back(self):
#make this object be drawn first, so all others appear above it if they're in the same tile.
global objects
objects.remove(self)
objects.insert(0, self)
And call it somewhere in the monster_death function:
monster.send_to_back()
Itâs finally ready to play, and it actually feels like a game! It was a long journey since we first printed the @ character, but weâve got random dungeons, FOV, exploration, enemies, AI, and a true combat system. You can now beat those pesky monsters into a pulp and walk over them! (Ugh!) See if you can finish off all of them before they do the same to you.
You may have noticed that this is fine and dandy for a turn-based game, but in a real-time game the monsters are just too fast! If that happens to be the case with your game, check out this Extra on real-time combat.
The whole code is available here.