2
\$\begingroup\$

I am new to Python and I have not yet done any OOP. I wrote this text adventure as a first project and I would like to know if there is a better and more efficient way to write my code within its current paradigm. Any suggestions or critique are more than welcome.

The code is quite long. I don't expect anyone to read all of it.

#!/usr/bin/env python

import cmd
import random
import time
import sys
import os


#### Effects ####

def print_slow(str):
    for letter in str:
        sys.stdout.write(letter)
        sys.stdout.flush()
        time.sleep(0.1)
    print("\n")

def load_effect(str):
    for letter in str:
        sys.stdout.write(letter)
        sys.stdout.flush()
        time.sleep(0.5)


#### Title Screen ####

def menu():
    print("\n")
    print("SHADOWS OVER LAURENDALE")
    print("Version 1.0")
    action = input("> ")
    if action.lower() == "p":
        intro()
    elif action.lower() == "q":
        sys.exit()
    elif action.lower() == "i":
        info_menu()
    elif action.lower() == "h":
        help_menu()
    while action.lower() not in ['p', 'q', 'i', 'h']:
        print("Invalid option! Enter 'p' to launch the game,")
        print("'q' to quit, 'i' for info, or 'h' for help.")
        action = input("> ")
        if action.lower() == "p":
            intro()
        elif action.lower() == "q":
            sys.exit()
        elif action.lower() == "i":
            info_menu()
        elif action.lower() == "h":
            help_menu()


def info_menu():
    print("www.shadows-over-laurendale.github.io")
    action = input("> ")
    if action.lower() == "p":
        intro()
    elif action.lower() == "q":
        sys.exit()
    elif action.lower() == "i":
        info_menu()
    elif action.lower() == "h":
        help_menu()
    while action.lower() not in ['p', 'q', 'i', 'h']:
        print("Invalid option! Enter 'p' to launch the game,")
        print("'q' to quit, 'i' for info, or 'h' for help.")
        action = input("> ")
        if action.lower() == "p":
            intro()
        elif action.lower() == "q":
            sys.exit()
        elif action.lower() == "i":
            info_menu()
        elif action.lower() == "h":
            help_menu()

def help_menu():
    print("Enter commands to interact with the world.")
    print("For more info and a list of commands, consult the manual.")
    action = input("> ")
    if action.lower() == "p":
        intro()
    elif action.lower() == "q":
        sys.exit()
    elif action.lower() == "i":
        info_menu()
    elif action.lower() == "h":
        help_menu()
    while action.lower() not in ['p', 'q', 'i', 'h']:
        print("Invalid option! Enter 'p' to launch the game,")
        print("'q' to quit, 'i' for info, or 'h' for help.")
        action = input("> ")
        if action.lower() == "p":
            intro()
        elif action.lower() == "q":
            sys.exit()
        elif action.lower() == "i":
            info_menu()
        elif action.lower() == "h":
            help_menu()


#### Player ####

player_health = 100
player_inventory = ["Map"]
player_gold = 50


#### Main Scenes ####

def intro():
    print("\n")
    load_effect("...loading...")
    print("\n")
    print_slow("'Wake up, Arris...'")
    print_slow("'You've been sleeping all day!'")
    print_slow("'Why don't you go outside and do something?'")
    print_slow("'It's so lovely today. Be back for dinner!'")
    print("\n")
    print_slow("'Okay...'")
    print("\n")
    village()


