Press "Enter" to skip to content

Lesson 23.1 – Creating a console front-end for the game

Lesson Objectives

At the end of this lesson, you will know…

  • How to create a console (text) application, using the existing Engine project
  • How to do simple parsing of user input

 

In this lesson, we will create a new front-end for the SuperAdventure program. This one will be a “console application” – a program that only uses text in the user interface.

This will let us play SuperAdventure the same way as the classic adventure game, “Hunt the Wumpus“.

The great thing about creating this second interface is that we can re-use a lot of the code we already have, especially since most of the game logic is in the Engine project (and not the UI code).

 

Step 1: Add the console project

Open the SuperAdventure solution in Visual Studio. In the Solution Explorer, right-click on the SuperAdventure solution, and select “Add” -> “New Project…”. Choose “Console Application”, in the Visual C# section, and name it “SuperAdventureConsole”

This will create the new UI project.

Right-click on the SuperAdventureConsole project, and select “Set as Startup Project”.

After you do this, when you run your solution, it will run the console UI. If you want to run the Windows Form UI again, right-click on the SuperAdventure project, and select “Set as Startup Project”.

If you don’t want to switch the startup project for the solution, you could also right-click on the project you want to run (SuperAdventure or SuperAdventureConsole), and select “Debug” -> “Start new instance”. That will run the project you clicked on.

Right-click on “References”, in the SuperAdventureConsole project, and add the Engine project. That will let us use the game objects in this version of the game.

 

Step 2: Edit Program.cs, in the SuperAdventureConsole project.

This class has a static function named “Main”. This is the function, and class, that is executed when you run the console program.

We are going to put an “infinite loop” in this function.

Normally, an infinite loop is a bad thing in a program. It means your program is stuck doing something, and it will never end. However, we are going to add a way for the player to exit the loop – by typing the command “Exit”. If the user types anything else, the program will continue running, and the player can continue playing.

Inside our loop, we will wait for the user to type something and press the Enter key. When they press Enter, we will try to determine what the game should do, based on the user’s input.

 

Step 3: Paste the code below into Program.cs.

 

