1
\$\begingroup\$

The game is designed to be a sort of foundation for any other projects I make.

#main --- This is line zero
import sys
import time

import areas
import items


def menu():
    while alive:
        print("\n")
        menu_input = input("> ")
        if menu_input == "inspect":
            if position == areas.east_sword:
                if "Sword" in items.inventory.items:
                    print("The casket is empty after taking the sword from its place.")
                else:
                    print("You open the casket and find...")
                    time.sleep(1)
                    print("A sword!")
                    items.inventory.add_item(items.Item("Sword"))
                    print("It even comes with a scabbard! You sheathe the sword and buckle it to your waist.")
            elif position == areas.west_west_gun:
                if "Gun" in items.inventory.items:
                    print("The case is empty after taking the gun from its place.")
                else:
                    print("You open the case and find...")
                    time.sleep(1)
                    print("A gun!")
                    items.inventory.add_item(items.Item("Gun"))
                    print("It's a Glock 17 pistol, by the looks of it."
                          "\nYou take the gun and wield it in your hands.")
            elif position == areas.north_monster:
                monster()
            elif position == areas.west_north_boss:
                boss()
            else:
                print(position.inspect)
        elif menu_input == "help":
            help()
        elif menu_input == "quit":
            sys.exit()
        elif menu_input == "items":
            items.inventory.__str__()
        elif menu_input == "go north":
            if not position.north:
                print("You cannot go in that direction!")
            else:
                global position
                position = position.north
                desc()
        elif menu_input == "go east":
            if not position.east:
                print("You cannot go in that direction!")
            else:
                global position
                position = position.east
                desc()
        elif menu_input == "go south":
            if not position.south:
                print("You cannot go in that direction!")
            else:
                global position
                position = position.south
                desc()
        elif menu_input == "go west":
            if not position.west:
                print("You cannot go in that direction!")
            if position == areas.cross and "Key" not in items.inventory.items:
                print("You need a key!")
            else:
                global position
                position = position.west
                desc()
        else:
            print("You have typed an invalid command. Type 'help' for available commands.")
    if not alive:
        print("Oh no! You are dead.")


def desc():
    if position == areas.north_monster:
        if monster_alive:
            print("You see two dim lights glinting back at you when you set foot in the cavern."
                  "\nYou enter it and... see a man crouching on the floor with his neck chained to"
                  "the wall behind him.")
        else:
            print("You see a dead man.")
    elif position == areas.west_south_pit:
        print(position.desc)
        global alive
        alive = False
    else:
        print(position.desc)


def boss():
    if "Gun" not in items.inventory.items:
        while True:
            print("\nCHOOSE:"
                  "\n1. Shoot the man and escape."
                  "\n2. Attempt to reason with him.")
            boss_input = input("> ")
            if boss_input == "1":
                print("You shoot the man in the back and find the exit tunnel. You also find his keyring."
                      "\nYou return back to the room with the old man and free him."
                      "\nYou both escape the cave."
                      "\nAlive.")
                end()
            elif boss_input == "2":
                boss_kill()
            else:
                print("You must enter either '1' or '2'!")
    else:
        boss_kill()


def boss_kill():
    print("You approach him slowly and...")
    time.sleep(1)
    print("He instantly kills you!")
    global alive
    alive = False


