16.1 Final Project | Data Driven Text Adventure


A “Data-driven” program uses a data file to decide what to do. If you have been following the rise of AI, you know how important that is. You can build much more complex behaviors if your program is data-driven rather than requiring you to explicitly type out everything it must do.

For your text adventure program, this means that you want your program to read in the data from a text file and then use that data within your program. In the Runestone textbook you learned how to open and read in ordinary text files that have very little structure (only lines of text).

Plain text file

To complete this assignment, if I were a beginner Python programmer I would probably create a plain text file called dlane.txt, and the first 3 lines would probably look something like this:

This is a text adventure, in each room you must type 'N', 'S', 'E', or 'W' to move to the next room.
Which way, N or S? |n|2|s|3
You're in room #2. N, S, E, or W? |n|4|s|1|e|5|w|6

The Python program in dlane.py might load this file and use it to control the game play with something like this:

text = open('dlane.txt').read()
lines = text.splitlines()
rooms = [line.split('|') for line in lines]  # rooms is list of lists

instructions = rooms[0][0]
print(instructions)
room_num = 1
while True:
    if room_num > len(rooms) or room_num < 0:
        break
    room = rooms[room_num]
    description = room[0]
    commands = room[1::2]
    destination_room_nums = room[2::2]
    player_answer = input(description)
    letter = player_answer.strip().lower()[0]

    if letter in commands:
        answer_index = commands.index(letter)
        room_num = int(destination_room_nums[answer_index])
    else:
        print(instructions)
print("You escaped!")

You probably would not have thought of this design or architecture, because it uses a lot of Python syntax that you might not have used before. I used a 3rd number in the slice to skip every other element in my lists: [0::2]. And to start a slice at the second element I used [1::2].

JSON

If you are interested in leveling up your Python skills, you can try using the built-in json package to give your text file a bit more structure, making it easier to understand and create more complicated, interesting games.

{"instructions":
    {
        "description": "This is a text adventure! In each room type N, S, E, or W to move to the next room.",
    },
},
{"start room":
    {
        "description": "Which way, N or S?",
        "n": "big room",
        "s": "south room",
    },
},
{"south room":
    {
        "description": "You're in the south room. N, S, E, or W?",
        "n": "small room",
        "s": "deep south room",
        "e": "southeast bedroom",
        "w": "sunset room",
    },
},

The JSON format gives your data more structure, which can make it easier to use in your program. Here’s the Python to load and “run” this JSON file to create a text adventure.

import json
rooms = json.load('dlane.json')

print(rooms['instructions']['description'])
room_name = "start room"
while True:
    room = rooms[room_name]
    player_answer = input(room["description"]).strip().lower()
    letter = player_answer[0]
    if letter in room:
        answer_index = commands.index(letter)
        room_name = room[letter]
    elif letter == x:
        break
    else:
        print(room['instructions']['description'])
print(f"You made it to {room_name}!")

Does the Python program that uses a JSON data file look a litter easier to read and understand?

Data-driven programs

For the final project, you will need to expand your text adventure game and make the code more compact and manageable (modular). To do this you will move some of the code to data files with a structure that you define. For the midterm project, you probably used several nested if-else statements and had to repeat yourself a lot. One of the cardinal rules in software development is “Do not Repeat Yourself” (DRY). You can remove duplication (“DRY your code”) using some of the skills you have learned in this class for working with functions, loops, and files:

Assign a variable to the data item you have repeated elsewhere. Create a function to reuse the same code multiple times. Parameterize your function by giving it input arguments you can use to change the way it runs. Remove hard-coded data from within your code and instead load it from data files. Use iteration, such as a while or for loop to have your program do things repeatedly in your game.

You have probably already experimented with the first two, variables and functions. Now you are ready for those last two which you can use to create a data-driven text adventure program. Your final project for the course is to create a more fun and more elegant text adventure program, using the data-driven software design pattern.

Now that you know how to read and write data to files and how to use advanced iteration techniques you may be able to turn your game into a much more fun adventure, while at the same time reducing the number of lines of code in your program. The first step in building a data-driven program is creating a data structure to hold all your rooms for your text adventure. You can also use your data structure to store the possible commands for a game player to choose from in each room. You don’t have to show them to the player, but they should be available in the data structure for your program.