using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Engine;
namespace SuperAdventureConsole
{
    public class Program
    {
        private const string PLAYER_DATA_FILE_NAME = "PlayerData.xml";
        private static Player _player;
        private static void Main(string[] args)
        {
            // Load the player
            LoadGameData();
            Console.WriteLine("Type 'Help' to see a list of commands");
            Console.WriteLine("");
            DisplayCurrentLocation();
            // Connect player events to functions that will display in the UI
            _player.PropertyChanged += Player_OnPropertyChanged;
            _player.OnMessage += Player_OnMessage;
            // Infinite loop, until the user types "exit"
            while(true)
            {
                // Display a prompt, so the user knows to type something
                Console.Write(">");
                // Wait for the user to type something, and press the <Enter> key
                string userInput = Console.ReadLine();
                // If they typed a blank line, loop back and wait for input again
                if(string.IsNullOrWhiteSpace(userInput))
                {
                    continue;
                }
                // Convert to lower-case, to make comparisons easier
                string cleanedInput = userInput.ToLower();
                // Save the current game data, and break out of the "while(true)" loop
                if(cleanedInput == "exit")
                {
                    SaveGameData();
                    break;
                }
                // If the user typed something, try to determine what to do
                ParseInput(cleanedInput);
            }
        }
        private static void Player_OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if(e.PropertyName == "CurrentLocation")
            {
                DisplayCurrentLocation();
                if(_player.CurrentLocation.VendorWorkingHere != null)
                {
                    Console.WriteLine("You see a vendor here: {0}", _player.CurrentLocation.VendorWorkingHere.Name);
                }
            }
        }
        private static void Player_OnMessage(object sender, MessageEventArgs e)
        {
            Console.WriteLine(e.Message);
            if(e.AddExtraNewLine)
            {
                Console.WriteLine("");
            }
        }
        private static void ParseInput(string input)
        {
            if(input.Contains("help") || input == "?")
            {
                Console.WriteLine("Available commands");
                Console.WriteLine("====================================");
                Console.WriteLine("Stats - Display player information");
                Console.WriteLine("Look - Get the description of your location");
                Console.WriteLine("Inventory - Display your inventory");
                Console.WriteLine("Quests - Display your quests");
                Console.WriteLine("Attack - Fight the monster");
                Console.WriteLine("Equip <weapon name> - Set your current weapon");
                Console.WriteLine("Drink <potion name> - Drink a potion");
                Console.WriteLine("Trade - display your inventory and vendor's inventory");
                Console.WriteLine("Buy <item name> - Buy an item from a vendor");
                Console.WriteLine("Sell <item name> - Sell an item to a vendor");
                Console.WriteLine("North - Move North");
                Console.WriteLine("South - Move South");
                Console.WriteLine("East - Move East");
                Console.WriteLine("West - Move West");
                Console.WriteLine("Exit - Save the game and exit");
            }
            else if(input == "stats")
            {
                Console.WriteLine("Current hit points: {0}", _player.CurrentHitPoints);
                Console.WriteLine("Maximum hit points: {0}", _player.MaximumHitPoints);
                Console.WriteLine("Experience Points: {0}", _player.ExperiencePoints);
                Console.WriteLine("Level: {0}", _player.Level);
                Console.WriteLine("Gold: {0}", _player.Gold);
            }
            else if(input == "look")
            {
                DisplayCurrentLocation();
            }
            else if(input.Contains("north"))
            {
                if(_player.CurrentLocation.LocationToNorth == null)
                {
                    Console.WriteLine("You cannot move North");
                }
                else
                {
                    _player.MoveNorth();
                }
            }
            else if(input.Contains("east"))
            {
                if(_player.CurrentLocation.LocationToEast == null)
                {
                    Console.WriteLine("You cannot move East");
                }
                else
                {
                    _player.MoveEast();
                }
            }
            else if(input.Contains("south"))
            {
                if(_player.CurrentLocation.LocationToSouth == null)
                {
                    Console.WriteLine("You cannot move South");
                }
                else
                {
                    _player.MoveSouth();
                }
            }
            else if(input.Contains("west"))
            {
                if(_player.CurrentLocation.LocationToWest == null)
                {
                    Console.WriteLine("You cannot move West");
                }
                else
                {
                    _player.MoveWest();
                }
            }
            else if(input == "inventory")
            {
                foreach(InventoryItem inventoryItem in _player.Inventory)
                {
                    Console.WriteLine("{0}: {1}", inventoryItem.Description, inventoryItem.Quantity);
                }
            }
            else if(input == "quests")
            {
                if(_player.Quests.Count == 0)
                {
                    Console.WriteLine("You do not have any quests");
                }
                else
                {
                    foreach(PlayerQuest playerQuest in _player.Quests)
                    {
                        Console.WriteLine("{0}: {1}", playerQuest.Name,
                            playerQuest.IsCompleted ? "Completed" : "Incomplete");
                    }
                }
            }
            else if(input.Contains("attack"))
            {
                if(_player.CurrentLocation.MonsterLivingHere == null)
                {
                    Console.WriteLine("There is nothing here to attack");
                }
                else
                {
                    if(_player.CurrentWeapon == null)
                    {
                        // Select the first weapon in the player's inventory 
                        // (or 'null', if they do not have any weapons)
                        _player.CurrentWeapon = _player.Weapons.FirstOrDefault();
                    }
                    if(_player.CurrentWeapon == null)
                    {
                        Console.WriteLine("You do not have any weapons");
                    }
                    else
                    {
                        _player.UseWeapon(_player.CurrentWeapon);
                    }
                }
            }
            else if(input.StartsWith("equip "))
            {
                string inputWeaponName = input.Substring(6).Trim();
                if(string.IsNullOrEmpty(inputWeaponName))
                {
                    Console.WriteLine("You must enter the name of the weapon to equip");
                }
                else
                {
                    Weapon weaponToEquip =
                        _player.Weapons.SingleOrDefault(
                            x => x.Name.ToLower() == inputWeaponName || x.NamePlural.ToLower() == inputWeaponName);
                    if(weaponToEquip == null)
                    {
                        Console.WriteLine("You do not have the weapon: {0}", inputWeaponName);
                    }
                    else
                    {
                        _player.CurrentWeapon = weaponToEquip;
                        Console.WriteLine("You equip your {0}", _player.CurrentWeapon.Name);
                    }
                }
            }
            else if(input.StartsWith("drink "))
            {
                string inputPotionName = input.Substring(6).Trim();
                if(string.IsNullOrEmpty(inputPotionName))
                {
                    Console.WriteLine("You must enter the name of the potion to drink");
                }
                else
                {
                    HealingPotion potionToDrink =
                        _player.Potions.SingleOrDefault(
                            x => x.Name.ToLower() == inputPotionName || x.NamePlural.ToLower() == inputPotionName);
                    if(potionToDrink == null)
                    {
                        Console.WriteLine("You do not have the potion: {0}", inputPotionName);
                    }
                    else
                    {
                        _player.UsePotion(potionToDrink);
                    }
                }
            }
            else if(input == "trade")
            {
                if(_player.CurrentLocation.VendorWorkingHere == null)
                {
                    Console.WriteLine("There is no vendor here");
                }
                else
                {
                    Console.WriteLine("PLAYER INVENTORY");
                    Console.WriteLine("================");
                    if(_player.Inventory.Count(x => x.Price != World.UNSELLABLE_ITEM_PRICE) == 0)
                    {
                        Console.WriteLine("You do not have any inventory");
                    }
                    else
                    {
                        foreach(
                            InventoryItem inventoryItem in _player.Inventory.Where(x => x.Price != World.UNSELLABLE_ITEM_PRICE))
                        {
                            Console.WriteLine("{0} {1} Price: {2}", inventoryItem.Quantity, inventoryItem.Description,
                                inventoryItem.Price);
                        }
                    }
                    Console.WriteLine("");
                    Console.WriteLine("VENDOR INVENTORY");
                    Console.WriteLine("================");
                    if(_player.CurrentLocation.VendorWorkingHere.Inventory.Count == 0)
                    {
                        Console.WriteLine("The vendor does not have any inventory");
                    }
                    else
                    {
                        foreach(InventoryItem inventoryItem in _player.CurrentLocation.VendorWorkingHere.Inventory)
                        {
                            Console.WriteLine("{0} {1} Price: {2}", inventoryItem.Quantity, inventoryItem.Description,
                                inventoryItem.Price);
                        }
                    }
                }
            }
            else if(input.StartsWith("buy "))
            {
                if(_player.CurrentLocation.VendorWorkingHere == null)
                {
                    Console.WriteLine("There is no vendor at this location");
                }
                else
                {
                    string itemName = input.Substring(4).Trim();
                    if(string.IsNullOrEmpty(itemName))
                    {
                        Console.WriteLine("You must enter the name of the item to buy");
                    }
                    else
                    {
                        // Get the InventoryItem from the trader's inventory
                        InventoryItem itemToBuy =
                            _player.CurrentLocation.VendorWorkingHere.Inventory.SingleOrDefault(
                                x => x.Details.Name.ToLower() == itemName);
                        // Check if the vendor has the item
                        if(itemToBuy == null)
                        {
                            Console.WriteLine("The vendor does not have any {0}", itemName);
                        }
                        else
                        {
                            // Check if the player has enough gold to buy the item
                            if(_player.Gold < itemToBuy.Price)
                            {
                                Console.WriteLine("You do not have enough gold to buy a {0}", itemToBuy.Description);
                            }
                            else
                            {
                                // Success! Buy the item
                                _player.AddItemToInventory(itemToBuy.Details);
                                _player.Gold -= itemToBuy.Price;
                                Console.WriteLine("You bought one {0} for {1} gold", itemToBuy.Details.Name, itemToBuy.Price);
                            }
                        }
                    }
                }
            }
            else if(input.StartsWith("sell "))
            {
                if(_player.CurrentLocation.VendorWorkingHere == null)
                {
                    Console.WriteLine("There is no vendor at this location");
                }
                else
                {
                    string itemName = input.Substring(5).Trim();
                    if(string.IsNullOrEmpty(itemName))
                    {
                        Console.WriteLine("You must enter the name of the item to sell");
                    }
                    else
                    {
                        // Get the InventoryItem from the player's inventory
                        InventoryItem itemToSell =
                            _player.Inventory.SingleOrDefault(x => x.Details.Name.ToLower() == itemName &&
                                                                   x.Quantity > 0 &&
                                                                   x.Price != World.UNSELLABLE_ITEM_PRICE);
                        // Check if the player has the item entered
                        if(itemToSell == null)
                        {
                            Console.WriteLine("The player cannot sell any {0}", itemName);
                        }
                        else
                        {
                            // Sell the item
                            _player.RemoveItemFromInventory(itemToSell.Details);
                            _player.Gold += itemToSell.Price;
                            Console.WriteLine("You receive {0} gold for your {1}", itemToSell.Price, itemToSell.Details.Name);
                        }
                    }
                }
            }
            else
            {
                Console.WriteLine("I do not understand");
                Console.WriteLine("Type 'Help' to see a list of available commands");
            }
            // Write a blank line, to keep the UI a little cleaner
            Console.WriteLine("");
        }
        private static void DisplayCurrentLocation()
        {
            Console.WriteLine("You are at: {0}", _player.CurrentLocation.Name);
            if(_player.CurrentLocation.Description != "")
            {
                Console.WriteLine(_player.CurrentLocation.Description);
            }
        }
        private static void LoadGameData()
        {
            _player = PlayerDataMapper.CreateFromDatabase();
            if(_player == null)
            {
                if(File.Exists(PLAYER_DATA_FILE_NAME))
                {
                    _player = Player.CreatePlayerFromXmlString(File.ReadAllText(PLAYER_DATA_FILE_NAME));
                }
                else
                {
                    _player = Player.CreateDefaultPlayer();
                }
            }
        }
        private static void SaveGameData()
        {
            File.WriteAllText(PLAYER_DATA_FILE_NAME, _player.ToXmlString());
            PlayerDataMapper.SaveToDatabase(_player);
        }
    }
}

 

