Hudzilla Coding Academy: Project Eight

Code
Hudzilla Coding Academy

 

Can computers learn? That's a deep question worthy of any philosopher's time, but we're not philosophers: we're coders, right? So, rather than try to prove or disprove what exactly a computer is capable of, we're going to pass away an idle hour or two producing a little game that actually does make computers learn - or at least gives the impression!

To perform this magic we're going to be drawing upon the database skills you learned in project seven, but rather than just reading from a database we're also going to be writing to it. The app we're going to produce will ask questions that can be answered either by "yes" or "no", and will try to guess the person the player is pretending to be. If it doesn't know the right answer, it will ask the player to give it a question it can use to make its guess accurate.

NB: it is strongly recommended that you have already completed Project 7: FoxySearch before starting this one, because a lot of theory is shared.

With that out of the way, let's begin!

First steps

Create a new MonoDevelop console project and call it Questions. From the Solution pane, right-click on References then choose Edit References - as with the FoxySearch project, you need to add a reference to Mono.Data.Sqlite to enable all the functionality for working with SQLite databases.

Add Mono.Data.Sqlite to your MonoDevelop project by right-clicking on References in the Solution pane and choosing Edit References.

Add Mono.Data.Sqlite to your MonoDevelop project by right-clicking on References in the Solution pane and choosing Edit References.

The first thing our app needs to do is connect to the database, creating it if doesn't already exist. What's more, we're going to seed the database with a simple question that gets the game going. Whereas before, we were just working with Firefox's table layout, we now need to make our own table based on what we need. There are all sorts of ways you could make this, but we're going to shoot for something that's very easy to understand - our table will store the following data:

  • A unique ID number for every question
  • The question text to ask the player
  • The text to print if the player answered "yes" and computer thinks it knows the correct answer.
  • The ID number of another question to ask if the player answered "yes" and a follow-on question exists.
  • The text to print and the ID number of another question if the player answers "no".

Some database systems are very complicated, allowing you to specify exact data types of things you want to store. But for SQLite, we can represent all that data using either data type "INTEGER" (for any numbers) or "TEXT" (for text of any size). As an added bonus, if we tell SQLite that the unique ID number is a primary key then it will ensure that every question does indeed have a unique number.

Once all the data is in the database, we need to enter into an infinite loop, reading input from the command line to see how the player answered each question. Once the computer runs out of questions to ask, one of two things will happen: if it has a guess to make, it should make the guess then ask if it was correct; if it doesn't have a guess to make, it should prompt the user to name who they were and also to provide a question that would have identified them correctly.

Put together, this should give us an app that gets better at identifying people and things the more it gets played - although it is, of course, only as good as the player data that gets provided to it!

How to create a database table

In SQLite terminology, a "database" is simply a file on your hard drive. Inside a single database file there are multiple, independent tables containing their own information. For the purpose of this game, we need just one table to store all the possible questions, but first we need to create the database itself.

First, modify the "using" statements at the top of your Main.cs file to this:

using System;
using System.IO;
using Mono.Data.Sqlite;

Now we need to declare a few variables to store the database connection, command and reader, again just like in the FoxySearch project. Modify your MainClass class to add three new variables, like this:

