4
\$\begingroup\$

I'm a beginner working on a text adventure. It's quite bare-bones at this point but I would appreciate feedback before I continue and the flaws in my code become further ingrained.

I plan to add an item pickup and inventory system which can store items to heal and deal damage to enemies, some problem-solving and expand the number of locations. I'd like to hear some things I should implement which could help me learn though.

# A Countryside Adventure

#from questionmodule import * # moved module to this file for posting on forum
from sys import exit
import random


def ask_yes_no(question):
    """Ask a yes or no question"""

    allowed_answers = ("y", "yes", "n", "no")
    response = None
    while response not in allowed_answers :
        response = input(question).lower()
        if response not in allowed_answers:
            print("That is not a valid response.")
    return response

def ask_yes_no_boolean(question):
    """As a yes or no question and return response as boolean"""

    allowed_answers = ("y", "yes", "n", "no")
    response = None
    while response not in allowed_answers:
        response = input(question).lower()
        if response not in allowed_answers:
            print("That is not a valid response.")
    if response in ("y", "yes"):
        return True
    return False

def two_option_q(question, option_one, option_two):
    """As a two option question and return response as boolean"""

    allowed_answers = (option_one, option_two)
    response = None
    while response not in allowed_answers:
        response = input(question).lower()
        if response not in allowed_answers:
            print("That is not a valid response.")
    if response == (option_one):
        return option_one
    else:
        return option_two

def ask_left_right(question):
    """Ask a left or right question"""

    allowed_answers = ("left", "l", "right", "r")
    response = None
    while response not in allowed_answers:
        response = input(question).lower()
        if response not in allowed_answers:
            print("Sorry that is not a valid response.")
    if response in ("r", "right"):
        return "right"
    else:
        return "left"


class Character(object):
    """A character in a fictional world"""

    def __init__(self, name, health):
        self.name = name
        self.health = health

    def __str__(self):
        rep = self.name + " has health: " + str(self.health)
        return rep

    def attack(self, other):
        raise NotImplementedError



class Player(Character):
    """A player in a fictional world"""

    STRIKES = {"headbutt" : 4 , "kick" : 3, "punch" : random.randrange(1, 4)}

    def __init__(self, name, health = 10):
        super(Player, self).__init__(name, health)

    def attack(self, other):
        strike = ask_yes_no_boolean("\nStrike the {}(yes/no)?".format(other.name))
        if strike:
            strike_type = input("What type of strike(headbutt, kick, punch)? ")\
                          .lower()
            if strike_type in Player.STRIKES:
                print("You got the {} with a {} and dealt {} damage!".format(\
                    other.name, strike_type, Player.STRIKES.get(strike_type)))
                other.health -= Player.STRIKES.get(strike_type)
            else:
                print("Oops. You missed!")
        else:
            print("You wuss!")



class Enemy(Character):
    """An enemy is a fictional world"""

    def __init__(self, name, health = 7, damage = 2):
        super(Enemy, self).__init__(name, health)
        self.damage = damage

    def attack(self, other):
        other.health -= self.damage
        print("The {} attacks and deals you {} damage!".format(self.name,\
                                                               self.damage))

    def die(self):
        print("You have defeated the {}!".format(self.name))


class Location(object):
    """A location in an adventure game"""

##    ENEMY_NAMES = ["Boar", "Troll", "Wild cat"]
##    enemy = Enemy(random.choice(ENEMY_NAMES))

    def enter(self):
        pass

    def battle(self, player, enemy):
        print("A {} suddenly appears!".format(enemy.name))
        fight = two_option_q("\nFight or run(fight/run)? ", "fight",\
                                 "run")
        if fight == "fight":
            while player.health and enemy.health > 0:
                #print("\n")
                enemy.attack(player)
                if player.health <= 0:
                    break
                print(player)
                print(enemy)
                player.attack(enemy)

            if enemy.health <= 0:
                print("You have defeated the {}!".format(enemy.name))
                #enemy.die()
            elif player.health <= 0:
                print("You are killed by the {}!".format(enemy.name))
                return True

        else:
            print("You escaped unharmed. But he shall be waiting should you \
venture")
            print("to these parts again!")



class Meadow(Location):
    """A meadow in an adventure game"""

    ENEMY_NAMES = ["Boar", "Troll", "Wild cat"]
    enemy = Enemy(random.choice(ENEMY_NAMES))

    def enter(self, player):
        print(player)
        print("{}, you are in a meadow.".format(player.name))

        if Meadow.enemy.health > 0:
            player_dead = self.battle(player, Meadow.enemy)
            if player_dead:
                return "death"
        else:
            print("The slain body of the {} lies slumped pathetically on the\
 ground.".format(Meadow.enemy.name))

        print("\nTo your left is a beach, to your right a forest.")
        left_right = two_option_q("Which way do you go(left/right)? ", "left",\
                                   "right")

        if left_right == "left":
            return "beach"
        else:
            return "forest"


