24
\$\begingroup\$

I've been using Python for around 3 months and decided to make a simple text adventure game for fun and to exercise a lot of ideas I've been learning about. I've just recently learned about classes and OOP, as well as modules so those are the two features I'm the least sure of. It's mostly a base and has two rooms set up, but I'm pretty proud of it so far as I feel like its fairly readable and has good style (though I admittedly have nothing to compare it with.) So far I have four modules that I've distinguished from each other, mostly for readability. My use of classes is shallow, let me know how I could improve on it! I have two rooms set up so far.

Main module:

 """ This is the main file, it contains the functions for the rooms as well as the startup sequences."""

import actions
import room_info as rooms
import character_and_items as char

"""TODO LIST:
-Implement save features(Save module?)
-Add some spunk into the descriptions, make it fun yet concise!!
"""

# output
def print_frog_title():
    print("   ____                ___     __              __              ")
    print("  / __/______  ___ _  / _ |___/ /  _____ ___  / /___ _________ ")
    print(" / _// __/ _ \/ _ `/ / __ / _  / |/ / -_) _ \/ __/ // / __/ -_)")
    print("/_/ /_/  \___/\_, / /_/ |_\_,_/|___/\__/_//_/\__/\_,_/_/  \__/ ")
    print("             /___/                                             ")

# input
def check_start():
    print_frog_title()
    print("                 A game by Mopi Productions ")
    print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
    print("For a list of commands type 'h', 'help', or '?'")


def load_room(room):
    char.current_room = room
    room_dict = {
        "burrow": burrow,
        "burrow_basement": burrow_basement,
        "front_of_house": front_of_house
    }
    func = room_dict.get(room, lambda: "ERROR")  # To be quite frank I don't really understand lambda: ERROR yet
    func()


def burrow():
    print("You are in your burrow. The front door is to the north.")
    while True:
        action = actions.get_action(rooms.burrow)
        if action[0] == "mopi":
            load_room(action[1])
        if action == "down":
            if rooms.burrow.variables["trapdoor_opened"] and "lantern" in char.character_inventory:
                load_room("burrow_basement")
                break
            elif not rooms.burrow.variables["rug_moved"]:
                print("You cant go that way!")
            elif rooms.burrow.variables["rug_moved"] and not rooms.burrow.variables["trapdoor_opened"]:
                print("The trapdoor is shut.")
            elif "lantern" not in char.character_inventory:
                print("It's far too dark down there, you should stay up here until you find a light source.")
        elif action == "north":
            if rooms.burrow.variables["door_unlocked"]:
                load_room("front_of_house")
            else:
                print("The front door is locked!")
        elif action in rooms.directions:
            print("You cant go that way!")
        elif action[0] == "examine":
            if action[1] in ("rug", "carpet", "lump"):
                if not rooms.burrow.variables["rug_moved"]:
                    print("Upon further examination, the 'lump' appears to be handle shaped. You should try moving "
                          "the rug.")
                else:
                    print("Your beloved family heirloom has been rudely shoved against a wall. What would Aunt Frogatha think?")
            elif action[1] == "door":
                print("Your door is locked. You'd unlock it, but you've lost the key.")
            elif action[1] == "cabinet":
                if rooms.burrow.variables["cabinet_opened"]:
                    if rooms.burrow.variables["jar_taken"]:
                        print("It's an open cabinet with barren shelves.")
                    else:
                        print("It's an open cabinet with barren shelves excepting a single empty jar.")
                else:
                    print("It's a closed cabinet where you keep your dishware.")
        elif action[0] == "move":
            if action[1] in ("rug", "carpet"):
                if not rooms.burrow.variables["rug_moved"]:
                    print("You moved the rug, and underneath you found a trapdoor! This isn't a new discovery.")
                    rooms.burrow.variables["rug_moved"] = True
                else:
                    print("You've already kicked the rug into the corner! Better to not do any more damage.")
        elif action[0] == "use":
            if action[1] == "trapdoor" and rooms.burrow.variables["rug_moved"]:
                print("You opened the trapdoor.")
                rooms.burrow.variables["trapdoor_opened"] = True
            elif action[1] == "door":
                if not rooms.burrow.variables["door_unlocked"] and "key" not in char.character_inventory:
                    print("The door is locked, and you seem to have misplaced your key.")
                elif not rooms.burrow.variables["door_unlocked"] and "key" in char.character_inventory:
                    print("The door is locked, you should using your key.")
                else:
                    load_room("burrow_basement")
                    break
        elif action[0] == "open":
            if action[1] == "trapdoor" and rooms.burrow.variables["rug_moved"]:
                print("You opened the trapdoor.")
                rooms.burrow.variables["trapdoor_opened"] = True
            elif action[1] == "door":
                if not rooms.burrow.variables["door_unlocked"] and "key" in char.character_inventory:
                    print("You unlocked the front door! Welcome to the outside ")
                elif "key" not in char.character_inventory:
                    print("The door is locked, and you seem to have misplaced your key.")
                else:
                    print("The door is already open!")
            elif action[1] == "cabinet":
                if not rooms.burrow.variables["jar_taken"]:
                    print("You opened the cabinet, there is an empty jar inside.")
                    rooms.burrow.room_contents.append("jar")
                else:
                    print("You opened the cabinet, it is empty")
        elif action[0] == "close":
            if action[1] == "trapdoor" and rooms.burrow.variables["rug_moved"]:
                print("You closed the trapdoor.")
                rooms.burrow.variables["trapdoor_opened"] = False
            elif action[1] == "door":
                print("The door is already closed, you like it that way.")
        else:
            continue

