Understanding C#: Use System.Console to build text-mode games

By Andrew Stellman
August 17, 2010 | Comments: 6

I'm a sucker for an old-school text-mode console game. Text-mode games rendered their "graphics" by drawing text characters at different positions on the screen using 16 background and foreground colors. They're also easier than ever to build in C# and .NET, thanks to the System.Console class, which lets you position the cursor, do animation by moving blocks of the buffer, use colors and special characters, and handle input from the user. In this tutorial post, I'll walk you through all of the tools you need to create a retro MS-DOS style text-mode video game, including a complete game that you can build yourself.

One thing I learned while I was writing on Head First C# is that building video games is a great way to improve your C# skills. (I wrote that post on Building Better Software just after the first edition went to press.) I put a lot of effort into helping people learn C#, and I often get the question, "What should I build to get practice?" The reason games make great projects for learning and experimenting because you start out with a good idea of what you're going to be building. So one goal of this post is to give you a new—and fun, I hope!—way to get some great practice honing your C# skills.

Head First C# Cover

Contents: What you need to build a text-mode game in C#

This post is structured like a tutorial, with a bunch of examples that should give you all of the pieces you need to create a great text-mode game. Here's what I'll cover:

A little history

There are a lot of .NET developers who, when they built console applications at all, have only ever used Console.ReadLine() and Console.WriteLine(). The System.Console class does so much more than just read and write text. It gives you enough control over the console window to build a complete text-mode game.

If you grew up in a world that always included a mouse—if you've never used a computer that only displayed text and not graphics—then take a minute and have a look at Snipes. It's one of the classic text-mode games from the 80s. You can download it from textmodegames.com—it runs just fine under DOSBox, and it's actually worth taking a minute to play if you haven't before. Here's a screenshot:

Snipes_-_Text_mode_game_by_Novell_-_screen_grab_Apr05.gif
Snipes screenshot courtesy of the Snipes entry on Wikipedia

I may be showing my age, but I think I love text-mode games because I grew up playing them.As a kid, I grew up playing Star Trek and Adventure on a VAX 11/780, which I dialed into using my parents' VT-180 Robin. (We eventually replaced it with a Rainbow 100, and at some point there was a Tektronix 4052, along with a couple of TRS-80s - a Model 100 and a CoCo2...) Finally, sometime in the mid-80s, we made it to the world of IBM PC clones. The family PC was an AT&T PC-6300 with a massive 20 megabyte hard drive that I did my best to fill up with games. And while games were increasingly taking advantage of those great 320x200 16-color CGA graphics, there were plenty of them that used text mode graphics.

There's one thing I need to bring up before we dig into the programming. Every variant of windows, going back at least to Windows 3.1, has a command prompt. Some people (like me) use it all the time, but I've worked with many really good developers who rarely use it at all. So just to make sure we're all on the same page, open the command prompt now. It lives in Start >> All Programs >> Accessories. When you run a text-mode game, you'll see it in that window. If you've only ever built Windows Forms applications in Visual Studio, this will be new—you'll be building console applications, and they'll run in a command prompt.

Positioning text

The first step in making a text mode game is drawing text anywhere you want on the screen. To do this, you'll move the cursor to a coordinate that's measured in columns from the left side of the screen and rows from the top of the screen using a few useful methods and properties from the Console class:

Those two properties are useful when you want to save the current location, write some text somewhere else on the screen, and then jump back to that location. Just save the values of the CursorLeft and CursorTop properties, draw what you need to draw, and then set them back (or use SetCursorPosition(), which does the same thing as setting those properties) to reset the position.

Here's a simple program that does exactly that. Normally, I'd ask you to create a new Console Application in Visual Studio and paste some code into its Main() method. But since we're talking about console applications, I think it's useful to actually create one from the ground up outside of Visual Studio, because it really reinforces just how basic and fundamental console applications are. Here's what to do:

First, open up Notepad and paste the following code into it:

PositionText.cs:

using System;

class PositionText
{
     static void Main(string[] args)
     {
          Console.WriteLine("It's time to enter some text.");
          Console.Write("Enter it here: ");
          string text = Console.ReadLine();
          int left = Console.CursorLeft;
          int top = Console.CursorTop;
          Console.SetCursorPosition(15, 20);
          Console.Write("You entered -> {0} <-", text);
          Console.SetCursorPosition(left, top);
          Console.WriteLine("Continuing where I left off.");
      }
}

