Difference between revisions of "Complete Roguelike Tutorial, using python+libtcod, part 11"

From RogueBasin
Jump to navigation Jump to search
 
(created first 2 sections)
Line 1: Line 1:
#REDIRECT [[Complete Roguelike Tutorial, using python+libtcod, extras scrolling code]]
<center><table border="0" cellpadding="10" cellspacing="0" style="background:#F0E68C"><tr><td><center>
This is part of a series of tutorials; the main page can be found [[Complete Roguelike Tutorial, using python+libtcod|here]].
</center></td></tr></table></center>
 
 
__TOC__
 
 
<center><h1>'''Character and dungeon progression'''</h1></center>
 
 
== Second floor please ==
 
We're approaching the point where we have a complete game really fast. A big step will be taken in this part, where we will focus on progression! It will deal with changing dungeon levels, advancing the character and acquiring skills, and finally varying the monsters and items with the dungeon level.
 
We'll cover a lot of ground, but don't worry, it's all familiar territory! There aren't a lot of new concepts involved, since we will reuse many functions we developed before.
 
A staple of roguelikes is the ''stairs'', which the player must find to advance to the next dungeon level. We will start by placing them, when generating a level. Right at the end of ''make_map'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    #create stairs at the center of the last room
    stairs = Object(new_x, new_y, '<', 'stairs', libtcod.white)
    objects.append(stairs)</syntaxhighlight></div>
 
 
As you can see, it's just a regular object! To identify it in other functions, we'll make it global, so add ''stairs'' to the globals list at the top of ''make_map''.
 
We must now let the player go down the stairs when standing on them and presses the ''' '<' ''' key (or ''' '>' ''' key if you prefer). It's easy to add this check at the end of ''handle_keys'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">            if key_char == '<':
                #go down stairs, if the player is on them
                if stairs.x == player.x and stairs.y == player.y:
                    next_level()</syntaxhighlight></div>
 
 
The ''next_level'' function is the most important bit. What happens when the player goes down the stairs? Well, for now all we have to do is generate a brand new level, with ''make_map()'' and ''initialize_fov()''. I will also heal the player because I'm such a nice guy!
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">def next_level():
    #advance to the next level
    message('You take a moment to rest, and recover your strength.', libtcod.light_violet)
    player.fighter.heal(player.fighter.max_hp / 2)  #heal the player by 50%
   
    message('After a rare moment of peace, you descend deeper into the heart of the dungeon...', libtcod.red)
    make_map()  #create a fresh new level!
    initialize_fov()</syntaxhighlight></div>
 
 
That's it! You can now advance indefinitely and fight as many monsters as you want.
 
However, we probably want to keep track of the dungeon level we're on. So create a variable for that, by initializing it in the function ''new_game'', before ''make_map()'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    dungeon_level = 1</syntaxhighlight></div>
 
 
Don't forget to add ''dungeon_level'' to the list of globals at the top of the function! Otherwise you'll just be setting a local variable, that won't be visible in your other functions. The dungeon level can then be increased in ''advance_level'', before ''make_map()'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    dungeon_level += 1</syntaxhighlight></div>
 
 
And declare it as global there too. To display it in the GUI, just print some informative text in ''render_all'', after the call to ''render_bar'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    libtcod.console_print_left(panel, 1, 3, libtcod.BKGND_NONE, 'Dungeon level ' + str(dungeon_level))</syntaxhighlight></div>
 
 
It's done! An important detail, however, is that we want this information (what object is the stairs, and what's the dungeon level) to be saved and loaded properly. We just have to follow the usual pattern, by adding to ''save_game'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    file['stairs_index'] = objects.index(stairs)
    file['dungeon_level'] = dungeon_level</syntaxhighlight></div>
 
 
And to ''load_game'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    stairs = objects[file['stairs_index']]
    dungeon_level = file['dungeon_level']</syntaxhighlight></div>
 
 
Again, don't forget to declare them as global in ''load_game''.
 
Now, since I like exploring a level thoroughly before going to the next, I found that quite often I can't remember where the stairs were! So let's add a bit of polish and allow some objects to be always visible, as long as they are in a tile that was explored once. This is most useful for stairs, but I think it makes sense for items as well. Just add a new optional property to objects, by modifying the Object's initialization:
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    def __init__(self, x, y, char, name, color, blocks=False, always_visible=False, fighter=None, ai=None, item=None):
        self.always_visible = always_visible</syntaxhighlight></div>
 
 