def burrow_basement():
    print("You are in your basement. There is a trapdoor above you.")
    while True:
        action = actions.get_action(rooms.burrow_basement)
        if action == "up":
            if rooms.burrow.variables["trapdoor_opened"]:
                load_room("burrow")
                break
            else:
                print("The trapdoor is shut.")
        elif action in rooms.directions:
            print("You can't go that way!")
        elif action[0] == "examine":
            if action[1] == "key_hook":
                if "key" in rooms.burrow_basement.room_contents:
                    print("It is a handmade key hook your Uncle Frogert made. Its single hook remains unoccupied.")
                else:
                    print("It is handmade key hook your Uncle Frogert made. It holds an old key.")
            elif action[1] == "icebox":
                print("It is an icebox where you store your cold foods, but it hasn't been filled in months.")
        elif action[0] == "move":
            if action[1] == "icebox" and not rooms.burrow_basement.variables["icebox_moved"]:
                pass
            elif action[1] == "icebox":
                pass
        elif action[0] == "use":
            if action[1] == "icebox" and not rooms.burrow_basement.variables["icebox_opened"]:
                rooms.burrow_basement.variables["icebox_opened"] = True
                if not rooms.burrow_basement.variables["fish_sticks_taken"]:
                    print("You opened the icebox, there's a lonely box of fish sticks inside with colorful writing on it.")
                    rooms.burrow_basement.room_contents.append("fish_sticks")
                else:
                    print("You opened the icebox, it's empty devoid of a few lonely ice cubes. ")
            elif action[1] == "icebox":
                print("You closed the icebox like a good roommate.")
                rooms.burrow_basement.variables["icebox_opened"] = False
                if "fish_sticks" in rooms.burrow_basement.room_contents and not rooms.burrow.variables["fish_sticks_taken"]:
                    rooms.burrow_basement.room_contents.remove("fish_sticks")
        elif action[0] == "open":
            if action[1] == "trapdoor" and not rooms.burrow.variables["trapdoor_opened"]:
                print("You opened the trapdoor.")
                rooms.burrow.variables["trapdoor_opened"] = True
            elif action[1] == "trapdoor":
                print("The trapdoor is already opened.")
            elif action[1] == "icebox" and not rooms.burrow_basement.variables["icebox_opened"]:
                rooms.burrow_basement.variables["icebox_opened"] = True
                if not rooms.burrow_basement.variables["fish_sticks_taken"]:
                    print("You opened the icebox, there's a lonely box of fish sticks inside with colorful writing on it.")
                    rooms.burrow_basement.room_contents.append("fish_sticks")
                else:
                    print("You opened the icebox, it's empty devoid of a few lonely ice cubes. ")
        elif action[0] == "close":
            if action[1] == "trapdoor" and rooms.burrow.variables["rug_moved"]:
                print("You closed the trapdoor.")
                rooms.burrow.variables["trapdoor_opened"] = False
            elif action[1] == "icebox" and rooms.burrow_basement.variables["icebox_opened"]:
                print("You closed the icebox like a good roommate.")
                rooms.burrow_basement.variables["icebox_opened"] = False
                if "fish_sticks" in rooms.burrow_basement.room_contents and not rooms.burrow.variables["fish_sticks_taken"]:
                    rooms.burrow_basement.room_contents.remove("fish_sticks")
        else:
            continue

def front_of_house():
    print("You are in front of your burrow")
    while True:
        break

"""
def room_template():
    print("ROOM. EXITS")
    while True:
        action = actions.get_action(rooms.ROOM)
        if action[0] == "mopi":
            load_room(action[1])
        if action == DIRECTION:

        elif action in rooms.directions:
            print("You cant go that way!")
        elif action[0] == "examine":

        elif action[0] == "move":

        elif action[0] == "use":

        elif action[0] == "open":

        elif action[0] == "close":

        else:
            continue
"""

def main():
    check_start()
    print(
        "You are in a burrow, but not just any burrow. The burrow you reside in is in fact"
        "\nthe estate of Von Frogerick III, who just so happens to be your great great grandfather."
        "\nThe immense and fascinating history of your lineage matters not though, for you are hungry."
        "\nYou should find a fly to eat.")
    load_room("burrow")


main()

Actions module:

  """This is the actions module, it contains functions required to process user input and
    processes minor actions itself"""

import random
import room_info as rooms
import character_and_items as char

snarky_remark_list = ["And how exactly are you going to do that?", "Fat chance", "In your dreams", "Inconceivable!",
                      "Aunt Frogatha would be ashamed.."]

