|Ned Batchelder : Blog | Code | Text | Site|
» Home : Text
Created 31 July 2005, last updated 10 August 2005
This is a programming project I undertook with my 13 year old son. Madlibs is a story game for kids: a story is written and a few dozen important words are taken out, replaced by blanks. The blanks are labelled with their part of speech or other category ("noun", "adjective", "an animal", and so on). One kid reads out the categories, another kid (or kids) supply new words without knowing the story. When all the blanks have been filled in, the story is read out, usually with comic results.
(For more about how we chose Madlibs as a subject, see my blog entry Programming with kids: Madlibs.)
The first program
I started by talking with my son about how the program would have to work. We talked about the structure of the story itself. It was a list of things. He said there were two types of things: static pieces and dynamic pieces. We talked about how a story was constructed: by looking at each piece in turn.
"And what should it do with each piece?" The first idea was that for static pieces, print the text, and for dynamic pieces, print the category and collect an answer. Son realized that this would expose the story as it went along, before the questions were answered. Here he was stuck.
I explained that we could build up the story in memory, and print the whole thing at the end. Eventually, we sat down at the computer and created this program:
I supplied the tricky parts (get_input, and the textwrap business), and suggested the technique of making the story list be a list of tuples. But my son did a lot of the thinking through what had to happen. The "dynamic" function is there because he wanted to use a function.
This program worked, and was a source of much joy. It did something interesting, and my son eagerly encouraged everyone to try it out. He really seemed pleased by the work he had done, and I was pleased that we could talk through some of the coding challenges and find answers together.
The object-oriented version
The first change my son wanted to make was to have the program ask the questions in a random order, so that the player had less chance of second-guessing the intent of the author. I had imagined a number of ways to expand on the program, but this wasn't one of them, so I was psyched: the Madlibs genre was going to be a fertile one!
In preparation for the feature, I thought it would be good to get us onto a more modern architectural foundation. I explained about classes and objects, and refactored our program into this:
There was a side discussion that didn't survive in the code. I showed how to use the same storylist variable two ways, first to play dynamically, with the program asking questions and producing the finished story, and second to play on paper, with the program printing a play sheet suitable for use with pencil and paper.
To do the random questioning, we'll need to split the main loop into two passes: one to get the dynamic pieces, shuffle them, and get the answers, then another to build the story. The dynamic pieces will have to store their answers between the two passes.
To store the answers, we talked about member variables. We had used them to store the prompt before, but the __init__ methods seem formal and uninteresting, and their values are constants. It wasn't until we got to prompting the user and storing the answer in a member variable that we were really talking about variables.
Here's the third version of the program. We make two passes over the list, first to collect the pieces that ask questions. Then we shuffle that list and have each slot ask its question. Then we walk the whole story from beginning to end building the finished story. It's important to understand the relationship between a list and the objects it contains. The two lists point at the same objects, otherwise the program wouldn't work.
Now the program asks for words in random order, making it more difficult for the player to second-guess how his words will be used.
The next feature to tackle was re-usable slots. For example, after asking for "an adjective", the word could be used twice or more in the same story. To make this work, we had to expand the types of objects in the storylist. We could have gone two different ways: add two new objects (one for the reusable word, and one for where it was reused), or add just one new object (the reused word) while expanding the dynamic slot to be reusable. Son decided to go the four-object route.
We also had to introduce the concept of dictionaries, so we could name and store the reusable words:
The end result works well, and everyone (mom, brothers, and so on) were pleased with the results. Stylistically, the reusable and dynamic classes are too similar. Eventually I'll probably encourage a refactoring to reduce the duplication.
One interesting feature was how we chose the pieces to shuffle and use in the first pass. Originally, the code was:
Son asked if we could combine the two clauses (good instinct!). So we simplified it to:
and then, using isinstance more cleverly, to:
This isn't bad, but I took the opportunity to advocate encapsulation and polymorphism. The fewer places we talk explicitly about classes, the more flexible the code will be. I pointed out that the only thing we did with the elements of dynamiclist was to call askUser on it, so why not just pick out the things that have an askUser method. Along the way, we changed the name to asklist to better reflect what it's doing:
External story files
The last feature was storing the story in an external file. This makes it possible to edit the story without changing the program, and allows us to have a number of stories we can choose from at run time.
The story is stored in a text file, usually with a file extension of .mad (the boy was very excited about having our own file extension!). For example:
The text is used verbatim. Slots are in square brackets, with the prompt inside them. A colon indicates the internal name of the value provided, which can be reused by a slot with no prompt, just a value name.
Here's the next version of the madlib code:
The getStoryPieces function does the work of reading the text format. This could probably be cleaned up, but it works. We had to do a lot of careful thinking about where in the string each variable pointed, and about the different forms of slots the parser might encounter. Personally, I would have made the story file name be an argument on the command line, but the son liked prompting better, so prompting it is. Here's a sample run:
More to come?
I think we are probably done with madlibs, but there are more possibilities for future work that we've talked about:
I really liked this project: it has enough interest to keep the son motivated, but is simple enough that an hour or two of talking and typing produced an interesting result.