This is everything we need to run SuperAdventure as a console application. You will probably notice that some of the functionality is from the Windows Form project.

 

Line 11-13: We set the variable for the saved game file, and the player. Because the Main function is a static function, we need to make the _player variable static. Also, the functions that Main calls will need to be static too. These are basically the same as lines 18-20, in SuperAdventure.cs, in the Windows Form project.

 

Line 18: We call the LoadGameData function. That function looks for a previously-saved game, or creates a new player. This code is the same as lines 26-38, in SuperAdventure.cs.

 

Lines 20-21: Console.WriteLine is a function you use to display text in the console UI. This displays the text you pass in the parameter, and does a line-feed (moves to the next line down). If you don’t want to move to the next line, you would use:

Console.Write("your text here");

 

Line 23: Calls a function that display’s the player’s current location. We put this in a function, so we can display the same text when the player moves to a new location.

 

Lines 25-27: We connect the events from the Player class to the functions that will handle them for the UI (similar to lines 96-97, of SuperAdventure.cs).

 

Line 30: This is our game loop. It will continuously run, waiting for the user’s input, until the user types “Exit”. A “while” loop evaluates the equation inside the parentheses, and runs until that equation is false.

However, we don’t have an equation in there. We only have “true”, which will always be true, which means the “while” will never end – until the player types the word “Exit”, and the code in the “if” on line 48 runs. The “break;” will exit the loop it is in, and finish running the rest of the code in the function. But, there is no code in this function, so the function will end – and the program will stop running.

 