Compiling and running a C# program from the command prompt

Save the file as PositionText.cs (I put it in the C:\temp\ folder). Next, open a command prompt, go to the folder where you saved the program, and run the following command:

%SYSTEMROOT%\Microsoft.NET\Framework\v3.5\csc.exe PositionText.cs

(You might notice that I'm using .NET Framework v3.5 to compile these programs. That's because the basic the System.Console class has remained stable, and really hasn't changed over the last few versions of the .NET Framework. But all of this code will work just fine in .NET 4.0 and Visual Studio 2010.)

Running CSC should create PositionText.exe. Now you can run it. Here's what it should look like when it runs:

Screenshot - PositionText.png

Notice how the program draws the text near the bottom of the window, then continues writing lines as usual. That's all the cursor positioning you need for a text-mode game.

Colors

Life is dull in monochrome, even for colorblind people like me. I may see the wrong colors, but I still see them! And if you're like me and you have trouble seeing colors, you'll love text mode—there are only 16 colors that you need to keep track of. You can do it with these System.Console members:

  • The ForegroundColor property sets the color of the letters
  • The BackgroundColor property sets the color of the background behind the letters
  • Both of those properties use the ConsoleColor enum,
  • The ResetColor() method resets the foreground and background colors

Go to your PositionText.cs program and replace this line:

Console.Write("You entered -> {0} <-", text);

with the following code:

        Console.ForegroundColor = ConsoleColor.DarkBlue;
        Console.BackgroundColor = ConsoleColor.Yellow;
        Console.Write("You entered -> ");
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.BackgroundColor = ConsoleColor.DarkGray;
        Console.Write(text);
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.BackgroundColor = ConsoleColor.Cyan;
        Console.Write(" <- ");
        Console.ResetColor();

Now when you run your program, the text it prints near the bottom of the screen should be colorful. (If you want, you can also add code to set the background color of the console using the Console.BackgroundColor property.)

Screenshot - PositionText in color.png

There's one more thing to try with your program. Fill up your command window by running dir %SYSTEMROOT%. Then run PositionText.exe again. Hey, wait a minute - where's the text? It should really stand out with those colors, but it doesn't seem to be anywhere.

Scroll up to the top of the buffer. The missing text should be up there at the top. If you run PositionText.exe a few times, you'll see it overwrite the same line over and over again. The reason is that when you set the cursor position, you're setting it relative to the buffer. In a minute, I'll show you how to change the size of the buffer and the size of the window.

Animation and Sound

Setting the cursor position and changing the text color is definitely enough for simple text and even basic animation. But if you want to do some more advanced animation, though, you'll need a few more tools:

  • The Console.MoveBufferArea() method moves a rectangular area of the console, leaving an empty space where the moved text was
  • It's got an overload that takes three extra parameters to let you leave a rectangle filled with characters (with a foreground and background color) instead of an empty space

Here's another quick program called HappyFaces that draws a simple animation using both overloads of the MoveBufferArea() method. You can compile and run HappyFaces.cs it just like you did with PositionText.cs.

HappyFaces.cs:

using System;
 
class HappyFaces
{
     static void Main(string[] args)
     {
          Console.Clear();
          Console.SetCursorPosition(0, 0);
          Console.ForegroundColor = ConsoleColor.Magenta;
          Console.WriteLine("o o        . .");
          Console.WriteLine(" )          ) ");
          Console.WriteLine("___        ###");
          for (int i = 0; i < 20; i++)
          {
               Console.MoveBufferArea(i + 11, i, 3, 3, i + 12, i + 1,
                   'x', ConsoleColor.Red, ConsoleColor.White);
               Console.MoveBufferArea(i, i, 3, 3, i + 1, i + 1);
               Console.Beep((i+10)*100, 100);
           }
          Console.SetCursorPosition(0, 23);
          Console.ResetColor();
      }
}

Did you notice the great—let's face it, awesome—sound? That's thanks to the Console.Beep() method, which emits a beep at a certain frequency for a certain number of milliseconds. I'm using it here to slow down the animation. If you call Beep() without arguments, it emits the standard system beep. And depending on your machine, it might send the beep out of the speaker inside your computer case instead of your speakers!

Special characters

Take another look at the screenshot from Snipes. Notice how there are lines, circles, triangles, boxes, and smiley faces? Those aren't letters or numbers! What gives?

Here's an experiment you can run. If you open up a command prompt, open up the Windows menu by clicking on the "C:\" icon in the upper right-hand corner, and choose "Properties" from the menu, you'll see that the command prompt typically uses the Lucida Console font. Open up Character Map (Start >> Run >> charmap.exe) and choose Lucida Console from the Font dropdown. Scroll down to the Greek letters and double-click on one of them (say, Theta: Θ) so it shows up in the "Characters to copy" box. Then scroll down to the Cyrillic letters and double-click on one of them (Zhe: Ж). Click the "Copy" button to copy them into the buffer. Now go back to the command prompt window, open the Windows menu again, and choose "Edit >> Paste". Notice what happens:

Screenshot - Theta question mark.png

Notice how the theta is pasted in just fine, but the zhe ends up being pasted as a question mark? That's because the MS-DOS console—and all of its descendant Windows consoles—can only display 250 characters. They were originally ASCII characters, 255 characters including 250 visible ones, plus six non-printing ones: beep, tab, null, backspace, line feed, and carriage return. Now they're represented by Unicode code page 437. If you're one of our Head First C# readers, then you learned chapter 9 about how Unicode works in chapter 9. But if not, don't worry—all you need to know is that you can convert a byte value (from 0 to 255) to its associated MS-DOS ASCII character:

// Convert an array of bytes to an array of MS-DOS characters
char[] charArray = Encoding.GetEncoding(437).GetChars(byteArray);

Here's a program that prints out an MS-DOS ASCII table, which is something that used to be printed on the inside cover of most programming books back in the '80s and early '90s. Luckily, you can just copy and paste characters right out of the command window. (Try turning on QuickEdit mode in the command window's Properties menu—that lets you select and copy by clicking and dragging, and you can paste by right-clicking.)