class MainClass
{
	static SqliteConnection DataConn;
	static SqliteCommand DataComm = new SqliteCommand();
	static SqliteDataReader DataReader;

And now the important bit: connecting to the database, creating it if it doesn't already exist. We covered how to connect to databases when working with FoxySearch, so that bit is old news. However, creating a database from scratch is new, so watch out for it - here's how your Main() method should look:

bool first_run = false;

if (!File.Exists("20q.db")) {
	SqliteConnection.CreateFile("20q.db");
	first_run = true;
}

string dsn = "Data Source=20q.db;";
DataConn = new SqliteConnection(dsn);
DataConn.Open();
DataComm.Connection = DataConn;

if (first_run) {
	// do stuff here
}

Hopefully you can see where the database is being created - it's easy to spot because Mono checks whether a file exists using the File.Exists() method, and SQLite creates a database using the CreateFile() method, which are both pleasantly easy to guess at!

But the interesting thing here is that we define a variable, first_run, which remembers whether we're running the app for the first time or not based on whether we have to create the database. This is important, because we need to tell SQLite what kind of data we want to store, then insert a simple seed for the first question to get the game going, otherwise the computer won't be able to make any guesses at all!

In FoxySearch, all we ever did was read data, so the SQL (Structured Query Language, remember?) to create a table and insert data is new. Let's start with creating a table, which looks like this in SQL:

CREATE TABLE table_name (SomeField TYPE, SomeOtherField TYPE);

As discussed, SQLite only cares about two data types as far as we're concerned: INTEGER and TEXT. So to tell SQLite to create a new table called Questions with the fields listed earlier, we need to run this command:

CREATE TABLE Questions (ID INTEGER PRIMARY KEY, Question TEXT, YesAnswer TEXT, YesID INTEGER,
	NoAnswer TEXT, NoID TEXT);

Remember, specifying "ID INTEGER PRIMARY KEY" will tell SQLite that the ID field should contain a number ("INTEGER") and that we want SQLite to ensure that every new entry in our table gets a new ID number ("PRIMARY KEY"). Being very technical, a primary key means other things too, but in SQLite-land the combination of "INTEGER" and "PRIMARY KEY" gives us the auto-incrementing behaviour as if by magic.

If you want to get a little extra practice using SQL and SQLite, install the sqlite3 program - it's a command-line app that lets you run SQL commands immediately and see the results.

Creating the empty table isn't enough - we need to enter the first question. To get things going, the first question will be "Are you human?" to which the answer if the player answers "yes" is "Hudzilla". Remember, each question has a YesAnswer, a YesID, a NoAnswer and a NoID; we're going to put -1 in both the YesID and NoID fields to show that there's nothing in there, then put empty text ("") in the NoAnswer field, again to show there's no data in there.

To insert data using SQL, you need to run an INSERT INTO query, which looks like this:

INSERT INTO table_name VALUES (value1, value2, "value 3 as text", value4);

Of course, this becomes slightly more complicated when put inside a C# string because you need to tell Mono that the quotes surrounding "value 3 as text" don't end the string, and that means putting back slashes (\) before the quotes like this:

string myvar = "INSERT INTO table_name VALUES (value1, value2, \"value 3 as text\", value4);";

Now you know how to create a table and add an example row to it, all that remains is knowing how to execute those queries inside SQLite. With FoxySearch we used the ExecuteReader() method to run an SQL command and read the results back, but neither of these two commands have any return value - they just create stuff. So instead of ExecuteReader() we need to use ExecuteNonQuery(), but otherwise everything is the same.

In the Main() method code from earlier, replace the "// do stuff here" line with this:

DataComm.CommandText = "CREATE TABLE Questions (ID INTEGER PRIMARY KEY, Question TEXT, YesAnswer TEXT,
	YesID INTEGER, NoAnswer TEXT, NoID TEXT);";

DataComm.ExecuteNonQuery();			
DataComm.CommandText = "INSERT INTO Questions VALUES (1, \"Are you human?\", \"Hudzilla\", -1, \"\", -1);";
DataComm.ExecuteNonQuery();

Notice how we force the ID number 1 for the initial question - it would probably have been given that anyway, but it's nice to be sure because we're going to tell our program always to start with that question. Remember, those lines will only be executed if first_run is set to true, which in turn is only going to happen if the 20q.db file doesn't exist. So, by the time the Main() method finishes, the database exists, we've connected to it, and made sure it has at least one question inside. Now for the games to begin!

The infinite loop

This game needs to start by asking the first question, "Are you human?" and continuing to ask questions until there's nothing else to ask, at which point it either admits it doesn't know the answer or it makes a guess. Because we don't know how many times the question asking loop needs to take place, we're going to make it an infinite loop, exiting only when the program is finished.