Line 33: Display a “>”, as a prompt for the user to enter their command. Because we use “Console.Write()”, the cursor will stay on the same line as the prompt.

 

Line 36: Console.ReadLine() is the function to read the user’s input, after they press the Enter key. There is a Console.Read() that you could use to read every key pressed, one character at a time. But we are going to wait for the user to press Enter. Whatever the user types will be assigned to the userInput variable.

 

Lines 39-42: If the user only hit the Enter key, and didn’t type anything else, the userInput will be null. The “continue;” commands tells the program to go back up to the “while” line and keep running. It does not execute the lines after it (45-56).

 

Line 45: We convert the user’s command to all lower-case letters. So, if the user types “EXIT”, “Exit”, or “exit”, the value in cleanedInput will be “exit”. This will make it easier for us when we try to parse what the user typed, to determine the action to perform.

 

Lines 48-53: If the user typed “exit”, call the SaveGameData function and break out of the loop – ending the game.

 

Line 56: If we got here, the user typed a non-blank line, that was not the word “exit”. Now we will call the ParseInput() function, and try to do what the user wants. This will be a bug function, since we need to manage every action that we had buttons for in the Windows Form UI.

 

Lines 60-71: This is the function we call when the player’s PropertyChanged event is fired. It is similar to PlayerOnPropertyChanged, in SuperAdventure.cs. However, since we don’t have buttons to hide and show, or datagrids to update, it is much smaller.