CodePage437.cs

using System;
using System.Text;

class CodePage437
{
     public static void Main(string[] args)
     {
          // Set the window size and title
          Console.Title = "Code Page 437: MS-DOS ASCII Characters";
      
          for (byte b = 0; b < byte.MaxValue; b++)
          {
               char c = Encoding.GetEncoding(437).GetChars(new byte[] {b})[0];
               switch (b)
               {
                    case 8: // Backspace
                    case 9: // Tab
                    case 10: // Line feed
                    case 13: // Carriage return
                        c = '.';
                        break;
                }
       
               Console.Write("{0:000} {1}   ", b, c);
       
               // 7 is a beep -- Console.Beep() also works
               if (b == 7) Console.Write(" ");
       
               if ((b + 1) % 8 == 0)
                   Console.WriteLine();
           }
          Console.WriteLine();
      }
}

You can see the screenshot in the next section. Pay special attention to characters 176, 177, and 178. They're especially useful for drawing text-mode graphics, especially when you combine them with different background and foreground colors. You can achieve some really intricate dithering effects. You can see an example of dithering using these characters if you look closely at the screenshot on the TheDraw Wiki page The half-block characters (219 through 223) are also useful, especially when you combine foreground and background colors. They make it possible to do intricate shadowing, piping, and checkerboard patterns.

There's one other thing you need to know about these characters. If you read chapter 9 of Head First C#, then you know that you can paste Unicode characters into Visual Studio and it knows how to handle them—it even knows that Hebrew or Arabic characters work right to left. If you paste line characters (179 through 218) into Visual Studio, they look correct. But if you paste them into Notepad, something really neat happens: Notepad knows how to convert the line characters to +, - and |. Be careful with this if you're pasting the WordFinder code at the bottom of this post into Notepad. It will still look okay, but you won't get the line characters.

You'll be amazed at how intricate text-mode graphics can get. When I was doing text-mode animation, I used to get a lot of use out of the shaded block characters which, when combined with colors, let me do some pretty intricate dithering. Do yourself a favor—have a look at some screenshots from The Amazing Adventures of ANSI Dude. You can download the game here, and it runs fine in DOSBox.

Scrolling the Window Contents

It's a little prettier if you resize the window so that it's just slightly larger than the contents. You can do that by calling SetWindowSize() and setting the BufferWidth and BufferHeight properties. Add this code to the top of the Main() method in CodePage437.cs:

        // Get rid of the scroll bars by making the buffer the same size as the window
        Console.Clear();
        Console.SetWindowSize(65, 33);
        Console.BufferWidth = 65;
        Console.BufferHeight = 33;