The behavior we talked about can be created by changing the ''if'' in the Object's ''draw'' method:
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">        #only show if it's visible to the player; or it's set to "always visible" and on an explored tile
        if (libtcod.map_is_in_fov(fov_map, self.x, self.y) or
            (self.always_visible and map[self.x][self.y].explored)):</syntaxhighlight></div>
 
 
You can now set ''always_visible=True'' when creating the stairs. I also set ''item.always_visible = True'' in ''place_objects'', for all items to do the same.
 
 
 
== Character progression ==
 
What's next? Let's see... Now that the player can fight so many monsters, it makes sense to gain some skill during the heroic quest! The simplest way to do this is to keep track of experience and player level. We'll store the amount of experience each monster gives in the Fighter component, by adding a new parameter ''xp'' to it:
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    def __init__(self, hp, defense, power, xp, death_function=None):
        self.xp = xp</syntaxhighlight></div>
 
 
When creating an orc's Fighter component in ''place_objects'', I set their ''xp'' to 25, and for trolls ''xp=100'' since they're harder to kill. I'm sure these numbers could use some tweaking!
 
When the player kills a monster, at the end of Fighter's ''take_damage'' method, it yields experience:
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">                if self.owner != player:  #yield experience to the player
                    player.fighter.xp += self.xp</syntaxhighlight></div>
 
 
As you can see I'm storing it in the player's Fighter component as well, but I guess you could store it anywhere else since the player's xp is special.
 
Ok, what does the player do with that experience? Let's level up! First, initialize the player's experience and character level in ''new_game'':
 
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    #create object representing the player
    fighter_component = Fighter(hp=30, defense=2, power=5, xp=0, death_function=player_death)
    player = Object(0, 0, '@', 'player', libtcod.white, blocks=True, fighter=fighter_component)
   
    player.level = 1</syntaxhighlight></div>
 
 
Now, we need to regularly check if the player has leveled up. I want it to become more difficult every time, so it takes 350 xp points to level up at the first level, and this increases by 150 points with every new level. The formula for this is ''200 + player.level * 150'', but I'll declare some constants so they're easier to adjust later:
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">#experience and level-ups
LEVEL_UP_BASE = 200
LEVEL_UP_FACTOR = 150</syntaxhighlight></div>
 
 
And the function that handles this will simply check the formula:
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">def check_level_up():
    #see if the player's experience is enough to level-up
    level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
    if player.fighter.xp >= level_up_xp:
        #it is! level up
        player.level += 1
        player.fighter.xp -= level_up_xp
        message('Your battle skills grow stronger! You reached level ' + str(player.level) + '!', libtcod.yellow)</syntaxhighlight></div>
 
 
Looks good so far! But when leveling up, the player should become stronger too. I'll go with a relatively simple choice, by allowing the player between 3 choices: to increase agility (defense), strength (power) or constitution (hp). However, this is one area where you can really get creative -- you can let the player acquire new abilities, increase skills, stats, and even learn to cast new spells!
 
Using the ''menu'' function, this is straightforward; just ask the player at the end of ''check_level_up'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">        choice = None
        while choice == None:  #keep asking until a choice is made
            choice = menu('Level up! Choose a stat to raise:\n',
                ['Constitution (+15 HP, from ' + str(player.fighter.max_hp) + ')',
                'Strength (+1 attack, from ' + str(player.fighter.power) + ')',
                'Agility (+1 defense, from ' + str(player.fighter.defense) + ')'], LEVEL_SCREEN_WIDTH)
       
        if choice == 0:
            player.fighter.max_hp += 15
            player.fighter.hp += 15
        elif choice == 1:
            player.fighter.power += 1
        elif choice == 2:
            player.fighter.defense += 1</syntaxhighlight></div>
 
 
