You have bugs, bugs everywhere! inventory=[]
looks innocent, nothing possibly can go wrong here! But it's actually very different to how I think you think it works.
To show this bug, I'll make a function, that takes some data, mutates it a little and returns it. If we pass nothing to it, it should return just the mutation.
So to keep it simple we append 'test'
to it. So test() -> ['test']
and test(['this', 'is', 'a']) -> ['this', 'is', 'a', 'test']
.
But I'll show you the bug.
>>> def test(data=[]):
data.append('test')
return data
>>> test()
['test']
>>> test(['this', 'is', 'a'])
['this', 'is', 'a', 'test']
>>> test() == test()
True
>>> test() is test()
True
>>> test()
['test', 'test', 'test', 'test', 'test', 'test']
Yes, you can test it yourself. That is not what you want.
And you should be able to see every time we call test()
it adds one 'test'
to the list.
This is as all function calls share the same instance of the list.
Which is why test() is test()
is True
.
To fix this you should change all default parameters to None
.
And overwrite it with a new instance of a list.
And so I'd take a leap out of JavaScripts book and use or
to the overwrite the list.
Allowing for:
>>> def test(data=None):
data = data or []
data.append('test')
return data
>>> test()
['test']
>>> test()
['test']
>>> test(['this', 'is', 'a'])
['this', 'is', 'a', 'test']
Or if you don't like that a more verbose, but probably a better function:
>>> def verb_test(data=None):
if data is None:
data = []
data.append('test')
return data
>>> l = []
>>> test(l)
['test']
>>> l
[]
>>> verb_test(l)
['test']
>>> l
['test']
But ultimately it's your choice. I'm going to use or
as it makes smaller code.
Your classes are good. But I'd change them. A lot.
The class SadSad
is bad as it's just going to end up as an instance of Enemy.
But if you really need this class I'd say use type
, SadSad = type('SadSad', (object,), {})
.
This is as you then don't need to write all the boilerplate to make a class, and you can create all the classes on one or two lines using a comprehension.
You may prefer the class
way however.
The reason I'd still make the minion a type, is as you can then just make a ton of them.
But I'd not make them a class as you aren't going to and don't add anything new.
I'd also not do goblin = Enemy(name="goblin", hp=50, attack=5)
. As then you can't make other goblins, and there is no hierarchy.
I'd move inspect
into Character
, and remove inspect_other
. This is as inspect_other
is going to be used to inspect a Character
or a subclass of it,
which already has all the attributes in inspect
.
I'd also remove the inspect_other
as it's a duplicate of inspect
. And should have no use after moving inspect
to Character
.
I'd re-organise your code so that you group classes. You broke the inventory management group by putting Room
between Player
and Item
.
And this threw me off when I was reading your code.
I'd finally add an Equipment
class that handles adding and removing items from your equipment and inventory.
All the additional code that you added to the Player
class modifies these properties.
And so I'd write a new class to handle this, so that if you need to add it to a special NPC you can add it with ease.
To make it somewhat easy to add I'd add it to a class via inheritance. This has the down-side of you having to manually initialize it.
But it allows you to add two things and for the code to be fine.
If I didn't like this idea then I would make it a stand alone class and manually instantiate it and call it from the child class.
This allows a separation of the two objects, which doesn't make too much sense, if the whole point of the class it to allow you to easily add a feature to an existing class.
Either way, to change to and from each other is easy.
I made some changes to the classes as I didn't like how you implemented some of the code.
If we look at equip_gear
you should be able to see that there is code duplication, and the only change is that you changed from self.weapon
to self.armor
.
Instead I'd make a function that tells us which slot the object belongs in, then a function that actually equips the item, and the original function who's purpose is to interface these two functions.
I also removed the print functions from this function, as I don't like the idea of printing and mutating data. As it violates SRP.
I'd also like to mention that unequip
wouldn't allow us to re-equip the gear, as it never changed the state of the item.
And I used the same approach as I did for equipping the item.
This resulted in me getting the following Equipment
class:
class Equipment:
def __init__(self, equipment=None, inventory=None):
self.equipment = equipment or {}
self.inventory = inventory or []
def collect(self, item):
if item.state == None:
self.inventory.append(item)
return True
return False
@staticmethod
def _get_slot(item):
types = [(Weapon, 'weapon'), (Armor, 'armor')]
for class_, slot in types:
if isinstance(item, class_):
return slot
return None
def equip(self, item):
slot = self._get_slot(item)
if slot is None:
return False
return self._equip(item, slot)
def _equip(self, item, slot):
if self.equipment[slot] is None and item in self.inventory:
self.equipment[slot] = item
item.state = True
self.inventory.remove(item)
return True
return False
def unequip(self, item):
slot = self._get_slot(item)
if slot is None:
return False
return self._unequip(item, slot)
def _unequip(self, item, slot):
if item == self.equipment[slot]:
item = self.equipment[slot]
self.inventory.append(item)
self.equipment[slot] = None
item.state = None
return True
return False
To use the inheritance approach, you will have to manually call the __init__
of the Equipment
class.
I also added the prints here so that I don't fully break your program.
And so I changed the Player
class to:
class Player(Character, Equipment):
def __init__(self, equipment=None, inventory=None, **kwargs):
super().__init__(**kwargs)
Equipment.__init__(self, equipment, inventory)
@property
def attack(self):
return self._attack + self.equipment['weapon'].attack
def collect(self, item):
if not super().collect(item):
print('You cannot collect that.')
def equip(self, item):
if not super().equip(item):
print('That cannot be equipped.')
From this you should notice that I added a property to the class, one called attack
.
This is to simplify the parent class, and all other sub-classes.
The property wrapper allows us to use it as if the function is just a variable.
It also means that we can't set it, with say player.attack = 6
.
And so is based off the other two attributes.
This is good as then we can just overwrite it in the other classes to allow for greater changes in how they attack.
I also changed the print
s, this is as reading self.name + " is attacking " + other.name + "!!!!"
is harder than if we were to use format
.
This is as with format
we can use a mini-language that lets us pass in a variable to any '{}'
.
I pass the object self
and other
, then in the string I index the variables you need such as name
.
Which resulted in print('{0.name} is attacking {1.name}.\nThey dealt {0.attack} damage!'.format(self, other))
.
I'd also like to mention that in Python3 you don't have to inherit from object
, as all classes are now new-style classes by default.
And you shouldn't use super
in it either as "object()
takes no parameters".
And so I implemented:
class Character:
def __init__(self, name='', hp=0, attack=0, **kwargs):
self.name = name
self.hp = hp
self._attack = attack
@property
def attack(self):
return self._attack
def Attack(self, other):
print('{0.name} is attacking {1.name}.\nThey dealt {0.attack} damage!'.format(self, other))
other.hp -= self.attack
def inspect(self):
print('{0.name} has {0.hp} HP and {0.attack} Attack.'.format(self))
eval
is evil! Don't use it!
Instead try to use strings, Python allows for a lot of ways to do this.
I personally would add say a get
command to the room to have an easy way to get both the item and the index of the item in one command.
The input should be the item name and the list, where the output should be the index and item (it's instance).
This allows for us to easily remove the item from the list if we need to.
As there is a possibility that we won't get an item I added a print to tell us that.
(Your program would just error)
Otherwise I'd room_actions["get"](item)
and remove it from the room, room.items.pop(index)
.
I also added a 'fight' and an 'inspect' method that worked in the same way.
I would also not base the program on repetitive calls to the same function rather than using a while
loop.
This is as the program will reach the maximum recursion depth.
And the only way to fix this is to not use recursion, ignore the answers on SO that say to change the recursion depth, as it won't work out.
I also changed a few of the variable names and ended up changeing Room
and room_logic
to:
class Room:
def __init__(self, ID=None, description='',
characters=[], items=[], **kwargs):
super().__init__(**kwargs)
self.ID = ID
self.description = description
self.characters = characters
self.items = items
def get(self, list, name):
list = getattr(self, list)
for index, item in enumerate(list):
if item.name == name:
return index, item
return -1, None
def __str__(self):
print(self.description)
def room_logic():
room = current_room
user = current_char
print("(type 'look' to look, type 'get sword' to get the sword, and type 'move north' to move to a different room)")
while True:
action = input("What will {0.name} do? ".format(user)).strip()
actions = action.split()
if action == "look":
print(room.description + '\n')
elif actions[0] == "get":
index, item = room.get('items', actions[1])
if item is None:
print('No item {}.'.format(actions[1]))
continue
room_actions["get"](item)
room.items.pop(index)
print("You picked up {}.".format(actions[1]))
elif actions[0] == "fight":
index, char = room.get('characters', actions[1])
if char is None:
print('No character {}.'.format(actions[1]))
continue
user.Attack(char)
if char.hp <= 0:
room.characters.pop(index)
print('{0.name} died.'.format(char))
else:
print('{0.name} has {0.hp} left.'.format(char))
elif actions[0] == "inspect":
index, char = room.get('characters', actions[1])
if char is None:
print('No character {}.'.format(actions[1]))
continue
char.inspect()
elif actions[0] == "move":
if actions[1] in game_map[room.ID]:
room = eval(game_map[room.ID][actions[1]])
print("You have moved " + actions[1] + "!")
else:
print("What does that mean?")
To further improve this you can look into graphs in Python.
They are quite easy in Python as Python is abstracted quite far from the metal.
And so to make a triangular graph you could do:
class Node:
def __init__(self, data, n=None):
self.data = data
self.n = n
a = Node(1)
b = Node(2, a)
c = Node(3, b)
a.n = c
print(a.data) # 1
print(a.n.data) # 3
print(a.n.n.data) # 2
print(a.n.n.n.data) # 1
Using something like this you could change your Room
class to work without eval
.
I'd also highly recommend that you read and follow PEP8, your code has a lot of inconsistency.
And it was hard to work with.
Here's all the changes I made:
import time
import random
class Equipment:
def __init__(self, equipment=None, inventory=None):
self.equipment = equipment or {}
self.inventory = inventory or []
'''
def __getattribute__(self, name):
equipment = self.equipment.get(name, None)
if equipment is not None:
return equipment
return self.__dict__[name]
'''
def collect(self, item):
if item.state == None:
self.inventory.append(item)
return True
return False
@staticmethod
def _get_slot(item):
types = [(Weapon, 'weapon'), (Armor, 'armor')]
for class_, slot in types:
if isinstance(item, class_):
return slot
return None
def equip(self, item):
slot = self._get_slot(item)
if slot is None:
return False
return self._equip(item, slot)
def _equip(self, item, slot):
if self.equipment[slot] is None and item in self.inventory:
self.equipment[slot] = item
item.state = True
self.inventory.remove(item)
return True
return False
def unequip(self, item):
slot = self._get_slot(item)
if slot is None:
return False
return self._unequip(item, slot)
def _unequip(self, item, slot):
if item == self.equipment[slot]:
item = self.equipment[slot]
self.inventory.append(item)
self.equipment[slot] = None
item.state = None
return True
return False
class Item(object):
def __init__(self, name='', state=None, **kwargs):
super().__init__(**kwargs)
self.name = name
self.state = state
class Weapon(Item):
def __init__(self, attack=0, **kwargs):
super().__init__(**kwargs)
self.attack = attack
class Armor(Item):
def __init__(self, armor=0, **kwargs):
super().__init__(**kwargs)
self.armor = armor
class Character:
def __init__(self, name='', hp=0, attack=0, **kwargs):
self.name = name
self.hp = hp
self._attack = attack
@property
def attack(self):
return self._attack
def Attack(self, other):
print('{0.name} is attacking {1.name}.\nThey dealt {0.attack} damage!'.format(self, other))
other.hp -= self.attack
def inspect(self):
print('{0.name} has {0.hp} HP and {0.attack} Attack.'.format(self))
class Enemy(Character):
def __init__(self, **kwargs):
super().__init__(**kwargs)
class Player(Character, Equipment):
def __init__(self, equipment=None, inventory=None, **kwargs):
super().__init__(**kwargs)
Equipment.__init__(self, equipment, inventory)
@property
def attack(self):
return self._attack + self.equipment['weapon'].attack
def collect(self, item):
if not super().collect(item):
print('You cannot collect that.')
def equip(self, item):
if not super().equip(item):
print('That cannot be equipped.')
class Room:
def __init__(self, ID=None, description='',
characters=[], items=[], **kwargs):
super().__init__(**kwargs)
self.ID = ID
self.description = description
self.characters = characters
self.items = items
def get(self, list, name):
list = getattr(self, list)
for index, item in enumerate(list):
if item.name == name:
return index, item
return -1, None
def __str__(self):
print(self.description)
fop = Weapon(name='Fists of Punching', attack=5)
sword = Weapon(name="sword", attack = 6)
goblin = Enemy(name="goblin", hp=50, attack=5)
SadSad = Enemy(name="SadSad", hp=5, attack=1)
r = Player(name="jeremyodell", hp=999, equipment={'weapon':fop})
Room0 = Room(ID="Room0", description="You are literally in Hell. There is fire all \
around. You are scared and tired. There is a weapon on the ground. \
You see a goblin and a sword.", characters = [goblin],\
items = [sword])
Room1 = Room(ID="Room1", description="You have escaped from Lucifer, and now find\
yourself surrounded by two SadSads. Also there are a pair of fists on the ground.",\
characters = [SadSad, SadSad], items = [sword, fop])
current_room = Room0
current_char = r
game_map = {"Room0": {"north":"Room1", "east":"Room2", "west":"Room3"},
"Room1": {"south":"Room0", "east":"Room4", "west":"Room5"}}
room_actions = {"get": current_char.collect, "talk":0,"menu":0, "fight":0}
def room_logic():
room = current_room
user = current_char
print("(type 'look' to look, type 'get sword' to get the sword, and type 'move north' to move to a different room)")
while True:
action = input("What will {0.name} do? ".format(user)).strip()
actions = action.split()
if action == "look":
print(room.description + '\n')
elif actions[0] == "get":
index, item = room.get('items', actions[1])
if item is None:
print('No item {}.'.format(actions[1]))
continue
room_actions["get"](item)
room.items.pop(index)
print("You picked up {}.".format(actions[1]))
elif actions[0] == "fight":
index, char = room.get('characters', actions[1])
if char is None:
print('No character {}.'.format(actions[1]))
continue
user.Attack(char)
if char.hp <= 0:
room.characters.pop(index)
print('{0.name} died.'.format(char))
else:
print('{0.name} has {0.hp} left.'.format(char))
elif actions[0] == "inspect":
index, char = room.get('characters', actions[1])
if char is None:
print('No character {}.'.format(actions[1]))
continue
char.inspect()
elif actions[0] == "move":
if actions[1] in game_map[room.ID]:
room = eval(game_map[room.ID][actions[1]])
print("You have moved " + actions[1] + "!")
else:
print("What does that mean?")
room_logic()