# My beginner attempt at a text adventure

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

"""Ask a yes or no question"""

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

"""As a yes or no question and return response as boolean"""

allowed_answers = ("y", "yes", "n", "no")
response = None
response = input(question).lower()
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"""

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

"""Ask a left or right question"""

allowed_answers = ("left", "l", "right", "r")
response = None
response = input(question).lower()
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):
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!")

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))

return "death"
else:
print("The slain body of the {} lies slumped pathetically on the\

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:
return "death"
else:
print("You pass the lifeless body of the {}.".format(Beach.enemy.\
name))
left_right = two_option_q("Which way do you go(left/ right)? ", "left"\
, "right")

if left_right == "left":
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:
return "death"
else:
print("You smile as you pass the corpse of the {}.".format(\
Forest.enemy.name))

left_right = two_option_q("Which way do you go(left/right)?", "left",\
"right")
if left_right == "left":
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):

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):

"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("\nI wish you the very best of luck, {}!.\n".format(name))

player = Player(name)
game = Game(a_map)
game.play()

play = True
while play:
main()
play = ask_yes_no_boolean("Do you want to play again? ")


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.

• 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! – Michael Johnson Aug 14 '16 at 7:49

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'

Food?
Options: yes no
Input: no
False

Food?
Options: yes no
Input: y
True

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.

• 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. – Michael Johnson Aug 14 '16 at 17:56
• Yeah, they still seem like magic to me as well, once in a while... – Graipher Aug 14 '16 at 19:26
• Can you check your code? Doesn't work for me. In the meantime I've made my own with yours as a template. – Michael Johnson Aug 15 '16 at 8:49
• @MichaelJohnson Fixed, had a vestigial any floating around. – Graipher Aug 15 '16 at 9:14
• 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. – Michael Johnson Aug 15 '16 at 10:10