def village():
    global player_gold
    global player_health
    print("\n")
    print("You are in the village.")
    while True:
        action = input("> ")
        if action == "1":
            river()
        if action == "2":
            farmlands()
        elif action == "3":
            forest()
        elif action == "4":
            shop()
        elif action == "5":
            home()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the river(1), the farmlands(2), the forest(3), the shop(4), home(5).")
        elif action == "e":
            print("Sounds of laughter and loud conversation can be heard in this lively village.")
            print("What a lovely place! I was born here and my heart will always be here...")
        elif action == "t":
            print("A merchant on the street waves you over to where he is.")
            print("'Care for some fresh bread, young man? It's only 5 gold.' He asks.")
            print("You have",player_gold,"gold.")
            print("Yes(y) or no(n)?")
            action = input("> ")
            if action == "y":
                if "Bread" in player_inventory:
                    print("Looks like you already have some bread, kid!")
                    print("Eat it up and come back... Hehe.")
                elif player_gold >= 5:
                    player_inventory.append("Bread")
                    player_gold -= 5
                    print("'Here you go!'")
                    print("You take the bread from the man.")
                elif player_gold < 5:
                    print("Sorry you don't have enough! Ask your mother for a little gold! Hehe.")
            elif action == "n":
                print("Okay. Maybe next time!")
            else:
                print("Invalid command! Try again.")
                action = input("> ")
                if action == "y":
                    if player_gold >= 5:
                        player_inventory.append("Bread")
                        player_gold -= 5
                        print("'Here you go!'")
                        print("You take the bread from the man.")
                    elif player_gold < 5:
                        print("Sorry you don't have enough! Ask your mother for a little gold! Hehe.")
                elif action == "n":
                    print("Okay. Maybe next time!")

        else:
            print("Invalid command! Try again.")


def river():
    global player_gold
    global player_health
    print("\n")
    print("You are by the river.")
    while True:
        action = input("> ")
        if action == "1":
            village()
        elif action == "2":
            farmlands()
        elif action == "3":
            forest()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the farmlands(2), the forest(3).")
        elif action == "e":
            print("I can hear the water flowing gently and the birds are singing...")
            print("A small boy is out here with his fishing net.")
        elif action == "t":
            print("The little boy with his net look over at you.")
            print("'Have you ever tried crossing this river?'")
            print("'It looks so beautiful on the other side.'")
            print("'I better get back to catching the fishes.'")
            print("'My papa will be proud!'")
            if "Raft" in player_inventory:
                print("/n")
                print("Will you cross the river? Yes(y) or no(n)?")
                action = input("> ")
                if action == "n":
                    print("That river looks dangerous anyways...")
                    print("It's infested with snakes.")
                elif action == "y":
                    outskirts()
                else:
                    print("Invalid command! Try again.")
                    action = input("> ")
                    if action == "n":
                        print("That river looks dangerous anyways...")
                        print("It's infested with snakes.")
                    elif action == "y":
                        outskirts()
        else:
            print("Invalid command! Try again.")


def farmlands():
    global player_gold
    global player_health
    global ruby_saved
    print("\n")
    print("You are in the farmlands.")
    while True:
        action = input("> ")
        if action == "1":
            village()
        elif action == "2":
            river()
        elif action == "3":
            forest()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the forest(3).")
        elif action == "e":
            print("Rosy red tomatoes and evergreen lettuce are growing from the ground...")
            print("A farmer waves to you and smiles.")
        elif action == "t":
            print("'Have you seen my lovely Ruby?' The farmer asks.")
            print("'She's run away again.'")
            print("He looks very concerned.")
        else:
            print("Invalid command! Try again.")


def forest():
    global player_gold
    global player_health
    print("\n")
    print("You are in the forest.")
    while True:
        action = input("> ")
        if action == "1":
            village()
        elif action == "2":
            river()
        elif action == "3":
            farmlands()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3).")
        elif action == "e":
            print("This is a far way from home...")
            print("The sound of a girl screaming can be faintly heard.")
            print("I'm afraid to go on.")
            if "Sword" in player_inventory:
                print("\n")
                print("But I do have a weapon to protect myself.")
                print("Shall I go on? Yes(y) or no(n)?")
                action = input("> ")
                if action == "n":
                    print("I won't go any further...")
                elif action == "y":
                    forest_two()
                else:
                    print("Invalid command! Try again.")
                    action = input("> ")
                    if action == "n":
                        print("I won't go any further...")
                    elif action == "y":
                        forest_two()
        elif action == "t":
            print("There's no one to talk to here...")
        else:
            print("Invalid command! Try again.")