Here's what your program should look like when it runs. Notice how it says Press any key to continue . . . at the bottom? That's because I ran it from inside Visual Studio 2010. Normally, when you run a console application from inside Visual Studio, it disappears when it's done. But if you run it outside the debugger by pressing Ctrl-F5, it displays that prompt. But it also disables the debugger!

Screenshot - Code page 437.png

You'll need to be a little careful with those buffer properties, because if you resize the window so it's larger than the buffer, your code will throw an exception. The reason is because the window always needs to be smaller than the buffer—that way it can scroll around. Plenty of text-mode games scroll around a maze or a battlefield. You can use the WindowLeft and WindowTop properties to do it. Add this code to the bottom of CodePage437.cs to see how scrolling works:

        // Make the window much smaller than the buffer and scroll around
        Console.SetWindowSize(20, 5);
        for (int left = 5; left < 25; left++)
        {
             Console.WindowLeft = left;
             System.Threading.Thread.Sleep(100);
         }
        for (int top = 5; top < 20; top++)
        {
             Console.WindowTop = top;
             System.Threading.Thread.Sleep(100);
         }
        Console.SetWindowSize(65, 33);

(One side note about these characters. It's possible to change the output encoding using the Console.OutputEncoding property: Console.OutputEncoding = new System.Text.UTF8Encoding();. But an old-school text-mode game will stick to the original MS-DOS ASCII characters.)

The Main Loop

There's one more thing you need to think about when you're building a text-mode game that runs in a console window: the main loop. Games typically have a main loop that keeps running until the game is over. Here's a useful pattern that you can use:

  • Use the Console.CursorVisible to make your cursor invisible—that way, you avoid annoying flickering
  • The Console.CancelKeyPress event is raised whenever the user hits ^C—unless you set the Console.TreatControlCAInput property to true
  • See the comments in the code below for a little more information on how to handle the CancelKeyPress event
  • Call System.Threading.Thread.Sleep() in the main loop to add a small delay, and also to make your game CPU cycle friendly
  • The Console.KeyAvailable property is true if there's a key waiting to be processed. If there is, you can read it using the Console.ReadKey() method.
  • Look in the code below for the comments that show you where to initialize the game and where to handle user input and update the screen
/// <summary>
/// Set this static field to true to quit the game
/// </summary>
static bool quit = false;

/// <summary>
/// The entry point sets up the screen, initializes the game, and kicks off the main loop
/// </summary>
static void Main(string[] args)
{
     // Make sure the game quits if the user hits ^C
     // Set Console.TreatControlCAsInput to true if you want to use ^C as a valid input value
     Console.CancelKeyPress += new ConsoleCancelEventHandler(Console_CancelKeyPress);
 
     Console.CursorVisible = false;
             
     /*** Initialize the game here! ***/
 
     MainLoop();
}

/// <summary>
/// Event handler for ^C key press
/// </summary>
static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
     // Unfortunately, due to a bug in .NET Framework v4.0.30319 you can't debug this 
     // because Visual Studio 2010 gives a "No Source Available" error. 
     // http://connect.microsoft.com/VisualStudio/feedback/details/524889/debugging-c-console-application-that-handles-console-cancelkeypress-is-broken-in-net-4-0
     Console.WriteLine("{0} hit, quitting...", e.SpecialKey);
     quit = true;
     e.Cancel = true; // Set this to true to keep the process from quitting immediately
}

/// <summary>
/// The main gameloop
/// </summary>
static void MainLoop()
{
     int elapsedMilliseconds = 0;
     int totalMilliseconds = TIME_LIMIT_SECONDS * 1000;
     const int INTERVAL = 100;
 
     while (elapsedMilliseconds < totalMilliseconds && !quit)
     {
          // Sleep for a short period
          Thread.Sleep(INTERVAL);
          elapsedMilliseconds += INTERVAL;
  
          /*** Update the screen and handle input here! ***/
      }
 
     Console.WriteLine("Game over!");
}

Putting it all together: Building the WordFinder game

Screenshot - Word finder.png

Now you've got all the pieces you need to build a great text-mode game! That's everything I needed to create my own game, WordFinder. It's a "find the word" puzzle, where the player has 60 seconds to find all of the words in a grid of random letters.

You can load all of these games into a Visual Studio 2010 project called WordFinder. If you do, you'll want to add a text file called words.txt to the project. Make sure to set its Copy to Output Directory property to Copy Always.

