Building a Text-Based RPG Engine in Python
This is a very simple introduction to how game engines are built. using a text-based RPG we can demonstrate common game building concepts.
Article available in Polish here.
I know what you are thinking… What… Why would anyone ever want to do this? My friend who is learning Python mentioned that he wanted to build a text-based role-playing game and it got me thinking. It’s been seven years since I tried to build my last text-based “game” monty.py which was just a simple Monty Python skit. That’s why I am giving it another go to create a text-based role-playing game engine in python.
Defining Our Inputs and Outputs
First thing first we need to set up our expectations. I think in its most simple form, the game will need to do the following things:
- Load file that will have the game info
- Show text output from the game
- Accept input from the player
The first point is simple. We will need some type of file that will contain the data. We will use Pickle to wrap up our objects. The shape of each of the elements will be determined more so by the next two requirements.
We will want to group outputs and inputs together. We will put them on something we will refer to as a page. Each page will have a number so we can keep track of them. Here is what a page will look like:1: {
'text':
'options':
}
We don’t want to have only large blocks of text, so ‘text’ should be a list element with strings. That way there can be multiple lines of dialogue that you have to click through before entering an input. Also, I really like it when it looks like the text is being typed onto the screen so we will implement that feature later.'text': [
"This is out first line",
"And this is our second"
]
Next, for the inputs. Each input statement should probably have one line of text and then also an indicator to say when this input is selected go to this page. Because this data is well defined we can use an array of tuples to store this like so:'options': [
("Option 1", 2)
("Option 2", 3)
]
Once we have our story written in the proper format we can use a script like the following to wrap it up into a binary pickle file that can be distributed and read from our engine.import picklestory = {
1: {
'Text': [
"Hello there..",
"I bet you werent exepecting to hear from me so soon...",
"...you seem a little confused do you know who I am?"
],
'Options': [
("Yeah of course!", 2),
("I'm sorry I dont", 3)
]
}
}with open('chapter1.ch', 'wb') as chapter:
pickle.dump(story, chapter)
Output Text
The first thing we want to do is figure out how to output text slowly onto the screen as if someone was typing it. Stack Overflow to the rescue with this wonderful solution. This iterates over each letter and puts it on the output terminal. The sys.stdout calls give lower-level access to the command prompt allowing you to override defaults set by Python.
import sys,time,random
def slow_type(t):
typing_speed = 100 #wpm
for l in t:
sys.stdout.write(l)
sys.stdout.flush()
time.sleep(random.random()*10.0/typing_speed)
Okay, now that we got the slow typing out of the way, we can get to the real work. We said we want each line from a page’s text to be printed one at a time. When you click on the enter key it should move to the next output. Here is what that function would look like.
It takes in the list of lines from the text portion of the page; then it iterates over the lines slow typing one line and then waiting for the enter key to be hit before moving to the next line.def display_page_text(lines: list):
for line in lines:
slow_type(line)# Make the user press enter to see the next line
get_input([''])
HOLD UP THOUGH! I know I just used get_input()
without showing what that function looks like. Next, let’s take a look at what getting our input values looks like.
Taking Inputs
Getting input from the command line is not too bad. First, we will create a function whose sole purpose is taking input. We will pass it a list of valid input strings. Then we will take the user input. If their input is not listed in the valid inputs list we notify the user, tell them what are valid inputs, and then clear the input. Otherwise, we return the input.def get_input(valid_input: list):
while True:
user_entered = input()if user_entered not in valid_input:
print("Invalid input. Please use one \
of the following inputs:\n")
print(valid_input)
user_entered = Noneelse:
return user_entered
We call get inputs with [‘’] like mentioned earlier if we expect the enter key. Otherwise, the primary use for getting inputs is deciding which page to go to next. This is the job of the get a response function. It is passed the list of tuples that represent the options. These have a string option and then the page number like such: (“Option 1”, 2).
Get response iterates over the tuples printing a number for the option and the option text. Then, passes in the valid inputs (the indexes of options) to get input function to get the users’ input. Finally, it returns the next page number.def get_response(options: list):
for index, option in enumerate(options):
print(index + “. “ + option[0])
valid_inputs = [str(num) for num in range(len(options))]
option_index = int(get_input(valid_inputs))
return options[option_index][1]
Pull it All Together
The last thing we need to do is pull it all together. When you load up the app we need to load the first page. Then we set up our program loop. To exit the loop we will set the page to none.
Then, we will get the current page from the story dict. If there is no page then we need to break out of the loop. This way if there isn’t a page with the index mentioned the program will gracefully exit.
Once we have fetched the page, we will display the page text using the function we defined earlier. Once they have made it through the page text we will get their input for which page they want to go to next. If there aren’t any options in the list, then we will say the story is finished and we can close the story. If there are options we will use our get response function to get their selection.def story_flow(story: dict):
curr_page = 1
while curr_page != None:
page = story.get(curr_page, None)
if page == None:
curr_page = None
break
display_page_text(page['Text'])
if len(page['Options']) == 0:
curr_page = None
break
curr_page = get_response(page['Options'])
Finally, we will have our script load up the pickle file to play the story. Here is how that is done.import pickle
if __name__ == "__main__":story= {}with open('chapter1.ch', 'rb') as file:
story = pickle.load(file)story_flow(story)
That is it! Check out the Github repo to see a fully working example. Also, congratulations! If you followed along with this example you have just implemented a map data structure!