#### Sub Scenes ####

def home():
    global player_gold
    global player_health
    print("\n")
    print("You are at home.")
    while True:
        action = input("> ")
        if action == "1":
            village()
        elif action == "2":
            river()
        elif action == "3":
            farmlands()
        elif action == "4":
            forest()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3), the forest(4).")
        elif action == "e":
            print("A warm fire is crackling in the hearth...")
            print("Something smells delicious!")
        elif action == "t":
            print("You call for your mother.")
            print("She comes inside from the back door and smiles.")
            print("'There you are! Now what have you been up to?'")
            if player_health < 100:
                print("\n")
                print("'You're hurt! Let me tend to your wounds.'")
                player_health = 100
                print("You feel better. You now have",player_health,"health.")
        else:
            print("Invalid command! Try again.")


def outskirts():
    global player_gold
    global player_health
    print("\n")
    print("You are in the outskirts.")
    while True:
        action = input("> ")
        if action == "1":
            village()
        elif action == "2":
            river()
        elif action == "3":
            farmlands()
        elif action == "4":
            forest()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3), the forest(4).")
        elif action == "e":
            print("You can see the boy fishing from across the river.")
            if "Sword" not in player_inventory:
                print("You see a shiny item lying in the grass.")
                print("Will you take it? Yes(y) or no(n)?")
                action = input("> ")
                if action == "n":
                    print("Some things are better left where you found them.")
                elif action == "y":
                    print("You reach for it. It's a beautiful and sharp sword!")
                    print("But as soon as you grab it, a snake bites your hand!")
                    snake_attack = random.randint(1,100)
                    player_health -= snake_attack
                    player_inventory.append("Sword")
                else:
                    print("Invalid command! Try again.")
                    action = input("> ")
                    if action == "n":
                        print("Some things are better left where you found them.")
                    elif action == "y":
                        print("You reach for it. It's a beautiful and sharp sword!")
                        print("But as soon as you grab it, a snake bites your hand!")
                        snake_attack = random.randint(1,100)
                        player_health -= snake_attack
                        player_inventory.append("Sword")
        elif action == "t":
            print("You yell to the boy across the river.")
            print("'Hey! How did you get over there?'")

        else:
            print("Invalid command! Try again.")


def shop():
    global player_gold
    global player_health
    print("\n")
    print("You are in the shop.")
    while True:
        action = input("> ")
        if action == "1":
            village()
        elif action == "2":
            river()
        elif action == "3":
            farmlands()
        elif action == "4":
            forest()
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3), the forest(4).")
        elif action == "e":
            print("A friendly woman is standing at the counter.")
            print("'Hello! Can I help you, young man?'")
        elif action == "t":
            print("'Please, browse our wares.'")
            print("'We have what you need.'")
            print("'Do you want to see what we have?'")
            print("Yes(y) or no(n)?")
            action = input("> ")
            if action == "y":
                print("You can buy:")
                print("a fishing pole(f) for 10 gold, a leather tunic(t) for 20 gold,")
                print("a guitar(g) for 30 gold, and a raft(r) for 15 gold.")
                action = input("> ")
                if action == "f":
                      if "Fishing poll" in player_inventory:
                          print("I aleady have one!")
                      elif player_gold >= 10:
                          player_inventory.append("Fishing poll")
                          player_gold -= 10
                          print("'Here you go!'")
                          print("You bought a lovely handmade fishing poll.")
                      elif player_gold < 5:
                          print("Sorry you don't have enough for this, kid.")
                elif action == "t":
                      if "Tunic" in player_inventory:
                          print("I aleady have one!")
                      elif player_gold >= 20:
                          player_inventory.append("Leather tunic")
                          player_gold -= 20
                          print("'Here you go!'")
                          print("You bought a bulky leather tunic.")
                      elif player_gold < 20:
                          print("Sorry you don't have enough for this, kid.")
                elif action == "g":
                      if "Guitar" in player_inventory:
                          print("I aleady have one!")
                      elif player_gold >= 30:
                          player_inventory.append("Guitar")
                          player_gold -= 30
                          print("'Here you go!'")
                          print("You bought a gorgeous guitar.")
                      elif player_gold < 30:
                          print("Sorry you don't have enough for this, kid.")
                elif action == "r":
                      if "Raft" in player_inventory:
                          print("I aleady have one!")
                      elif player_gold >= 15:
                          player_inventory.append("Raft")
                          player_gold -= 15
                          print("'Here you go!'")
                          print("You bought a small wooden raft.")
                      elif player_gold < 15:
                          print("Sorry you don't have enough for this, kid.")
                else:
                      print("Invalid command! Try again.")

            elif action == "n":
                print("'Okay, no worries! I just love your company.'")
                print("'Tell your friends about this place, okay?'")

            else:
                print("Invalid command! Try again.")
                print("'Please, browse our wares.'")
                print("'We have what you need.'")
                print("'Do you want to see what we have?'")
                print("Yes(y) or no(n)?")
                action = input("> ")

        else:
            print("Invalid command! Try again.")