But you can also compile it from the command line, just like the programs above. Here's how:

  1. Save each of the following four files into a folder.
  2. Use Notepad to create a text file in the same folder. Save it as words.txt, and fill it with all the valid words for the game (I used this list).
  3. Compile your program like this:
    %SYSTEMROOT%\Microsoft.NET\Framework\v3.5\csc.exe /out:WordFinder.exe Program.cs Game.cs Puzzle.cs WordChecker.cs
  4. Make sure the words.txt fie is in the same folder as WordFinder.exe when you run it. (Yes, there are more elegant solutions to this—like using resource files—but I wanted my game to be quick and dirty!)

One note—if you're using Notepad to save the four files, you'll get a warning about Unicode characters that will be lost. That's because the Game.cs code contains line drawing characters that I pasted in. You have two choices. You can save it using Notepad using its default settings, which will automatically convert the characters to +'s, -'s, and |'s. But if you want to save the Unicode characters properly, make sure you choose "Unicode" from the Encoding dropdown in the "Save As..." window in Notepad.

Between everything I wrote above and the comments in the code, you should be able to figure out how this game works. Good luck writing your own text-mode games! If you come up with something cool, definitely let me know about it.

Program.cs

using System;
using System.IO;
using System.Threading;

namespace WordFinder
{
     /// <summary>
     /// The Program class contains the main loop, ending the game, running the timer,
     /// and handling it when the user presses ^C to quit. All of the gameplay and drawing
     /// is handled in the Game class.
     /// </summary>
     class Program
     {
          /// <summary>
          /// Length of the puzzle in letters
          /// </summary>
          const int PUZZLE_LENGTH = 49;
  
          /// <summary>
          /// Every nth letter must be a vowel
          /// </summary>
          const int VOWEL_EVERY = 5;
  
          /// <summary>
          /// The time limit for the puzzle
          /// </summary>
          const int TIME_LIMIT_SECONDS = 60;
  
          /// <summary>
          /// The word list -- I downloaded it from http://unix-tree.huihoo.org/V7/usr/dict/words.html 
          /// and pasted it into words.txt, making sure to set the Copy to Output Directory property
          /// to "Copy Always" (so it ends up in the same foler as the executable). We could also use
          /// a resource, but this makes it easy to expand the game to use any word list.
          /// </summary>
          static string[] words = File.ReadAllLines("words.txt");
  
          /// <summary>
          /// Game object to track the gameplay
          /// </summary>
          static Game game;
  
          /// <summary>
          /// Set this static field to true to quit the game
          /// </summary>
          static bool quit = false;
  
          /// <summary>
          /// The player's current input
          /// </summary>
          static string word = String.Empty;
  
          /// <summary>
          /// The entry point sets up the screen, initializes the game, and kicks off the main loop
          /// </summary>
          static void Main(string[] args)
          {
               // Make sure the game quits if the user hits ^C
               // Set Console.TreatControlCAsInput to true if you want to use ^C as a valid input value
               Console.CancelKeyPress += new ConsoleCancelEventHandler(Console_CancelKeyPress);
   
               Console.CursorVisible = false;
               
               game = new Game(PUZZLE_LENGTH, VOWEL_EVERY, words);
               game.DrawInititalScreen();
               MainLoop();
           }
  
          /// <summary>
          /// Event handler for ^C key press
          /// </summary>
          static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
          {
               // Unfortunately, due to a bug in .NET Framework v4.0.30319 you can't debug this 
               // because Visual Studio 2010 gives a "No Source Available" error. 
               // http://connect.microsoft.com/VisualStudio/feedback/details/524889/debugging-c-console-application-that-handles-console-cancelkeypress-is-broken-in-net-4-0
               Console.SetCursorPosition(0, 19);
               Console.WriteLine("{0} hit, quitting...", e.SpecialKey);
               quit = true;
               e.Cancel = true; // Set this to true to keep the process from quitting immediately
           }
  
          /// <summary>
          /// The main gameloop
          /// </summary>
          static void MainLoop()
          {
               int elapsedMilliseconds = 0;
               int totalMilliseconds = TIME_LIMIT_SECONDS * 1000;
               const int INTERVAL = 100;
   
               while (elapsedMilliseconds < totalMilliseconds && !quit)
               {
                    // Sleep for a short period
                    Thread.Sleep(INTERVAL);
                    elapsedMilliseconds += INTERVAL;
    
                    HandleInput();
    
                    PrintRemainingTime(elapsedMilliseconds, totalMilliseconds);
                }
   
               Console.SetCursorPosition(0, 20);
               Console.WriteLine(Environment.NewLine + Environment.NewLine
                   + "Game over! You found {0} words.", game.NumberFound);
           }
  
