Hudzilla Coding Academy: Project Two

Code
Hudzilla Coding Academy

 

As your second programming project, this will really step up the pace: there's no more "getting started" nonsense now, which means you may need to refer back to Project One if you forget basic skills, such as setting up a new solution in MonoDevelop. By the end of this tutorial, you'll know nearly all the fundamentals of C#, so sit back, strap yourself in, and get ready for a real code-fest...

First steps

If you read the introduction to this academy, you should remember that our plan is to alternate projects between serious apps and games, or at least programs that are a bit more light-hearted and fun. Well, this is one of the games projects, so if you follow this through to completion you will have produced a command-line game that loads a list of English words and prompts the player to find as many smaller words using those letters as they can.

So, let's not hang around: fire up MonoDevelop and create a new command-line C# solution called WordScramble. Don't bother enabling any of the features such as Gtk support. If you've forgotten how to do this, go back and read Project One again, because I'm not going to be repeating myself here.

Press F8 to build the project so that MonoDevelop creates the bin/Debug directory, then go to Applications > Terminal to bring up a shell window then change directory to WordScramble/WordScramble/bin/Debug. If you run "ls" you should see WordScramble.exe in the directory. Again, all this was covered in Project One, so refer back to there if you have to.

The first thing we're going to do is create a list of words that are acceptable spellings, then load that list into our program. If you're on Ubuntu, it's very easy to generate a world list. If you're on a different Linux distro, you may first need to install Aspell from your package manager. Anyway, from within the bin/Debug directory, run this command:

aspell dump master | uniq | sort > wordlist

That will cause the Aspell dictionary system to dump its word list to a file, while also sorting it all and making sure there are no duplicates. The finished wordlist file should be around 1.3MB, and contains a huge number of words, one per line - just as we like it.

Creating a word list using Aspell is easy.

Open a terminal window then change to the bin/Debug directory for your project and run the Aspell command, and you should get a 1.3MB list of English words that you're free to use in your program.

Switch back to MonoDevelop, because we want to load that word list into our program. If you remember from Project One, working with files requires you to add System.IO to your "using" lines. But this time I want you to add something else - change your "using" lines to this:

using System;
using System.Collections.Generic;
using System.IO;

The new one there is System.Collections.Generic, and it's actually one of the most common "using" lines around because it's brings with it some core features of Mono. From a high perspective looking down, what System.Collections.Generic brings is the ability to have different kinds of arrays that hold different types of data. The Mono developers have already written a huge amount of code to make these arrays as fast as possible, which means you get a lot of functionality and a lot of speed for free. Perfect!

To make this program work, we need to load and store all the words in RAM, so what we need is a special kind of array that holds text and can be searched quickly. In Mono-speak, what we want is called a List, and Mono lets us define exactly what we want that to hold. What's more, Lists also come with lots of cool functionality, including the ability to add and remove items while the program is running.

Towards the end of Project One, I said "all this time you've been using 'var' to store data in variables, Mono has, behind the scenes, been figuring out exactly what kind of data you want to store." Well, from now on we're not going to be leaving Mono to do the guesswork - we're going to be telling it exactly what kind of data we want, so that it can run to its full potential.

Change the top part of your code so that it reads this:

namespace WordScramble
{
	class MainClass
	{
		static List<string> WordList = new List<string>();
		