def forest_two():
    global player_gold
    global player_health
    print("\n")
    print("You are deep into the forest.")
    while True:
        action = input("> ")
        if action == "1":
            print("I don't know how to get there. I'm lost.")
        elif action == "2":
            print("I don't know how to get there. I'm lost.")
        elif action == "3":
            print("I don't know how to get there. I'm lost.")
        elif action == "4":
            print("I don't know how to get there. I'm lost.")
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3), the forest entrance(4)")
        elif action == "e":
            print("You are in a part of the forest you've never seen before.")
            print("You are lost...")
        elif action == "t":
            print("You call for help.")
            print("You suddenly hear the girl scream again.")
            print("This time louder...")
            print("Will you investigate?")
            print("Yes(y) or no(n)?")
            action = input("> ")
            if action == "n":
                print("You decide not to go.")
                print("A wolf howls in the distance.")
                print("The moon glistening above you.")
            elif action == "y":
                final()
            else:
                print("Invalid command! Try again.")

        else:
            print("Invalid command! Try again.")


def final():
    global player_gold
    global player_health
    print("\n")
    print("You are even further into the woods.")
    print("You see an old, abandoned cabin just ahead.")
    while True:
        action = input("> ")
        if action == "1":
            print("I can't go there. I'm lost!")
        elif action == "2":
            print("I can't go there. I'm lost!")
        elif action == "3":
            print("I can't go there. I'm lost!")
        elif action == "4":
            print("I can't go there. I'm lost!")
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3), the forest entrance(4).")
        elif action == "e":
            print("The screaming is now even closer...")
            print("You are frozen with terror.")
        elif action == "t":
            print("You call for the girl.")
            print("She can hear you now!")
            print("'Help me! Please, help me!'")
            print("Her voice sounds like it's coming from the cabin.")
            print("Will you go?")
            print("Yes(y) or no(n)?")
            action = input("> ")
            if action == "n":
                print("You hide behind some trees.")
                print("You look behind you into the forest.")
                print("There's no turning back.")
            elif action == "y":
                cabin()
            else:
                print("Invalid command! Try again.")

        else:
            print("Invalid command! Try again.")