# Commands to add, - eat - anything else?

# output
def print_help():
    print("--HELP--\n-'l' or 'look' - Provides description of room\n-'north' or 'n' - goes in specified direction"
          "\n-'examine' + object to examine it more closely\n-'move' + object to move an object"
          "\n-'use' + object to use an object\n-'down' or 'd' - move up or down"
          "\n-'inventory' or 'i' - displays inventory\n-'open' or 'close' - opens or closes an object"
          "\n-'read' - reads an object\n-'use __ on __' - uses one object on another")


# this checks if the complex action is able to be performed on the object given
def actions_match(complex_u_in, room):
    if (complex_u_in[0] == "take" and complex_u_in[1] in rooms.take_ob(room)) \
            or (complex_u_in[0] == "use" and complex_u_in[1] in rooms.use_ob(room)) \
            or (complex_u_in[0] == "examine" and complex_u_in[1] in rooms.exam_ob(room)) \
            or (complex_u_in[0] == "move" and complex_u_in[1] in rooms.mov_ob(room)) \
            or (complex_u_in[0] in ("open", "close") and complex_u_in[1] in rooms.open_ob(room)):
        return True
    else:
        return False

# Consider moving this into the items module?
def use_on(obj1, obj2):
    if obj1 == "key" and char.current_room == "burrow" and "key" in char.character_inventory and obj2 == "door":
        rooms.burrow.variables["door_unlocked"] = True
        print("You unlocked the door!")

# Sort objects under the world class into an array.
def get_room_contents(room):
    return rooms.exam_ob(room) + rooms.use_ob(room) + rooms.mov_ob(room) + rooms.take_ob(room) + rooms.open_ob(room)


# Every action that doesn't return an input should be ended with return False
# a complex action is an action containing more than one word.
def check_complex_actions(room, complex_u_in):
    room_contents_list = get_room_contents(room)

    # relabeled for readability
    action = complex_u_in[0]
    item = complex_u_in[1]

    try:
        # Dev command to load rooms for testing
        if action == "mopi" and item in rooms.all:
            return ["mopi", item]
        # Dropping items is a character based action, and not a room based. Therefore it is separated.
        if len(complex_u_in) == 4 and (action == "use" and complex_u_in[2] == "on") \
                and (char.use_on_items[item] == complex_u_in[3]):
            use_on(item, complex_u_in[3])
            return False
        elif len(complex_u_in) == 3 and (item + "_" + complex_u_in[2]) in char.double_word_list:
            item = item + "_" + complex_u_in[2]
            complex_u_in.pop()

        if action == "drop" and item in char.character_inventory:
            rooms.append_to_room(room, item)
            char.character_inventory.remove(item)
            print("You dropped the " + item)
            return False
        elif action == "take" and item in rooms.take_ob(room):
            if item in rooms.contents(room):
                char.character_inventory.append(item)
                rooms.remove_from_room(room, item)
                # This removes the underscore in two-word objects
                if "_" in item:
                    item = item.replace("_", " ")
                print("You took the " + item)
                return False
            else:
                print("You see no such thing!")
                return False
        elif action == "read" and (item in room_contents_list or item in char.character_inventory):
            item_description = char.get_description(item, "read")
            if item_description != "":
                if "_" in item:
                    item = item.replace("_", " ")
                print("The " + item + " reads: " + item_description)
            else:
                print("You can't read that!")
            return False
        elif action == "examine" and (item in char.character_inventory or
                            (item in rooms.contents(room) and item not in rooms.exam_ob(room))):
            item_description = char.get_description(item, "examine")
            if "_" in item:
                item = item.replace("_", " ")
            print(item_description)
            return False
        elif item in room_contents_list:
            if actions_match(complex_u_in, room):
                return complex_u_in
            elif action[0] == "examine":
                print("There's nothing to see of importance.")
            else:
                print(random.choice(snarky_remark_list))
                return False
        else:
            print("You see no such thing!")
            return False
    except IndexError:
        # In case user inputs half of a command
        print(action + " what?")
        return False

def get_action(room):
    complex_actions = ["move", "use", "examine", "take", "drop", "open", "close", "read", "mopi"]
    while True:
        # u_in stands for user input
        u_in = input("-> ").lower().strip()
        print()
        complex_u_in = u_in.split(" ")
        if complex_u_in[0] in complex_actions:
            u_in = check_complex_actions(room, complex_u_in)
            if u_in != False:  # Consider editing this bit? little sloppy
                return u_in
            else:
                # This loops if an action is not returned.
                continue
        if u_in in ("h", "?", "help"):
            print_help()
        elif u_in in ("look", "l"):
            rooms.get_description(room)
        elif u_in in ("inventory", "i"):
            char.print_inventory()
        elif u_in in ("north", "n"):
            return "north"
        elif u_in in ("south", "s"):
            return "south"
        elif u_in in ("west", "w"):
            return "west"
        elif u_in in ("east", "e"):
            return "east"
        elif u_in in ("northwest", "nw"):
            return "northwest"
        elif u_in in ("northeast", "ne"):
            return "northeast"
        elif u_in in ("southwest", "sw"):
            return "southwest"
        elif u_in in ("southeast", "se"):
            return "southeast"
        elif u_in in ("down", "d"):
            return "down"
        elif u_in in ("up", "u"):
            return "up"
        elif u_in in ("ribbit", "r"):
            print("you let out a cute ribbit")
        elif u_in in ("fuck", "shit", "damn"):
            print("That was awfully rude of you :(")
        elif u_in == "heck":
            print("No swearing!!!! >:(")
        elif u_in in ("mopi", "mopi productions"):
            print("Mopi says: 'eat hot pant,, lie.'")
        else:
            print("That command was not recognized")