		public static void Main(string[] args)
		{

The new part is, of course, the "static List<string>" line, and we can break that down into several parts:

  • static - ignore this bit; it's basically required for now
  • List<string> - this tells Mono that we want to have an array ("List") that contains text ("string")
  • = new List<string>()> - this tells Mono to find the memory for this array now so that we can start using it straight away.

In Project One you met integers (whole numbers, eg 3, 3141 or 31415926), and now you know strings too - they hold anything from short pieces of text ("hello") to huge pieces of text (an entire book). These are called data types, because they define the type of data you want to store in a variable.

The code above gives us an array that can hold strings (the official, Mono name for bits of text), and Mono has allocated us enough RAM to use it. Now it's time to load the Aspell file into Mono, trimming out anything we don't want.

Filtering the word list

In this game, players must type words that can be made out of random letters. But to make it a bit more challenging, we're going to force them to find words that are least three letters long. While we're at it, the Aspell dictionary includes words with apostrophes, such as "programmer's". This is very important for spell-checking normal text such as "the programmer's desk was very messy," but it's not much fun in a letter game so we're going to remove words that have apostrophes.

Replace the Console.WriteLine() line with this:

string[] lines = File.ReadAllLines("wordlist");
			
foreach(string word in lines) {
	if (word.Length < 3) continue;
	if (word.Contains("'")) continue;
	WordList.Add(word.ToLower());
}

The call to ReadAllLines() should be familiar from Project One, but now notice that we're assigning it to a variable of type "string[]" rather than "var". These are, in this example, the same thing, and you can use either of them and it won't matter. But as I said in the beginning, we are from now on always going to use specific data types - it takes a little more thinking, but it's the best option.

If you recall, [] means "array", so string[] means "an array of strings" - which is exactly what File.ReadAllLines() returns. We then put that into a foreach loop (this time assigning each line to a string variable rather than "var") so we can put it through our two tests: is it at least three characters long, and is it free of apostrophes?

These two tests work because of the "continue" keyword, which means "OK, I'm done with this iteration of the loop - go back to the top and get the next item, please." So the loop will start with the word "a" and it will fail the first test because it's under three characters. When Mono calls "continue", it will skip the second test and the WordList.Add() line, and go straight onto the next word in the array. In Project One you learned the == (is equal to) and != (is not equal to) tests, and now you can see the < test, which means "is less than". So the first if statement reads "if the length of this word is less than 3 (ie, if it's 1 or 2), go to the next item in the array."

Now, the List<string> variable we defined earlier - WordList - has special code that lets us add items to it, and that's Add(). What's more, because we've told Mono specifically that we want it to hold only variables that are text strings, Mono won't let us try to add integers (whole numbers) or other data types in there. In essence, when you say to Mono you want to use a List<string>, you're making a promise that you'll only put strings into that array, which means Mono can help you spot errors when you accidentally try to put a different data type in there.

Adding items to a List array of strings is done using Add(), and you can either use hand-typed strings, like this:

WordList.Add("Hello World!");

...or use variables, like this:

WordList.Add(word);

In our code, we're using something a bit special: word.ToLower(). I introduced you to ToUpper() in Project One - it converts a string into uppercase. Hopefully you can guess that ToLower() converts a string to lowercase, so what WordList.Add(word.ToLower()) does is to add the lowercase version of this word to the list.

If you run the program now, it will do nothing visible - it will load the word list, remove all the words that don't fit our criteria, then shutdown. Boring! Let's fix that by making it read some text from the user...

Infinite loops and switch/case

This program doesn't have a definite end where it can terminate once it's finished its work - it needs to run and run until the user has said, "OK, I've had enough." So far you've only been introduced to one type of loop, called the foreach loop. This is used to go over an array item by item (known as "iterating over an array" to geeks), which is just what you want when you have a set of items in an array and you want to execute an action on every one of them.

But here we need something different, called a "while" loop. This will keep on looping for as long as the condition you specify is true. As soon as that condition stops being true, the loop ends. This is perfect for our needs, because we want the game to keep on running until the user specifically requests to quit.

Put this code straight after the foreach loop you just wrote:

bool running = true;

while (running) {
	// loop code goes here
}

That introduces another new data type called "bool", which can hold one of two values: true or false. So, keep track - integers hold whole numbers, strings hold text, and now bools hold either true or false. You could of course use an integer for the same job as a bool: if it holds 0 then it's false, and it holds 1 then it's true. But if all you want to do is store true or false, yes or no, on or off, or something similar, a bool is the most efficient data type to use.

In this case, true or false is exactly what we want: we set a variable called "running" that will store whether the game is running or not, and that's used inside the while loop so that as long as running is set to true, all the code in the "// loop code goes here" part will be executed again and again.

Right now, this is what's called an infinite loop: the program will loop continuously without interruption, which chews up a lot of CPU time and really isn't recommended. Let's do something more useful than just melting your CPU - let's read some text from the command line.

In Project One we made extensive use of Console.WriteLine() to write text to the terminal window. What should we use to read text back in from the terminal window? You can, if you want to, just type "Console" then press "." and search through the list of options show to you by MonoDevelop, but hopefully you should be able to guess that the opposite to WriteLine() is ReadLine(), and it's really easy to use - watch this:

bool running = true;

while (running) {
	string input = Console.ReadLine();
	Console.WriteLine(input);
}

Try running that from the command line, and what you'll find is that the program waits for you type something in and press Enter, then prints out what it was that you typed. It automatically pauses the program so the user can type, reads all the letters they enter, waits until the Enter key is pressed, then sends the whole line of text back to your program and into the "input" variable so we can play with it. Your program won't continue until the user has pressed Enter, which means you'll always get something back from Console.ReadLine(), even if that's an empty string.

Sending the input from Console.ReadLine() straight to Console.WriteLine().

Sending the input from Console.ReadLine() straight to Console.WriteLine() causes your program to echo everything you type.

In Project One we used if and else statements to decide what happened. For example:

if (args[0] == "add") {
	// do add stuff
} else if (args[0] == "del") {
	// do del stuff
} else {
	// just list the items
}

That works for very simple checks, but if you have lots of conditions you want to match, it becomes hard to read. A better alternative is known as a switch/case, and if I were to rewrite the previous code block using switch/case it would look like this:

switch (args[0]) {
	case "add":
		// do add stuff
		break;
	case "del":
		// do del stuff
		break;
	default:
		// just list the items
		break;
}

Now, you're probably thinking, "wait a minute... that's more code! Why bother?" Well, the truth is this: switch/case statements like the one above are less flexible than using if/else, because you can only check one variable against a variety of values, whereas with if/else you can do crazy stuff like this:

if (foo == bar) {
} else if (wom == bat) {
} else if (baz < zab) {
}

But while you can make all sorts of clever if/else statements to check for all sorts of eventualities, the problem is that Mono must execute them in the exact order you specify - even if that order is sub-optimal. With switch/case, you don't care what order it checks the variable, as long as it executes the right code. As a result, Mono is free to optimise your code. It's similar to data types: if you give Mono a little more information about how you want your code to run, it can make it faster.

Now, the reason I've introduced you to switch/case is because we want our user to be able to exit cleanly by typing "!quit" and pressing Enter. If they type anything else, we'll assume that's them playing the game. Here's how that's done:

bool running = true;

while (running) {
	string input = Console.ReadLine();

	switch (input) {
		case "!quit":
			running = false;
			break;

		default:
			Console.WriteLine(input);
			break;
	}
}

Note how each case block has to end with "break", which means "break out of this switch/case because I'm done" - if you don't include that, Mono will get very upset when you try to compile your program. Also note that we don't say "case default:" for the block of code we want to execute when none of the other cases match; you just need to use "default:".

Now when you run that program, it will have the same behaviour as before, except when you type "!quit" and hit Enter it will exit. Note that it will look for that precise string: if you put a space before it, Mono will see " !quit" (which is different from "!quit" because of the space) and execute the "default" code.

Creating letters to play with

The next thing we need to do is have the game pick some letters to give the player something to work with. Remember, the idea is that they'll see eight letters or so, then try to find smaller words they can make using those letters. So, at the start of the game we need to get some letters for the player to work with. Also, if the player gets bored with those letters and wants something different, we need to let them have different letters using the same code.

I want to explain something really important to you, so listen carefully: when you write code, you naturally include a few bugs here and there - you're human, so you make mistakes from time to time. If you write some nice code to generate letters for the player to work with, then copy and paste it somewhere else in your program because you need to be able to generate letters there too, you're opening yourself up to big problems. You see, if you spot a problem in the code, you need to remember to change it in both places. If you fix it in one place and forget the other - or perhaps 10 others, depending on how big your code is! - then horrible little bugs creep into your programs that are very hard to fix.

The solution is to write "methods". That's one of those geeky jargon terms that geeky jargon-loving people use a lot. But it's also a very common one, so we'll be using it from now on. Basically, a method is any piece of functionality that performs a simple task and can be re-used. Console.WriteLine(), for example, is a method - it's one of the built-in methods that Mono provides. You send it a string of text to write, and it prints it out.

The cool thing about methods is that you can write your own: you specify what input must be provided, you specify what output you will return, and you fill in all the functionality. That way, you can call your method from elsewhere in your program without having to copy and paste code. We're going to use this to write a method to pick out some letters for the player to use.

The easiest way to pull out some good letters (as opposed to "xyzzyx", which is possible if you use completely random letter choices!) is to use one of the words from our word list. Once we choose a random word, we need to store its letters somewhere so that the player can be shown them - let's start with that. Put this just after the List<string> line you added earlier:

static string PromptLetters;

Don't worry about the "static" nonsense for now - it's needed, but it's really not important. With that line we set aside space for a string of text that will contain the letters for the player to work with.

Now, to add a new method you first need to see where the Main() method ends. That's the one we've been using all the time so far, because that's the one that automatically runs when your program starts. If you follow the { and } symbols (they're called braces, remember?), you should see where Main() ends - that's where you need to put your own method. This is important, so I'm going to show you exactly where it is:

// Main.cs created with MonoDevelop
// User: hudzilla at 20:47 31/01/2009
//
// To change standard headers go to Edit->Preferences->Coding->Standard Headers
//
using System;
using System.Collections.Generic;
using System.IO;

namespace WordScramble
{
	class MainClass
	{
		static List<string> WordList = new List<string>();
		static string PromptLetters;
		