class Beach(Location):
    """A beach location in an adventure game"""

    ENEMY_NAMES = ["Boar", "Troll", "Wild cat"]
    enemy = Enemy(random.choice(ENEMY_NAMES))

    def enter(self, player):
        print(player)
        print("You are at the beach.")

        if Beach.enemy.health > 0:
            player_dead = self.battle(player, Beach.enemy)
            if player_dead:
                return "death"
        else:
            print("You pass the lifeless body of the {}.".format(Beach.enemy.\
                                                                 name))
        print("\nTo your left is a meadow, to your right a forest.")
        left_right = two_option_q("Which way do you go(left/ right)? ", "left"\
                                  , "right")

        if left_right == "left":
            return "meadow"
        else:
            return "forest"


class Forest(Location):
    """A forest in an adventure game"""

    ENEMY_NAMES = ["Boar", "Troll", "Wild cat"]
    enemy = Enemy(random.choice(ENEMY_NAMES))

    def enter(self, player):
        print(player)
        print("You are in the forest.")

        if Forest.enemy.health > 0:
            player_dead = self.battle(player, Forest.enemy)
            if player_dead:
                return "death"
        else:
            print("You smile as you pass the corpse of the {}.".format(\
                Forest.enemy.name))

        print("\nTo your left is meadow, to your right a beach.")
        left_right = two_option_q("Which way do you go(left/right)?", "left",\
                                   "right")
        if left_right == "left":
            return "meadow"
        else:
            return "beach"


class Death(object):
    """The death of a player in an adventure game"""

    def enter(self, player):
        print("Game Over.")
        return "break"



class Game(object):
    """A fictional countryside adventure"""

    def __init__(self, location_map):
        self.location_map = location_map

    def play(self):
        current_location = self.location_map.start_location()
        while True:
            next_location_name = current_location.enter(self.\
                                                        location_map.player)
            if next_location_name == "break":
                break
            current_location = self.location_map.next_location\
                               (next_location_name)


class Map(object):

    LOCATIONS = {"meadow" : Meadow(),
                 "beach" : Beach(),
                 "forest" : Forest(),
                 "death" : Death()}

    def __init__(self, location, player):
        self.location = location
        self.player = player

    def next_location(self, location_name):
        return Map.LOCATIONS.get(location_name)

    def start_location(self):
        return self.next_location(self.location)


def main():
    print("\n\n\t\tCountryside adventure")
    name = input("\n\nWelcome adventurer! What might be your name? ")
    print("\nI wish you the very best of luck, {}!.\n".format(name))

    player = Player(name)
    a_map = Map("meadow", player)
    game = Game(a_map)
    game.play()

play = True
while play:
    main()
    play = ask_yes_no_boolean("Do you want to play again? ")
\$\endgroup\$
3
\$\begingroup\$

This is a lot of code to run through, but I'll pick apart some sections of the code that stood out to me as needing improvement. Mind you, I'm privy to Python 2.7. Hopefully my code won't be foreign to you.

First, starting with the question functions -- you could cut down quite a few things. For example, how you take user input, and go about using the data. A simpler way of writing the function ask_yes_no would be like this:

def ask_question(question, boolean):
    while True:
        inp = raw_input(question + ': ').lower()
        lst = [x for x in ['yes', 'no'] if x.startswith(inp)]
        if len(lst) == 0:
            print 'Not a valid input.'

        elif len(lst) == 1:
            response = lst[0]
            break

    if boolean is True:
        if response == 'yes':
            return True

        else:
            return False

    else:
        return response

ask_question( ''' Question, Return as boolean or not? ''' )

As you can see, you can simply combine your first two question functions into one, by adding an argument to tell the code whether or not to return the response as a boolean. Also, by using the list [x for x in ['yes', 'no'] if x.startswith(inp)], users are now able to type just the first letter of a response, e.g. y for yes. If you sincerely want to keep specific lists on what response to allow, I would recommend adding another argument titled responses, and calling the function ask_question with a list of responses as an argument -- if you ever want to expand outside of just yes / no questions. Honestly, you could probably squeeze most of your questions into just a couple of functions, instead of the four you currently have.

Secondly, I would like to note the Player class for a moment. To start, you might want to scrap the class Death and add the concept as a variable or method of the Player class. Also, don't reference the actual class name inside of its own methods, i.e. with the line if strike_type in Player.STRIKES: in the method attack, the Player.STRIKES part could just be self.STRIKES. Though maybe you should just include it as an attribute of the __init__ statement? You may also want to add a take_damage method to the player class instead of changing it through a method of the Enemy class?