Character and items module:

"""This is the character module, it stores information on the character and items."""

double_word_list = ["fish_sticks", "key_hook", "front_door", "bedroom_door"]

character_inventory = []
current_room = None

def print_inventory():
    if len(character_inventory) < 1:
        print("Your pockets are empty! Wait, how do frogs have pockets?")
    else:
        print("--INVENTORY--")
        for item in character_inventory:
            print("-" + item)

# This could get tedious for every item in the game, look for an alternative solution?
def get_description(item, form):
    item_dict = {
        "pamphlet": pamphlet,
        "fish_sticks": fish_sticks,
        "key": key,
        "lantern": lantern,
        "photo": photo,
        "jar": jar
    }

    ob = item_dict.get(item, lambda: "ERROR")

    try:
        if form == "read":
            return ob.read_description
        elif form == "examine":
            return ob.exam_description
    except AttributeError:
        return "There's nothing to see of importance."

use_on_items ={
    "key": "door"
}


class Items:
    def __init__(self, read_desc, exam_desc):
        self.read_description = read_desc
        self.exam_description = exam_desc

pamphlet = Items("The flies future is YOUR Future. Donate today!",
                 "This is an annoying pamphlet, it came in the mail")
fish_sticks = Items("MOPI Brand Fish Sticks! Unethically sourced, deep fried fun!",
                    "These fish sticks are LONG expired, and unethically sourced at that. Better leave them be.")
key = Items("34TH BURROW STREET",
            "It's an old cast iron key, it's been in your family for generations.")
lantern = Items("",
                "It's a trusty oil lamp. It provides plenty of light, and was just recently topped off.")
photo = Items("Forever yours and always :) Jan. 10, 1993",
              "It's a wedding photo of your Aunt Frogatha and Uncle Frogert. They're posing in front of the pond, "
              "many flies can be seen buzzing in the background. There's something written on the back.")
jar = Items("",
            "It is an empty glass jar.")

Room info module:

"""This module contains information on every room as well as the room class"""

directions = ["north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d"]
all = ["burrow", "burrow_basement"]

# This is a workaround for the from room_info import * method.
# Ideally this would all be one function, with a room parameter and an info parameter
def exam_ob(room):
    return room.examinable_objects
def use_ob(room):
    return room.usable_objects
def mov_ob(room):
    return room.movable_objects
def take_ob(room):
    return room.takeable_objects
def open_ob(room):
    return room.open_ob
def contents(room):
    return room.room_contents
def append_to_room(room, item):
    room.room_contents.append(item)
def remove_from_room(room, item):
    try:
        room.room_contents.remove(item)
    except ValueError:
        print("You see no such thing!")


def get_description(room):
    print(room.description)
    if room == burrow:
        if not burrow.variables["rug_moved"]:
            print("There is a handwoven rug lying in the center of your room, with a bothersome lump in the middle.")
        elif burrow.variables["rug_moved"] and not burrow.variables["trapdoor_opened"]:
            print("There is a handwoven rug rudely shoved in to the corner. In the center of your room, there is a "
                  "shut trapdoor.")
        else:
            print("There is a handwoven rug rudely shoved in to the corner. In the center of your room is an open "
                  "trapdoor. Talk about a safety hazard!")

        if "lantern" in burrow.room_contents:
            print("A lit lantern hangs loosely on the wall.")
        if "pamphlet" in burrow.room_contents:
            print("An annoying informational pamphlet lies on the floor.")
        if "photo" in burrow.room_contents:
            print("A framed photo rests cheerily on the mantle.")
        if burrow.variables["cabinet_opened"]:
            print("There is an open cabinet on the far wall where you store dishware.")
        else:
            print("There is a shut cabinet on the far wall.")

        for item in burrow.room_contents:
            if item not in ("lantern", "pamphlet", "cabinet", "photo", "jar"):
                print("You've left a " + item + " here.")
    if room == burrow_basement:
        if not burrow_basement.variables["icebox_moved"]:
            if not burrow_basement.variables["icebox_opened"]:
                print("A securely shut ice box sits near the ladder.")
            else:
                print("An open ice box sits near the ladder")
        elif burrow_basement.variables["icebox_moved"]:
            if not burrow_basement.variables["icebox_opened"]:
                print("A knocked over ice box lies miserably near the ladder, luckily its still latched shut.")
            else:
                print("A knocked over ice box lies miserably near the ladder, spilling its contents everywhere."
                      "\nIf only someone hadn't knocked it over...")
        if "fish sticks" in burrow_basement.room_contents:
            if burrow_basement.variables["icebox_moved"] and burrow_basement.variables["icebox_opened"]:
                print("A box of fish sticks is rapidly defrosting on the floor.")