I set the constant ''LEVEL_SCREEN_WIDTH = 40'' at the top of the file. You can now call ''check_level_up()'' in the main loop after ''libtcod.console_flush()'', so the check happens before every turn. This way the menu renders properly (remember we erase all objects before processing a turn, so otherwise they wouldn't show up behind the level up menu).
 
 
This seems great, but how do you know it's working? You need a way to check the character info! The character screen can be just a message box that pops up when you press the 'C' key. It's a little messy since it's just pasting together all the info in a string. In ''handle_keys'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">            if key_char == 'c':
                #show character information
                level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
                msgbox('Character Information\n\nLevel: ' + str(player.level) + '\nExperience: ' + str(player.fighter.xp) +
                    '\nExperience to level up: ' + str(level_up_xp) + '\n\nMaximum HP: ' + str(player.fighter.max_hp) +
                    '\nAttack: ' + str(player.fighter.power) + '\nDefense: ' + str(player.fighter.defense), CHARACTER_SCREEN_WIDTH)</syntaxhighlight></div>
 
 
And set the constant ''CHARACTER_SCREEN_WIDTH = 30''. It would also be polite to tell the player how much experience he or she gained when slaying a monster. So I modified the log message in ''monster_death'':
 
 
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="python">    message('The ' + monster.name + ' is dead! You gain ' + str(monster.fighter.xp) + ' experience points.', libtcod.orange)</syntaxhighlight></div>
 
 
That's it! You can now become ridiculously overpowered in no time, and see how you're doing by pressing 'C'. But don't worry, we'll take care of that shortly -- the monsters will grow stronger too!
 
...

Revision as of 05:05, 18 April 2012

This is part of a series of tutorials; the main page can be found here.



Character and dungeon progression


Second floor please

We're approaching the point where we have a complete game really fast. A big step will be taken in this part, where we will focus on progression! It will deal with changing dungeon levels, advancing the character and acquiring skills, and finally varying the monsters and items with the dungeon level.

We'll cover a lot of ground, but don't worry, it's all familiar territory! There aren't a lot of new concepts involved, since we will reuse many functions we developed before.

A staple of roguelikes is the stairs, which the player must find to advance to the next dungeon level. We will start by placing them, when generating a level. Right at the end of make_map:


    #create stairs at the center of the last room
    stairs = Object(new_x, new_y, '<', 'stairs', libtcod.white)
    objects.append(stairs)


As you can see, it's just a regular object! To identify it in other functions, we'll make it global, so add stairs to the globals list at the top of make_map.

We must now let the player go down the stairs when standing on them and presses the '<' key (or '>' key if you prefer). It's easy to add this check at the end of handle_keys:


            if key_char == '<':
                #go down stairs, if the player is on them
                if stairs.x == player.x and stairs.y == player.y:
                    next_level()


The next_level function is the most important bit. What happens when the player goes down the stairs? Well, for now all we have to do is generate a brand new level, with make_map() and initialize_fov(). I will also heal the player because I'm such a nice guy!


def next_level():
    #advance to the next level
    message('You take a moment to rest, and recover your strength.', libtcod.light_violet)
    player.fighter.heal(player.fighter.max_hp / 2)  #heal the player by 50%
    
    message('After a rare moment of peace, you descend deeper into the heart of the dungeon...', libtcod.red)
    make_map()  #create a fresh new level!
    initialize_fov()


That's it! You can now advance indefinitely and fight as many monsters as you want.

However, we probably want to keep track of the dungeon level we're on. So create a variable for that, by initializing it in the function new_game, before make_map():


    dungeon_level = 1


Don't forget to add dungeon_level to the list of globals at the top of the function! Otherwise you'll just be setting a local variable, that won't be visible in your other functions. The dungeon level can then be increased in advance_level, before make_map():


    dungeon_level += 1


And declare it as global there too. To display it in the GUI, just print some informative text in render_all, after the call to render_bar:


    libtcod.console_print_left(panel, 1, 3, libtcod.BKGND_NONE, 'Dungeon level ' + str(dungeon_level))


It's done! An important detail, however, is that we want this information (what object is the stairs, and what's the dungeon level) to be saved and loaded properly. We just have to follow the usual pattern, by adding to save_game:


    file['stairs_index'] = objects.index(stairs)
    file['dungeon_level'] = dungeon_level


And to load_game:


    stairs = objects[file['stairs_index']]
    dungeon_level = file['dungeon_level']


Again, don't forget to declare them as global in load_game.

Now, since I like exploring a level thoroughly before going to the next, I found that quite often I can't remember where the stairs were! So let's add a bit of polish and allow some objects to be always visible, as long as they are in a tile that was explored once. This is most useful for stairs, but I think it makes sense for items as well. Just add a new optional property to objects, by modifying the Object's initialization:


    def __init__(self, x, y, char, name, color, blocks=False, always_visible=False, fighter=None, ai=None, item=None):
        self.always_visible = always_visible


The behavior we talked about can be created by changing the if in the Object's draw method:


        #only show if it's visible to the player; or it's set to "always visible" and on an explored tile
        if (libtcod.map_is_in_fov(fov_map, self.x, self.y) or
            (self.always_visible and map[self.x][self.y].explored)):