		public static void Main(string[] args)
		{
			string[] lines = File.ReadAllLines("wordlist");
			
			foreach(string word in lines) {
				if (word.Length < 3) continue;
				if (word.Contains("'")) continue;
				WordList.Add(word.ToLower());
			}
	
			bool running = true;
			
			while (running) {
				string input = Console.ReadLine();
				
				switch (input) {
					case "!quit":
						running = false;
						break;
					
					default:
						Console.WriteLine(input);
						break;
				}
			}
		} // this brace closes Main()
		
		// you need to put your new method right here!
	}
}

Your code should look very similar to that right now, except that I've added those two comments near the end to point out where the Main() method ends and where your new method should go.

Now, what this method should do is:

  1. Clear the list of existing letters
  2. Pick out a random word from the list of words
  3. Split that word into individual letters so we print "c a t c h" rather than just "catch"

Here's how that looks in C# - put this code where I marked above:

static void GetLetters() {
	PromptLetters = "";
		
	Random Rand = new Random();
	string word = WordList[ Rand.Next(WordList.Count) ];
			
	foreach (char letter in word) {
		PromptLetters = PromptLetters + letter + " ";
	}
}

There's quite a lot new in there, so let me explain:

  • "static void GetLetters()" can be broken into three parts: "static" is the bit I've told you to ignore because it's useless right now, "void" is what the method returns ("void" means "nothing", meaning that this function returns no value), and "GetLetters" is the name of the method. So when you want to call it, you just write GetLetters().
  • "Random" is the Mono random number generator. In this code we create a new one every time the method is run.
  • To find a random word, we use Rand.Next() and provide it with a value to tell it the highest number it should generate. Using this system, Rand.Next() will return a value between 0 and the number we specified, which in this case is the number of words in WordList. So, Rand.Next(WordList.Count) will return a number between 0 and the total number of words.
  • Once we've picked a random word, we need to use a foreach loop to go over each letter, adding it and a space to our PromptLetters string.

That last part is particularly important - if we just set PromptLetters to be the random word, it would be a bit harder to read. By looping over the word, letter by letter, we can insert spaces as needed. Previously you just used foreach to loop over a string array, but if you think about it a string is actually also an array - an array of characters. So, to loop over every character in the array, we use a forloop and assign the letter to a variable of type "char", which is short "character".

Data types cheat sheet

These are the four data types you've learned so far - there are two more important ones to come in later tutorials.

Once that method has been written, we can call GetLetters() elsewhere to make it generate some letters for the player to use. For now, put these two lines of code just before the line "bool running":

GetLetters();
Console.WriteLine(PromptLetters);

That will now print out the letters when the game is first started, then it will back to its infinite loop where the player can type in words.

Matching words

Now we're ready to tackle the main part of the game: when users enter words, we need to check whether they are valid or not. To make the game play correctly, there are three things we need to do, and we're going to tackle them in order from easiest to hardest. First, a word should only be accepted if it is in the dictionary of words. Replace the Console.WriteLine() you have in the "default" case with this:

if (WordList.Contains(input)) {
	Console.WriteLine("Good!");
} else {
	Console.WriteLine("That word doesn't exist!");
}

That code should be pretty easy to read: if the WordList contains the word the user typed, print out a message saying "Good!" otherwise print out a complaint. As with the strings we were searching through for apostrophes previously, Contains() returns true if it finds the text you specified, otherwise it returns false.

Moving on, a word should only be accepted if it hasn't already been used. To check this one, we need to keep a list of all the words the user has had so far, and eliminate the word if it has been used already. We're already using a List<string> to hold all the words in the word list, so I hope you find it easy to create another List<string> to hold used words - you need to put this code just after the existing List<string> line:

static List<string> UsedList = new List<string>();

With that in place, we can modify the word checking code to be like this:

if (!UsedList.Contains(input)) {
	if (WordList.Contains(input)) {
		UsedList.Add(input);
		Console.WriteLine("Good!");
	} else {
		Console.WriteLine("That word doesn't exist!");
	}
} else {
	Console.WriteLine("You've had that word already!");
}

There are a couple of new things there, so let me go over them quickly:

  • Using ! in a condition makes it mean the opposite. For example, UsedList.Contains(someword) returns true if it contains that string. But if you use !UsedList.Contains(someword), then it returns false if the string exists in the array. This is very similar to how != means "not equal to". Fortunately, that's exactly what we want here: if the word doesn't exist in the array then we proceed, otherwise we print an error message saying the word has been tried already.
  • Notice how I've put the UsedList.Contains() check before the WordList.Contains() check. That's because the list of words in UsedList will almost certainly be smaller than the list of words in WordList, so we search the smallest one first - there's no point spending a long time (comparatively!) checking whether a word exists, only to find out that it's already been used.
  • Once a word has been entered and has been confirmed to exist, we add it to UsedList so the player can't use it again.

There is one minor flaw with this plan, but it's easily fixed: eventually, it will be possible for the player to call GetLetters() on demand to get new letters when they feel like it, but although the method deletes the contents of PromptLetters and generates new letters, it doesn't clear the contents of UsedList. To fix that, change the beginning of GetLetters() to this:

static void GetLetters() {
	PromptLetters = "";
	UsedList.Clear();

The third and final check we have to introduce to make sure a word is valid is more complicated than the other two: we have to make sure that each letter in PromptLetters is used only once. For example, if the prompt letters were "p e o l e", the player shouldn't be able to spell "people".

To do this task, we're going to create another method. This time it's going to accept one argument - the word the player typed - and return a bool. That means it will return either true or false, depending on whether the player's word can be made if each of the available letters are used only once.

There are several ways this problem could be solved, but I'm going to pick one that's easy to get your brain around: we're going to take a copy of the PromptLetters variable, then loop over each letter in the word that the player submitted and check whether it's in our PromptLetters copy. If it is, we remove the letter so that it can't be used again. If we've gone through every character and found it, then we'll return true to signal that the word is OK; otherwise we'll return false.

With all that in mind, let's get on with the code. As with the previous method we added, you need to scroll to the very bottom of your code file, then go up past the bottom two closing braces (the } symbol).

static bool WordIsPossible(string word) {
	string letters = PromptLetters;
	
	foreach(char letter in word) {
		int pos = letters.IndexOf(letter);
		if (pos != -1) {
			letters = letters.Remove(pos, 1);
		} else { 
			return false;
		}
	}
	
	return true;
}

As before, forget the "static" part. Our previous method, GetLetters(), didn't need to return anything, so specified "void" as the return type. This new one needs to return true or false, so we specify "bool" so that Mono knows a value will be sent back. The nice thing about telling Mono this is that it will ensure a value always gets returned - if its possible for your method to end without a value being returned, MonoDevelop will flag up an error during compilation.

MonoDevelop will not let you build a program if your method says it will return something then doesn't.

MonoDevelop will not let you build a program if your method says it will return something then doesn't - this is actually very helpful!

Continuing on, you can see I've named the method "WordIsPossible()". This makes conditional statements using this method very easy to read, because you can write "if (WordIsPossible(someword)) {", which is almost plain English!

The "string word" part of "WordIsPossible(string word)" means that this method must have a variable passed to it that is of type string. If you try and pass anything else to it, or indeed try passing nothing at all, Mono won't compile the program. The name "word" means that when the string gets passed in, Mono will assign it the variable name "word" for us to work with inside the method - just like the way "string[] args" is passed into the Main() method so that we can read the args array.

The first thing the method does is to take a copy of the PromptLetters string, which is needed because we're going to be removing letters to ensure they are used only once, and removing them from PromptLetters itself would screw up the rest of the program. Once that's done, we use a foreach loop to go over every letter in the word the user typed, and that's where a new Mono method is introduced: IndexOf(). This method belongs to any string-type variable, and you pass in either a char (a single letter) or a string (lots of letters) that you want to search for. In our example, we loop over each letter in the word that the user entered, and pass it into letters.IndexOf(): if the letter exists in the "letters" variable, IndexOf will return the position of that letter in the string, counting from 0.

This is quite important, so I want to be clear. Let's say the variable "fizz" contains the string "hello". If you run fizz.IndexOf("h"), Mono will return 0, because it's the first letter. If you run fizz.IndexOf("e"), Mono will return 1. If you run fizz.IndexOf("l"), Mono will return 2, because it returns the position of the first instance of the letter. But what if you run fizz.IndexOf("m")? "Hello" doesn't contain any "m"s, so what happens is that Mono scans the entire string, can't find the letter, and returns -1, meaning "not found".

Mono counts its arrays starting from 0, and a string is really just an array of characters.

Mono counts its arrays starting from 0, and a string is really just an array of characters, so in this example IndexOf("h") will return 0 and IndexOf("o") will return 4.

This is exactly what we're using in the WordIsPossible() method: we run letters.IndexOf() and store the result in an integer variable called "pos". If pos is set to anything other than -1, it means it was found inside the string. Otherwise, the "else" block will execute and hit the line "return false". We looked at the "return" keyword in Project One, so you should recall that it tells Mono to exit the current method. But remember: we've told Mono that this method will always send back a value, so we can't just say "return;" - we have to tell Mono the value we want to return, which in this case is "false" because the player's word can't be made using the letters.

The really interesting stuff happens when IndexOf() returns something other than -1, because that means the letter has been found. What happens then is that we use another new Mono method that's built into strings: Remove(). This removes a chunk of a string that we specify by passing in the position of the first letter to remove and the number of letter we want taken out. In this case, we already know the position of the letter we want to remove, because IndexOf() told us where it is, so we just have to pass that in along with 1 as the second parameter because we want to remove just one letter, and it's done.

If Mono makes it to the end of the foreach loop, it means every letter in the player's word has been found and removed from the PromptLetters copy, which means every letter is used just once and thus that the word is good. So, just after the foreach loop there's a "return true" to send a positive signal back that the word is possible.

So, you now know how to accept variables into methods, how to return variables from methods, plus how to search for and remove strings inside other strings - that's quite a lot, but it's a powerful little method you just wrote, so let's put it to use: we need to rewrite the word checking code in the switch/case block from this...

if (!UsedList.Contains(input)) {
	if (WordList.Contains(input)) {
		UsedList.Add(input);
		Console.WriteLine("Good!");
	} else {
		Console.WriteLine("That word doesn't exist!");
	}
} else {
	Console.WriteLine("You've had that word already!");
}

... to this:

if (!UsedList.Contains(input)) {
	if (WordIsPossible(input)) {
		if (WordList.Contains(input)) {
			UsedList.Add(input);
			Console.WriteLine("Good!");
		} else {
			Console.WriteLine("That word doesn't exist!");
		}
	} else {
		Console.WriteLine("Did you forget your letters or something?");
		Console.WriteLine("Reminder: " + PromptLetters);
	}
} else {
	Console.WriteLine("You've had that word already!");
}			

The changes should be clear: we've added the WordIsPossible() check to the other two, and, if the word isn't possible, we print a reminder of the letters so the player can try again. Note that I've ordered the three "if" statements by approximate complexity to maximise performance: first check whether word has been used already, then check whether the word is possible using each letter just once, then check whether the word exists by consulting the big word list. Each check now prints a message if the check failed so that the player knows what's going on.

Adding some polish

Before this game is done, there are a few finishing touches I want to apply to make it nicer to use:

  • We're going to add the commands "!next" to change the letters and "!letters" to print a reminder of the current letters, in case they have gone off the screen
  • When the program starts, we're going to print a message explaining the commands that are available
  • We're going to make GetLetters() a little bit smarter

First up, adding commands is very easy because we're using switch/case rather than if/else. In fact, I should be able to just show you the code and it should it just make sense, because there's nothing new there at all. Amend the first part of your switch block to this:

switch (input) {
	case "!quit":
		running = false;
		break;
		
	case "!next":
		GetLetters();
		Console.WriteLine(PromptLetters);
		break;
		
	case "!letters":
		Console.WriteLine(PromptLetters);
		break;

That adds two new cases: !next (which runs GetLetters() again to generate new letters to play with, then prints out those new letters) and !letters (which just prints out the letters as a reminder to the player).

Now that we have three commands that the player can use, it's important to print them when the program starts so there's no confusion. Your Main() method starts with the call to ReadAllLines, then there's the foreach loop to load the words into WordList. After that come these three lines of code:

GetLetters();
Console.WriteLine(PromptLetters);

bool running = true;

Just before those three - ie, just after the end of the first foreach loop - I want you to add this new code:

Console.WriteLine("");
Console.WriteLine("Welcome to WordScramble!");
Console.WriteLine("Type !quit to exit, !letters for a reminder, or !next for a new word.");
Console.WriteLine("");

There's nothing really amazing there - sending an empty string to Console.WriteLine() just causes it to print an empty line, so now running the program automatically prints those instructions before printing out the initial set of letters.

And now onto the final task: making GetLetters() a little smarter. There are two simple changes we can make, with the first one just being about sharing resources. Right now GetLetters contains the line "Random Rand = new Random();", which creates a random number generator so we can pull out a random word from the list. Random numbers are very handy to have around in programs, so why not share the generator with the whole program by moving that line out of GetLetters() and up next to the List<string> lines near the top of the file.

You see, when you declare any variable inside a method, it's only available inside that method - no other part of your program can reach it, and as soon as Mono finishes with that method (when you call "return" or when there's no more code to execute), all those local variables are deleted. Sharing the random number generator with the whole program has the added benefit that it doesn't need to be recreated every time GetLetters() runs, which makes our program a little bit more efficient.

So, go ahead and select the "Random Rand = new Random();" line in GetLetters(), then press Ctrl+X to copy it to the clipboard and delete it from MonoDevelop, then scroll up to where the other variables are defined (eg "string PromptLetters") and put it next to them by pressing Ctrl+V. When that's done, put the word "static" in front of it so that it matches the other variables there.

Put the Rand variable up with the others so that your entire program can access it.

Put the Rand variable up with the others so that your entire program can access it - and don't forget to put the word "static" in front of it, otherwise you'll get errors!

The second change we're going to make in GetLetters() is to force it to choose long words. We're making this change because right now it could choose words containing just three letters, which isn't much fun for the player! We're going to change the code so that words are only chosen if they have at least eight letters.

First, here's how we're currently choosing a word:

string word = WordList[ Rand.Next(WordList.Count) ];

What we're going to do is use a new "while" loop that will keep on pulling out random words from WordList until it finds one that's at least eight letters long. Here's how it's done - replace the line above with this:

string word = "";

while (word.Length < 8) {
	word = WordList[Rand.Next(WordList.Count)];
}

So, first "word" is set to be an empty string, then our "while" loop starts. Clearly an empty string contains fewer than eight letters, so Mono will pull out a random word. The length will be checked again, and again, and again, etc, until eventually we get a word with enough letters, at which point the method continues as before - we loop over every letter to space it out neatly.

Let's wrap up

This has been another long tutorial, but again you have learned lots of new skills that are absolutely fundamental to programming. In this tutorial you've learned about lists, while loops, switch/case blocks, bools, reading from the command line, generating random numbers, and more - and that's a lot! Most importantly, you have learned how to create your own methods, which let you package up code in one re-usable chunk, then pass data to it and receive data back.

So, pat yourself on the back: that's your second project finished, plus several more key programming techniques learned. 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.

The homework for this project is made up of four coding problems; all are required.

  • When players enter a word, the program currently prints out "Good!". Change that so it keeps count of the player's score (one point for each correct word) and prints out the score each time the player gets one right or requests new letters.
  • Once you have scoring working, make it so that you get one point for each letter in the word you submit, ie entering "cat" scores you 3 points, "catch" scores you 5 points and "catching" 8.
  • Allow players to specify as an argument to your program the filename to load for the word list. So, rather than just loading a file called "wordlist", check the args variable to see whether a specific word list was requested, and, if it was, load that instead. If no file is specified, just load "wordlist" as per usual.
  • Don't allow the player to enter the same word that was used to create PromptLetters. For example, if the prompt letters are "w o m b a t", don't allow the player to enter "wombat" as a valid word. The smart solution here doesn't involve comparing the player's input directly against PromptLetters.

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

Excellent Tutorial

I have throughly enjoyed the first and second installments of this tutorial and am looking forward to the remaining lessons.

Glen Wittig, CNE, BSEE

Great tutorial

I really like these tutorial series. I have experience with Java and C and I'm finding that this a great way to quickly get used to the C# syntax and ways of doing things.

Homework answers?

Hi,

First of all: Thank you very much for the great tutorial! It is really a lot of fun learning all the different aspects of writing C# code with nice examples and put it to use right away.
Also the homework assignments are great to see if you really grasped the theory and didn't just copy everything from the tutorial and just think you grasped it!
But it would also be great to see (at one point or another) how the teacher would have done it. :)

Keep up the good work!

aspell

If you use aspell with another language you can use this:
aspell -l en dump master | uniq | sort > wordlist

Oops!

IMHO it should be:
aspell -l en dump master | sort | uniq > wordlist
because uniq only removes *successive* identical lines.

I wonder if others had the

I wonder if others had the same prob as me with the third item of homework (pasing a file path as an argument).

First, here's my code (the relevant stuff anyway):
<code>
public static void Main(string[] args) {
// check the args array for length
if (args.Length > 0) {
if (File.Exists(args[0])) {
lines = File.ReadAllLines(args[0]);
} else {
Console.WriteLine("Sorry, can't find that file name");
return;
}
} else if (args.Length == 0) {
lines = File.ReadAllLines("wordlist");
}

//... it then moves into the foreach loop...
</code>

To test this I created a file from the Debug directory:
bravo@highnoon:~/WordScramble/WordScramble/bin/Debug$ echo "hello" >> myfile && echo "there" >> myfile && echo "brainy" >> myfile

I then fire up the app...
bravo@highnoon:~/WordScramble/WordScramble/bin/Debug$ ./WordScramble.exe myfile

Typing the !next command doesn't cause anything other than the CPU to max out and the fan to kick off.
Good old ctrl z to exit...

The funny thing is that if I create a test file using the following command:
bravo@highnoon:~/WordScramble/WordScramble/bin/Debug$ cp wordlist myfile2

...and then run the program then there is no issues. So what's going on here you reckon?
Any thoughts?

I imagine when I get to exception handling the answer will be revealed ;-)

good tut's... as a lapsed java programmer it's quite a nice reintro. Pity no more printed copies for sale in New Zealand... would love a copy.

Re: I wonder if others had the

Check the lengths of the words in your alternative wordlist and the minimal length of the words that GetLetters() looks for...

aha... I see. I haven't

aha... I see.
I haven't tested it but you look right. It's infinitely looping cos none of the words in my test list is greater than 8. Doh!

while (word.Length < 8) {
word = WordList[Rand.Next(WordList.Count)];
}

Might put a tweak by creating a variable (PromptLetterLength) that is checked when file is loaded to make sure there is at least one word with the specififed length.

Good spotting. I was stuck on this for ages and refactored a number of times. Is there anyway I can step through the code? perhaps a plugin to monodevelop?

Re: ok... found what I

Been looking for a working step-through debugger for ages... Could you confirm that this works for you?

Yep, definitely works... so

Yep, definitely works... so good to be able to step through the code. MonoDevelop2.2 has a lot more features than 2.0!

Re: Yep, definitely works... so

Great to hear... will be testing it this evening (tomorrow in your timezone :)

Project 2 Homework part 4

hi im new to programming and im struggling with the final piece of homework for project 2 :( could anyone help me?

thanks i know im cheating by asking online but its really doing me head in that i cant do it lol i thought that an if statement with input==PromptLetters conditions would work but it doesnt seem to :(

thanks in advance

Project 2 Homework Part 4

First of - very nice series and an equally good refresher course for someone like me who has not done any coding for a long time and was feeling rusty...

about the 4th part of homework, there maybe several ways of doing it but the easiest and quickest one line solution was:

Add the following line in GetLetters method after the random word has been assigned.

That way it gets into the used list and hence will be treated as an existing entry and will not be accepted.

I don't know if it's perfect or not but I being lazy just felt this will do...I don't want to be the topper....just pass is good enough...:)

and the line was

UsedList.Add(word);

nice

Great work....In order to have the code sink-in to my brain, I read the tutorials 3 times and added some print statements for debugging purposes pretty cool

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