class World:
    def __init__(self, exam_ob, mov_ob, use_ob, take_ob, open_ob):
        self.examinable_objects = exam_ob
        self.movable_objects = mov_ob
        self.usable_objects = use_ob
        self.takeable_objects = take_ob
        self.open_ob = open_ob

burrow = World(["rug", "door", "lump"], ["rug"], ["door", "trapdoor", "cabinet"], ["lantern", "pamphlet", "photo", "jar"],
               ["front_door", "trapdoor", "bedroom_door", "cabinet"])
burrow.room_contents = ["lantern", "pamphlet", "photo", "cabinet"]
burrow.description = "You're in a cozy burrow, and a well furnished one at that. The room is lit by an assortment of " \
                     "\nwarm candles and lanterns. Your front door is to the north, and the door to your Aunt and " \
                     "Uncle's bedroom lies to the South West"
burrow.variables = {
    "rug_moved": False,
    "trapdoor_opened": False,
    "door_unlocked": False,
    "cabinet_opened": False,
    "jar_taken": False
}

burrow_basement = World(["key_hook", "key", "icebox", "fish_sticks"], ["icebox"], ["icebox"], ["fish_sticks", "key"], ["trapdoor", "icebox"])
burrow_basement.room_contents = ["key", "icebox"]
burrow_basement.description = "You're in a muddy basement that serves as more of a storage room than any sort of " \
                              "living quarters.\nThere is a key hook on the far wall." \
                              " A ladder leads out of the basement."
burrow_basement.variables = {
    "icebox_moved": False,
    "icebox_opened": False,
    "fish_sticks_taken": False
}
\$\endgroup\$
3
  • 2
    \$\begingroup\$ If you're doing this to learn programming and Python, carry on, a game is a very good experiment for that. If you're actually trying to make a text adventure game, I suggest you look into using TADS 3, which is a very well-designed system just for that. \$\endgroup\$
    – xxbbcc
    Apr 10, 2020 at 15:51
  • 2
    \$\begingroup\$ As @xxbbcc correctly notes, this is a good way to learn Python, but if your goal is to write interactive fiction, look into Inform 7. It is an astonishingly sophisticated system for building such games. \$\endgroup\$ Apr 11, 2020 at 8:22
  • 1
    \$\begingroup\$ An alternative way to structure things is like LambdaMOO, where everything is an object, and the commands the player types are implemented as methods on those objects. Objects have inheritance and a couple of built-in properties such as owner and location, but almost everything, including people, rooms, containers, doors, even a text editor, are implemented with end-user-written methods and properties added to those minimal objects. \$\endgroup\$ Apr 11, 2020 at 10:02

2 Answers 2

21
\$\begingroup\$

An interesting beginning to your game. I'd be interested to seeing how it turns out.


Text adventure games are usually data-driven. This means you would have a game engine (program) that would read a game description file (data), and execute the same instructions for every action in every location in the game, producing different outcomes based on the game data.

What you have written is not data driven. You have a function for handling actions in the burrow(), another for the burrow_basement(), and another for front_of_house(). The code is these functions look very similar, and very repetitive, which is why we can write it once, and change the outcome based on the data.

A problem with the way you've written the game is recursion. If you start in the burrow, and you can go down into the basement or north to the front of the house. Each of those actions calls load_room(location) which never returns, and calls its own function for handling that new room location. For instance, if you go down to the basement, load_room("burrow_basement") is called, and it calls burrow_basement(), which in response to an "up" action would call load_room("burrow"), which would call burrow(). We keep getting deeper and deeper into the call stack. If the player keeps exploring, eventually they will run into Python's stack limit. You could fix this by increasing the stack size, but that is just a kludge; the correct solution is to get rid of the recursion.


Object model

Places

You should start by defining some things. Like rooms. A room is location the player can be in. It should have a short name ("Kitchen", "Burrow's Basement"), and a longer description. The player may or may not have been in the room before. It will have connections (exits) to other locations. There may be items in the room.

class Room:
    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.visited = False
        self.exits = { }
        self.contents = []

Things

Beyond rooms, the world is full of objects, or things. Things will have a name, and perhaps a description. Some things can be carried (photos, pamphlets, jars, ...) where as other things must remain where they are (stoves, sinks). Some things can be worn, like a coat or a hat. It might be hidden:

class Thing:
    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.fixed = False
        self.moved = False
        self.wearable = False
        self.concealed = False

Containers

Some things can be placed inside other things. Fish sticks may be found in an ice box. The ice box is usually closed, but occasionally open. Some containers may be locked, like a cabinet. Some containers may be see through, like a china cabinet. You can climb into some containers, like a wardrobe or a car, but most you cannot.