def cabin():
    global player_gold
    global player_health
    print("\n")
    print("You are right outside of the cabin.")
    print("You draw your sword...")
    while True:
        action = input("> ")
        if action == "1":
            print("No turning back.")
        elif action == "2":
            print("No turning back.")
        elif action == "3":
            print("No turning back.")
        elif action == "4":
            print("No turning back.")
        elif action == "h":
            print("Health:",player_health)
        elif action == "i":
            print("Inventory:")
            for items in player_inventory:
                print(items)
        elif action == "g":
            print("Gold:",player_gold)
        elif action == "m":
            print("Map: the village(1), the river(2), the farmlands(3), the forest entrance(4).")
        elif action == "e":
            print("You walk up to the door and raise your sword.")
            print("Will you enter? Yes(y) or no(n)?")
            action = input("> ")
            if action == "n":
                print("You wait to regain your composure.")
                print("You muster up a little courage.")
                print("Something terrible is waiting behind the door.")
            elif action == "y":
                print("\n")
                print("You open the door. It creaks loudly as peak inside.")
                print("You see a small girl with red hair lying in the corner.")
                print("'Help me! Someone brought me here and he's going to kill me!'")
                print("She sobs hopelessly.")
                print("You ask her where he is.")
                print("'I don't know.' She says. 'He left a few minutes ago to get an axe!'")
                print("'Please help me! My name is Ruby. My dad is looking for me!'")
                time.sleep(10)
                print("\n")
                print("A man in a black robe bursts into the cabin with a bloody axe!")
                time.sleep(5)
                print("\n")
                print("Ruby screams.")
                print("'No! Please. I didn't do anything...'")
                print("'Help me, boy!'")
                time.sleep(5)
                print("\n")
                print("The man raises his axe to slice you apart.")
                print("But you swiftly parry his attack with your sword.")
                time.sleep(5)
                print("\n")
                print("You raise your sword to strike back.")
                print("Ruby puts her hands over her face.")
                time.sleep(5)
                print("\n")
                print("You slice into the man's shoulder.")
                time.sleep(5)
                print("\n")
                print("He strikes again...")
                enemy_attack = random.randint(1,100)
                player_health -= enemy_attack
                print("He swings his axe furiously toward you...")
                time.sleep(5)
                print("\n")
                print("He cuts into your arm as you moan in agony!")
                print("Ruby screams.")
                time.sleep(5)
                print("\n")
                print("You fall on the floor and drop your sword...")
                if player_health > 0:
                    time.sleep(5)
                    print("\n")
                    print("In a last burst of strength,")
                    print("You lift up the sword with both hands")
                    print("and charge toward the shadowy figure.")
                    time.sleep(5)
                    print("\n")
                    print("You cut into the man's chest and pierce his heart.")
                    time.sleep(5)
                    print("\n")
                    print("He falls to the floor.")
                    print("He lies in a puddle of his own blood.")
                    time.sleep(5)
                    print("\n")
                    print("Ruby runs toward you, crying.")
                    print("'You saved my life!' She exclaims enthusiastically.")
                    print("You drop your sword, exausted")
                    time.sleep(5)
                    print("\n")
                    print("She embraces you, tightly.")
                    print("'Please take me home to my daddy's farm...' She begs.")
                    time.sleep(5)
                    print("\n")
                    print("You both return to the farm.")
                    print("The farmer sees you and his daughter walking toward the farm.")
                    print("'Ruby! You're back!'")
                    time.sleep(5)
                    print("\n")
                    print("'You were the boy I saw yesterday!'")
                    print("You shrug in agreement.")
                    time.sleep(5)
                    print("\n")
                    print("'You found my daughter, boy!' He says excitedly.")
                    print("'I can't thank you enough.'")
                    print("Ruby runs home to her father.")
                    print("She looks back at you and smiles.")
                    time.sleep(5)
                    print("\n")
                    print("You return home.")
                    print("Your mother must be worried sick!")


            elif player_health <= 0:
                print("You fall unconscious as the hooded figure drags you away...")
                sys.exit

            else:
                print("Invalid command! Try again.")

        elif action == "t":
            print("You anxiously hyperventilate. You cannot make a sound.")


        else:
            print("Invalid command! Try again.")


menu()
\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! I changed your title so that it hopefully better follows our guide on How do I ask a good question?. Feel free to have a look at the resource I linked and edit it again to make it even more expressive. \$\endgroup\$ – AlexV Jun 19 at 22:49
  • \$\begingroup\$ Thanks for the clarification, AlexV. \$\endgroup\$ – Elijah Jun 19 at 23:08
2
\$\begingroup\$