When thinking about data structures, the most useful container data types in Python are lists and dictionaries. For a list, you can only access the records in sequence or by knowing the integer position of an item in that list ahead of time. For a dictionary, you need to know the name of the key in order to find a particular value. A data structure will usually nest lists and dictionaries within each other to help you organize your data in a way that makes it easier for you to work with in your program.

Common data structures:

A list of lists — a table where you access the rows and columns based on their integer position, like in a spreadsheet or CSV file A list of dictionaries — each data record in your list can have different keys to make it easier to find something on that row. A dictionary of dictionaries — the dictionary keys can help you find both the row and the data within that row that you are looking for. A dictionary of lists — you can find a row of data based on its name (key) and then iterate through a sequence of objects within the inner list.

1. list of lists

>>> X = [
...     [.707, .707, 0.0],
...     [.707, .707, 0.0],
...     [ 0.0,  0.0, 1.0],
... ]
>>> Y = [1, 0, 0]
>>> rotate45deg = [sum([x * y for x, y in zip(Xrow, Y)]) for Xrow in X]
>>> rotate45deg
[0.707, 0.707, 0.0]

Lists of lists, such as the one above, are often used to store numerical data for linear algebra and computer-generated graphics (CG) programs. You can also use them for a data-driven text adventure if you’re careful about how you number your rooms and use a consistent ordering of your commands.

2. list of dictionaries

>>> peeps = [
...     {'name': "Kim", "age": 21},
...     {'name': 'Joe', 'phone': '867-5309'}
... ]
...
>>> [peep.get('age') for peep in peeps]
[21, None]
>>> [peep.get('name') for peep in peeps]
['Kim', 'Joe']

Lists of dictionaries, are used to store mixed data. You don’t even have to plan ahead of time for all the pieces of information that you might want to store. And each piece of information is optional.

3. dictionary of lists

>>> peeps = {
    'Lam':   ["Kim", 21,   None],
    'Smith': ['Jon', None, '867-5309'],
}
>>> peeps['Lam'][:2]
['Kim', 21]
>>> peeps['Smith'][:-1]
['Jon', None]

A dictionary of lists, are a good option if you always know the order of a set of information for each record or row in your dataset. You can always access each on with its key.

### 4. dictionary of dictionaries
>>> peeps = {
...     'Lam': {'name': "Kim", "age": 21},
...     'Smith': {'name': 'Jon', 'phone': '867-5309'},
... }
>>> peeps['Lam'].get('age')
21
>>> peeps['Smith']['age']
KeyError                                  Traceback (most recent call last)
>>> peeps['Smith'].get('age')

A dictionary of dictionaries, gives you the ultimate in flexibility. You don’t need to control the order of the records in your data structure. You can always access each with its key.

Text adventure data structure

A computer scientist would say that rooms in your text adventure game represent the “state” of your program. You can use the Listener Loop pattern (while True:) to listen for user commands forever. And you can use break statements to break out of the loop to print “Game Over,” or continue statements to go back to the beginning of the while loop and try again (“and do not pass Go”). A dictionary of dictionaries or a list of dictionaries, one inner dictionary for each room, is probably a good data structure for your game. This is what I used in my example game at the beginning of this module and in the GitLab repository for this course.

Requirements

Your text adventure game must:

  • have at least 10 rooms or states in your game
  • have at least 4 commands in some rooms
  • give your player a score at the end of the game (you could give them one point for each room they visit)
  • only reachable rooms. All the rooms in your game must be reachable, but not necessarily in one session (it’s OK for your game to have traps that end the game early).
  • store all text displayed to the user within nested lists, dictionaries, or tuples.
  • save all text displayed to the user in a text file (lastname.txt, lastname.csv, lastname.json, or even lastname.md).
  • be runnable from a terminal: I will run your code automatically on GitLab in the cisc-179-spring-2024/src/data_driven using the shell (terminal) command python yourmesaemailusername.py, and you should too!

Search this module to find an example of how to use the json package to read and save *.json files. You can investigated the pandas package if you want an easy way to read CSV files.

10 point Bonus: provide math or programming challenges in one of your rooms that requires a player to enter a correct number or a Python programming question that requires the user to enter a correct Python expression.

10 point Bonus: if your game uses generative text based on your n-gram parser from earlier modules you will get a 10% bonus.

If you are feeling creative, it is possible to demonstrate all of these programming patterns and acheive the requirements (and bonus points) of this project even if you program is not a text adventure game; but you probably want to talk to me about your idea first to make sure.