We can represent our user-input logic with a simple flow-chart: we need the infinite loop to get going round and round until the user provides either yes, no or quit.

We can represent our user-input logic with a simple flow-chart: we need the infinite loop to get going round and round until the user provides either yes, no or quit.

So, the first thing we need to do is track the ID number of the last question that was asked (starting with 1), and also to store a simple boolean variable to track when the game is finished. So, put these two up with the other three variables we defined earlier just under "class MainClass":

static int LastID = 1;
static bool GameIsRunning;

The more complicated part comes in how we actually run the game. Put simply, we need the following steps:

  • Print out a welcome message explaining how to play.
  • Enter into the infinite loop using GameIsRunning.
  • Pull out the current question using LastID to keep track of which question is active.

This is quite a chunk of code, so the best way for me to show it to you is simply to print the code and intersperse it with comments. Here goes:

public static void StartGame() {
	// print a general welcome message with instructions
	Console.WriteLine("Welcome to 20Questions. Please answer all questions with either");
	Console.WriteLine("'yes' or 'no', or 'quit' if you want to exit the game.");
	
	GameIsRunning = true;
	
	// our infinite loop starts here
	while (GameIsRunning) {
		// remember, LastID starts at 1, so this starts by reading the first question
		DataComm.CommandText = "SELECT * FROM Questions WHERE ID = " + LastID + ";";

		// read precisely one row back
		DataReader = DataComm.ExecuteReader(CommandBehavior.SingleRow);
		DataReader.Read();

		// pull out all the fields as variables
		string Question = DataReader["Question"].ToString();
		string YesAnswer = DataReader["YesAnswer"].ToString();
		string NoAnswer = DataReader["NoAnswer"].ToString();
		int YesID = Convert.ToInt32(DataReader["YesID"].ToString());
		int NoID = Convert.ToInt32(DataReader["NoID"].ToString());								

		// then tell SQLite we're done and it can free memory
		DataReader.Close();
		
		// set up a variable to read user input
		string input;
		
		// now loop until valid input is received
		do {
			// ask the current question
			Console.WriteLine(Question);

			// suck in the player's answer
			input = Console.ReadLine().ToLower().Trim();
		} while (input != "yes" && input != "no" && input != "quit");
		
		// DONE!
		// if we made it here, it means the user provided either
		// "yes", "no" or "quit" - we need to act on that input!
	}
}

That's a lot of code, but large chunks aren't new: we looked at running SQL back in the FoxySearch, as well as executing a reader and using ToString() to retrieve variables from a database result. The important bit is the do...while loop: this is new, and it's like a plain while loop except the condition comes at the end. What's the difference? Well, here's how a while loop normally works:

  • Is the loop variable OK?
  • If yes, run the contents of the loop.
  • If no, don't run the content of the loop.

A do...while loop is different because the condition comes at the end, after the contents of the loop. That means the loop's contents are always executed at least once, even if the condition turns out to be false. So, it works like this:

  • Run the contents of the loop.
  • Is the loop variable OK?
  • If so, run the contents of the loop again.
  • If not, continue after the loop.

The reason we're using do...while here is because our program needs to print the first question and ask for user input at least once before we start caring about what the user wants to do.

C# offers several different kinds of loops, with the do...while loop easily being the least frequently used. As you can see, it's just like a normal while loop, except the condition is checked at the end, which in turn means the code inside the loop is always executed at least once.

C# offers several different kinds of loops, with the do...while loop easily being the least frequently used. As you can see, it's just like a normal while loop, except the condition is checked at the end, which in turn means the code inside the loop is always executed at least once.

The slightly complicated bit

Even if you find yourself disliking SQL, you have to admit it's pretty easy to understand - you don't have to care at all how the data is stored, how to retrieve it or how to parse it into meaningful data types. Instead, you just say "I want fields X, Y and Z from this table, please" and it serves them up to you.