          /// <summary>
          /// Write the remaining time at the top right corner of the screen
          /// </summary>
          /// <param name="elapsedMilliseconds">Time elapsed since the start of the game</param>
          /// <param name="totalMilliseconds">Total milliseconds allowed for the game</param>
          private static void PrintRemainingTime(int elapsedMilliseconds, int totalMilliseconds)
          {
               int milliSecondsLeft = totalMilliseconds - elapsedMilliseconds;
               double secondsLeft = (double)milliSecondsLeft / 1000;
               string timeString = String.Format("{0:00.0} seconds left", secondsLeft);
   
               // Save the current cursor position
               int left = Console.CursorLeft;
               int top = Console.CursorTop;
   
               // Draw the time in the upper right-hand corner
               Console.SetCursorPosition(Console.WindowWidth - timeString.Length, 0);
               Console.ForegroundColor = ConsoleColor.Magenta;
               Console.Write(timeString);
   
               // Restore the console text color and put the cursor back where we found it
               Console.ResetColor();
               Console.SetCursorPosition(left, top);
           }
  
          /// <summary>
          /// Handle any waiting user keystrokes 
          /// </summary>
          static void HandleInput()
          {
               Thread.Sleep(50);
               if (Console.KeyAvailable)
               {
                    ConsoleKeyInfo keyInfo = Console.ReadKey(true);
                    if (keyInfo.Key == ConsoleKey.Backspace)
                    {
                         if (word.Length > 0)
                             word = word.Substring(0, word.Length - 1);
                     }
                    else if (keyInfo.Key == ConsoleKey.Escape)
                    {
                         word = String.Empty;
                     }
                    else
                    {
                         string key = keyInfo.KeyChar.ToString().ToUpper();
                         if (game.IsValidLetter(key))
                         {
                              word = word + key;
                          }
                     }
                    game.CurrentInput = word;
                    game.ProcessInput();
                    game.UpdateScreen();
                }
           }
      }
}

Game.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace WordFinder
{
     /// <summary>
     /// The Game class keeps track of the state of the game and draws the screen
     /// updates. It uses an instance of the Puzzle class to keep track of the letters on
     /// the puzzle grid, and an instance of WordChecker to keep track of the valid
     /// words and check the player's answers.
     /// </summary>
     class Game
     {
          /// <summary>
          /// The WordChecker object to check the words that were found
          /// </summary>
          private WordChecker wordChecker;
  
          /// <summary>
          /// Get the number of words that were found
          /// </summary>
          public int NumberFound
          {
               // The WordChecker object keeps track of this
               get { return wordChecker.NumberFound; }
           }
  
          /// <summary>
          /// The Puzzle object keeps track of the random letters and checks words
          /// </summary>
          private Puzzle puzzle;
  
          /// <summary>
          /// The player's current input
          /// </summary>
          public string CurrentInput { private get; set; }
  
          /// <summary>
          /// Game constructor
          /// </summary>
          /// <param name="puzzleLength">The number of letters in the puzzle</param>
          /// <param name="vowelEvery">Add a vowel every Nth letter</param>
          /// <param name="validWords">The sequence of valid words</param>
          public Game(int puzzleLength, int vowelEvery, IEnumerable<string> validWords)
          {
               this.wordChecker = new WordChecker(validWords);
               this.puzzle = new Puzzle(puzzleLength, vowelEvery);
               CurrentInput = String.Empty;
           }
  
          /// <summary>
          /// Draw the screen to the console when the program starts
          /// </summary>
          public void DrawInititalScreen()
          {
               Console.Clear();
               Console.Title = "Word finder";
               puzzle.Draw(25, 3);
               Console.SetCursorPosition(7, 11);
               Console.Write("┌───────────────────────────────────────────────────────╖");
               Console.SetCursorPosition(7, 12);
               Console.Write("");
               Console.SetCursorPosition(63, 12);
               Console.Write("");
               Console.SetCursorPosition(7, 13);
               Console.Write("╘═══════════════════════════════════════════════════════╝");
               UpdateScreen();
           }
  
          /// <summary>
          /// Update the screen when it's refreshed
          /// </summary>
          public void UpdateScreen()
          {
               // Use String.PadRight() to make sure the yellow entry box remains a constant
               // size, no matter how long the word is or the word number
               Console.SetCursorPosition(8, 12);
               Console.ForegroundColor = ConsoleColor.DarkGray;
               Console.BackgroundColor = ConsoleColor.Yellow;
               string message = String.Format("Enter word #{0}: {1}", 
                       wordChecker.NumberFound, CurrentInput);
               Console.Write(message.PadRight(54));
               Console.ResetColor();
   
               Console.SetCursorPosition(0, 17);
               Console.Write("Found words: ");
               foreach (string word in wordChecker.FoundWords)
                   Console.Write("{0} ", word);
   
               Console.SetCursorPosition(7, 14);
               Console.Write("Type in any words you find, press <ESC> to clear the line");
           }
  
          /// <summary>
          /// Process input any time the player enters a new letter
          /// </summary>
          public void ProcessInput()
          {
               wordChecker.CheckAnswer(CurrentInput, puzzle);
           }
  
          /// <summary>
          /// Return true if a key press is a valid letter
          /// </summary>
          /// <param name="key">Key that was pressed</param>
          /// <returns>True only if the key is a valid consonant or vowel in the puzzle</returns>
          public bool IsValidLetter(string key)
          {
               if (key.Length == 1)
               {
                    char c = key.ToCharArray()[0];
                    return Puzzle.Consonants.Contains(c) || Puzzle.Vowels.Contains(c);
                }
               return false;
           }
  
      }
}