Speaking of the Enemy class, perhaps you should simply write a function named 'generate' that creates enemies? For instance, when a player walks into a new location, you can have an argument for the location that takes an enemy object, and then create one through the class method.

class Enemy(Character):
    enemy_list = ['boar', 'troll', 'wild cat']

    def __init__(self, name, health, damage):
        self.damage = damage
        super(Enemy, self).__init__(name, health)

    @classmethod
    def generate(cls, player):
        name = choice(cls.enemy_list)
        damage = randint(1, player.health / 2)
        health = player.health - (damage)
        return cls(name, health, damage)

enemy = Enemy.generate(player)
print enemy.name, enemy.damage, enemy.health

If you ran Enemy.generate('''player'''), it would return an object with the values printed above: name: troll, dmg: 3, health: 7, or something of the like. Doing things like this would allow you to run this function to generate a new enemy every time the player enters a new location, or a combat situation. Maybe you should create an __init__ function for class Location, and include an argument for enemies. And for the Map class, perhaps you should create an actual map, through something like a 2-D array, and create a custom world!

In any case, your game seems to be coming along. Try implementing more efficient functions, improve and expand on your Player and Enemy classes, a 2-D array to hold a world map, and possibly a visual map as well.

Inventory systems are fairly easy to put together. I would recommend using @classmethods to generate weapons, armor, and other in-game items just like you would generate enemies.

Hope my answer was informative!

\$\endgroup\$
  • \$\begingroup\$ Thank you very much! I really appreciate that. I've read through your post and am in the process of amending my code. I'll report back soon! \$\endgroup\$ – Michael Johnson Aug 14 '16 at 7:49
3
\$\begingroup\$

I don't completely agree with @PyDive's answer on the function ask_question. While I do agree that you should combine the different asking functions, I would do it more like this:

def ask_question(question, boolean=False, options=["yes", "no"]):
    """
    Ask user a question. Returns True or False if boolean (default: False).
    User can choose from options (default: ["yes", "no"])
    Always returns a string (or boolean), regardless of type in options.
    """
    options = ["yes", "no"] if boolean else [str(option) for option in options]
    while True:
        print(question)
        print("Options: {}".format(" ".join(options)))
        response = input("Input: ").lower()
        if boolean and response[0] in ("y", "n"):
            return response[0] == "y"
        if response in options:
            return response
        print("Invalid choice")

Example usages:

>>> ask_question("Food?")
Food?
Options: yes no
Input: yes
'yes'

>>> ask_question("Food?", boolean=True)
Food?
Options: yes no
Input: no
False

>>> ask_question("Food?", boolean=True)
Food?
Options: yes no
Input: y
True

>>> ask_question("Food?", options=["burger", "pizza"])
Food?
Options: burger pizza
Input: yes
Invalid choice
Food?
Options: burger pizza
Input: burger
'burger'

>>> ask_question("How many?", options=[1, 2, 10])
How many?
Options: 1 2 10
Input: 10
'10'

This has the disadvantage that boolean questions are hardcoded to be "yes" or "no". It has the advantage that any question is possible and the return logic is simplified with respect to @PyDive's. It always returns a string as response (or boolean if boolean == True). It has a docstring :)

(what raw_input used to be in python 2.x is now just input in python 3.x)

Apart from this I totally agree with @PyDive, exploiting the @classmethods will help you greatly.

\$\endgroup\$
  • \$\begingroup\$ I appreciate that, thanks! Class methods are completely new to me (aware of static methods which I believe are somewhat similar) as is some of the other material so I've some swatting up to do. \$\endgroup\$ – Michael Johnson Aug 14 '16 at 17:56
  • \$\begingroup\$ Yeah, they still seem like magic to me as well, once in a while... \$\endgroup\$ – Graipher Aug 14 '16 at 19:26
  • \$\begingroup\$ Can you check your code? Doesn't work for me. In the meantime I've made my own with yours as a template. \$\endgroup\$ – Michael Johnson Aug 15 '16 at 8:49
  • \$\begingroup\$ @MichaelJohnson Fixed, had a vestigial any floating around. \$\endgroup\$ – Graipher Aug 15 '16 at 9:14
  • \$\begingroup\$ I made just one small change: if boolean and response in ["y", "n"] + options: That way the word 'yesterday' for example won't be accepted. Only "y", "n", "no", "yes". It seems to work fine, I hope there's not something I'm missing. \$\endgroup\$ – Michael Johnson Aug 15 '16 at 10:10

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.