def monster():
    if "Key" not in items.inventory.items:
        print("Man: *coughs*"
              "\nMan: Wha... what are you?!"
              "\nYou: It's okay, I just want to get out of here."
              "\nMan: St... stay away from me!"
              "\nMan: *takes out a shiv and points it towards you*"
              "\nMan: Stay back!")
        if "Sword" not in items.inventory.items:
            print("You: I'm not here to hurt you, I'm here to help both of us!"
                  "\nMan: Why should I trust you?")
            while "Key" not in items.inventory.items:
                print("\nCHOOSE:"
                      "\n1. Please, just help me!"
                      "\n2. What makes you think you can survive in this?"
                      "\n3. If I successfully escape, I promise I will help you."
                      "\n4. *Attempt to snatch the shiv from his hand*")
                monster_input = input("> ")
                if monster_input == "1":
                    print("You: Please, just help me! I don't want to be stuck in this hellhole anymore!"
                          "\nMan: Get outta here.")
                elif monster_input == "2":
                    print("You: What makes you think you can survive in this?"
                          "\nMan: What makes you think I won't stab you with this here shiv?")
                elif monster_input == "3":
                    print("You: If I successfully escape, I promise I will help you."
                          "\nYou: This is your only chance of salvation."
                          "\nMan: I guess you're right."
                          "\nMan: Take this.")
                    items.inventory.add_item(items.Item("Key"))
                    print("\nYou: I will be back for you.")
                elif monster_input == "4":
                    print("You attempt to quickly snatch his shiv...")
                    time.sleep(2)
                    print("\nHe stabs you in the chest!")
                    global alive
                    alive = False
                    break
                else:
                    print("You must enter either '1', '2', '3', or '4'!")
        else:
            print("\nYou: *unsheathes a sword and points it back towards the man*")
            while "Key" not in items.inventory.items:
                print("\nCHOOSE:"
                      "\n1. Stab the man, stealing the key."
                      "\n2. Persuade the man to give you the key.")
                monster_input = input("> ")
                if monster_input == "1":
                    print("You stab the man in the chest, killing him while taking the key from his neck.")
                    items.inventory.add_item(items.Item("Key"))
                    global monster_alive
                    monster_alive = False
                elif monster_input == "2":
                    print("You: GIVE IT TO ME!"
                          "\nMan: *whimpers*")
                    items.inventory.add_item(items.Item("Key"))
                else:
                    print("You must enter either '1' or '2'!")
    else:
        if monster_alive:
            print("Man: I trust you'll get me out of here.")
        else:
            print("You smell the fresh corpse of the old man chained to the wall.")


def help():
    print("AVAILABLE COMMANDS:"
          "\n> go [north/east/south/west] = moves you to said direction"
          "\n> inspect = investigates your surroundings"
          "\n> items = lists the items you currently possess"
          "\n> help = shows you the command list (this)"
          "\n> quit = exits the game")


def end():
    print("Congratulations! You have won!")
    print("It took you %s seconds to complete the game!" % (time.time() - start_time))
    sys.exit()

monster_alive = True
alive = True
position = areas.start
start_time = time.time()

print("The following is a work in progress.")
time.sleep(1)
print("...")
time.sleep(1)
print("...")
time.sleep(1)
print("...")
time.sleep(1)
print("You wake up in a dark corner of a spacious underground cavern, the light seeps through above."
      "\nYou find yourself barefooted, hungry, and fearful.")
time.sleep(2)
print("\nYou try to get yourself out of this hole and begin to know what you can do.")
time.sleep(3)
print("\n")
help()
time.sleep(2)
print("\nHave fun. The game starts.")
menu()

#areas --- This is line zero
class Area(object):
    def __init__(self, north, east, south, west):
        self.north = north
        self.east = east
        self.south = south
        self.west = west

    def inspect(self):
        return self.inspect

    def north(self):
        return self.north

    def east(self):
        return self.east

    def south(self):
        return self.south

    def west(self):
        return self.west


start = Area(True, False, False, False)
start.desc = "You return to the area you started in."
start.inspect = "The rays of light trickle down to the cavern you started in. You can only walk north from this" \
                " location. There is nothing else interesting here."

cross = Area(True, True, True, True)
cross.desc = "You find yourself in a crossroad with four paths."
cross.inspect = "There are four smaller tunnels leading to other areas of the cave," \
                " heading to all four cardinal directions."

east_sword = Area(False, False, False, True)
east_sword.desc = "You enter the cave and find a open casket."

north_monster = Area(False, False, True, False)
north_monster.inspect = "The man looks aggressive and wears old rags. He doesn't seem like the type of person that" \
                        "you would want to be enemies with. He reeks of repulsive odour."

west = Area(True, True, True, True)
west.desc = "This place looks empty and you find yourself three more tunnels with even more caverns."
west.inspect = "There's a sign that says 'DO NOT ENTER' beside the northern entrance."

west_north_boss = Area(True, False, True, False)
west_north_boss.desc = "You enter the cavern and ... a man in leather and boots appears," \
                       "\nholding a whip with other prisoners in cages." \
                       "\nIt seems that this section leads to the exit tunnel."