Puzzle.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace WordFinder
{
     /// <summary>
     /// The Puzzle class keeps track of the puzzle grid. The Game uses it to draw
     /// the initial grid to the screen, and the WordFinder class uses it to check if the
     /// player's input contains only letters from the grid.
     /// </summary>
     class Puzzle
     {
          /// <summary>
          /// Randomizer
          /// </summary>
          private Random random = new Random();
  
          /// <summary>
          /// Consonants (including Y)
          /// </summary>
          public static readonly char[] Consonants = { 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 
               'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' };
  
          /// <summary>
          /// Vowels (including Y)
          /// </summary>
          public static readonly char[] Vowels = { 'A', 'E', 'I', 'O', 'U', 'Y' };
  
          /// <summary>
          /// Backing field for Letters property
          /// </summary>
          char[] letters;
  
          /// <summary>
          /// Get the letters in the puzzle
          /// </summary>
          public IEnumerable<char> Letters
          {
               get { return letters; }
           }
  
          /// <summary>
          /// The number of letters in the puzzle
          /// </summary>
          private int puzzleLength;
  
          /// <summary>
          /// Puzzle Constructor
          /// </summary>
          /// <param name="puzzleLength">The number of letters in the puzzle</param>
          /// <param name="vowelEvery">Every nth letter is a vowel</param>
          public Puzzle(int puzzleLength, int vowelEvery)
          {
               this.puzzleLength = puzzleLength;
   
               letters = new char[puzzleLength];
   
               for (int i = 0; i < puzzleLength; i++)
               {
                    if (i % vowelEvery == 0)
                        letters[i] = Vowels[random.Next(Vowels.Length)];
                    else
                        letters[i] = Consonants[random.Next(Consonants.Length)];
                }
           }
  
          /// <summary>
          /// Draw the puzzle at a specific point on the screen
          /// </summary>
          /// <param name="left">The column position of the cursor</param>
          /// <param name="top">The row position of the cursor</param>
          public void Draw(int left, int top)
          {
               int oldTop = Console.CursorTop;
               int oldLeft = Console.CursorLeft;
   
               Console.BackgroundColor = ConsoleColor.Gray;
   
               // Create the random puzzle using random letters and print them
               for (int i = 0; i < puzzleLength; i++)
               {
                    // Use cursor movement to draw the rows of the square puzzle grid
                    if (i % Math.Floor(Math.Sqrt(puzzleLength)) == 0)
                    {
                         Console.CursorTop = top++;
                         Console.CursorLeft = left;
                     }
    
                    if (Vowels.Contains(letters[i]))
                        Console.ForegroundColor = ConsoleColor.DarkRed;
                    else
                        Console.ForegroundColor = ConsoleColor.DarkBlue;
    
                    Console.Write(" {0} ", letters[i]);
                }
   
               Console.ResetColor();
   
               Console.CursorTop = oldTop;
               Console.CursorLeft = oldLeft;
           }
      }
}

