NB This post is aimed at entry-level coders. It will have limited value or interest to experienced Rubyists.
Inspired to continue working through David H. Ahl's BASIC Computer Games book from the 70s, I'm turning my attention to "Animal" – a 20-questions style game where the computer tries to guess the name of an animal you are thinking of:
Animal Adapted from a BASIC game from Creative Computing - Morristown, New Jersey. Play 'Guess the Animal' Think of an animal and the computer will try and guess it. Are you thinking of an animal? N Are you thinking of an animal? Y Does it swim? Y Is it a fish N The animal you were thinking of was a? dolphin Please type a question that would distinguish a fish from a dolphin Is it a mammal? For a dolphin the answer would be? Y Are you thinking of an animal? LIST Animals I know dolphin fish bird Are you thinking of an animal? Y Does it swim? Y Is it a mammal? Y Is it a dolphin Y Why not try another animal? Are you thinking of an animal?
Animal was quite a popular BASIC game in its day. Along with ELIZA it's an example of how simple programs can simulate human-like behaviour - even on the limited computing platforms we had in the 70s and early 80s. It's also not hard to see it as the predecessor for modern web-based games like Akinator. Indeed, the accompanying text in Ahl's book suggests that the program could be reworked to cover different knowledge domains - geography, cell structures… even characters from television and movies. Akinator is a bit more sophisticated than "Animal", but the basic idea is the same.
"Animal" (source) is actually the third program in the David Ahl's "Basic Computer Games". I had intended on progressing through the book in sequence, but I'm skipping "Amazing" (source) as the code seems more convoluted than the mazes it produces. We may come back to that program in due course.
Apart from the obvious nostalgia value, the main reason for reviewing these old BASIC games is to be able to talk to my kids over the essentials of coding. After working in management for a decade, and having precious little time to sit down and code with them, I've long been concerned they'd pick up bad habits like I did as a young coder. Indeed, when I was interviewed for my first gig post-management I gave "making sure my kids don't end up as PHP hacks" as the chief reason for returning to coding.
My fears were somewhat realised when it became clear that my kids don't have a strong understanding of object-oriented programming. As such, this post is aimed at an introductory level and covers some of the basics of Ruby programming. It won't have huge value to a seasoned Rubyist, but at a personal level I hope it will remind me of some of the reasons why I love Ruby so much - and why I believe Ruby is an ideal first programming language.
The original Animal game stores knowledge of animals in a simple array:
70 DIM A$(200) 80 FOR I=0 TO 3 90 READ A$(I) 100 NEXT I
It took me a while to remember what this meant, but in BASIC the
READ command is used to load into memory data that is stored elsewhere in the program - often declared at the end of the rest of the code. In Animal's case, the data is embedded in the second half of the code.
530 DATA "4", "\QDOES IT SWIM\Y2\N3", "\AFISH", "\ABIRD"
The upshot of this is we end up with an in-memory data model that looks something like this:
A$(0) "4" A$(1) "\QDOES IT SWIM\Y2\N3" A$(2) "\AFISH" A$(3) "\ABIRD"
Already we are seeing some of the limitations of the program. The BASIC version of Animal can only store 199 records - with the first element in the array being used to maintain a count of the questions and animals. This was fine in the days of VIC-20s and the like, but we're not so constrained with today's computing platforms.
The data model is also rather dense, with the questions and paths to other questions (the
\Y2\N3 part of each question) packed into strings that have to be reinterpretted by the program itself.
Happily we can unpack the BASIC data model into Ruby objects with a clearer and cleaner intent, breaking it into the blocks of code - i.e. classes - that make sense.
class Animal attr_reader :name def initialize(args) @name = args[:name] end end class Question attr_reader :text attr_accessor :yes_path, :no_path def initialize(args) @text = args[:text] @yes_path = args[:yes_path] @no_path = args[:no_path] end end
Animal class represents an animal: a bird, frog, baboon, etc. It is our definition, as far as the game is concerned, of what an animal should look like. Similarly our
Question class captures the elements of a question: its text and where to take the user next based on a yes/no answer.
Now we have our new data model we can build the initial state of the program as Ruby objects:
def starting_position @starting_position ||= Question.new( text: "Does it swim?", yes_path: Animal.new(name: 'fish'), no_path: Animal.new(name: 'bird') ) end
The notion of a 'position' is used throughout the Ruby version, with the variables
previous_position used internally to keep a track of where the user is in the tree as they progress through the tree.
starting_position generates a tree-like structure representing all the knowledge program holds about animals.
Question: Does it swim? / \ / \ [yes path] [no path] / \ / \ Animal: Fish Animal: Bird
As the user adds more knowledge the tree gradually expands:
Question: Does it swim? / \ / \ [yes path] [no path] / \ / \ Question: Is it a mammal? Animal: Bird / \ / \ [yes path] [no path] / \ / \ Animal: Dolphin Animal: Fish
Side note: I considered drawing this tree as a graphic, but somehow using old-style ASCII just appeals. That old nostalgic feeling again.
Remininscing on how my friends and I learnt programming as kids, we spent a lot of time reading code - and our spry little minds could tease out the data structures and flows despite the
GOTO spaghetti. Ahl's book packs in almost 100 example programs and so has little space for explanation of the internals of each. This, coupled with the mechanical activity of typing in each program (there were no downloads back then), meant that we had to get a good idea of the purpose of each line of code and how it related to the rest of the program.
The original draft of this post went into great detail about classes and objects, how to pass parameters and so forth. Given the value in reading code - it still takes up a large chunk of what I do as a day-to-day coder - I'd encourge my sons and other interested parties to read the code.
The full source of the Ruby version of Animal is available on Github
At the start of each run through the game, the user can type the 'LIST' command to retrieve all the animals the program currently knows about.
When I talked over this part of the program with my eldest, we came up with a variety of ways to list the animals. Initially we considered having a separate array of
Animal objects, but this would have required more code to maintain the list - both in the initial setup and as the game progresses.
Given that the data we need is already in the tree, we settled on simply 'walking' through it and finding all the animals.
def list_animals puts 'Animals I know' animals = collect_animals(starting_position, ) animals.each do |animal| puts animal.name end end def collect_animals(position, animals) if position.is_a?(Animal) animals << position else animals = collect_animals(position.yes_path, animals) animals = collect_animals(position.no_path, animals) end animals end
collect_animals method is a recursive algorithm. In my experience a small number of otherwise good professional coders don't understand recursion very well, and will happily brute force their way through a data structure. Recursion is an important concept in coding and to this end it's probably worth deconstructing how
The method takes a
position - a node in our tree - and a list of animals as an array. The
list_animals method kicks this off by calling
collect_animals(starting_position, ), passing in the start of our tree and an empty array. This empty array will act as an 'accumulator', filling up with
Animal objects as we find them.
The first thing we check for in
collect_animals is our end point - finding an Animal object at the end of a branch. It adds the animal to the
animals array and then returns the accumulating array.
If we don't find an
Animal (i.e. the
position is a
Question) then we follow each yes or no path, continually calling
collect_animals until it finds terminating
All in all, recursing through the tree like this is a more satisfactory solution than maintaining a separate list of
Animal objects. Obviously with a very large tree this could take some time, but it's not an issue at the moment. And in that we have another important lesson for young coders: don't complicate your code solving problems you don't currently have (otherwise known as the YAGNI - You Aint Gonna Need It - principle).
The original Animal BASIC code does little to check input and it's very easy to scramble the program by entering in bad data - or even just hitting return. My son and I decided to try and prevent bad data entry as much as possible, trapping empty responses or input that didn't make sense.
Doing this we found ourselves writing the same sort of code over and over, and I took it as a good moment to talk to him about reusing code via Ruby's blocks.
get_input method takes a
prompt argument as text to present to the user.
def get_input(prompt) is_valid = false while !is_valid puts prompt input = gets.chomp is_valid = !input.empty? is_valid = is_valid && yield(input) if block_given? end input end
The method loops around while
is_valid remains false. A blank response from a user (e.g. hitting the carriage return) will always mark the input as invalid. If a block is passed to the method (tested with
if block_given?) the block will be called to test if the input data is valid (returning a
get_yn_answer method, calling the
get_input will loop until the user enters a 'Y' or 'N' (or 'y' / 'n') response to the prompt.
def get_yn_answer(prompt) get_input(prompt) do |input| input.upcase == 'Y' || input.upcase == 'N' end.upcase end
prompt_to_start the user can't continue through the program until a 'Y' (or 'y') response has been entered.
def prompt_to_start get_input "Are you thinking of an animal?" do |input| case input.upcase when 'Y' then true when 'LIST' then list_animals false else false end end end
prompt_to_start also allows the user to request a 'LIST' of existing animals. Arguably calling the
list_animals method potentially creates a side-effect and normally I'd discourage kids from this style of coding. But as the purpose is to manage the flow it's a reasonable compromise here.
This is quite a limited use of Ruby's block idiom, but the idea was to give my kids a taste of how sections of code can be reused.
The main game code is still quite procedural - it feels like a BASIC program that's been converted to Ruby, albeit with some minor additions. There are probably things that can be done to make the code more Ruby-like.
Working on Animal together, my son and I managed to cover blocks, classes, tree structures and recursion. There were probably more informal lessons along the way - in just being able to sit down and talk code with my kids - but that's a reasonable take-away.
The question now is whether my kids take any of this on and do more coding of their own. They've both expressed a desire to get into software development as a career, but it doesn't feel like they have the same opportunities to learn their craft – to do the 10,000 hours that I did as a kid – with the constant distractions of the internet and games.
If I had my time again I'd find a way to record more of the conversations we had while coding.
I plan on taking more time to code with my kids, to talk through design and the sorts of compromises you always have to address even when building small applications.
As long as they end up doing what they want, what they enjoy… hell, I'd even be happy if they end up coding in PHP.
Feel free to look over the code for BASIC Computer Games (Ruby Edition), make comments or criticism.
Any feedback is appreciated. Thanks for reading.