The only thing we care about is if the player is at a new location. Then, we display the location’s information, and the vendor information (if there is a vendor at this location).

 

Lines 73-81: Display messages from the player’s events, like DisplayMessage() in SueprAdventure.cs.

 

Lines 83-392: This is where we look at the user’s input, and try to do what the action they want to perform. It’s a long series of “if”s and “else if”s. If we cannot determine what the user wants to do, we eventually reach the “else” at line 384 and give the user a message that we don’t understand their input, and they can type “Help” to see a list of the valid commands.

You should be familiar with much of the logic in this function, but I will explain the new things.

 

Line 85: The Contains() is a new function. You can use it to see if a string (“input”, on this line) contains the string used as the parameter (“help”, in this code).

We use this, because we want this “if” to run when the user types the word “help” anywhere in their input. So, if they type, “I need help”, or “HELP ME!!!!”, we will display the list of valid commands.

We also check if they typed “?”, a common way used in console app games to display the help information.

 

Line 107: If you want to create a string, with variable values inside it, you could do it with string concatenation – adding pieces of the string together, like this:

string myMessage = "Current Hit Points: " + _player.CurrentHitPoints.ToString();

However, Console.WriteLine() and string.Format() have a little bit cleaner way to do this. You have the string you want, with {0} in the location where you want to include a variable value. After the string, you have the variable you want to insert into the string. The function is smart enough to automatically do a .ToString(), if it is needed.

If you want to put several variables in your string, you only need to add more sets of curly braces, and more variables to your list. Just remember that the position is important. The first variable in your list will go where {0} is. The second will go where {1} is, and so on. So, you could have something like this:

string myMessage = string.Format("X={0}, Y={1}, Z={2}", x, y, z);

 

Lines 117-160: In the Windows Form version, we hide the movement buttons for directions where there is no location. We can’t do that in the console version, so we need to check if there is a valid location, before trying to move the player there. To be nice, we display a message if the location does not exist.

 

Lines 183-207: Similar to the movement validations, we need to check if there is a monster at the location, before we try to allow the player to “attack”. Because we don’t have a combobox, with the player’s weapons listed in it, we do a few checks, and try to pick a default weapon, when the player tries to attack the monster.

 

Lines 208-233: This is a more complex command. We are looking for the word “equip”, but we also need to know what weapon the player is trying to equip (set as their default weapon).

On line 208, we check if the player’s input had “equip ” – notice the space at the end. Then, we take the rest of the player’s input string, starting at the sixth position

NOTE: For functions like Substring, the first character of a string is at position 0, not position 1.

The Trim() function removes any leading or trailing spaces from a string.

So, if the user types “Equip rusty sword”, inputWeaponName will be “rusty sword”.

On line 218, we look through the player’s inventory and try to find an item with the same name as what the player is trying to equip. If it finds something, it will assign it to the “weaponToEquip” variable. If it does not find a matching weapon, that variable will be null (the default value).

If we don’t find the weapon, we display a failure message to the player. If we do find it, we set it to the player’s current weapon, and display a success message.

 

Lines 234-257: We do the same thing here, as we did for the equip weapon section, except we look for a potion to drink.

 

Lines 258-300: When the player inputs “trade”, we will show the player’s inventory and the vendor’s inventory, if there is a vendor at the player’s current location.

On line 260, we check how many items the player has that they can sell (the price does not match our “flag” price that indicates the item cannot be sold). If the player does not have any sellable items, we display a message. If they do, we go to line 275, loop through their inventory, and display each item with its price.

In lines 283-298, we do the same for the vendor’s inventory – except we don’t need to check for unsellable items.

 

Lines 301-345: This is where we try to handle the player buying an item from a vendor.

After making sure there is a vendor at the player’s current location, we do the same type of substring function that we did for equipping a weapon and drinking a potion. Then, we check if the vendor has that item, and if the player has enough gold to buy it. If so, we purchase it on lines 337-338, the same way we do on lines 167-170 of TradingScreen.cs.

 