WordChecker.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace WordFinder
{
     /// <summary>
     /// The WordChecker class keeps track of the list of valid words and checks 
     /// to see if a given word is valid and only made up of letters from the grid.
     /// </summary>
     class WordChecker
     {
          /// <summary>
          /// The valid words
          /// </summary>
          private List<string> words = new List<string>();
  
          /// <summary>
          /// The found words
          /// </summary>
          private List<string> foundWords = new List<string>();
  
          /// <summary>
          /// Return the number of words that were found
          /// </summary>
          public int NumberFound
          {
               get { return foundWords.Count; }
           }
  
          /// <summary>
          /// Get the set of words that were found
          /// </summary>
          public IEnumerable<string> FoundWords {
               get
               {
                    List<string> value = new List<string>();
                    foreach (string word in foundWords)
                    {
                         value.Add(word.ToUpper());
                     }
                    return value;
                }
           }
  
          /// <summary>
          /// WordChecker Constructor
          /// </summary>
          /// <param name="validWords">The set of valid words</param>
          public WordChecker(IEnumerable<string> validWords)
          {
               // Make each word uppercase and add it to the word list
               foreach(string word in validWords)
                   this.words.Add(word.ToUpper());
           }
  
          /// <summary>
          /// Check if a player's word is a valid word that's contained in the puzzle
          /// </summary>
          /// <param name="word">Word to check</param>
          /// <param name="puzzle">Reference to the Puzzle object</param>
          public void CheckAnswer(string word, Puzzle puzzle)
          {
               // Make sure the word is a non-empty, valid word that's at least 4 characters long
               if (String.IsNullOrEmpty(word) || foundWords.Contains(word) || word.Length < 4)
                   return;
   
               // Make sure the word is upper case -- and the upperCaseWord string will be destroyed
               // so we need to make a copy. We'll remove each puzzle letter from the word. If any
               // letters are left over, the word is not in the puzzle.
               string upperCaseWord = word.ToUpper();
               if (words.Contains(upperCaseWord))
               {
                    // Make sure it's made up entirely of letters in the puzzle
                    foreach (char letter in puzzle.Letters)
                    {
                         // Remove each puzzle letter from the word
                         if (upperCaseWord.Contains(letter))
                         {
                              // If the word starts with the letter, Substring(0, index - 1) will throw an exception
                              if (upperCaseWord.StartsWith(letter.ToString()))
                                  upperCaseWord = upperCaseWord.Substring(1);
                              else
                              {
                                   int index = upperCaseWord.IndexOf(letter);
                                   upperCaseWord = upperCaseWord.Substring(0, index - 1) + upperCaseWord.Substring(index + 1);
                               }
                          }
                     }
                }
   
               // If removing all the puzzle letters from upperCaseWord left us with an empty string,
               // we found a word. Beep and add it to the found words list.
               if (String.IsNullOrEmpty(upperCaseWord))
               {
                    Console.Beep();
                    foundWords.Add(word);
                }
           }
      }
}

Good luck building your own text-mode games! If you come up with something cool, definitely share!

Andrew Stellman is the author of Head First C# and other books from O'Reilly. You can read more from Andrew at Building Better Software.


You might also be interested in:

6 Comments

Very nice! I am currently writing a book about C# programming in game examples and my second chapter describes how to do workfinder very similar (not so complex, thouhg) to you.
I thought how unique idea I got :)))

Just curious if you have any solution for needing to update individual characters on the screen. You can obviously do it using System.Console, but it is really slow. In other words, how would you do the following type of thing:

for(int x = 0; x < width; x++)
for(int y = 0; y < height; y++)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = buffer[x, y].color;
Console.Write(buffer[x, y].character);
}

I would really love to find SOMETHING like this explaining how to do this on a Windows form to develop a Mud client. I can't find anything after Googling for days and really would like to build my own mud client as a project.

I really enjoyed reading your article and also following the links to the cool text based games you mentioned. You helped me find out how to use special characters on a 'snake' game I've written to give it much nicer graphics so thanks for that.

Neat and clean article...awesome read

Concerning the slow displaying when writing every single character, i found out that if you save the rows that you update into strings and then write them at the correct posotion, displaying is much faster.

News Topics

Recommended for You

Got a Question?