Text adventures are an old game genre where the entire world is textually described as a series of rooms. Play involves entering simple text commands such as 'move north' or 'eat pie'. Each room in the game world has its own text description, items on the ground, and exits to adjacent rooms. 'Room' is a general name for a single area in the game world: a room can be a large open canyon or the inside of a wardrobe. Multi-user text adventures, called MUDs or Multi-User Dungeons, were the precursor to modern MMORPGs. You can still play MUDs today by finding them on The Mud Connector.
A text adventure game can be represented as a state machine (with some extra properties like health, etc.). A good way to model a state machine is with first-class functions.
- In my last article, I explained why text adventure games are some of the most Steampunk computer games out there.I even shared a free Steampunk text adventure game that I'd made myself! Now, I grew up playing text-based games (which are now known as interactive fiction) such as Zork, Lurking Horror, and Starcross.
- Create your own Classic Text Adventure with Python. These games may be simple, but they’re a critical part of getting comfortable with text-based programming. Many of today’s most famous programmers started out building simple text-based games, and classics like Zork and ADVENT inspired a whole generation of programmers to use computers for creative pursuits.
Text adventures are easyto make because they don't require graphics. This tutorial uses two Python modules,
cmd
and textwrap
and makes minimal use of object-oriented programming, but you don't have to know OOP concepts to follow. (But in general, text adventures would do very well with an object-oriented approach.) This tutorial is for beginner Python 3 programmers.The
cmd
module provides a generic command-line interface that has several useful features:- Tab-completion - Type a partial command and press tab, and the game can guess the rest of the command.
- History browsing - Press the up arrow key to bring up the previously entered commands.
- Automatic help - A help system for commands is automatically generated for your commands.
The Python Debugger (tutorial here) in the
pdb
module makes use of the cmd
module for its command line interface.To get tab-completion to work on Windows, download and install this module: https://pypi.python.org/pypi/pyreadline/2.0 (On Windows with Python 3, open a command window and type
cd c:Python34Scripts
and then pip install pyreadline
.Our program will be about turn-based, single-player, and about 800 lines long. You can download the complete version from here: Download textadventuredemo.py or look at the GitHub page.
Demo of Game Play
Here's what our text adventure game will look like. It won't have combat, but will have a small town that you can navigate while picking up and looking at different things. Money isn't implemented, but shops to buy and sell at are.
Text Adventure Demo!
(Type 'help' for commands.)
Town Square
The town square is a large open space with a fountain in the center. Streets
lead in all directions.
The town square is a large open space with a fountain in the center. Streets
lead in all directions.
A welcome sign stands here.
A bubbling fountain of green water.
A bubbling fountain of green water.
Bandicam keygen crack download. North: North Y Street
South: South Y Street
East: East X Street
West: West X Street
South: South Y Street
East: East X Street
West: West X Street
> look sign
The welcome sign reads, 'Welcome to this text adventure demo. You can type
'help' for a list of commands to use. Be sure to check out Al's cool programming
books at http://inventwithpython.com'
The welcome sign reads, 'Welcome to this text adventure demo. You can type
'help' for a list of commands to use. Be sure to check out Al's cool programming
books at http://inventwithpython.com'
> look fountain
The water in the fountain is a bright green color. Is that.. gatorade?
The water in the fountain is a bright green color. Is that.. gatorade?
> north
You move to the north.
North Y Street
The northern end of Y Street has really gone down hill. Pot holes are everywhere, as are stray cats, rats, and wombats.
You move to the north.
North Y Street
The northern end of Y Street has really gone down hill. Pot holes are everywhere, as are stray cats, rats, and wombats.
A sign stands here, not bolted to the ground.
South: Town Square
East: Bakery
West: Thief Guild
East: Bakery
West: Thief Guild
> look sign
The sign reads, 'Do Not Take This Sign'
The sign reads, 'Do Not Take This Sign'
> take sign
You take a sign.
You take a sign.
> inventory
Inventory:
Do Not Take Sign Sign
Donut
Sword
README Note
Inventory:
Do Not Take Sign Sign
Donut
Sword
README Note
> look readme
The README note reads, 'Welcome to the text adventure demo. Be sure to check out the source code to see how this game is put together.'
The README note reads, 'Welcome to the text adventure demo. Be sure to check out the source code to see how this game is put together.'
> eat donut
You eat a donut
You eat a donut
> eat readme
You eat a README note
You eat a README note
> eat sword
You cannot eat that.
You cannot eat that.
> inventory
Inventory:
Sword
Do Not Take Sign Sign
Inventory:
Sword
Do Not Take Sign Sign
> e
You move to the east.
Bakery
The delightful smell of meat pies fills the air, making you hungry. The baker flashes a grin, as he slides a box marked 'Not Human Organs' under a table with his foot.
You move to the east.
Bakery
The delightful smell of meat pies fills the air, making you hungry. The baker flashes a grin, as he slides a box marked 'Not Human Organs' under a table with his foot.
A 'Shopping HOWTO' note rests on the ground.
South: East X Street
West: North Y Street
West: North Y Street
> list
For sale:
- Meat Pie
- Donut
- Bagel
For sale:
- Meat Pie
- Donut
- Bagel
> buy pie
You have purchased a meat pie
You have purchased a meat pie
> buy pie
You have purchased a meat pie
You have purchased a meat pie
> buy pie
You have purchased a meat pie
You have purchased a meat pie
> inventory
Inventory:
Meat Pie (3)
Sword
Do Not Take Sign Sign
Inventory:
Meat Pie (3)
Sword
Do Not Take Sign Sign
> quit
Thanks for playing!
Thanks for playing!
The 'Choose Your Own Adventure' Mistake
Beginners tend to write code that looks like a 'Choose Your Own Adventure' book: the execution starts at the beginning of the program, then it jumps to another part of the code (just like the reader would turn to some page at a branching point in the book). I have an example of this type of program in my dragon2.py game. This works, but the program becomes unwieldy as it gets larger.
You won't have a
townSquare()
function that displays the Town Square text when called like so:A better way to program a game is a more data structure-centric approach. This sounds very abstract. Here's an example of the data structure you'll have for locations in your game:
You can see that the general structure of the
worldRooms
dictionary is:And here's a data structure to hold the different types of items in the game:
You can see that the general structure of the
worldRooms
dictionary is:By putting the rooms and items into data structures, now you can write a single
displayLocation()
function that can display any room in the game because it works off of the values in the worldRooms
data structure. You can write code that deals with rooms and items generically. Writing a big game world will be much faster, just like how interchangeable parts speed up manufacturing and factories.A map of the full world looks something like this:
(Data structures like these are normally put into classes in objet-oriented programming. To keep this text adventure demo simple, I'm using dictionaries and lists.)
Constant Variables and Global Variables
Start from scratch with a blank
textadventuredemo.py
file. Add a #! python3
shebang line, some comments describing the program, and the worldRooms
and worldItems
data structures. Since this is mostly text and not code, you might want to just copy and paste the code from the snippet on GitHub.The keys in the above data structures are constant variables:
The reason that constant variables are used instead of typing the string directly is to minimize the chance of errors. If you mistype the string
'desc'
as 'dsec'
, Python will not immediately complain because since 'dsec'
is a valid string. Later there will be a KeyError when you try to use 'dsec'
as a key, but this is a vague error message that could have many causes.But if you are using constant variables instead, mistyping
DESC
as DSEC
will immediately crash the program with a NameError exception since DSEC
isn't a variable name. The sooner your program crashes, the easier it is to find the crash-causing bug. If your program silently continued to work, it will take longer to trace the original bug.There is also the
SCREEN_WIDTH
constant, which will be used by the textwrap
module to determine how wide the program should assume the screen is:Th
SCREEN_WIDTH
constant will be explained in the next section.The
location
and inventory
global variables will keep track of the player's position and inventory. The showFullExits
variable tracks whether to show full or brief versions of the current room's exits (this is explained later in the displayLocation()
function). Add the following code to your program:(Normally your programs should avoid using global variables, but they're used here to keep the text adventure demo code simple.)
Also, import the following modules into you program:
The textwrap Module
The
textwrap
module can intelligently break up a string into multiple lines of a given width. You might think this is easily done with code such as the following, which breaks the desc
string up into lines 80 characters wide:But this can cut off a line right in the middle of words, like this:
The
textwrap.wrap()
function is smart enough to avoid chopping lines in the middle of a word. It returns a list, with each line as a string in the list:To make it easy to change the screen width that
textwrap.wrap()
uses, pass the SCREEN_WIDTH
constant for the second argument. That way, if you want to change the screen width you only need to change the SCREEN_WIDTH
value.Location and Movement Functions
When the player walks into a room, the displayed text looks something like this:
Town Square
The town square is a large open space with a fountain in the center. Streets
lead in all directions.
The town square is a large open space with a fountain in the center. Streets
lead in all directions.
A welcome sign stands here.
A bubbling fountain of green water.
A bubbling fountain of green water.
North: North Y Street
South: South Y Street
East: East X Street
West: West X Street
South: South Y Street
East: East X Street
West: West X Street
The data for each room is tied up in the
worldRooms
variable. You need a function that, given the name of the room as a string, will print out text like the above. This will need to print out:- The room's name
- The room's description
- A list of the items on the ground in this room
- A list of all the available exits
Add the following code to your program:
There will also be a
moveDirection()
function that, given a north/south/east/west/up/down string argument, will change the location
global variable to the new location.But it will only do this if the direction was a valid one to make. If the player is able to move in the given direction, the new room's description will be displayed on the screen. Add the following code to your program:
You'll see how these functions are used by the command line interface in a bit. But if you want to see how the code you have so far works, add the following temporary code:
Your program will now look like this source code on GitHub. When you run this program, you will be able to move around the game world by typing
north
, south
, east
, west
, up
, and down
.Notice a few things about this game world:
- Moving in an invalid direction results in
'You cannot move in that direction'
printed to the screen. - At the top of the wizard's tower, the 'Magical Escalator to Nowhere' room's up exit leads to itself. This means if you exit up from this room, you will arrive in the same room. You can effectively go up an infinite amount of times. This is a side effect of the way the room data structures are set up.
There are just a few more helper functions you'll need.
Item Helper Functions
These functions all work with the item data structure you have already set up. You won't quite see how these functions are needed right now, but they will be explained later.
Remember that the
worldItems
variable is a dictionary with key-value pairs that look like this:The helper functions for items are:
getAllDescWords()
- Given a list of strings of item names, return a list with all of these items' DESCWORDS strings.getAllFirstDescWords()
- Given a list of strings of item names, return a list with all of these items' first DESCWORDS strings.getFirstItemMatchingDesc()
- Given a string and a list of item names, return a string of the name of the first item that has a DESCWORD string that matches the given string argument.getAllItemsMatchingDesc()
- Given a string and a list of item names, return a list of strings of item names for items that have the given string argument as one of their DESCWORD strings.
Add the following code to your programs:
The
list(set(itemList))
code is a Pythonic way to get rid of duplicate values in a list by coverting the list to a set and back to a list. Note that this might change the order of the values in the list, since the set data type is unordered.Creating a Command Line Interface with the cmd Module
The
cmd
module frees us from reinventing the wheel when creating the command line interface for the text adventure. First, create a class that subclasses cmd.Cmd
. The cmd
module will read any methods that begin with do_
, complete_
and help_
for it's command, tab-completion, and help system features, respectively.Here's a short example. Add the following code to your program:
There are several cmd-specific things about this code:
- The string in the
prompt
member variable is printed when the player is expected to begin typing in a command. In your text adventure's case, this will be the'n>'
string: a newline followed by a > character. - The
default()
method will be called by the command line interface when it cannot understand the command the player typed. In this case, a 'I do not understand' message is printed to the screen. - When the player types in a command, the command line interface checks if there are any methods of the same name as the command to handle it. For example, 'quit' would be handled by
do_quit()
. If the player entered the 'qwerty' command but there is nodo_qwerty()
method, thedefault()
method is called instead. - When a
do_<command>()
returns, the command line interface will let the player type in another command. If ado_<command>()
returns the valueTrue
, the command line interface will stop asking for commands. - The
do_quit()
method implements the 'quit' command. Since it returnsTrue
, the command line interface will stop asking the player for commands. - The docstring at the top of each
do_<command>()
function will automatically be used for the command line interface's help system. - The
help_combat()
method implements the 'help combat' command. You can add as many help topics as you want: typing 'help' by itself lists all of them. Since there is no combat in this text adventure, this function prints a message telling the player there is no combat. - The
arg
parameter indefault()
anddo_quit()
will be explained later.
To kick off the command line interface code, you will need to call the
cmd.Cmd
object's cmdloop()
method. Take out the temporary code in the while True:
loop and add the following to your program:Your program should now look like this file on GitHub.
The cmd Help System
When you run this program, the movement commands won't work (they were implemented in the temporary
while
loop you removed) but you can run the 'help', 'help quit', and 'help combat' commands:> help
Documented commands (type help ):
help quit
help quit
Empire goodgame studios sign in. Miscellaneous help topics:
combat
combat
> help quit
Quit the game.
Quit the game.
> help combat
Combat is not implemented in this program.
Combat is not implemented in this program.
Notice that the 'quit' command's docstring has been used for the 'help quit' command. You can end this program by running 'quit', which makes the command line interface call the
do_quit()
method. Since it is a do_<command>()
method that returns True
, it causes the TextAdventureCmd().cmdloop()
function return and the execution reaches the end of the source code.As a free feature provided by the command line interface, pressing the up arrow key will cycle through the command history. This provides an easy way to re-enter commands you previously typed.
The Move Commands and Tab Completion
In order to move the player around, you'll need a
do_north()
method for the 'north' command, a do_south()
method for the 'south' command, and so on. All of these methods will simply call the moveDirection()
function with the appropriate argument. Add the following to your program:Because the 'n' and 'north' commands would do the same thing, we can create another function with the same code by assigning
do_n = do_north
, and so on.To toggle the Boolean value in the global
showFullExits
variable, an 'exits' command can be implemented in do_exits()
. Add the following to your program:With this code, your program should look like this file on GitHub. When you run this program, the north/south/east/west/up/down commands will all work.
Inventory
The 'inventory' command will display the contents of the
inventory
global variable. Rather than just a do_inventory()
method that runs print(inventory)
, there's additional code that checks if the player has more than one of a type of item. So an inventory
value of ['Donut', 'Donut', 'Donut']
will display a single line: Donut (3)
.Add the following code to your program:
To add an identical 'inv' shortcut command, the
do_inv = do_inventory
line is added after the do_inventory()
function.Taking and Dropping Items
Every room in the
worldRooms
data structure has a GROUND
key whose value is a list of items on the ground of that room. By removing items from the inventory
list and adding them to the GROUND
list, the player can 'drop' the item in a certain room. The items will continue to be on the ground even if the player leaves the room and comes back. Similarly, by removing a value from the GROUND
list and adding it to the inventory
list, the player can 'take' the item.The 'take' and 'drop' commands are a bit more complicated: There is text that follows the 'take' and 'drop' words such as in 'take sign' or 'drop sword'. The additional text is passed to the
do_<command>()
function for its arg
parameter. So if the player enters the command 'drop sword', the do_drop()
method is called with 'sword'
passed for its arg
parameter.The drop and take methods will have to do more than modify
inventory
and the GROUND
key's value. They must handle the player forgetting to specify what to take/drop, check that the item is actually on the ground/inventory, and if the object is 'takeable' (that is, it's TAKEABLE
key's value is True
).Also, items can be referred to by any of it's
DESCWORDS
from the worldItems
https://washingtontree830.weebly.com/design-expert-10-crack.html. data structure. For example, the sword item:..has a
DESCWORDS
value of ['sword', 'exkaleber', 'longsword']
. So the 'drop sword', 'drop exkaleer', and 'drop longsword' would all refer to the same sword to drop.Add the following code to your program:
After you are finished, your program will look like this file on GitHub. When you run the program, you will be able to drop and take items (provided they have the
TAKEABLE
setting).Tab Completion
if the player types a partial command, such as 'nor', they can press the Tab key and the command line interface will complete the command: 'north'. The command line interface knows this because 'north' is the only command that begins with 'nor'. If there were other commands that began with 'nor', pressing Tab would bring up a list of possible complete commands.
However, say that the player has an
inventory
value of ['sword', 'swingset', 'swampwater']
and had entered 'drop sw' and pressed Tab. The command line interface knows all the commands because it can see what do_%lt;command>()
functions there are. But items on the ground or in the inventory are specific to your program, so you will need a way to tell the command line interface what it should return for possible tab completions.This is done by the
complete_<command>()
methods. These methods have the following parameters:text
is the part after the command. If 'drop sw' were being completed,text
would be set to'sw'
.line
is the entire command that was entered. If 'drop sw' were being completed,line
would be set to'drop sw'
.begidx
is index inline
where the last word begins. If 'drop sw' were being completed,begidx
would be5
, which is where'sw'
begins.endidx
is index inline
where the last word begins. If 'drop sw' were being completed,endidx
would be7
, which is where'sw'
ends.
When the Tab key is pressed, the command's
complete_<command>()
method is called and passed the command typed in so far. For example, typing 'drop sw'-Tab will call the complete_drop()
method and typing 'take sw'-Tab will call the complete_take()
method. If a complete_<command>()
method command doesn't exist, such as not complete_qwerty()
method for 'qwerty sw', then the command line interface does nothing.Using the
getAllFirstDescWords()
helper function, you can add complete_take()
and complete_drop()
methods to determine what item the player is trying to take or drop. Add the following to your program:Your program will look like this file on GitHub. When you run the program, you will be able to type 'drop sw', then press Tab, and the command will complete to 'drop sword'.
Looking at Things
The 'look' command will have similar ' will print the name of the room in that direction. The player can also type 'look - ' to look at a item that is either on the ground or in their inventory.
do_look()
and complete_look()
methods. Typing 'look' will print the current room's DEC
key's value. Typing 'look exits' will print the names of all the adjacent rooms, while 'look Enter the following code for the 'look' command:
The program will now look like this file on GitHub.
Shops
Shops are rooms that have a
SHOP
key, such as the Bakery room:The value for the
SHOP
is a list of items that the shop sells. Inside these rooms, the player can run the 'list' command (to see what is for sale), the 'buy' command (to purchase an item), and the 'sell' command (to pawn an item from the player's inventory). For simplicity, any item can be sold to any shop. Also, money is currently not implemented in this game, so items in the shop are free.Several things must be checked when the player tries to run these commands: Is the player currently in a shop room? Did the player forget to specify what they want to buy or sell? Does the shop sell what the player wants to buy? Does the player have the item they want to sell to the shop? The following code implements the 'list', 'buy', and 'sell' commands and addresses all these issues. Add the following code to your program:
When you are finished typing in this code, your program will look like this file on GitHub.
Eating
Some items are marked as edible by having an
EDIBLE
key set to True
For example, Meat Pies, Bagels, and Donuts are all edible items:For simplicity, eating things doesn't do anything other than remove them from your inventory. But you could implement health or hunger levels in your text adventure, and need eat (or drink) items or else have adverse effects. There's nothing new about how the
do_eat()
and complete_eat()
functions work. Add the following code to your program:With the above code added, you will have finished the entire text adventure program. This program can be downloaded from GitHub: https://raw.githubusercontent.com/asweigart/textadventuredemo/master/textadventuredemo.py
Ideas for New Features
That's it for this text adventure tutorial. Because of the
cmd
module's command line interface features, it is fairly easy to add new commands to your game. From here, there are several things you could add to your game:- Add HP, hunger/thirst levels, and status effects. (These are common to RPG-like games.)
- Combat with randomly wandering monsters.
- Casting magic spells, or learning new spells from spellbook items.
- Drinking items, including potions which can have magical effects.
- Equipping items, such as wearing helmets or wielding swords.
- Money, including different types of currencies that shops can accept or deny.
- Several new rooms to expand the world.
If you are interested in creating a roguelike (a genre that is sort of like the Diablo games except with ASCII-art graphics), the libtcod module will be very helpful. There are tutorials here and here.
I detail the differences between these genre of text-based games in my blog post, Text Adventure vs. MUD vs. Roguelike vs. Dwarf Fortress.
You can try playing MUDs you find through The Mud Connector to get new ideas for additional features you'd like to add. Good luck, and have fun!
Active2 years, 10 months ago
Mildly long question, tl;dr at the bottom.
Quite the python beginner here. I am having such a frustrating time trying to get items to interact with the world in this text adventure. I can get the player to pick up items and trade items, however I can't get items to interact with the world. My goal: for a room to require an item in order for the player to continue. As of right now I don't need anything more complicated than just displaying different text, i.e. 'The cell is locked. Find the key!' without the key in the inventory, and 'You unlocked the cell!' with the key in the inventory. Eventually I'll require player input. I know this can be done with a simple 'if item in inventory' statement, but it doesn't seem to be working.
My code for the room:
(edited) My apologies, the code above does not return an error - it simply doesn't work. All that displays is the text 'You enter the cell of your derelict uncle..' even when the key is in the player's inventory. The Player class being referenced:
I have this line in the main game code, so the capitalization is not an issue:
And I have other rooms that use the 'player.inventory' call (to append items, to remove items, etc.) and those work just fine, like the very similar code below:
That code displays the correct text before/after the item is picked up and it appends the item to the player's inventory with no problem.
Eventually I want to make games with more complex 'lock-and-key' puzzles and it's bothering me that I can't do this simple example. Any and all help would be appreciated.
Tl;dr My room displays something like 'The prison cell is locked. The key must be somewhere..' while the player does not have the key. When the player has the key, I want it to display 'You unlocked the cell!' or whatever. Tried 'if item in inventory' to no avail. Frustrated. Please. Help.
Edit:Sorry, this might help. Main game function:
Edit:I'm so sorry, I edited the original question because the code I have displayed above does not give me an error. Definite mistake on my part. That error was taken care of. My issue is that the code flat-out doesn't work the way I want it to. All it returns is the intro text for when the player doesn't have the key (even when they do). It's as similar as I could make it to FindWeaponTile() code which works correctly.
Allie Curtis
Allie CurtisAllie Curtis
1 Answer
Python Text Game Source Code
Instead of using items.CellKey() itself I accessed it's name attribute (a string) and it worked. Thank you all for your help!
Allie CurtisAllie Curtis