class Container(Thing):
    def __init__(self, name, description):
        super().__init__(name, description)
        self.contents = []
        self.open = False
        self.locked = False
        self.key = None
        self.transparent = False
        self.enterable = False

Is a room a container? Rooms have names, descriptions, and contents. And they are enterable.

Supporters

Some things can be placed on other things. You can put a jar on a table. You can put a key on a hook. So perhaps some containers should be considered a supporter, instead. You can even enter some supporters, such as climbing into a bed.

Animate

Some things can move around, and have behaviours. You can talk to some of them, such as a shopkeeper, and they will talk back. Other actors, might not talk back, such as a cat.

A shopkeeper (an animate thing) might be wearing pants with pockets (a container) which may contain a pocket watch (openable).

Is the player an animate container??? They can hold things (inventory), move around (animate), take things (container), hold things, wear things.

Doors

A door sits between two rooms. A door could be open or closed. If it is closed, it may or may not be locked.

class Door(Thing):
    def __init__(self, name, description, key=None)
        self.name = name
        self.description = description
        self.open = False
        self.lockable = key is not None
        self.locked = True if key else False
        self.key = key
        self.connects = []


living_room = Room("Living Room", "A place for the family to hang out")
dining_room = Room("Dining Room", "Many meals were had here")
basement = Room("Basement", "A dark and dingy hole in the ground")

trap_door = Door("Trap door", "A piece of wood, with a basic hinge and a ring handle")
trap_door.concealed = True
trap_door.connects = [living_room, basement]

dining_room.exits["south"] = living_room
living_room.exits["north"] = dining_room
living_room.exits["down"] = trap_door
basement.exits["up"] = trap_door

Here, we have two exits from the living room, north directly to the dining room (no door), and down to a trap_door. The trap_door is concealed, so if the player is in the living room, and the try to go "down", initially they should get a "you can't go that way". Moving the rug should reveal the trap door (marking it not concealed), allowing travel through the door to the other location it connects to. Maybe:

rug = Thing("A rug", "A thick heavy rug, passed down through the ages")
rug.fixed = True
def rug_move():
    print("You pull back the corner of the rug, revealing a trap door")
    rug.description = "One corner of the rug has been lifted to reveal a trap door"
    trap_door.concealed = False
rug.on_move = run_move

Now if your game logic allows you to type "lift rug" or "move rug", you could parse the word "rug", find the object with that name in the location.contents, and call that object's .on_move() function, which tells you what happened, changes the rug's description, and removes the concealed attribute from the trap door.


Example

Something to get you started. The lantern is just a Thing. The rug is actually a Rug (a special Thing), which has an action defined in the frog.py game file.

Notice the adventure game framework can be reused for many different games.

This is a "better" way that you're previous approach, but I wouldn't say it is a good way, yet. The framework has a lot of detail to flush out, and reworked to include better visibility and touchability. Items should optionally have a .has_light attribute. A room .has_light if it itself has that attribute set to True, or if an item within it has that attribute, unless the item is in a closed container (unless that container is transparent).

If you continue down this road, eventually you'll re-invent the Inform 7 interactive fiction framework. Good luck.

adventure.py

COMMANDS = { 'go', 'move', 'use', 'examine', 'open', 'close', 'inventory' }

DIRECTIONS = set()
REVERSE_DIRECTION = {}

for fwd, rev in (('n', 's'), ('e', 'w'), ('u', 'd')):
    DIRECTIONS.add(fwd)
    DIRECTIONS.add(rev)
    REVERSE_DIRECTION[fwd] = rev
    REVERSE_DIRECTION[rev] = fwd

class CantSee(Exception):
    pass

class Thing:
    def __init__(self, short_description, **kwargs):
        self.short_description = short_description
        self.long_description = None
        self.concealed = False
        self.scenery = False
        self.fixed = False
        self.openable = False

        for key, value in kwargs.items():
            if not key in self.__dict__:
                raise ValueError("Unrecognized argument: "+key)
            self.__dict__[key] = value

    def description(self):
        return self.short_description

    def examine(self):
        if self.long_description:
            print(self.long_description)
        else:
            print("There is nothing special about it")

    def move(self):
        if self.fixed:
            print("You can't move it")
        else:
            print("You move it a bit.")


class Container(Thing):
    def __init__(self, short_description, **kwargs):
        self.contents = {}
        self.openable = True
        self.open = False
        self.transparent = False

        super().__init__(short_description, **kwargs)

    def containing():
        if self.contents:
            return ", ".join(item.description() for item in self.contents())
        return "nothing"


    def description(self):
        text = self.short_description
        if self.openable:
            if self.open:
                text += " (which is closed)"
            else:
                text += " (which is open)"

        if self.open or self.transparent:
            if self.contents:
                text += "(containing " + self.containing() + ")"

        return description


class Door(Thing):
    def __init__(self, short_description, **kwargs):

        self.lockable = False
        self.locked = False
        self.key = None
        self.connects = {}

        super().__init__(short_description, **kwargs)

        self.fixed = True
        self.closed = True

