Scenery and items are in the bag and my project begins to resemble a true game more and more, as I have begun to fill in various little details the player can interact with.
However, I can tell I’m still a long way from a full game because tons of features are still missing that make professional games so incredibly rich—and surprising at times.
My next mission is to create containers. I want to have a box in my game that the player can open and find an item inside. My initial approach was to create an entirely new class for this but then I’ve been informed that there might be an easier way.
A box in a room is really a scenery object, so why abandon the scenery concept? There’s no need. Everything that applies to a scenery object also applies to my box. So instead of duplicating all that functionality, why not build upon it and expand.
The keyword here becomes multiple inheritance. It is possible in Python to have a class inherit from a number of different superclasses. This means I can create a Box class that inherits from Scenery and also from a new Container class that I will write, that handles only the content part of the object. Once again, I begin to see why object-oriented programming is so popular.
class Container ( object ): """ This class defines all sorts of container behavior """ def __init__ ( self, _initState=globals.ContainerStates.Open, _contents=None, *args, **kwargs ): super ().__init__( *args, **kwargs ) self.State = _initState self.Contents = _contents
This is my class definition and it deserves some explanation, I think. When using multiple inheritance, things can get very confusing, real quick, I’ve found, and it took me a while to work my way through all of this. I’ll try to explain the process as best as I can.
When you work with multiple inheritance, you want to make sure that the
__init__() function for each of your superclasses are called. It is necessary or else parts of your code will never execute. Just creating the function won’t do the trick. You have to tell the program that you want to call the superclass.
Because my newly created box takes more initialization parameters than the original Scenery class, I essentially need to tell Python, which of these parameters go where. Which ones are to be used for the Scenery and which ones are used for the Container class. To do that I have to go back and adjust my Scenery class definition.
class Scenery ( object ): def __init__ ( self, _token, _name, _description, *args, **kwargs ): super ().__init__( *args, **kwargs ) self.Token = _token self.Name = _name self.Description = _description
What’s happening here is that I take the original parameters and then add
**kwargs at the end. This frightening-looking construct essentially means “and whatever else there might be.”
With this change in place, a scenery initialization with three parameters will work just as well as one with ten, because the Scenery class will isolate the parameters it needs to work with and ignore the rest—(whatever else there might be.).
It does, however, pass these extra parameters on to its superclass as a result of the call to
super ().__init__( *args, **kwargs ).
You can see that I did the exact same thing in my Container class definition, only that that class refers to different parameters and, once again, ignores whatever else there might be.
When it is time to declare my actual Box class, it looks like this.
class Box ( Scenery, Container ): def __init__ ( self, _token, _name, _description, _initState, _contents=None ): super ().__init__ ( _token=_token, _name=_name, _description=_description, _initState=_initState, _contents=_contents )
Note how this class takes a number of parameters and then passes them down to its superclasses by name. This makes it possible for my two superclasses, Scenery and Container, to grab whatever parameters they need and go to work. It looks confusing but really isn’t once you understand the concept.
So next, I am creating an actual box with all the parameters needed. I’m even filling the box with an old newspaper.
Box ( Tokens.Box, "cardboard storage box", "It looks quite ordinary, really. A bit scuffed up, with the lid a little bent, " "but otherwise, perfectly intact.", globals.ContainerStates.Closed, [ Item ( Tokens.Newspaper, "old newspaper", "It's nothing but a thin, old newspaper, faded and yellowed with age.") ] )
When initializing the Box class, the program will first call the Scenery superclass, from where it will call the Box superclass, making sure all elements of the class are properly initialized.
Now that I have a box, of course, I’ll need the proper functions to interact with the container items but as soon as I started working on that part, I realized that it is no different from the inventory really, because it’s, once again, just a list that is being manipulated. So functions like checking or removing items really come down to simply running through the list and comparing tokens.
def HasItem ( self, _token ): if self.Contents: for _item in self.Contents: if _token == _item.Token: return _item return None def RemoveItem ( self, _token ): if self.Contents: for _item in self.Contents: if _token == _item.Token: self.Contents.remove ( _item )
The same is true for listing the items in a container or adding a new item, which will simply be appended to the Contents list.
def AddItem ( self, _item ): self.Contents.append( _item )
Functions like opening and closing the box can easily be done using the
self.State flag to determine whether the box is currently open or closed.
It is important to point out that containers complicate the room logic a little. You may recall that if a sentence contains a noun, the room logic searches the room for an item of that name. It looks in the inventory first, then the room items and then the scenery. If it doesn’t find it, it will continue on to the default responses. In this scenario, however, it will not be able to see items inside a container. This may be suitable if the player has not yet discovered the item in the container, but it isn’t if there is an open box in the middle of the room with the item in it. In that case, the game should be smart enough to realize that the player is referring to it.
Not a big deal. All we have to do when running through the scenery list is to check if any given scenery object is a container and then also check the contents. If you wish, you can make that search dependent on whether the container is open or closed.
To make it happen, here is a modified version of the room function I use to check my scenery for an object.
def HasScenery ( self, _token ): for _obj in self.Scenery: if _token == _obj.Token: return _obj if isinstance( _obj, Container ): # Check inside containers, if necessary _item = _obj.HasItem( _token ) if None != _item: return _item return None
The code should be self-explaining, really. It uses Python’s
isinstance() function to check if a scenery object is derived from the Container class, and if it is, it proceeds to check the contents of the container, using the container’s
There is another moment that requires special attention—when the player wants to pick up an item that is inside a container because instead of removing the item from the room, it will have to be removed from the container. A few lines of code take care of that…
def RemoveItem ( self, _token ): for _obj in self.Items: # Is it an item in the room? if _token == _obj.Token: self.Items.remove ( _obj ) return True for _obj in self.Scenery: if isinstance( _obj, Container ): # Check inside containers, if necessary _item = _obj.HasItem( _token ) if None != _item: _obj.Contents.remove ( _item ) return True
As before, the code should be self-explaining as it simply checks the room items and if it can’t find the item in question there, it will go through the scenery and check all containers to see if it is in any of those.
And once again, with a small bit of code, the complexity of the game from the player’s perspective has grown quite significantly.