Complete Roguelike Tutorial, using python3+libtcod, part 11
This is part of a series of tutorials; the main page can be found here.
Dungeon levels and character progression
Second floor please
We're approaching the point where we have a complete game really fast. A big step will be taken in the next two parts, where we will focus on progression! They will deal with changing dungeon levels, advancing the character and acquiring skills, and finally varying the monsters and items as the player progresses. At the end you will have a game that can take the player quite some time to finish, and to master its strategies too.
We'll cover a lot of ground, but don't worry, this part is all familiar territory! We will reuse many functions from 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:
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:
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!
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():
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 next_level, before make_map():
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:
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:
And to load_game:
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:
The behavior we talked about can be created by changing the if in the Object's draw method:
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. If you added the optional room-numbering labels in part 3, set them to always be visible too. They haven't been much use since the fog-of-war code was added, but this makes them useful again.
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:
When creating an orc's Fighter component in place_objects, I set their xp to 35, 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:
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.
But what does the player do with that experience? Let's level up! First, change the initialization of the player object to also set the experience and character level, in new_game:
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:
And the function that handles this will simply check the formula:
Looks good so far! But when leveling up, the player should become stronger too. My idea is to allow 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:
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:
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:
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!
By the way, if you're missing diagonal movement with the numpad keys, now's a good chance to do it. The key codes are KEY_KP0 to KEY_KP9. Have the player rest when the keypad '5' is pressed. This allows you to wait for a monster to come to you, rather than charging into a crowded room. Things are getting pretty serious!
The whole code is available here.