But here is where the easy ride ends, because we need a little bit of logic. The do...while loop will terminate because the user typed one of "yes", "no" or "quit", and that answer will be stored in the "input" variable. At this point, one of several things should happen - let's start with what should happen when the user says "yes".

So, the game just asked them, "are you human?" And the player answered "yes". What we need to do next is look at the YesID field for the current question, because if that has a number that isn't -1 it means we have a follow-on question we can ask to guess more accurately who the player is. In this situation, we just need to set LastID to YesID and let the loop repeat, because as a result of that the next question will be asked.

If the YesID field is set to -1 for the current question, it means we don't have a follow-on question to ask, so instead we have to look at the YesAnswer field. If this contains any text, then it's our guess of who the player is. If not, then we have no idea who the player is, and need to ask them for the answer.

The other two possibilities are that the user might say "no", in which case we just do the same thing as with "yes" except with NoID and NoAnswer, or "quit", in which case we just set GameIsRunning to be false so that the loop terminates and the game ends.

Now, because the "yes" and "no" routes share so much functionality, it makes sense to turn them into methods that take that right action whether we do or don't know who the player is. Of course, if there are more questions to be asked, we just set LastID to be either YesID or NoID.

For now we'll just call these two methods UnknownAnswer() (for when we don't know who the player is) and MakeGuess() for when we think we know who the player is. Both of these need to accept parameters to work properly, namely whether the player's response was "yes" or "no" and, for MakeGuess(), what the guess text is.

And that's it - we now have enough information to handle the player's input properly, albeit without any code for UnknownAnswer() or MakeGuess() as yet! Put this code where "// DONE!" is in your StartGame() method:

if (input == "yes") {
	if (YesID == -1) {
		if (string.IsNullOrEmpty(YesAnswer)) {
			UnknownAnswer(true);
		} else {
			MakeGuess(YesAnswer, true);
		}
	} else {
		LastID = YesID;	
	}
} else if (input == "no") {
	if (NoID == -1) {
		if (string.IsNullOrEmpty(NoAnswer)) {
			UnknownAnswer(false);
		} else {
			MakeGuess(NoAnswer, false);
		}
	} else {
		LastID = NoID;
	}
} else {
	GameIsRunning = false;
}

In the situation where we've asked all the questions we have and we just don't know who the player is, the game needs to come clean that it's lost, terminate the game loop, then prompt the player for a new question. In C#, that becomes this:

public static void UnknownAnswer(bool yes) {
	Console.WriteLine("I don't know what you are!");
	GameIsRunning = false;

	AskForNewQuestion("", yes);
}

Yes, that's pretty sneaky - that method does pretty much nothing except print out a message and call another new method, AskForNewQuestion(). Don't worry about the parameters that it takes - it will become ever-so-slightly clearer after you see the code for the MakeGuess() method:

public static void MakeGuess(string guess, bool yes) {
	GameIsRunning = false;
	Console.WriteLine("You're " + guess + "!");
	Console.WriteLine("Am I right?");
	
	string input;
		
	do {
		input = Console.ReadLine().ToLower();
	} while (input != "yes" && input != "no" && input != "quit");
	
	if (input == "no") {
		AskForNewQuestion(guess, yes);
	}
	
	Console.WriteLine("Thanks for playing!");
}

Read that through and see if you can figure out a) how it works, and b) what those two parameters to AskForNewQuestion() must do.

If you reckon you figured it out, great! It's important to try to solve problems like this yourself, and understanding how someone else's code works is always going to be a valuable skill. If you didn't quite get it, that's OK - let's walk through the code together...

  • First, we terminate the game loop so that no further questions are asked.
  • Next, print the best guess we have, then ask the player to confirm whether we were right or wrong.
  • Using the same technique as earlier, we ask the player to confirm whether we were right or wrong, storing their answer in the "input" variable.
  • If we got the answer wrong, AskForNewQuestion() is called, passing in the guess we made as well as whether the user answered "yes" or "no" to our final question.
  • Finally, it prints out a thank you message then quits.