west_west_gun = Area(False, True, False, True)
west_west_gun.desc = "You enter the room and find a black case on table."

west_south_pit = Area(True, False, True, False)
west_south_pit.desc = "You enter the cavern and..." \
                      "\nYou find yourself at the edge of a deep pit! You quickly try to escape!" \
                      "\nYou slip and fall to your death!"

start.north = cross
cross.north = north_monster
cross.east = east_sword
cross.south = start
cross.west = west
east_sword.west = cross
north_monster.south = cross
west.north = west_north_boss
west.south = west_south_pit
west.west = west_west_gun
west_north_boss.south = west
west_west_gun.east = west

#items --- This is line zero
class Item(object):
    def __init__(self, name):
        self.name = name


class Inventory(object):
    def __init__(self):
        self.items = {}

    def add_item(self, item):
        self.items[item.name] = item
        print("YOU HAVE ADDED A {} TO YOUR INVENTORY.".format(item.name.upper()))

    def __str__(self):
        for item in self.items.values():
            print('\t'.join([str(x) for x in [item.name]]))
        if not inventory.items:
            print("You have nothing!")

inventory = Inventory()

You can also see the files on GitHub.

This is my first working text adventure game written in Python. What do you think? I would love some constructive feedback and some suggestions as well.

I have a few issues and questions at hand however.

  1. I can't seem to find a shortcut for the go command at lines 51 to 73. This also brings another problem. I want the game to be configured that when position changes to west, it requires "Key" in inventory.items otherwise it won't change. I want the solution to be universal, as in instead adding an if statement to elif menu_input == "go west":, I can add the same statement to the other directions, i.e. north, east, etc.
  2. When I show the contents of inventory.items, it prints:

    Item #1
    Item #2
    

    I'm trying to find a solution that prints:

    Item #1, Item #2
    
\$\endgroup\$
3
\$\begingroup\$

Whenever there's a question, the first thing I do is point everyone at PEP 8, the official style guide for Python.

In your case, this means doc strings. Use them. PEP 257 goes into detail about conventions. Basically, use """triple quote multi-line strings""" as the first statement of files/objects/functions. This allows your IDE to show you nice descriptions of your file/object/function. Let your IDE help you. (The keybind to show this is CTRL-Q in PyCharm, which based on the .idea directory in your GitHub repo, I assume you are using.)


Bugs!

I downloaded the files as-is from git, ran main.py, and got this output at the front:

/main.py:48: SyntaxWarning: name 'position' is used prior to global declaration
  global position
/main.py:55: SyntaxWarning: name 'position' is assigned to before global declaration
  global position
/main.py:62: SyntaxWarning: name 'position' is assigned to before global declaration
  global position
/main.py:71: SyntaxWarning: name 'position' is assigned to before global declaration
  global position

(Python --version: Python 3.5.1)

The game crashes if you try to > go west from the starting area:

Have fun. The game starts.

> inspect
The rays of light trickle down to the cavern you started in. You can only walk north from this location. There is nothing else interesting here.

> go west
You cannot go in that direction!
Traceback (most recent call last):
  File "/main.py", line 226, in <module>
    menu()
  File "/main.py", line 73, in menu
    desc()
  File "/main.py", line 93, in desc
    print(position.desc)
AttributeError: 'bool' object has no attribute 'desc'

Process finished with exit code 1

Problems!

Global state is bad!

Why is Global State so Evil? (Programmers.SE)

Very briefly, it makes program state unpredictable.

  • Inflexibility - what if one part of the code requires one value in the global state, but another part requires another value (e.g. a temporary value during a transaction)? You suddenly have a nasty bit of refactoring on your hands
  • Code comprehension - code behaviour that depends on a lot of mutable global variables is much trickier to understand - you need to understand the range of possible interactions with the global variable before you can reason about the behaviour of the code. In some situations, this problem can become intractable.

The solution is dependency injection.

I go over my suggested model for this game at the bottom of this post.

Run your main.py in a def main()!

Python code actually runs faster inside a function! Additionally, we want to avoid import side effects: currently, if someone were to import main, they'd have the game run in the console for them. The solution is to use the if __name__ == "__main__" construct:

def main():
    # your code

if __name__ == "__main__":
    main()

Shadowing!