In print_slow:

  • I would pass in a time delay value, or at the very least have the delay value as a "constant" above. Using magic numbers isn't a good habit to get in to. I'd lean to either one of these:

    def print_slow(str, delay = 0.1):
        for letter in str:
            sys.stdout.write(letter)
            sys.stdout.flush()
            time.sleep(delay)
    
        print("\n")
    

    or

    SLOW_PRINT_DELAY = 0.1
    
    def print_slow(str):
        for letter in str:
            sys.stdout.write(letter)
            sys.stdout.flush()
            time.sleep(SLOW_PRINT_DELAY)
    
        print("\n")
    

    The first has the benefit that you can alter how slowly it prints for different circumstances. This does away with the need for a separate nearly identical function load_effect. I'm having it to default to 0.1 just in case they don't want to specify the delay. You could even combine both of these options and have the constant, and use it as the default value that print_slow defaults to.

  • I'm not entirely sure why you're using sys.stdout directly. It doesn't seem to be for portability reasons since you use plain print right below that. If it's to avoid the newline being added at the end when using print, I'll just point out that you can use the end parameter of print to avoid that:

    print("Some text", end = "") # Will not print a newline after
    

At the top of menu you have a dispatch where you're using lower then checking the input against several characters. My issue with how you have it here is you're calling lower repeatedly (and repeating yourself is never a good thing), and then you repeat that whole thing below that in a verification loop! Wrap the repetitious code up in a function and call it as needed:

def dispatch_action(action_key):
    std_action = action_key.lower() # Standardize it once

    if std_action == "p":
        intro()

    elif std_action == "q":
        sys.exit()

    elif std_action == "i":
        info_menu()

    elif std_action == "h":
        help_menu()

Then, you can return False if they entered a bad action, and True if it was a good action, and loop in menu until dispatch_action returns True.

Or, you could get a little fancy and neaten it up a bit by having a map of functions:

MENU_ACTIONS = {"p": intro,
                "q": sys.exit,
                "i": info_menu,
                "h": help_menu}

def dispatch_action(action_key):
    std_action = action_key.lower() # Standardize it once

    f = MENU_ACTIONS.get(std_action, None) # Default to None if the command is bad

    if f:
        f() # Call the menu function returned by the lookup

    else:
        # Handle a bad command

