Rogue C# – Building the Class Structure

Now that we can consistently generate a roguelike game map, it’s time to make that map part of a larger game and we need a couple more classes. First, a Game class will manage the process of the game itself, which is pretty extensive and then a Player class will hold all the properties of the player’s character and encapsulate any functions that are needed there. In this chapter, you’ll start to see how your custom classes can work together to get things done.

This post is part of a series on creating a roguelike game in C#. For the latest chapters and more information, please visit the Official Project Page on ComeauSoftware.com. The current code for this project is also available on Github.

Adding Classes

The Game Class

One of the purposes of Object Oriented Programming is to separate the program logic from the user interface so they are not dependent on each other and to better organize the code. We could throw all the game logic on the Windows form itself but then we would be trying to track down errors in the game amidst code for buttons and other controls. Also, what if we want to give the game an entirely different interface for more advanced graphics? Having a system of classes that’s separate from the interface makes those things a lot easier.

The Game class will oversee all the operations of a game session, from getting the player’s name to moving between different map levels to the scoreboard at the end. It will call other classes as needed to carry out their functions, such as the MapLevel class when a new map is needed for the next level.

Classes don’t have to be written all at once; they can grow as new functions are needed. There are a few things we can anticipate for the Game class already.

Remember that the code for this series is available on Github.

namespace RogueGame
{    
    internal class Game
    {
        public MapLevel CurrentMap { get; set; }
        public int CurrentLevel { get; }
        public Player CurentPlayer { get; }
        public int CurrentTurn { get; }

        public Game() {
            // Setup a new game
            this.CurrentLevel = 0;
            this.CurrentMap = new MapLevel();
            this.CurentPlayer = new Player("Rogue");
            this.CurrentTurn = 0;
        }
    }
}
  • I’m putting the class in the same namespace as the main form to make it easily accessible. You can use different namespaces if you like for your own organization needs but it’s good to keep things simple.
  • The game will only be working with one map at a time so a class-level MapLevel object can be declared along with a CurrentLevel integer to keep track of what level (0-26) the player is on.
  • There’s also only one player so we can have an instance of the new Player class which I’ll show next.
  • Rogue is a turn-based game and I’m going to be using the turn number to govern the length of things like hunger and potion effects so the CurrentTurn property will keep track of how many turns have been played so far.
  • The constructor is pretty straightforward, initializing the defined properties. Notice that all these properties are auto-properties as there’s no need for them to have any specific functionality of their own at this point although this might change later.

The Player Class

A look at the stats display in the traditional Rogue game will give us an idea of what properties are needed for the new Player class. Again, we can add more later if needed.

The Level number on the left is already handled by the Game class as it’s a property of the game. The rest fall under the player’s character.

  • Hit Points, both maximum and current.
  • Strength, both maximum and current.
  • Amount of gold in the purse.
  • Current armor level – I’ll leave this for another class as it’s a property of whatever armor the player is wearing at the moment which is an inventory item.
  • Experience level and points – Some versions of the game simply give an experience rank like “Apprentice” but I prefer this numbered system where the player gains points and then levels up to the next experience level based on a scale.
  • The player name is not shown but the game will ask for it at the beginning and will use it in the scoreboard.

Your character will also be picking up food from time to time and will need to eat. According to a game analysis that I found, the player is required to eat at least every 1700 turns.

“An average ration of food will keep you going for 1300 turns. After at most 1700 turns without eating, you will be told that you are hungry. If you persist, you will be told 150 turns later that you are weak from hunger. The remedy is to eat some food. If you do not, after another 150 turns you will start fainting at frequent, unpredictable intervals. Eventually you will die of starvation.”

George Sicherman – “The Rogue’s Vade-Mecum

This seems a little high from my experience but I’ve never actually counted and it’s easy to lose track. Food is also an inventory item and we haven’t worked out a way for the player to keep an inventory so it’s a little early but I’ll work it into the properties for now and we can worry about the functions later.

You can do a lot of planning in a class before the functions are ever written:

namespace RogueGame
{
    internal class Player
    {
        private const int STARTING_HP = 12;
        private const int STARTING_STRENGTH = 16;
        public const int MAX_FOODVALUE = 1700;
        public const int MIN_FOODVALUE = 900;
        private const int HUNGER_TURNS = 150;

This class can have its own set of constants to define some limits. The max HP and player strength will start at 12 and 16, respectively. Since food is supposed to keep you going for an average of 1300 turns, I’m using a minimum of 900 and max of 1700. You can probably see the Random class showing up in the near future.