Not difficult at all, as long as you've understood the previous techniques. But did you manage to guess how AskForNewQuestion() must work? Let's break it down.

Asking for new questions

Think about it: the game asks exactly one question, "are you human?" when it starts. If you answer yes, the game's answer is "you're Hudzilla", because that's all it knows. But let's say that's the wrong answer. You're not Hudzilla, you're Spudzilla, Hudzilla's evil, potato-loving doppelganger. So when the game says "am I right?" you answer "no".

This is where AskForNewQuestion() kicks in: it needs to ask the player to provide a new question that would have correctly identified them. You enter "are you obsessed with potatoes" then tell the game you were Spudzilla. The game can now distinguish between Hudzilla and Spudzilla, because when asked the question "are you obsessed with potatoes?" Spudzilla would answer "yes", whereas Hudzilla would answer "no".

Now, the important thing here is that the game's guess of "you're Hudzilla" needs to move from the first question to the second question, because "Hudzilla" can still be the correct answer. And so the two variables that AskForNewQuestion() needs to accept are the previous guess, so it can be copied into one of the two answers for the new question, and whether the player answered the last question with "yes" or "no" so we whether to update the YesID or the NoID of the previous question to point at the new question.

All that might be a bit overwhelming, so I've sprinkled some comments through the code:

public static void AskForNewQuestion(string guess, bool yes) {
	// first, ask who the player was and what question would have identified them with "yes"
	Console.WriteLine("What were you?");
	string thing = Console.ReadLine();

	Console.WriteLine("Please enter a question that would have identified you correctly:");
	string question = Console.ReadLine();

	// now create that new question in our database, with their correct identity as
	// the YesID and the game's previous answer the NoID
	DataComm.CommandText = string.Format("INSERT INTO Questions VALUES (null, \"{0}\", \"{1}\",
		-1, \"{2}\", -1);", question, thing, guess);
	DataComm.ExecuteNonQuery();

	// this is a new method that tells us the unique ID number of the new question
	// we'll look at it in a minute!
	int answerid = GetLastInsertID();

	// we now need to update the previous question to point to the new question
	if (yes) {
		DataComm.CommandText = string.Format("UPDATE Questions SET YesID = {0}, YesAnswer = \"\"
			WHERE ID = {1};", answerid, LastID);
	} else {
		DataComm.CommandText = string.Format("UPDATE Questions SET NoID = {0}, NoAnswer = \"\"
			WHERE ID = {1};", answerid, LastID);
	}

	DataComm.ExecuteNonQuery();
}

Remember, each time a new question is inserted into the table, we want to let SQLite give it a unique ID number for us - that's why we provide the ID field as "null" in the INSERT INTO statement. When you do that, SQLite will use its own unique ID number.

That said, because SQLite gives out ID numbers automatically, we don't know what the ID number for the new question is, and we need to know so that we can update the YesID or NoID field for the previous question. We can fix that by creating a simple, re-usable little method called GetLastInsertID(), and this is one you can easily copy into your own projects for use elsewhere, because it stands alone quite neatly. Here it is:

public static int GetLastInsertID() {
	DataComm.CommandText = "SELECT last_insert_rowid() as LastID;";
	DataReader = DataComm.ExecuteReader();
	DataReader.Read();
	int answerid = Convert.ToInt32(DataReader["LastID"].ToString());
	DataReader.Close();

	return answerid;
}

You don't really need to understand how it works, but, for the sake of completeness, let me walk you through it:

  • SQLite has a special function, last_insert_rowid(), that returns the last ID number issued automatically.
  • The first line asks SQLite to read that in, and return it to us with the name LastID.
  • The command is executed, and the first row is read in.
  • We then read the LastID field out of the database result, and return it from the method.

The "AS LastID" part of the SQL query allows us to rename fields in the returned data, and makes it easy to reference as DataReader["LastID"]. It's a whole lot of code just to pull out a single number, which is why it's best left as a standalone method - it's much easier to call GetLastInsertID() whenever you need it.

And that's it: the game is now ready to run. Usually our projects can be run in parts along the way, with more functionality being added later. But in this project you've had to wait until now to run the whole thing, so press F8 to build the program now, then open up a terminal window and run the app from there. Don't try and run it from within MonoDevelop, because it requires terminal input to work properly.

Let's wrap up

Although we looked at SQL previously, this project should really have started to cement the concepts in your brain - not only are we reading data now, but we're also creating new tables, inserting rows and updating existing rows with new data. The only thing we haven't covered is how to delete data, but we can leave that for another project. You've also now tried a new type of loop, the do...while loop - this doesn't get used as much as foreach or while loops, but it's good to have in your programming arsenal for these kinds of situations.

But hopefully the main thing you've learned is that programming logic really isn't that hard when you break it down into smaller parts. If I had asked you to write the logic for this game on paper - no code, just how it should work - you'd probably have found it quite hard. But once you split it into parts, "get user input", "ask for question", "make a guess", etc, then it's much easier, because you solve each small problem as you go, and the end result is one large solution. Well done!

Homework

If you're following this with a tutor, you will be required to complete the following homework before continuing. If you're working by yourself, I strongly recommend you find someone who can help check your work and provide feedback.

There is only one piece of homework for this project, but it's very tricky indeed - or at least so it appears before you break it down into smaller parts.

Your homework is this: if the user passes the parameter "list" to the program when it's run, instead of playing the game like normal instead it should print a list of all the questions in the game, with sub-questions indented neatly. This output should list all possible eventualities of your game: what happens for every "yes" and "no", whether that results in more guesses (in which case you should just add more indenting), a guess (print the guess) or a dead end (print "I don't know who you are!"). That is, your output should look like this:

Are you human?
   Yes: Do you like programming?
      Yes: Are you English?
         Yes: You're Hudzilla!
         No: You're Miguel de Icaza!
      No: Are you obsessed with potatoes?
         Yes: You're Spudzilla!
         No: I don't know who you are!
   No: You're Hudzilla's wife!

It's important to understand that this isn't a very hard problem to solve. The main thing you'll need to do is use recursion, as shown in Project Three - start with the question with ID 1, then read its YesID/NoID/YesAnswer/NoAnswer fields, then move onto any subquestions, and so on. The reason for the careful indenting is to make it easy to follow.

For example, using the example output above I can see that if someone answers "yes" to "are you human?" then "no" to "do you like programming?" they'll be asked "are you obsessed with potatoes?" at which point either the guess is printed out ("you're Spudzilla!") or the game admits it doesn't know the answer.

If you have problems, try to solve them yourself - you might not succeed, but you'll learn a lot by trying! If you're still having problems, drop your tutor an email and ask for help.

The small print

This work was produced for TuxRadar.com as part of the Hudzilla Coding Academy series. All source code for this project is licensed under the GNU General Public License v3 or later. All other rights are reserved.

You should follow us on Identi.ca or Twitter


Your comments

Mono contamination disqualifies this project

The subject line really says it all.

Surely you cannot be serious recommending anything using Mono in Linux.

A couple of things :)

Perhaps some more homework for the reader than intended ;)

The 'CommandBehaviour' enumeration is defined in System.Data so the reader will need to add this to the references.

I didn't see StartGame() being called from anywhere, unless I missed it! Add 'StartGame()' to the end of the Main method.

Also, it doesn't run from inside MonoDevelop due to the Console.ReadLine() throwing an exception (naturally, it's running in the silly 'Application Output' window). I recommend the reader hit the project options and select 'Run on external console'.

May have been covered in a previous lesson but, you know ;)

THANK YOU spaceyjase ^^^

I couldnt get:
DataReader = DataComm.ExecuteReader(CommandBehavior.SingleRow);
to work... Utill i read your comment about adding system.data

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

CAPTCHA
We can't accept links (unless you obfuscate them). You also need to negotiate the following CAPTCHA...

Username:   Password:
Create Account | About TuxRadar