In late June of 2020 (while trapped in the Austin summer heat), I had the idea of getting my older daughter to learn programming with a “big kids” language. Something other than Scratch (which she had dabbled with on and off). I decided that Python was the logical choice based on its low barrier to entry, intuitive syntax, elegant object-oriented programming (OOP) support and the interactive interpreter shell (for “doodling” code).
There are some decent books out there for teaching kids how to program, but my goal was to create something that we could discuss and extend as a longer-term educational project.
This is not one of my regular posts. It is intended to be fun and somewhat journalistic. If you are new to programming or are a parent and looking for something for your child to play around with to learn programming, my hope is that this may of some use to you.
Since the entire purpose of this little project was educational in nature, I wanted to keep things very grounded, transparent, and frankly avoid spending too much time on this, although it did end up being a several multi-hour sessions over a series of weekends sort of thing.
As such, the overall project goals were as follows:
- Incorporate Python’s core features : lists, dictionaries, strings, functions, files
- Use classes for everything, but keep it very lightweight. Introducing my daughter to the actual structure of a class was important since OOP is such a prevalent paradigm of almost any modern language
- No use of special game-engines (keep it grounded and transparent, but this meant more work for me)
- Must be extensible via a simple API. The idea was that the first step would be for the student to extend the game (e.g., create new levels and test them out)
- Keep it simple and have fun
With these goals in mind, I quickly decided that what was achievable was to create a text-based adventure game (and associated creation kit) in a 2D world. This would be reminiscent of terminal text-based adventure games of the early 80’s. There would be no graphics (although that changed during the development).
We decided to name this game Adventure. Because that is what entire process was.
From the very outset, I had no intention of coding up a pre-baked adventure with a fixed set of rooms. This would have had a limited pedagogical-replay value. This would go against the “extensibility” goal. This meant that Adventure would need to be a game-engine and creation-kit first and foremost. It would provide a framework to build adventures allowing any arbitrary maze of rooms and objects to formulate levels.
In keeping with the OOP-lite goal, everything was going to be based on objects and so the design started with defining the main objects. The first set of objects were the true “game objects”. These were the “things” inside the game world.
- Items — These were movable and usable objects in the game (health-packs, food, weapons, potions, spells etc.) — these ended up just being Python strings
- Player — The protagonist. Has an inventory and a health level. Can pick-up, use and drop Items
- Location — A location can have “items” and up to four directions of travel (forward, backward, left, right) to another location
- Connector — Connectors represent the “fabric” pieces which connect locations. The reason this is a separate object is because it can contain obstructions
- Obstructor — A stateful obstacle which occupies the connections of locations (enemies, fire, traps). Obstructors can damage a player’s health but can be overcome with certain Items. Obstructors can also drop Items once overcome.
Then came the objects which controlled the game world. These were the “machinery” of the game.
- Adventure — For lack of a better name, the main game-controlling object which deals with movement and interaction of the Player with items, Locations and Obstructors. This object contains handles to the active Player and Level object
- GUIWindow — This came later after a request by little sister who could simply not accept a game that did not have “graphics”.
The actual terminal interface is a simple procedural loop with few functions in the main game file. Some support was added (via regular expressions) to provide variation in the user input, for example, the following would all be equivalent in the terminal app:
- “use key door”
- “use key on door”
- “use key on the door”
- “use the key on the door”
Below is a screenshot of the gameplay. The last time I did a graphical interface was a Java application years ago at a start-up and it was not much better (did I mention this is not my day job?)
Level Construction Design
The level creation part of the game also had some objects and utilities
- Level — A container object representing all locations in a game level, the start location and the current location
- A module called Utils provided common routines to select level files
- A utility script called level_analyzer.py (separate from the main game file) which performs checks on a game level by recursively analyzing the graph formed by the connected Locations.
The primary goal of the level-construction was to make it be Python based and easy for someone to first design a level on paper and then transcribe it in the lexicon of the game objects.
Below is the schematic of one of the example levels which illustrates the various components:
A level is added by adding a Python module and registering the module within a game JSON file. Below is a partial screen-shot of the level creation code for the above level, followed by a link to the code listing:
The full source-code for this level can be found here: https://github.com/sebastian-ahmed/adventure_game/blob/main/adventure_pkg/levels/level4.py
Automated Level Testing
After the initial writing of this article, I identified that I did not provide any sort of automated testing capability, so on a Sunday morning during my winter holiday break, I spent a couple of hours adding infrastructure for automatically testing the playthrough of levels. Note that prior to this, the level_analyzer already provided a static graph connectivity test but this was more of a debug feature.
The approach I took was to essentially provide a hook in each Level object to specify a simple script of action commands.
This enabled a regression test of all included levels which had such a testScript. In order to make this happen, I had to do the following
- Add an action-command logging ability, so scripts could be generated by reference playthroughs. I found this to be the easiest way to create the script. At the end of any game, a JSON file would be dumped with the full command history (valid commands only)
- Encapsulate all the user-input handling in the main game file into a context-managed class which would provide the main game control-loop with input either from the terminal or from a script sequence. This class would handle anything related to game input and also implemented the logging of input and dumping to a file. This kept the main game loop oblivious to where it was getting its input, it simply called for input from the input management object.
- Make the main game function callable with arguments to enable the scriptable mode and to allow disabling the GUI (for speed)
- Writing a test script to automatically process all level files, find a playthrough script (in their Level object) and call into the game in the scriptable mode for each level (with GUI disabled).
Despite the various mechanics, this ended up being a simple “developer experience” and leveraged the manual-testing that was being done anyway. One could simply save their test playthrough so that it could be automatically regressed after the fact. Encapsulating the playthrough script in the level file/object also kept things nicely organized and canonical.
Another feature which was added during the 2020 winter break, was the ability to save and load games. Adding this feature was relatively simple (with a one minor surprise) and only required one game object to be modified (re-locating the current location handle into the Level object where it belonged in the first place). In the end however a subsequent refactor resulted in the Adventure object being able to completely encapsulate the state of the game.
I had not used the Python pickle module extensively in the past. Most of my object serialization work in the past was based on JSON in order to support different tools using the intermediate representation. JSON has limited constructs it can support, but I did not have the time to write custom JSON serializers for the Adventure objects, so I decided to go the pickle route. Pickle is the built-in Python serialization package which utilizes binary files.
The solution involved making Adventure encapsulate all the stateful game objects (namely, Player and Level since Level already contained all Location and Obstructor objects). This allowed a single pickle dump() and load() call. The only surprise was that
I was not able to pickle my self
This is true. When de-serializing a pickle object, one cannot direct the de-serializer (pickle.load()) from within an object bound method to “self”. This actually makes sense since you would be overriding the object within a method call, but Python has this tendency to make you think you can do crazy stuff like this. The solution was to simply create a static class-method to return the de-serialized object and create a new object handle to assign it to.
With this minor blunder aside, it was surprisingly easy to retrofit a full game state save and restore feature which again is a testament to the upfront decision to use OOP (and the simplicity and elegance of Python).
Things I found out
- Teaching kids to code is not easy in terms of getting them excited about it. As professionals, we don’t need a lot of reasons to code. There is always some problem to solve, something to model, calculate or data to process. This is not true for say a nine- or ten-year-old. Helping with checking math worksheets is one area I have been exploring lately
- Testing a game even this simple is work! This is quite different than the typical programming I am used to. Admittedly a lot of my initial testing (during the summer of 2020), was manual including my daughters going through levels. During my winter holiday break I have since added the automated level testing capability.
- Writing tools for debugging what is effectively a quad-linked-list data structure was interesting, but in the end saved a lot of frustration when debugging such graphs
- This exercise has gotten me thinking about writing a book on helping parents teach kids how to program (with Python). It is on my to-do list and I have some ideas on how to make it interesting for both parent and child.
- An object cannot pickle-load itself within a bound method call (but it can pickle-dump)
- I spent more time on this than I had originally anticipated, but enjoyed this nevertheless
Let me have a play!
All the code is on my (new) personal GitHub project: https://github.com/sebastian-ahmed/adventure_game
All the steps need to run and extend the game (via new levels) is hopefully explained in a clear way.