        public enum HungerLevel
        {
            Satisfied = 3,
            Weak = 2,
            Faint = 1,
            Dead = 0
        }

The program needs specific states to transition between when deciding just how hungry the player is for food so an enumeration serves the purpose. Descending values will permit the program to do an easy calculation. In reality, it’s rare that I’ve actually died of starvation in this game; an opportunistic monster is likely to take care of things first. Still, it happens for the persistent player.

        public string PlayerName { get; set; }
        public int HP { get; set; }
        public int HPDamage { get; set; }
        public int Strength { get; set; }
        public int StrengthMod { get; set; }
        public int Gold { get; set; }
        public int Experience { get; set; }
        public HungerLevel HungerState { get; set; }
        public int HungerTurn { get; set; }

HP and Strength are both properties where there’s a value that indicates that indicates full health and another value that indicates your actual condition at the moment. RPGs often make use of modifiers that are added to or subtracted from a dice roll or stat to determine how things go from moment to moment. For both the strength and HP, I decided to have a main property for default health and a modifier property that will be subtracted from the main to get the current condition. When the player is at full health, HPDamage and StrengthMod should be 0. Otherwise, they’ll usually be negative numbers.

The player also has a property based on the HungerLevel enumeration type and a HungerTurn property that specifies the turn at which that property will change. This is why the Game class keeps track of the current turn number. This property, and eventually many others, will be compared to that turn number value to determine if something needs to happen.

        public Player(string PlayerName) {
            // Create a new player object.
            var rand = new Random();
            this.PlayerName = PlayerName;
            this.HP = STARTING_HP;
            this.HPDamage = 0;
            this.Strength = STARTING_STRENGTH;
            this.StrengthMod = 0;
            this.Gold = 0;
            this.Experience = 1;
            this.HungerState = HungerLevel.Satisfied;
            this.HungerTurn = 
                 rand.Next(MIN_FOODVALUE, MAX_FOODVALUE + 1);            
        }

Finally, the class constructor accepts a player name which the main form can pass along and sets all the other properties to some starting values. The player is at full health, they have no gold and one point of experience just for showing up. They have a grace period on finding food which is determined at random between the two constants. The program isn’t going to look at that value yet anyway so we’ll just assume the character is so fascinated by the dungeon that they’re oblivious to whatever hunger they do feel.

Calling the Classes

The two buttons on the main form that I used to generate new maps for testing are going away for now along with all their code. I’ll be coding up a development testing panel later so that I can have some oversight while the game is running. For now, the main form’s code becomes a lot simpler.

public partial class DungeonMain : Form
{
    Game currentGame;

    public DungeonMain()
    {
        InitializeComponent();
        currentGame = new Game();
    }

    private void DungeonMain_Load(object sender, EventArgs e)
    {
        lblArray.Text = currentGame.CurrentMap.MapText();
    }

}

If you place a breakpoint on the line that instantiates the new Game object, you can see everything that the program now does:

  1. The form maintains the Game object at class level because it’s going to be calling it frequently as you’ll see in upcoming chapters. The form’s constructor instantiates the currentGame object which sets the current dungeon level, sets the current turn number to 0 and then calls the MapLevel and Player classes. Because the object is maintained at class level, C# prefers that it have an actual value before the form’s constructor finishes.
  2. The MapLevel class creates a new map and stores it in the currentMap object.
  3. The Player class sets up a new player with the rather generic name of “Rogue” which we’ll fix later and stores that in the currentGame object.
  4. Going back to the Load event, it references the currentGame object’s CurrentMap property and runs the MapText() function to get the map contents for the form.

This can also be shown in a flowchart which is common in the development of software specs. LucidChart is a great tool for developing quick flowcharts like this one.

Click for full-size view.

So you see the program works as before but a lot of the functionality has been transferred into classes which now do a lot more with minimal code on the form itself.

In upcoming chapters, you’ll see how the classes continue to call each other as they work together to manage the progress of the game.

Sign up for our newsletter to receive updates about new projects, including the upcoming book "Self-Guided SQL"!

We respect your privacy and will never share your information with third-parties. See our privacy policy for more information.

×