class Room(Thing):

    def __init__(self, name, **kwargs):
        self.exits = {}
        self.visited = False
        self.contents = set()

        super().__init__(name, **kwargs)

    def exit_to(self, direction, destination, door=None):
        reverse = REVERSE_DIRECTION[direction]

        if door:
            door.connects[direction] = destination
            door.connects[reverse] = self
            self.exits[direction] = door
            destination.exits[reverse] = door
        else:
            self.exits[direction] = destination
            destination.exits[reverse] = self

    def enter(self):
        print("Location:", self.short_description)
        if not self.visited:
            self.describe()
            self.visited = True

    def visible_things(self):
        return [item for item in self.contents if not item.concealed]

    def describe(self):
        if self.long_description:
            print(self.long_description)
            print()

        items = [item for item in self.visible_things()  if not item.scenery]

        for item in items:
            if item.concealed or item.scenery:
                continue

        if items:
            print("You see:")
            for item in items:
                print("   ", item.description())

class Player(Container):
    def __init__(self):
        super().__init__("yourself")
        self.long_description = "As good looking as ever."

        self.openable = False
        self.location = None
        self.alive = True

    def inventory(self):
        if self.contents:
            print("You are carring:")
            for item in self.contents:
                print("   ", item.description)
        else:
            print("You have nothing.")

    def go(self, direction):
        destination = self.location.exits.get(direction, None)
        if isinstance(destination, Door):
            door = destination
            destination = door.connects[direction]
            if door.concealed:
                destination = None
            elif door.closed:
                if door.locked:
                    print("You'd need to unlock the door first")
                    return
                print("First opening the", door.short_description)

        if destination:
            self.location = destination
            destination.enter()
        else:
            print("You can't go that way")

class Game:
    def __init__(self, protagonist):
        self.player = protagonist
        self.game_over = False
        self.turns = 0

    def welcome(self):
        print("A text adventure game.")

    def help(self):
        print("Examine everything.")

    def get_action(self):
        while True:
            command = input("\n> ").lower().split()

            if command:
                if len(command) == 1:
                    if command[0] in DIRECTIONS:
                        command.insert(0, 'go')
                    if command[0] == 'i':
                        command[0] = 'inventory'

                if command == ['inventory']:
                    self.player.inventory()

                elif command == ['help']:
                    self.help()

                elif command[0] == 'go':
                    if len(command) == 2 and command[1] in DIRECTIONS:
                        return command
                    else:
                        print("I'm sorry; go where?")

                elif command[0] in COMMANDS:
                    return command

                else:
                    print("I don't understand")

    def go(self, direction):
        self.player.go(direction)

    def item(self, thing):
        items = self.player.location.visible_things()
        for item in items:
            if thing in item.short_description:
                return item
        raise CantSee(thing)

    def move(self, thing):
        item = self.item(thing)
        item.move()        

    def perform_action(self, command):
        if command[0] == 'go' and len(command) == 2:
            self.go(command[1])
        elif command[0] == 'move' and len(command) == 2:
            self.move(command[1])
        else:
            print("Command not implemented")


    def play(self):
        self.welcome()

        self.player.location.enter()

        while not self.game_over:
            command = self.get_action()
            try:
                self.perform_action(command)
                self.turns += 1
            except CantSee as thing:
                print("You don't see a", thing)

        if not self.player.alive:
            print("You have died.")
        else:
            print("Game over.")

frog.py

from adventure import Thing, Room, Door, Player, Game

burrow = Room("Your Burrow")
basement = Room("Your Basement")
front_yard = Room("Your Front Yard")

front_door = Door("Front Door")
trap_door = Door("Trap Door", concealed=True)
burrow.exit_to('n', front_yard, front_door)
burrow.exit_to('d', basement, trap_door)

class Rug(Thing):
    def move(self):
        if trap_door.concealed:
            print("Moving the rug reveals a trap door to the basement.")
            trap_door.concealed = False
        else:
            super().move()

rug = Rug("a rug", fixed=True)
burrow.contents.add(rug)
lantern = Thing("a lantern")
burrow.contents.add(lantern)


player = Player()
player.location = burrow

class FrogGame(Game):
    def welcome(self):
        print("""\
You are in a burrow, but not just any burrow.  The burrow you reside in is in
fact the estate of Von Frogerick III, who just so happens to be your great
great grandfather.  The immense and fascinating history of your lineage matters
not though, for you are hungry.  You should find a fly to eat.
""")

game = FrogGame(player)

if __name__ == '__main__':

    game.play()
\$\endgroup\$
2
  • \$\begingroup\$ Hey! First of all, thanks for the response. It's immensely insightful and really shows how useful classes are in python. The concept is still pretty new to me, so I hope you don't mind if I try to clarify. From my current understanding, switching to an object model would basically remove the need for room functions, and have one major function that processes actions by referring to classes and class methods? Would it be subdivided into rooms still? Or could I just process the action "move rug" by having it refer to the rug which is known to be in the burrow? What if I have duplicate items? \$\endgroup\$ Apr 10, 2020 at 6:51
  • 3
    \$\begingroup\$ I would also add that each object (including actions) should have a list of synonyms that can be used, so if the player opts to "lift carpet", it will understand that this is the same as "move rug". \$\endgroup\$ Apr 10, 2020 at 15:23
