Skip to content

Instantly share code, notes, and snippets.

@Pinacolada64
Created November 28, 2021 21:11
Show Gist options
  • Save Pinacolada64/69b911a226caf77b049b0570f8c13ed1 to your computer and use it in GitHub Desktop.
Save Pinacolada64/69b911a226caf77b049b0570f8c13ed1 to your computer and use it in GitHub Desktop.
Modification of Al Swiegart's adventure game demo
import cmd
import textwrap
import logging
import pyreadline3 # for tab-completion and command line editing
# PyCharm had me rename worldItems -> world_items, but camelCase variables are legal in Python
# there is an option in the IDE to ignore camelCase variables, so I set that
# class design resources:
# https://codereview.stackexchange.com/questions/91069/simple-text-rpg-in-python
# https://codereview.stackexchange.com/questions/189654/stats-classes-for-a-text-based-adventure
# __str__ method:
# https://codereview.stackexchange.com/questions/131570/classes-and-maybe-subclasses-for-characters-in-a-python-game
"""
Text Adventure Demo by Al Sweigart ([email protected])
This program is a small text-adventure game that demonstrates the cmd
and textwrap Python modules. You can find other tutorials like this at
http://inventwithpython.com/blog
The tutorial for this game program is located at:
http://inventwithpython.com/blog/2014/12/11/making-a-text-adventure-game-with-the-cmd-and-textwrap-python-modules/
The github repo for this program is at:
https://github.com/asweigart/textadventuredemo
This tutorial does not use classes and object-oriented programming
in order to make it simpler to understand for new programmers. Although
you can see how using dictionaries-in-dictionaries to structures your
data quickly gets unwieldy.
The same goes for using global variables and functions such as inventory,
location, and moveDirection(). These are fine for a small program,
but if you ever want to extend the game they can become burdensome to
work with. But if you are starting out with a toy project, they're fine.
"""
"""
First, we will create some data structures for our game world.
The town looks something like this:
+---------+ +---------+
| Thief O | Bakery |
| Guild | | |
+------++------O--+ +----O----+
| Used |
|Anvils| Town Square +--------+
| O |Obs Deck|
+------++----O----+ +----O----/ /
| Black- O | Wizard / /
| smith | | Tower /
+---------+ +---------+
"""
"""
These constant variables are used because if I mistype them, Python will
immediately throw up an error message since no variable with the typo
name will exist. If we mistyped the strings, the bugs that it produces
would be harder to find.
"""
DESC = 'desc'
NORTH = 'north'
SOUTH = 'south'
EAST = 'east'
WEST = 'west'
UP = 'up'
DOWN = 'down'
GROUND = 'ground'
SHOP = 'shop'
GROUNDDESC = 'grounddesc'
SHORTDESC = 'shortdesc'
LONGDESC = 'longdesc'
TAKEABLE = 'takeable'
EDIBLE = 'edible'
DESC_WORDS = 'DESC_WORDS'
READABLE = 'readable' # ryan added this
SCREEN_WIDTH = 80
"""
The game world data is stored in a dictionary (which itself has dictionaries
and lists). Python's normal rules of indentation are suspended when typing out
a dictionary value until it encounters the closing curly brace, which is
helpful for us to make the dictionary value readable.
Each dictionary value inside the world variable is a different area in the
game world. The key is a string (i.e. 'Town Square') that is the reference
name of the location. It will also be displayed as the title of the area.
The value is another dictionary, which has keys 'desc', 'north', 'south',
'east', 'west', 'up', 'down', 'shop', and 'ground'. We use the constant
variables (e.g. DESC, NORTH, etc.) instead of strings in case we make
typos.
DESC is a text description of the area. SHOP, if it exists, is a list of
items that can be bought at this area. (We don't implement money in this
program, so everything is free.) GROUND is a list of items that are on
the ground in this area. The directions (NORTH, SOUTH, UP, etc.) are the
areas that exist in that direction.
"""
world_rooms = {
'Town Square': {
DESC: 'The town square is a large open space with a fountain in the center. Streets lead in all directions.',
NORTH: 'North Y Street',
EAST: 'East X Street',
SOUTH: 'South Y Street',
WEST: 'West X Street',
GROUND: ['Welcome Sign', 'Fountain']},
'North Y Street': {
DESC: 'The northern end of Y Street has really gone down hill. Potholes'
'are everywhere, as are stray cats, rats, and wombats.',
WEST: 'Thieves Guild',
EAST: 'Bakery',
SOUTH: 'Town Square',
GROUND: ['Do Not Take Sign Sign']},
'Thieves Guild': {
DESC: 'The Thieves Guild is a dark den of unprincipled types. You clutch'
'your purse (though several other people here would like to'
'clutch your purse as well).',
SOUTH: 'West X Street',
EAST: 'North Y Street',
GROUND: ['Lock Picks', 'Silly Glasses']},
'Bakery': {
DESC: '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.',
WEST: 'North Y Street',
SOUTH: 'East X Street',
SHOP: ['Meat Pie', 'Donut', 'Bagel'],
GROUND: ['Shop Howto']},
'West X Street': {
DESC: 'West X Street is the rich section of town.'
'So rich, they paved the streets with gold.'
'This probably was not a good idea.'
'The Thieves\' guild opened up the next day.',
NORTH: 'Thieves Guild',
EAST: 'Town Square',
SOUTH: 'Blacksmith',
WEST: 'Used Anvils Store',
GROUND: []},
'Used Anvils Store': {
DESC: 'The anvil store has anvils of all types and sizes, each'
'previously-owned but still in serviceable condition.'
'However, due to a bug in the way this game is designed,'
'you can buy anvils like any other item and walk around,'
'but if you drop them they cannot be picked up since their'
'TAKEABLE value is set to False. The code should be changed'
'so that it\'s not possible for shops to sell items with'
'TAKEABLE set to False.',
EAST: 'West X Street',
SHOP: ['Anvil'],
GROUND: ['Anvil', 'Anvil', 'Anvil', 'Anvil']},
'East X Street': {
DESC: 'East X Street. It\'s like X Street, except East.',
NORTH: 'Bakery',
WEST: 'Town Square',
SOUTH: 'Wizard Tower',
GROUND: []},
'Blacksmith': {
DESC: 'The blacksmith loudly hammers a new sword over her anvil.'
'Swords, axes, and butter knives all line the walls of her'
'workshop, available for a price.',
NORTH: 'West X Street',
EAST: 'South Y Street',
SHOP: ['Sword', 'War Axe', 'Chainmail T-Shirt'],
GROUND: ['Anvil', 'Shop Howto']},
'South Y Street': {
DESC: 'The Christmas Carolers of South Y Street are famous for all'
'legally changing their name to Carol. They are also famous'
'for singing year-round, in heavy fur coats and wool mittens,'
'even in the summer. That\'s dedication to their craft!',
NORTH: 'Town Square',
WEST: 'Blacksmith',
GROUND: []},
'Wizard Tower': {
DESC: 'Zany magical antics are afoot in the world-famous Wizard Tower.'
'Cauldrons bubble, rats talk, and books float midair in this'
'center of magical discovery.',
NORTH: 'East X Street',
UP: 'Observation Deck',
GROUND: ['Crystal Ball', 'Floating Book', 'Floating Book']},
'Observation Deck': {
DESC: 'You can see the entire town from the top of the Wizard Tower.'
'Everybody looks like ants, especially the people transformed'
'into ants by the wizards of the tower!',
DOWN: 'Wizard Tower',
UP: 'Magical Escalator to Nowhere',
GROUND: ['Telescope']},
'Magical Escalator to Nowhere': {
DESC: "No matter how much you climb the escalator, it doesn't seem to"
"be getting you anywhere.",
UP: 'Magical Escalator to Nowhere',
DOWN: 'Observation Deck',
GROUND: []},
}
"""
This is the index of all possible items in the game world. Note that These
key-value pairs are more like blueprints than actual items. The actual
items exist in the GROUND value in an area's entry in the world variable.
The GROUNDDESC value is a short string that displays in the area's description.
The SHORTDESC value is a short string that will be used in sentences like, "You
drop X." or "You buy X."
The LONGDESC value is displayed when the player looks at the item.
The TAKEABLE Boolean value is True if the player can pick up the item and put
it in their inventory.
The DESC_WORDS value is a list of strings that can be used in the player's
commands. For example, if this is ['welcome', 'sign'] then the player can type
a command such as "take sign" or "look welcome".
The TAKEABLE value is True if the item can be picked up off the ground. If
this key doesn't exist, it defaults to True.
The EDIBLE value is True if the item can be eaten. If this key doesn't exist,
it defaults to False.
rs:
The READABLE value is True if the item can be read (like "Sign").
"""
world_items = {
'Welcome Sign': {
GROUNDDESC: 'A welcome sign stands here.',
SHORTDESC: 'a welcome sign',
LONGDESC: """
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.
""",
TAKEABLE: False,
READABLE: True,
DESC_WORDS: ['welcome', 'sign']},
'Do Not Take Sign Sign': {
GROUNDDESC: 'A sign stands here, not bolted to the ground.',
SHORTDESC: 'a sign',
LONGDESC: 'The sign reads, "Do Not Take This Sign"',
DESC_WORDS: ['sign']},
'Fountain': {
GROUNDDESC: 'A bubbling fountain of green water.',
SHORTDESC: 'a fountain',
LONGDESC: 'The water in the fountain is a bright green color. Is that... Gatorade?',
TAKEABLE: False,
DESC_WORDS: ['fountain']},
'Sword': {
GROUNDDESC: 'A sword lies on the ground.',
SHORTDESC: 'a sword',
LONGDESC: 'A long sword, engraved with the word, "Exkaleber." (Obviously a cheap knock-off.)',
DESC_WORDS: ['sword', 'exkaleber', 'longsword']},
'War Axe': {
GROUNDDESC: 'A mighty war axe lies on the ground.',
SHORTDESC: 'a war axe',
LONGDESC: 'The mighty war axe is made with antimony impurities from a fallen star,'
'rendering it surprisingly brittle.',
DESC_WORDS: ['axe', 'war', 'mighty']},
'Chainmail T-Shirt': {
GROUNDDESC: 'A chainmail t-shirt lies wadded up on the ground.',
SHORTDESC: 'a chainmail t-shirt',
LONGDESC: 'The chainmail t-shirt has a slogan and arrow engraved on the front: "I\'m with Stupid"',
DESC_WORDS: ['chainmail', 'chain', 'mail', 't-shirt', 'tshirt', 'stupid']},
'Anvil': {
GROUNDDESC: 'The blacksmith\'s anvil, far too heavy to pick up, rests in the corner.',
SHORTDESC: 'an anvil',
LONGDESC: 'The black anvil has the word "ACME" engraved on the side.',
TAKEABLE: False,
DESC_WORDS: ['anvil']},
'Lock Picks': {
GROUNDDESC: 'A set of lock picks lies on the ground.',
SHORTDESC: 'a set of lock picks',
LONGDESC: 'A set of fine picks for picking locks.',
DESC_WORDS: ['lockpicks', 'picks', 'set']},
'Silly Glasses': {
GROUNDDESC: 'A pair of those silly gag glasses with the nose and fake mustache rest on the ground.',
SHORTDESC: 'a pair of silly fake mustache glasses',
LONGDESC: 'These glasses have a fake nose and mustache attached to them. The perfect disguise!',
DESC_WORDS: ['glasses', 'silly', 'fake', 'mustache']},
'Meat Pie': {
GROUNDDESC: 'A suspicious meat pie rests on the ground.',
SHORTDESC: 'a meat pie',
LONGDESC: 'A meat pie. It tastes like chicken.',
EDIBLE: True,
DESC_WORDS: ['pie', 'meat']},
'Bagel': {
GROUNDDESC: 'A bagel rests on the ground. (Gross.)',
SHORTDESC: 'a bagel',
LONGDESC: 'It is a donut-shaped bagel.',
EDIBLE: True,
DESC_WORDS: ['bagel']},
'Donut': {
GROUNDDESC: 'A donut rests on the ground. (Gross.)',
SHORTDESC: 'a donut',
LONGDESC: 'It is a bagel-shaped donut.',
EDIBLE: True,
DESC_WORDS: ['donut']},
'Crystal Ball': {
GROUNDDESC: 'A glowing crystal ball rests on a small pillow.',
SHORTDESC: 'a crystal ball',
LONGDESC: 'The crystal ball swirls with mystical energy, forming the words'
'"Answer Unclear. Check Again Later."',
DESC_WORDS: ['crystal', 'ball']},
'Floating Book': {
GROUNDDESC: 'A magical book floats here.',
SHORTDESC: 'a floating book',
LONGDESC: "This magical tome doesn't have a lot of pictures in it. Boring!",
READABLE: True,
DESC_WORDS: ['book', 'floating']},
'Telescope': {
GROUNDDESC: 'A telescope is bolted to the ground.',
SHORTDESC: 'a telescope',
LONGDESC: 'Using the telescope, you can see your house from here!',
TAKEABLE: False,
DESC_WORDS: ['telescope']},
'README Note': {
GROUNDDESC: 'A note titled "README" rests on the ground.',
SHORTDESC: 'a README note',
LONGDESC: '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."',
EDIBLE: True,
READABLE: True,
DESC_WORDS: ['readme', 'note']},
'Shop Howto': {
GROUNDDESC: 'A "Shopping HOWTO" note rests on the ground.',
SHORTDESC: 'a shopping howto',
LONGDESC: 'The note reads, "When you are at a shop, you can type'
'"list" to show what is for sale. "buy <item>" will'
'add it to your inventory, or you can sell an item'
'in your inventory with "sell <item>". (Currently,'
'money is not implemented in this program.)',
READABLE: True,
EDIBLE: True,
DESC_WORDS: ['howto', 'note', 'shop']},
}
"""
These variables track where the player is and what is in their inventory.
The value in the location variable will always be a key in the world variable
and the value in the inventory list will always be a key in the world_items
variable.
"""
location = 'Town Square' # start in town square
inventory = ['README Note', 'Sword', 'Donut'] # start with 3 items
showFullExits = True
def comma_separated(value, right_justify=True):
"""
Convert int to a comma-separated string with "," as a grouping separator
Optionally right-justify the output
:param value: 1 - 999,999,999
:param right_justify: True: include leading spaces to right-align string
:return: string to print
by ryan
"""
# However, standard library also includes these functions:
# >>> print("{:,}".format(3024))
# 3,024
# >>> right-justify numbers:
# >>> print("{:>11}".format("{:,}".format(987654321)))
# 987,654,321
# >>> print("{:>11}".format("{:,}".format(100)))
# 100
v = value
if v < 1 or v > 999999999:
return ValueError
digits = []
for d in str(v):
digits.append(d)
# reverse list so we can start from tens place, working to the left
digits.reverse()
logging.info(digits)
output = []
count = 0
for d in digits:
# this is really weird, but these next 3 lines need to be in this order to work
count += 1
if count == 4 or count == 7: # and not 3 or 6...
output.append(",")
output.append(d)
# logging.info(f"intermediate: {output}")
# swap the reversed number around again to make it readable:
output.reverse()
# convert list to string:
string = ''.join(output)
logging.info(f"string: {string}")
if right_justify:
return " "[:11 - len(string)] + string
return string
def display_location(loc):
"""A helper function for displaying an area's description and exits."""
# Print the room name.
print(loc)
print('=' * len(loc))
# Print the room's description (using textwrap.wrap())
print('\n'.join(textwrap.wrap(world_rooms[loc][DESC], SCREEN_WIDTH)))
# Print all the items on the ground.
if len(world_rooms[loc][GROUND]) > 0:
print()
for item in world_rooms[loc][GROUND]:
print(world_items[item][GROUNDDESC])
# Print all the exits.
exits = []
for direction in (NORTH, SOUTH, EAST, WEST, UP, DOWN):
if direction in world_rooms[loc].keys():
exits.append(direction.title())
print()
if showFullExits:
for direction in (NORTH, SOUTH, EAST, WEST, UP, DOWN):
if direction in world_rooms[location]:
print('%s: %s' % (direction.title(), world_rooms[location][direction]))
else:
print('Exits: %s' % ' '.join(exits))
def move_direction(direction):
"""A helper function that changes the location of the player."""
global location
if direction in world_rooms[location]:
print(f'You move to the {direction}.')
location = world_rooms[location][direction]
display_location(location)
else:
print('You cannot move in that direction')
def get_all_desc_words(item_list):
"""Returns a list of "description words" for each item named in item_list."""
item_list = list(set(item_list)) # make item_list unique
description_words = []
for item in item_list:
description_words.extend(world_items[item][DESC_WORDS])
return list(set(description_words))
def get_all_first_desc_words(item_list):
"""Returns a list of the first "description word" in the list of
description words for each item named in item_list."""
item_list = list(set(item_list)) # make item_list unique
DESC_WORDS = []
for item in item_list:
DESC_WORDS.append(world_items[item][DESC_WORDS][0])
return list(set(DESC_WORDS))
def get_first_item_matching_desc(desc, item_list):
"""
:param desc: Given a string...
:param item_list: ...and a list of item names...
:return: (string) ...return name of the first item that has a DESCWORD
string matching "param".
"""
item_list = list(set(item_list)) # make item_list unique
for item in item_list:
if desc in world_items[item][DESC_WORDS]:
return item
return None
def get_all_items_matching_desc(desc, item_list):
item_list = list(set(item_list)) # make item_list unique
logging.info(f"get_all_items_matching_desc(): {item_list}")
matching_items = []
for item in item_list:
if desc in world_items[item][DESC_WORDS]:
matching_items.append(item)
return matching_items
class TextAdventureCmd(cmd.Cmd):
prompt = '\n> '
completekey = 'tab'
# The default() method is called when none of the other do_*() command methods match.
def default(self, arg):
print('I do not understand that command. Type "help" for a list of commands.')
# A very simple "quit" command to terminate the program:
def do_quit(self, arg):
"""Quit the game."""
return True # this exits the Cmd application loop in TextAdventureCmd.cmdloop()
def help_combat(self):
print('Combat is not implemented in this program.')
# These direction commands have a long (i.e. north) and show (i.e. n) form.
# Since the code is basically the same, I put it in the move_direction()
# function.
def do_north(self, arg):
"""Go to the area to the north, if possible."""
move_direction('north')
def do_south(self, arg):
"""Go to the area to the south, if possible."""
move_direction('south')
def do_east(self, arg):
"""Go to the area to the east, if possible."""
move_direction('east')
def do_west(self, arg):
"""Go to the area to the west, if possible."""
move_direction('west')
def do_up(self, arg):
"""Go to the area upwards, if possible."""
move_direction('up')
def do_down(self, arg):
"""Go to the area downwards, if possible."""
move_direction('down')
# Since the code is the exact same, we can just copy the
# methods with shortened names:
do_n = do_north
do_s = do_south
do_e = do_east
do_w = do_west
do_u = do_up
do_d = do_down
# rs: added the alias 'i' for 'inventory'
# do_i = do_inventory
def do_exits(self, arg):
"""Toggle showing full exit descriptions or brief exit descriptions."""
global showFullExits
showFullExits = not showFullExits
if showFullExits:
print('Showing full exit descriptions.')
else:
print('Showing brief exit descriptions.')
def do_inventory(self, arg):
"""Display a list of the items in your possession."""
print('Inventory:')
if len(inventory) == 0:
print(' (nothing)')
return
# first get a count of each distinct item in the inventory
item_count = {}
for item in inventory:
if item in item_count.keys():
item_count[item] += 1
else:
item_count[item] = 1
# get a list of inventory items with duplicates removed:
for item in set(inventory):
if item_count[item] > 1:
print(' %s (%s)' % (item, item_count[item]))
else:
print(' ' + item)
def do_take(self, arg):
""""take <item>" - Take an item on the ground."""
# TODO: put this value in a more suitably named variable
item_to_take = arg.lower()
if item_to_take == '':
print('Take what? Type "look" the items on the ground here.')
return
cant_take = False
# get the item name that the player's command describes
for item in get_all_items_matching_desc(item_to_take, world_rooms[location][GROUND]):
if world_items[item].get(TAKEABLE, True) is False:
cant_take = True
continue # there may be other items named this that you can take, so we continue checking
print('You take %s.' % world_items[item][SHORTDESC])
world_rooms[location][GROUND].remove(item) # remove from the ground
inventory.append(item) # add to inventory
return
if cant_take:
print('You cannot take "%s".' % item_to_take)
else:
print('That is not on the ground.')
def do_drop(self, arg):
""""drop <item> - Drop an item from your inventory onto the ground."""
# put this value in a more suitably named variable
item_to_drop = arg.lower()
# get a list of all "description words" for each item in the inventory
invdesc_words = get_all_desc_words(inventory)
# find out if the player doesn't have that item
if item_to_drop not in invdesc_words:
print('You do not have "%s" in your inventory.' % item_to_drop)
return
# get the item name that the player's command describes
item = get_first_item_matching_desc(item_to_drop, inventory)
if item is not None:
print('You drop %s.' % (world_items[item][SHORTDESC]))
inventory.remove(item) # remove from inventory
world_rooms[location][GROUND].append(item) # add to the ground
def complete_take(self, text, line, begidx, endidx):
possible_items = []
text = text.lower()
# if the user has only typed "take" but no item name:
if not text:
return get_all_first_desc_words(world_rooms[location][GROUND])
# otherwise, get a list of all "description words" for ground items matching the command text so far:
for item in list(set(world_rooms[location][GROUND])):
for desc_word in world_items[item][DESC_WORDS]:
if desc_word.startswith(text) and world_items[item].get(TAKEABLE, True):
possible_items.append(desc_word)
return list(set(possible_items)) # make list unique
def complete_drop(self, text, line, begidx, endidx):
possible_items = []
item_to_drop = text.lower()
# get a list of all "description words" for each item in the inventory
invdesc_words = get_all_desc_words(inventory)
for desc_word in invdesc_words:
if line.startswith('drop %s' % desc_word):
return [] # command is complete
# if the user has only typed "drop" but no item name:
if item_to_drop == '':
return get_all_first_desc_words(inventory)
# otherwise, get a list of all "description words" for inventory items matching the command text so far:
for desc_word in invdesc_words:
if desc_word.startswith(text):
possible_items.append(desc_word)
return list(set(possible_items)) # make list unique
def do_look(self, arg):
"""
Look at an item, direction, or the area:
"look" - display the current area's description
"look <direction>" - display the description of the area in that direction
"look exits" - display the description of all adjacent areas
"look <item>" - display the description of an item on the ground or in your inventory"""
looking_at = arg.lower()
if looking_at == '':
# "look" will re-print the area description
display_location(location)
return
if looking_at == 'exits':
for direction in (NORTH, SOUTH, EAST, WEST, UP, DOWN):
if direction in world_rooms[location]:
print('%s: %s' % (direction.title(), world_rooms[location][direction]))
return
if looking_at in ('north', 'west', 'east', 'south', 'up', 'down', 'n', 'w', 'e', 's', 'u', 'd'):
if looking_at.startswith('n') and NORTH in world_rooms[location]:
print(world_rooms[location][NORTH])
elif looking_at.startswith('w') and WEST in world_rooms[location]:
print(world_rooms[location][WEST])
elif looking_at.startswith('e') and EAST in world_rooms[location]:
print(world_rooms[location][EAST])
elif looking_at.startswith('s') and SOUTH in world_rooms[location]:
print(world_rooms[location][SOUTH])
elif looking_at.startswith('u') and UP in world_rooms[location]:
print(world_rooms[location][UP])
elif looking_at.startswith('d') and DOWN in world_rooms[location]:
print(world_rooms[location][DOWN])
else:
print('There is nothing in that direction.')
return
# see if the item being looked at is on the ground at this location
item = get_first_item_matching_desc(looking_at, world_rooms[location][GROUND])
# rs added check for readable item - if True, use "read <item>" instead
# with .get(<key>, <default>) is <bool> syntax, <default> is the return value
# if <key> does not exist. therefore <default> and <bool> are separate entities
if world_items[item].get(READABLE, False) is True:
print('(This is a readable item. Try "read <item>" instead.)')
return
if item is not None:
print('\n'.join(textwrap.wrap(world_items[item][LONGDESC], SCREEN_WIDTH)))
return
# see if the item being looked at is in the inventory
item = get_first_item_matching_desc(looking_at, inventory)
if item is not None:
print('\n'.join(textwrap.wrap(world_items[item][LONGDESC], SCREEN_WIDTH)))
return
print('You do not see that nearby.')
def complete_look(self, text, line, begidx, endidx):
possible_items = []
looking_at = text.lower()
# get a list of all "description words" for each item in the inventory
invdesc_words = get_all_desc_words(inventory)
grounddesc_words = get_all_desc_words(world_rooms[location][GROUND])
shopdesc_words = get_all_desc_words(world_rooms[location].get(SHOP, []))
for desc_word in invdesc_words + grounddesc_words + shopdesc_words + [NORTH, SOUTH, EAST, WEST, UP, DOWN]:
if line.startswith('look %s' % (desc_word)):
return [] # command is complete
# if the user has only typed "look" but no item name, show all items on ground, shop and directions:
if looking_at == '':
possible_items.extend(get_all_first_desc_words(world_rooms[location][GROUND]))
possible_items.extend(get_all_first_desc_words(world_rooms[location].get(SHOP, [])))
for direction in (NORTH, SOUTH, EAST, WEST, UP, DOWN):
if direction in world_rooms[location]:
possible_items.append(direction)
return list(set(possible_items)) # make list unique
# otherwise, get a list of all "description words" for ground items matching the command text so far:
for desc_word in grounddesc_words:
if desc_word.startswith(looking_at):
possible_items.append(desc_word)
# otherwise, get a list of all "description words" for items for sale at the shop (if this is one):
for desc_word in shopdesc_words:
if desc_word.startswith(looking_at):
possible_items.append(desc_word)
# check for matching directions
for direction in (NORTH, SOUTH, EAST, WEST, UP, DOWN):
if direction.startswith(looking_at):
possible_items.append(direction)
# get a list of all "description words" for inventory items matching the command text so far:
for desc_word in invdesc_words:
if desc_word.startswith(looking_at):
possible_items.append(desc_word)
return list(set(possible_items)) # make list unique
def do_read(self, arg):
"""
Read an item:
"read <item>" - read the text of an item in room, or in your inventory
"""
# rs added this function
to_read = arg.lower()
# see if the item being read is in room (GROUND) or inventory:
# can we combine the functions?
object_here = get_first_item_matching_desc(to_read, world_rooms[location][GROUND])
logging.info(f"do_read(arg={arg}): to_read={to_read}, object_here={object_here}")
# + get_all_items_matching_desc(reading, world_rooms[location])
if object_here is None:
print('You do not see that nearby.')
return
logging.info(f"Readable: {world_items[object_here].get(READABLE)}")
# dict.get(key, default) returns whether <key> is in dict
# (returning <default> if <key> is not in dict),
# and returns True, False (or None if <key> not in dict)
if world_items[object_here].get(READABLE, False) is False:
print(f"You can't make out any text to read on the {world_items[object_here][SHORTDESC]}.")
return
print(f"You read {world_items[object_here][SHORTDESC]} and see:")
print('\n'.join(textwrap.wrap(world_items[object_here][LONGDESC], SCREEN_WIDTH)))
return
# see if the item being read at is in the inventory
object_here = get_first_item_matching_desc(to_read, inventory)
if object_here is not None:
print(f"You read {world_items[object_here][SHORTDESC]} and see:")
print('\n'.join(textwrap.wrap(world_items[object_here][LONGDESC], SCREEN_WIDTH)))
return
def do_list(self, arg):
"""List the items for sale at the current location's shop. "list full" will show details of the items."""
if SHOP not in world_rooms[location]:
print('This is not a shop.')
return
arg = arg.lower()
print('For sale:')
for item in world_rooms[location][SHOP]:
print(' - %s' % item)
if arg == 'full':
print('\n'.join(textwrap.wrap(world_items[item][LONGDESC], SCREEN_WIDTH)))
def do_buy(self, arg):
""""buy <item>" - buy an item at the current location's shop."""
if SHOP not in world_rooms[location]:
print('This is not a shop.')
return
item_to_buy = arg.lower()
if item_to_buy == '':
print('Buy what? Type "list" or "list full" to see a list of items for sale.')
return
item = get_first_item_matching_desc(item_to_buy, world_rooms[location][SHOP])
if item is not None:
# TODO: If you wanted to implement money, here is where you would add
# code that checks if the player has enough, then deducts the price
# from their money.
print(f'You have purchased {world_items[item][SHORTDESC]}.')
inventory.append(item)
return
print(f'"{item_to_buy}" is not sold here. Type "list" or "list full" to see a list of items for sale.')
def complete_buy(self, text, line, begidx, endidx):
if SHOP not in world_rooms[location]:
return []
item_to_buy = text.lower()
possible_items = []
# if the user has only typed "buy" but no item name:
if not item_to_buy:
return get_all_first_desc_words(world_rooms[location][SHOP])
# otherwise, get a list of all "description words" for shop items matching the command text so far:
for item in list(set(world_rooms[location][SHOP])):
for desc_word in world_items[item][DESC_WORDS]:
if desc_word.startswith(text):
possible_items.append(desc_word)
return list(set(possible_items)) # make list unique
def do_sell(self, arg):
""""sell <item>" - sell an item at the current location's shop."""
# TODO rs: put "shop type" flags on shops and items so you cannot sell
# a sword in a bakery
if SHOP not in world_rooms[location]:
print('This is not a shop.')
return
item_to_sell = arg.lower()
if item_to_sell == '':
print('Sell what? Type "inventory" or "inv" to see your inventory.')
return
for item in inventory:
if item_to_sell in world_items[item][DESC_WORDS]:
# NOTE - If you wanted to implement money, here is where you would add
# code that gives the player money for selling the item.
print(f'You have sold {world_items[item][SHORTDESC]}.')
inventory.remove(item)
return
print(f'You do not have "{item_to_sell}". Type "inventory" or "inv" to see your inventory.')
def complete_sell(self, text, line, begidx, endidx):
if SHOP not in world_rooms[location]:
return []
item_to_sell = text.lower()
possible_items = []
# if the user has only typed "sell" but no item name:
if not item_to_sell:
return get_all_first_desc_words(inventory)
# otherwise, get a list of all "description words" for inventory items matching the command text so far:
for item in list(set(inventory)):
for desc_word in world_items[item][DESC_WORDS]:
if desc_word.startswith(text):
possible_items.append(desc_word)
return list(set(possible_items)) # make list unique
def do_eat(self, arg):
""""eat <item>" - eat an item in your inventory."""
item_to_eat = arg.lower()
if item_to_eat == '':
print('Eat what? Type "inventory" or "inv" to see your inventory.')
return
cant_eat = False
for item in get_all_items_matching_desc(item_to_eat, inventory):
if world_items[item].get(EDIBLE, False) is False:
cant_eat = True
continue # there may be other items named this that you can eat, so we continue checking
# TODO: If you wanted to implement hunger levels, here is where
# you would add code that changes the player's hunger level.
print('You eat %s' % (world_items[item][SHORTDESC]))
inventory.remove(item)
return
if cant_eat:
print('You cannot eat that.')
else:
print(f'You do not have "{item_to_eat}". Type "inventory" or "inv" to see your inventory.')
def complete_eat(self, text, line, begidx, endidx):
item_to_eat = text.lower()
possible_items = []
# if the user has only typed "eat" but no item name:
if item_to_eat == '':
return get_all_first_desc_words(inventory)
# otherwise, get a list of all "description words" for edible inventory items matching the command text so far:
for item in list(set(inventory)):
for desc_word in world_items[item][DESC_WORDS]:
if desc_word.startswith(text) and world_items[item].get(EDIBLE, False):
possible_items.append(desc_word)
return list(set(possible_items)) # make list unique
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] - %(message)s')
print('Text Adventure Demo!')
print('====================')
print()
print('(Type "help" for commands.)')
print()
display_location(location)
TextAdventureCmd().cmdloop()
print('Thanks for playing!')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment