Writing a Python Dungeon Game Part I
Python for Gaming
Python is not known for being a game-development-friendly language; it usually is done as an exercise for new programmers that want to practice their object oriented skills, or by Python developers who just want to fool around. This has not changed, and if you are looking seriously into game development there are plenty of options, most of which are not Python. However, if you want to learn Python in a fun way, or already know Python and want to get a few ideas as to how to make a game, this may be a good place for you.
On this first part we won’t worry about things such as the UI, we’ll leave that for the next part. For now, let’s create a game for the CLI that uses the most basic Python concepts and that will allow us to focus on the algorithms we’ll need to implement.
What are we making?
In this tutorial we’ll be making a dungeon game; a simple game where the goal is to go from one end to the other in a square grid. Along the way we’ll find monsters and items, and we’ll have to figure out the safest path to traverse as the dungeon becomes more and more dangerous! Your personal idea might be a little or a lot different, but the fundamentals we’ll go over should apply to your own project.
I came up with this game while studying algorithms, in what I realized later was a website specially dedicated to learning algorithms for game development (highly recommended!). So I wasn’t being as original as I thought when I decided to use graph-as-grid algorithms for a game, but going through the implementation process was a ton of fun and I hope you enjoy it too.
All we’ll be using for now is Python, and any text editor you want. Could be PyCharm, could be vim, could be your notepad, doesn’t matter; we’ll only need Python’s built in functions. Originally I used Python 2.7, but this time around I’ll be using Python 3 — not sure if it will even make a difference, but it’s worth mentioning. I’ll try to make it backwards compatible.
Let’s get started!
First, we have to define how we’ll represent our game map. We’ll start as simple as we can, first creating a grid to hold our map and a function to display the map:
Pretty straight forward, our grid is simply defined by its width and height (in this case 30 and 15 accordingly.) The local width variable has to do with the spacing, so it’s more of an aesthetic thing, try it out and play around with the value. You should see something along these lines:
Not very exciting — our dungeon can’t be an open field, that’s the opposite of a dungeon. We need walls! Let’s modify our functions a bit to add this new feature. Give yourself a few minutes to think, how would you implement walls on this grid? When you are done, check the code on the next section.
Spicing up the map
Just one more data attribute walls was added to the Map code, which we initialized as an empty list. Walls will be defined as tiles that occupy one space on the grid; so they are represented in code as an (x, y) tuple, which the draw function will display as a pound or hashtag symbol (#). Optionally, we added a function named get_walls to create a nice list of tiles for us.
The get_walls function is worth taking a closer look, as it’s doing some interesting operations. The arguments are g (the graph) and pct (defaulted at .3, indicates the percentage of the map you would like to cover in walls), and based on them we use the built in library random to generate a nicely distributed set of walls.
To have the walls nicely distributed we first need to get the total number of tiles (width*height) and multiply by the percentage of those tiles we want to make into walls. Then we iterate over half of that number and make two tiles in each loop; one slightly changed by some random noise so that we get a more cave-looking distribution with many wall tiles next to each other. You could even achieve this further by doing 3 tiles per loop, but it created too much clustering for my liking. Then again, depends on your use case, but you should have something that looks like this:
Play around with the values and let us know what you think is the best way to achieve a nicely distributed set of walls for randomly generated maps.
So far, so good. But what good is a map with no objective, characters, or interactions? Well, we said the goal was going to be getting from one end to the other, say the entrance to the end of the dungeon (or the dungeon’s level). To make it easy, we’ll say that the top left corner will be the entrance and the bottom right will be the exit; after all our wall generation function was also designed to avoid creating dead ends from/to these two points (chances are small, but some maps generated will have dead ends, so we’ll have to handle that eventually, just not yet.) We will also need a character to move around the map, which we’ll represent as (x, y) values, and the same way we’ll represent the entrance and goal.
After a few changes this is what we want to achieve:
As you can see we have 4 simple controls and a character (literally though) that we can move around from a start to a goal! It’s not much, but it’s starting to take the shape of a game. Try it out on your own first and see what you can come up with, then see how we did it on the code below.
The next step is to add the actual game part of it. We will add value to the non occupied tiles around the cave and give our hero a maximum health. With every step, they will have to defeat the monster on the tile they move to, which will do damage to our hero equals to the random value we set on the space. As he gets to the end they have to be careful to use the most efficient path as to not lose all of their health before the end.
How do we ensure that there’s a path that will guarantee they can actually make it to end? How do we make it so that there are not too many paths and that the game is challenging? The game can’t just end, how can we make it loop? Even better, how do we make it get harder as you go on?
We will address these questions on the next part! In the meantime, try to come up with something on your own. How can we make what we already wrote more efficient or intuitive? Leave your input in the comments below and we’ll address them on the next post. Thank you for reading, keep coding :)