It took nearly two and a half weeks but I finished converting the procedural version of my Ex36 game over to OOP for Ex45. I’m putting it up for anyone to look at and provide feedback, particularly over the level and amount of comments and docstrings I put in it. I know comments should be pithy and only used when necessary but I also know that they help the uninitiated understand the logic behind the code so there are some functions which have comments above almost every line because of the game’s architecture.
One of the things I didn’t like about the Ex36 version was its heavy use of globals. I wanted no globals in this version. I figure if I’m doing this OOP style and I’m doing it right there should be no need for a global (Feel free to correct me if I’m wrong. Wouldn’t be the first time. Won’t be the last). However that does mean the architecture needs to route/pass stuff around through many classes and functions and that’s where documenting what the code is supposed to do in the form of comments, when reading the code you just see something like
if spam == foo.bar[eggs]:
which just by looking at it isn’t going to tell you squat (and I see no reason why someone should put in a bunch of print statements to figure out what it’s supposed to do when I can tell them). So there are quite a few comments in some parts of the code.
There are three files for this project. ex45.py is the file you run. Originally there were two files but I decided to move the entire UseObjectResponses() class into a separate file just to make it easier for me to go through the code in ex45_API.py.
Lessons learned during coding:
You can underestimate the complexity of something. 3/4 of the way through this I was really unhappy with the structure of the get() and use() functions in the Commands() class because each consisted almost entirely each 25 if statements in a row. All the if statements I used in Ex36 (intermingled with a bunch of text for the game) was one of the reasons I wanted to do this OOP style and yet I still had a ton of if statements which made code navigation painful to understand even with the prolific commentary.
One of the things I decided to do going in to 45 was break up those massive blocks of ifs by moving all the text that was needed for entering rooms into room classes (based on the room in) and construct a simple algorithm that would use dictionary keys to control movement and actions and the corresponding values to hold variables and function or class names which would then be plugged into the algorithm and the end result would be the necessary action would be taken. It’s more complicated than that. One function uses a dictionary to set values in another dictionary while also referencing things in a third dictionary.
It was around this time that Zed’s rant about programmers loving indirection began creeping into my head. The code totally made sense to me but at the same time I started wondering if I had fallen into the indirection trap. But I digress.
Anyways, I had structured the code so that using this algorithm…and I’m not even sure algorithm is the right word here but it’s all I could think of…in combination with dictionaries, functions, and classes, eliminated those huge intertangled blocks of ifs and text and global variables that infested the Ex36 version. Instead of 800 lines of code as existed for get() in ex 36, I had less than 60 in ex 45 and roughly the same for use().
However I still had 25 or so two line if statements in a row in the get() and use() functions. And I hated that. It looked ugly. So I stopped developing and went into refactor mode. I moved the get() and use() functions out of ex45.py and into ex45_API.py and turned both into classes. Though I did keep simple get() and use() functions in the Commands() class in ex45.py just to route the data being passed in there on to the UseObject() and GetObject() classes in ex45_API.py. Otherwise I would have had to rewrite the Commands() class and I really liked the existing architecture there.
I redid the algorithm and created new dictionaries in each to be the reference point for how things are done in each class. It was easier to do for the new GetObject() class because I could base the values off keys for each room.
But I underestimated how much code was going to need to go into the UseObject() class; specifically the UseObject.using() function. When you have to account for using not only single objects but objects with other objects which are listed in an inventory that may or may not have the item you want to use and what happens in certain rooms would be different than in other rooms…well it gets nebulous fast with all the logic holes you have to plug by adding more if statements even though I broke it up by basing the top of each nested if chain on the object being used. I see now that what I should have done is turn UseObject() into a superclass (probably in a separate .py file) and subclass out each item to be used. But the code works now and after 2.5 weeks I need to put a lid on this as I could keep tinkering with it for a while if I wanted to. I thought I could lower the amount of code needed down from the 1,000+ lines in Ex36 if I did it OOP style but the code for Ex45 is 2-3 times larger. Ooops…though docstrings for every class, init, and function does bloat the lines of code out really fast.
I feel a little down that I didn’t put more Python into this. I’ve read about what some others have done with their games and I feel like I’ve come up short in the Python department. This exercise’s challenge for me was less about cramming in new Python and more about solving data parsing and routing with conditional scenarios in (some) rooms where the player could go in any direction at any time and carry out commands which may or may not turn out differently in one room than in another. It’s a real logic exercise. I coded in scenarios and outcomes that if the game is played through properly, the player will never encounter. And I architected this, indirection or not, so that it’s easily extendable. But this is an adventure game and adventure games are hard but they also mean the player has to try out different scenarios in order to solve the problems. And the code needs to account for that…otherwise I might as well have coded up a state machine that locks the player in a predetermined course of action. In truth my code does this too…it just provides different ways to go through the states and in many cases, in different order.
Anyway, the code is on GitLab. It’d been a while since I used GitLab. I originally set this up as a private project by mistake so I had to tear it down and create a public one. So you should be able to download this now. My home machine didn’t have GIT on it so I had to go set everything up. Now I need to refamiliarize myself with command line Git again.