Lines 346-382: Try to sell an item, using very similar parsing and logic as we did to buy an item.

 

Line 384: If the user’s input did not match any of our previous checks, we reach the final “else” and display our error message.

 

We have the DisplayCurrentLocation function in order to have a consistent format. We want to display this information from a couple different places. So, instead of duplicating code, we have a single function.

 

The LoadGameData function is the same as what we have in lines 26-38 of SuperAdventure.cs. The SaveGameData function is the same as the SuperAdventure_FormClosing function in SuperAdventure.cs.

 

NOTE: If you did not do the SQL lessons, you can delete, or comment out, lines 406 (where we try to load the saved game from the database) and 425 (where we try to save it to the database).

 

Check that your program works

Run the program and try typing in some commands. Type “Help”, if you forget the game’s commands.

 

Summary

Now, you have the same game, with two different front-ends.

The nice thing about having most of our game logic in the Engine class is that we can create this new front-end very quickly. I wrote all the code in Program.cs in less than two hours. In fact, it took me longer to write this lesson, than to write the code.

If you know XAML, you could probably create a WPF/XAML front-end project even faster. You would have similar UI controls (buttons, datagrids, etc.), and not need to figure out the user input parsing logic.

You could also create a unit test project, to do some automated testing of the game logic.

 

Source code for this lesson

Source code on GitHub

Source code on Dropbox

 

Next lesson: Lesson 24.1 – Make the SuperAdventure source code easier to understand and modify

Previous lesson: Lesson 22.3 – Creating the SQL to save and load the saved game data

All lessons: Learn C# by Building a Simple RPG Index

9 Comments

  1. J
    J June 12, 2016

    Hi Scott,

    I think I caught a minor error in the Main method. userInput after Console.ReadLine(); can never be null. If you enter nothing it will be String.Empty or “”. The only way for it to be null should be when it’s declared but un-initialised.

    Kind regards,

    J

    • Scott Lilly
      Scott Lilly June 12, 2016

      True, it would be better to use this:

      if(string.IsNullOrWhiteSpace(userInput))

      Thanks!

  2. J
    J June 12, 2016

    Obviously this doesn’t break the game because it will do the same (continue looping) except without entering the if (userInput == null) statement.

  3. Gregory
    Gregory March 8, 2017

    I am having a bunch of squiggly red lines underlining parts of the code in Program.cs, such as _player.OnMessage. What references should be added to the Engine project and what references should be added to the project containing the Program.cs?

    • Scott Lilly
      Scott Lilly March 8, 2017

      The SuperAdventureConsole project needs to have a reference to the Engine project. That’s the only reference needed. You might also see the squiggly lines if Program.cs does not have the “using Engine;” line at the top of the class.

      Please tell me if you try those things, and they do not fix the problem.

  4. Tim
    Tim May 18, 2021

    When I pasted the code, I got an error on line 406: System.IO.FileNotFoundException: ‘Could not load file or assembly ‘System.Data.SqlClient, Version=4.6.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’. The system cannot find the file specified.’
    I tried setting a breakpoint to step through the PlayerDataMapper.CreateFromDatabase() function but it just immediately throws the error. It ran fine with the SuperAdventure form. Is it having trouble loading System.Data.SqlClient from its own library?

    • Scott Lilly
      Scott Lilly May 18, 2021

      Hi Tim,

      I saw some comments on StackOverflow. Can you try running the NuGet Package Manager in Visual Studio and add the System.Data.SqlClient package to the console application?

      Let me know if that doesn’t get rid of the error.

  5. Tim
    Tim May 19, 2021

    Thanks for responding! I followed your advice and it works fine now. Do you know what caused this? I’d like to avoid it in the future if I can.

    • Scott Lilly
      Scott Lilly May 19, 2021

      You’re welcome. Can you check the project details for each of your projects? It looks like the problem might happen if there is a mix of using .NET Framework and .NET Core or .NET Standard.

      When I wrote the guide, .NET Core didn’t exist. Many of the problems people have reported have been because they have a .NET Core project, but the guide expects every project to be .NET Framework.

Leave a Reply

Your email address will not be published. Required fields are marked *