I like dictionaries when you're just doing simple matching against something like a String or number. It'll be potentially faster than an if tree (although that doesn't matter here), and I just personally like how they read. It makes it so you don't have to write (and read) std_action == over and over again.


There's a lot more to get into here, but I'm quite tired. Hopefully someone else can comment on the rest.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your especially useful and thought-provoking feedback. And thank you for taking the time out of your day to reply despite being tired. \$\endgroup\$ – Elijah Jun 24 at 2:06
1
\$\begingroup\$

The right tool for the job

Adventure games are data-driven programs. In the game, you will have objects which have descriptions. You will have places, which have descriptions, contents, and connections to other locations. You will have a player, who will have a description, an inventory (contents), and move from location to location.

Inform 7 is an interactive fiction programming language, which allow you to describe the world to the game engine in a natural language, and it will help you build the interactions by providing a standard library of actions, input parsing, etc., and allow someone to play the story.

But if your goal is to learn Python, and this is just a project to work on while you are learning...

Infinite Recursion

You game never ends.

menu() can call help_menu() which can call help_menu() which can call info_menu() which can call help_menu() which can call intro() which will call village(), which can call home() which can call village() which can call ...

Your stack can become infinitely deep. Well, actually, it can't; the interpreter will eventually give up and raise an exception if it gets too deep. You probably will never encounter this is normal game play, but if you ever had a robot tester for the program, it could move back and forth between locations and eventually cause a stack overflow.

You want your functions to return to their caller, eventually.

Don't Repeat Yourself (DRY)

The opposite of DRY code is WET code ... Write Everything Twice. In your shop() function, you've written almost exactly the same code 4 times!

            if action == "f":
                  if "Fishing poll" in player_inventory:
                      print("I aleady have one!")
                  elif player_gold >= 10:
                      player_inventory.append("Fishing poll")
                      player_gold -= 10
                      print("'Here you go!'")
                      print("You bought a lovely handmade fishing poll.")
                  elif player_gold < 5:
                      print("Sorry you don't have enough for this, kid.")
            elif action == "t":
                  # Almost identical code
            elif action == "g":
                  # Almost identical code
            elif action == "r":
                  # Almost identical code
            else:

Let's dry this up:

            if action == "f":
                buy("Fishing poll", 10, "lovely handmade fishing pole")
            elif action == "t":
                buy("Tunic", 20, "bulky leather tunic")
            elif action == "g":
                buy("Guitar", 30, "gorgeous guitar")
            elif action == "r":
                buy("Raft", 30, "small wooden raft")
            else:

def buy(item, cost, description):
    global player_gold, player_inventory

    if item in player_inventory:
        print("I already have one!")
    elif player_gold >= cost:
        player_inventory.append(item)
        player_gold -= cost
        print("'Here you go!'")
        print(f"You bought a {description}.")
    else:
        print("Sorry you don't have enough for this, kid.")

This will fix two bugs:

  1. Buying a Tunic will add a Leather Tunic to your inventory, not a Tunic, so you can keep buying a Tunic until you run out of money.
  2. A fishing pole cost 10 gold, but you are only told you can't buy one if you have less than 5 gold.

We still repeat ourself for the items and their costs when the shopkeeper describes what you can buy. So let's keep going:

            store_inventory = {
                'f': ("Fishing poll", 10, "lovely handmade fishing pole"),
                't': ("Tunic", 20, "bulky leather tunic"),
                'g': ("Guitar", 30, "gorgeous guitar"),
                'r': ("Raft", 30, "small wooden raft")
            }

            print("You can buy:")
            for key, item_data in store_inventory.items():
                item, cost, description = item_data
                print(f"   a {item.lower()}({key}) for {cost} gold")
            action = input("> ")

            if action in store_inventory:
                item, cost, description = store_inventory[action]
                buy(item, cost, description)

This can work for your other bread merchant as well.

Certain player actions are available in every location/scene.

  • h - Player health
  • i - Player inventory
  • g - Player gold
  • m - Map

With the exception of the map command, these are all identical. The map command displays different text depending on the location.

def player_commands(action):
    if action == 'h':
        print(f"Heath: {player_health}")
    elif action == 'i':
        print("Inventory:")
        for item in player_inventory:
            print(item)
    elif action == 'g':
        print(f"Gold: {player_gold}")
    elif action == 'm':
        show_map()
    else:
        return False     # Not a Player Command, so not handled
    return True          # Player Command handled

Now, in the village, or forest, or outskirts, or shop, or river, or cabin, or home, you can use the following, instead of repeating the same 10 lines over and over again:

    if player_command(action):
        pass
    elif action == 'e':
    # etc.

Object Orientation

From your code, we can tell you know how to use strings, lists, functions, loops, and if/elif/else statements. I introduced a dictionary and a tuple in the store’s inventory, above. Those, along with classes, are going to make your game easier to write.

Right now you have player_gold, player_inventory and player_health. 3 separate global variables which are all related to the player. We can encapsulate these into an object.

player = object()
player.health = 100
player.inventory = ["Map"]
player.gold = 50

This is an ad-hoc object, with 3 members. Having it has the advantage that you don’t need to pull 3 global variables into various functions, just the one player global. And since you will never modify the player global (just its contents), you don’t even need global player in any function; it you haven’t defined a local player variable, the function will use the global player automatically.

We can be a bit more structured. The player is a person, but so it the merchant, the little boy, the farmer, your mother, the friendly woman, Ruby and the man in a black robe. You fight the man in the black robe, so he should probably have health that can be eroded. Both you and Ruby have names. Maybe you should declare a Person class.

class Person:
    def __init__(self, name):
        self.name = name
        self.health = 100
        self.inventory = []
        self.gold = 0
        self.location = None

player = Person("Arris")
player.gold += 50
player.inventory.append("Map")

mother = Person("your mother")

ruby = Person("Ruby")

The world is full of things. Things have names. Things have descriptions. Things should have a class:

class Thing:
    def __name__(self, name, description):
        self.name = name
        self.description = description

sword = Thing("Sword", "a beautiful and sharp sword")
bread = Thing("Bread", "fresh bread")

In a similar vein, the world is full of locations. The locations have names. The locations have contents. The locations have connections to other locations. A location should be a class, and each location should be a member of that class.

class Location:
    def __init__(self, name):
        self.name = name
        self.contents = []
        self.connections = {}

    def remove_item(self, item):
        self.contents.remove(item)

home = Location("at home")
village = Location("in the village")
river = Location("by the river")
outskirts = Location("in the outskirts")

# ... etc ...

home.contents.append(mother)
home.connection['1'] = village
home.connection['2'] = river
# ... etc ...

outskirts.contents.append(sword)

It may not be immediately obvious, but a Person is a Thing, and a Location is a Thing, so you could could make the Person and Location classes derive from Thing. The Person’s inventory would be their contents. If you extend your game, a Thing could be a Container (such as a box), which also has contents ... so you might consider deriving Person and Location from Container which is derived from Thing. Some class hierarchies will add value; some class hierarchies may just create confusion. Is a Person really a Container? Does that add any useful simplifications from a programming point of view?

I mentioned Player actions earlier, which can be invoked anywhere the player is. These actions could be attached to a Player object:

class Player(Person):
    def __init__(self, name):
        super().__init__(name)

    def perform(self, action):
        if action == 'h':
            print("Health: {self.health}")
        # ... etc

When the player is in a different locations, the e and t actions behave differently.

         elif action == 'e':
             self.location.explore()
         elif action == 't':
             self.location.talk()

... and we can attach actions to the Location class:

class Location:

    def explore(self):
        if self.contents:
            print("You see:")
            for item in self.contents:
                print(item)
        else:
            print("You poke around, but don’t find anything interesting")

    def talk(self):
        print("You mutter to yourself.")

Different locations may have different behaviours. For example, taking the sword in the outskirts might invoke the wrath of a snake. (Talking to you mom will get her to heal you.) So you might want to further specialize the Location class.

class Outskirts(Location):

    def remove_item(self, item):
        if item == sword:
            print("A snake bites you")
            player.health -= 40
        super().remove_item(item)

Framework

Once you have completed your framework for a game, with many objects, many rooms, many actions, and so on, you can create a completely different adventure game by changing the items, descriptions, and triggers. You might consider loading the world from a structured file, with the same adventure game framework supporting multiple games, by operating on different data. Of course, this means you have effectively reinvented the Inform7 game engine, mentioned at the start.

\$\endgroup\$
  • \$\begingroup\$ Great in-depth review. I'm not convinced about 'Thing' though. Perhaps copy-pasting 'Name' and 'Description' to the classes that require these attributes is better. I'm not a fan of object hierarchies that contain very lightweight bases classes. \$\endgroup\$ – dfhwze Jun 23 at 5:30
  • 1
    \$\begingroup\$ @dfhwze Python’s mix-in classes might be more appropriate, to make something that is a Container (you can put things in), a Supporter (you can put things on), Animate (does stuff by itself), Enterable (a location a player can move to). class SelfDrivingCar(Thing, Container, Supporter, Animate, Enterable): .... Alternately, everything could be a Thing and the those attributes could be stored in an EnumFlag in the Thing object. Many many ways to do this. I look forward to reviewing iterations 2, 7, and 12 of the resulting framework. \$\endgroup\$ – AJNeufeld Jun 23 at 14:16
  • \$\begingroup\$ @AJNeufeld Thank you so much for the in-depth micro lesson on OOP and writing clean code. I very much appreciate the feedback. I know the code was repetitious but I also expected I would get some real solutions to refactor it and I got more than I was expecting. Do you know any books on OOP that you would suggest? \$\endgroup\$ – Elijah Jun 24 at 2:03

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.