You can now set always_visible=True when creating the stairs. I also set item.always_visible = True in place_objects, for all items to do the same.


Character progression

What's next? Let's see... Now that the player can fight so many monsters, it makes sense to gain some skill during the heroic quest! The simplest way to do this is to keep track of experience and player level. We'll store the amount of experience each monster gives in the Fighter component, by adding a new parameter xp to it:


    def __init__(self, hp, defense, power, xp, death_function=None):
        self.xp = xp


When creating an orc's Fighter component in place_objects, I set their xp to 25, and for trolls xp=100 since they're harder to kill. I'm sure these numbers could use some tweaking!

When the player kills a monster, at the end of Fighter's take_damage method, it yields experience:


                if self.owner != player:  #yield experience to the player
                    player.fighter.xp += self.xp


As you can see I'm storing it in the player's Fighter component as well, but I guess you could store it anywhere else since the player's xp is special.

Ok, what does the player do with that experience? Let's level up! First, initialize the player's experience and character level in new_game:


    #create object representing the player
    fighter_component = Fighter(hp=30, defense=2, power=5, xp=0, death_function=player_death)
    player = Object(0, 0, '@', 'player', libtcod.white, blocks=True, fighter=fighter_component)
    
    player.level = 1


Now, we need to regularly check if the player has leveled up. I want it to become more difficult every time, so it takes 350 xp points to level up at the first level, and this increases by 150 points with every new level. The formula for this is 200 + player.level * 150, but I'll declare some constants so they're easier to adjust later:


#experience and level-ups
LEVEL_UP_BASE = 200
LEVEL_UP_FACTOR = 150


And the function that handles this will simply check the formula:


def check_level_up():
    #see if the player's experience is enough to level-up
    level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
    if player.fighter.xp >= level_up_xp:
        #it is! level up
        player.level += 1
        player.fighter.xp -= level_up_xp
        message('Your battle skills grow stronger! You reached level ' + str(player.level) + '!', libtcod.yellow)


Looks good so far! But when leveling up, the player should become stronger too. I'll go with a relatively simple choice, by allowing the player between 3 choices: to increase agility (defense), strength (power) or constitution (hp). However, this is one area where you can really get creative -- you can let the player acquire new abilities, increase skills, stats, and even learn to cast new spells!

Using the menu function, this is straightforward; just ask the player at the end of check_level_up:


        choice = None
        while choice == None:  #keep asking until a choice is made
            choice = menu('Level up! Choose a stat to raise:\n',
                ['Constitution (+15 HP, from ' + str(player.fighter.max_hp) + ')',
                'Strength (+1 attack, from ' + str(player.fighter.power) + ')',
                'Agility (+1 defense, from ' + str(player.fighter.defense) + ')'], LEVEL_SCREEN_WIDTH)
        
        if choice == 0:
            player.fighter.max_hp += 15
            player.fighter.hp += 15
        elif choice == 1:
            player.fighter.power += 1
        elif choice == 2:
            player.fighter.defense += 1


I set the constant LEVEL_SCREEN_WIDTH = 40 at the top of the file. You can now call check_level_up() in the main loop after libtcod.console_flush(), so the check happens before every turn. This way the menu renders properly (remember we erase all objects before processing a turn, so otherwise they wouldn't show up behind the level up menu).


This seems great, but how do you know it's working? You need a way to check the character info! The character screen can be just a message box that pops up when you press the 'C' key. It's a little messy since it's just pasting together all the info in a string. In handle_keys:


            if key_char == 'c':
                #show character information
                level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
                msgbox('Character Information\n\nLevel: ' + str(player.level) + '\nExperience: ' + str(player.fighter.xp) +
                    '\nExperience to level up: ' + str(level_up_xp) + '\n\nMaximum HP: ' + str(player.fighter.max_hp) +
                    '\nAttack: ' + str(player.fighter.power) + '\nDefense: ' + str(player.fighter.defense), CHARACTER_SCREEN_WIDTH)


And set the constant CHARACTER_SCREEN_WIDTH = 30. It would also be polite to tell the player how much experience he or she gained when slaying a monster. So I modified the log message in monster_death:


    message('The ' + monster.name + ' is dead! You gain ' + str(monster.fighter.xp) + ' experience points.', libtcod.orange)


That's it! You can now become ridiculously overpowered in no time, and see how you're doing by pressing 'C'. But don't worry, we'll take care of that shortly -- the monsters will grow stronger too!

...