You have a few cases of variable shadowing (a local variable hides access to a variable defined higher up with the same name):

  • main.help shadows builtin help
  • area.Area.__init__(self, north, east, south, west) shadows area.west

Responsibility!

Your main.menu function contains your main game loop. This seems out of place to me.

The main.desc function contains logic that changes depending on which Area you are in. Rather than set it here, it should be handled within the Area instance.

Object Construction!

Classes do not have to inherit from object explicitly; the standard is to omit the parenthesis entirely if an object doesn't inherit from an object directly.

In Area, your constructor takes four properties, and then you add two more on every time. This is error prone: you could forget to add those to a constructed Area and cause a crash. It's best practice to define all properties that an object owns in its __init__ method.


NOTE: All code below this point is for example purposes and untested.

Your Questions!

> go [north|south|east|west]

There are a few ways to handle this. This is one potential way.

On your Area, declare a dictionary. The keys will be the direction ("north", "south", "east", "west"). In your main loop, the condition for the go command would be if menu_input[:3] == "go ":. This uses slicing to get the first three characters of menu_input. The contained block could then be:

try:
    global position
    position = position.neighbor[menu_input[3:]]
    desc()
except KeyError:
    print("You cannot go in that direction!")

This also allows you to dynamically use other directions, such as "up", "weast", and "winter".

str(inventory.items)

BIG NOTE! __str__ should NEVER print -- it should return a string representation of the object.

First of all, use str(thing) rather than thing.__str__() (main.py). A simple (though a bit hacky) way to print a comma separated list is to use the string that exists. str(dict.values())[13:-2] will give you a comma separated list of the values in the dict.

A question, though: why are you using a dict at all? It seems that you'd be better off just using the string item names directly, as you aren't storing any other information and every use of an item just goes directly for item.name. Just store a list of strings, and then you can simply operate on that.


Improvements!

There is a lot that you could do, and all of it is a trade-off between time, speed, functionality, maintainability, and comprehensibility. (If you want fast code, you shouldn't be using Python.) There is a reason that Game Dev is a complicated thing: there are multiple available patterns, each with their own advantages and drawbacks. As a friend once said, in programming, there is no right, only different shades of wrong[citation needed].

As such, I try to focus here on a solution that will help point you in the direction of maintainable, extensible code, while maintaining a approachable form appropriate for a small command line text adventure.

Player

You should create and manage an object that holds your player's state. This object would know whether the player is alive, their current location, etc.

Area / Location

Each place you can be in your game should have an object.

Any place-specific information, such as descriptions, neighbors, and requirements to go to the neighbor, should be contained in this object.

Commands

Ideally, commands would be implemented in a dictionary. The key is the exact command needed to do the command, and the value is a function that handles the command.

If more complicated matching is desired, the dictionary keys can be a function that takes a string and returns a truthy/falsy value depending on if it matches. You would then iterate over the keys, and if a key matches your command, retrieve the handler and run it.

Special Locations

Special areas such as the boss room would contain that information. One potential solution is to take out the idea of the go command and have the area store a dictionary of area-specific commands: boss areas would have different commands available than the regular go [direction].

You could also handle item requirements in a similar matter, if using the matcher commands: have the matcher take the Player object and check that the inventory contains the required item(s).


And let me just say that I know how to over-engineer a text-based choose-your-adventure game; I have a data-driven text-game engine that I've been working on on-and-off for at least a few years. I've tried to avoid doing that level of over-engineering here, though.

Speaking of, it's about time I refactor the entire thing again. I came up with a couple things while writing this review that could improve it...

\$\endgroup\$
  • \$\begingroup\$ Sorry for the late reply. Thank you for the feedback! I still do not understand how to replace global functions for my variables; I added: if __name__ == "__main__": main() Could you explain a little further? \$\endgroup\$ – Pradana Aumars Aug 14 '16 at 14:42
  • \$\begingroup\$ @Pradana define a new object that encapsulates what was your global state, instantiate it at the beginning of main, and then pass that object to whatever methods need it. \$\endgroup\$ – CAD97 Aug 14 '16 at 15:00
  • \$\begingroup\$ can you please give me an example? \$\endgroup\$ – Pradana Aumars Aug 15 '16 at 12:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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