7
\$\begingroup\$
import room_info as rooms
import character_and_items as char

If you're going to import them this way all the time, why didn't you just name the modules rooms and char respectively? What's the point of all the extra typing? Btw, it's conventional in text adventures to refer to the player; I'd go with import player over import char, just to de-confuse the syntax highlighter.

Your function print_frog_title is used in only one place. You should just inline it there.


snarky_remark_list = ["And how exactly are you going to do that?", "Fat chance", "In your dreams", "Inconceivable!",
                      "Aunt Frogatha would be ashamed.."]

When you start version-controlling your source code, you'll realize that it's better to indent sequences so that they have one element per line. This allows you to add and remove elements without introducing extra diffs into your history. Also add a trailing comma on each element; don't sigil the last element specially from all the rest.

snarky_remark_list = [
    "And how exactly are you going to do that?",
    "Fat chance",
    "In your dreams",
    "Inconceivable!",
    "Aunt Frogatha would be ashamed..",
]

English grammar nit: I notice that some of these remarks end with punctuation and some don't. Another: You wrote mantle when you meant mantel.

Your room_dict has nice git-friendly indentation, but is also missing that trailing comma on the last element.


character_inventory = []
current_room = None

These two variables feel like they should be data members of a class Player. But then there's other stuff in this module that is clearly unrelated to the player, e.g.

# This could get tedious for every item in the game...
def get_description(item, form):

So number one, I'd consider splitting that out into an items or objects module; and number two, you already know the non-tedious solution! Just imagine that instead of writing

pamphlet = Items("The flies future is YOUR Future. Donate today!",
                 "This is an annoying pamphlet, it came in the mail")

you wrote

pamphlet = Item(
    name="pamphlet",
    read="The flies future is YOUR Future. Donate today!",
    examine="This is an annoying pamphlet, it came in the mail",
)

And instead of

key = Items("34TH BURROW STREET",
            "It's an old cast iron key, it's been in your family for generations.")

imagine that you wrote

key = Item(
    name="key",
    read="34TH BURROW STREET",
    examine="It's an old cast iron key, it's been in your family for generations.",
    use_on=["door"],
)

Can you see how to implement the constructor of class Item now?

EDITED TO ADD: It'd be something like this.

class Item:
    def __init__(self, name, read, examine, use_on=None):
        self.name = name
        self.read = read
        self.examine = examine
        self.use_on = use_on or []
        # self.location = burrow
        # self.inroom_description = "There is a %s here." % name

No member functions are necessary yet.

And how to implement get_description? ...Well, almost. Suppose we wrote our list of "all the items in the game" like this!

all_items = [
    Item(
        name="pamphlet",
        read="The flies future is YOUR Future. Donate today!",
        examine="This is an annoying pamphlet, it came in the mail",
    ),
    Item(
        name="key",
        read="34TH BURROW STREET",
        examine="It's an old cast iron key, it's been in your family for generations.",
        use_on=["door"],
    ),
]

Now get_description starts out something like

def get_description(verb, noun):
    for item in all_items:
        if item.name == noun:
            if verb == "examine":
                return item.examine
            elif verb == "read":
                return item.read
    return "I don't see what you're referring to."

You could preprocess all_items into a dict mapping from names to Item objects, to save two levels of indentation in that loop.

def get_description(verb, noun):
    item = all_items_by_name.get(noun, None)
    [...]

Notice that I quietly turned Items plural into Item singular, and also turned form into verb.


# This is a workaround for the from room_info import * method.
# Ideally this would all be one function, with a room parameter and an info parameter
def exam_ob(room):
    return room.examinable_objects
def use_ob(room):
    return room.usable_objects

Okay, this seems insane. Why not just take the one place you call rooms.exam_ob(room) and write room.examinable_objects instead?


Your handling of takeable objects seems tedious and naïve... but also probably unavoidable, if you want to have that level of control over the messages. (The photo is cheerily on the mantel; the fish sticks are defrosting on the floor; etc.) If you want to add the ability to drop arbitrary objects in arbitrary places, you'll have to find a new way of doing these messages.

Have you seen Donald Knuth's literate version of Adventure? See particularly section 63, on page 43.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you a ton for the smaller style pointers, it really helps. The better utilization of the item class was super smart, and I'm not quite sure why I didn't think of it! That being said, how would you go about defining the class? Would it be a simple class Item: pass, or does it use parameters still? Classes are definitely still confusing to me, but I learn best by asking questions. Thanks for your help! \$\endgroup\$ Apr 10, 2020 at 7:00
  • 1
    \$\begingroup\$ Added a definition for class Item. \$\endgroup\$ Apr 10, 2020 at 15:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.