Rogue C# – Building on the Foundation

We’ve come a long way in the construction of our roguelike game but there’s still a lot to be done; we have a playable game with some challenges but a lot of the details that help to build the game experience still need to be finished and fine-tuned. As noted at the end of the last chapter, there will also be some bugs to take care of.

The focus of this series so far has been to show the process of building an application from the ground up. You’ve seen the initial planning and requirements analysis and how it was translated into a set of classes that serve as the foundation of the program. Now that the foundation is in place and the core of the program is working, the focus from this point will be on adding and refining the features that add value to the existing program and thorough testing.

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.

Bugfix – Disappearing player

One of the bugs that was … well, bugging … me recently was how the player would sometimes disappear while fighting a monster. I knew that the two were occupying the same space somehow because the player would reappear on the other side of the monster. When it was still happening after the Great Refactoring from the last couple of chapters, I finally decided to start with the Game.MovePlayer function and go line by line until I found it.

invFound = CurrentMap.DetectInventory(adjacent[direct]);
canMove = MapLevel.SpacesAllowed.Contains(visibleCharacter) 
    || invFound != null;

The decision about whether the player can move to a certain space was looking at a list of allowed spaces and also allowing for a space that had inventory in it. So, if the prioritized character for that space was in that list or the inventory list on the map contained something for the space, the player could move onto it.

That ignored the possibility that a monster was sitting in the space … on top of inventory. The prioritized character could even represent a monster but since the statement used an OR condition and then looked to the DetectInventory() function, it wouldn’t see the monster in the space and would allow the player to move. A simple change alters the logic to also check for the presence of a monster.

monster = CurrentMap.DetectMonster(adjacent[direct]);
canMove = MapLevel.SpacesAllowed.Contains(visibleCharacter) ||
    (invFound != null && monster == null);

If the displayed character is in the list or there’s inventory there but no monster, allow the player to move; otherwise, don’t. The parentheses in the above code are important since they combine the second and third conditions as a single choice.

Expanding the inventory

It’s time to start building up the inventory again and working out how to code for some of the individual items. Armor was the easy first choice as none of the items required special coding, just the ability to remove and wear different items.

private void RemoveArmor()

private bool WearArmor(char? ListItem)

The Game.KeyHandler() method was updated to assign these to the uppercase T (take armor off) and W (wear armor) keys.

WearArmor() is patterned after the Eat() function which shows the inventory screen and, when the user selects an option from the list, calls itself again to complete the task based on the option.

The inventory list should also indicate which armor and rings are being worn.

Fights

Weapons and ammunition didn’t require any extra coding, either. Whatever is being wielded just needs to be factored into the fight mechanics which are coded into the Game.Attack() method.

// Chance of landing a punch - 30% + (5% * XP level) - (5%
// * monster armor class).
hitChance = 30 + (5 * CurrentPlayer.ExpLevel) - (5 * Defender.ArmorClass);
hitSuccess = rand.Next(1, 101) <= hitChance;

// Either way, if the monster wasn't angry before, it sure is now.
Defender.CurrentState = Monster.Activity.Angered;

// Get weapon damage rating, default to bare hands (1-4)
if(weapon != null)
{
    minDamage = weapon.MinDamage; 
    maxDamage = weapon.MaxDamage;
}

// Random HP damage within weapon potential.
if (hitSuccess)
{
    UpdateStatus(
        $"You hit the {Defender.MonsterName.ToLower()}.", false);
    damage = rand.Next(minDamage, maxDamage + 1);
}
else UpdateStatus(
    $"You missed the {Defender.MonsterName.ToLower()}.", false);

When launching an attack, the player starts with a 30% chance of hitting with another 5% for each experience level and minus 5% for each armor class point of the monster. For example, a level 2 player attacking a hobgoblin with an armor class of 3 has a 25% chance of hitting (30 + 10 – 15). The monster has its own formula.

hitChance = 30 + (Attacker.MinStartingHP * 5) - (armorRating * 5);

So, the hobgoblin would start with a 30% chance of hitting, plus 5% since it’s minimum HP is 1 and minus 5% for every point of armor the player has. If the player has an armor class of 3, the hobgoblin has a 20% chance of hitting. I also realized while writing this that I’d forgotten to factor in the increments on the player’s armor as shown in the Inventory.Increment property so that’s been updated.

These chances are a little low at this point and tend to draw fights out a bit in the early game but that could be seen as a grace period for the player.

The amount of damage a player deals is completely random between the min and max damage rating for the weapon carried. Each monster has its own min and max attack damage rating which is used in the same way.

Game.MoveMonster() needed some rework in order to keep the monsters from running away when the player attacked. I won’t get into the code here but it was mostly about factoring in the Angered setting of the monster’s CurrrentState property. After that was fixed, the game became a little more challenging and less embarrassing for the monsters.

Mystery Items

The last five inventory types actually do require some coding as they have special functions and powers that affect everything from other inventory items to the game map and player. Rings, scrolls, wands, staffs (staves?) and potions all need to be coded specifically to carry out whatever effect the item is supposed to have.

These items are also unidentified when the player first finds them and each category has its own rules for its descriptive name.

  • Rings are named after their stone – “a moonstone ring”, “a diamond ring”.
  • Scrolls usually have gibberish names. In this version, I’m doing something a little different.
  • Wands are made out of various metals – “a copper wand”.
  • Staffs are made out of various types of wood – “a pine staff”
  • Potions are described by their color.

These items are also groupable in the inventory slot so you might have three “crimson potions” and then when you identify one of them by drinking it or using a scroll of identify, the remaining potions are shown by their real name.

Descriptive names are also supposed to be randomized between games so a crimson potion that turns out to be a potion of healing in one playthrough might be a potion of poison or paralysis in the next.

If we’re going to have items with randomized names like these, we need a source for the names so let’s start with a new list in the Inventory class that will hold potential names and the types they can be used for.

public static List<Tuple<InvCategory, string>> CodeNames 
    = new List<Tuple<InvCategory, string>>()
{
    new Tuple<InvCategory, string>(InvCategory.Ring, "agate"),
    new Tuple<InvCategory, string>(InvCategory.Ring, "adamite"),
    new Tuple<InvCategory, string>(InvCategory.Scroll, "Audentes fortuna iuvat."),
    new Tuple<InvCategory, string>(InvCategory.Scroll, "Carpe noctem."),
    new Tuple<InvCategory, string>(InvCategory.Wand, "copper"),
    new Tuple<InvCategory, string>(InvCategory.Wand, "gold"),
    new Tuple<InvCategory, string>(InvCategory.Staff, "birch"),
    new Tuple<InvCategory, string>(InvCategory.Staff, "cedar"),
    new Tuple<InvCategory, string>(InvCategory.Potion, "crimson"),
    new Tuple<InvCategory, string>(InvCategory.Potion, "clear"),
    new Tuple<InvCategory, string>(InvCategory.Potion, "black"),
};

The above list shows just a sampling from the CodeNames list of tuples that will hold the pairs of inventory category and code name to be made available to the program. Yes, those are Latin phrases for the scrolls; the idea of finding mysterious scrolls in a dungeon with cryptic Latin phrases just feels a little more intriguing to me than gibberish names.

Next, we need a master list of the game’s inventory items that’s independent of the player’s inventory so that we can track what items have been identified and what hasn’t. Fortunately, we already have that in the inventory template listing which supplies the definitions for the various items.

The list is static so it’s not tied to any instance of the Inventory or Game class but the templates can still be changed during the operation of the program. Notice that the two scrolls in the above screenshot have blank strings for code names. The first boolean value after the scroll names also indicates if it’s been identified or not (Inventory.IsIdentified). That value can be flipped when the item is identified and then that status can be applied to any instances of the item that exist.

The next step is to fill in those blank code names with random items from the CodeNames list. This should be done when the Game class object is instantiated at the start of the game.

public static void InitializeInventory()
{
    List<Tuple<InvCategory, string>> names = 
        new List<Tuple<InvCategory, string>>();
    Tuple<InvCategory, string> code;

    // For every inventory template that is marked as non-identified, 
    // select a random code name from the same category and then remove 
    // it from the list.

    foreach (Inventory item in InventoryItems)
    {
        if (!item.IsIdentified)
        {
            names = CodeNames.Where(c => c.Item1 == item.ItemCategory).ToList();
            code = names[rand.Next(0, names.Count)];
            item.CodeName = code.Item2;
            CodeNames.Remove(code);
        }
    }
}

In the Game class constructor ...

// Initialize inventory with code names
InitializeInventory();

The Inventory.ListingDescription() function already distinguishes between identified and non-identified items so it will insert the code name of any inventory object that has been created from a template that is not identified at the time. The only thing left to do is when the player identifies an item; then both the template and any existing instances of it need to be updated as identified but that’s not a big deal as you’ll see in an upcoming chapter.


C# 13 and .NET 9 – Modern Cross-Platform Development Fundamentals: Start building websites and services with ASP.NET Core 9, Blazor, and EF Core 9
  • Explore the newest additions to C# 13, the .NET 9 class libraries, and Entity Framework Core 9
  • Build professional websites and services with ASP.NET Core 9 and Blazor
  • Enhance your skills with step-by-step code examples and best practices tips
ComeauSoftware.com uses affiliate links through which we earn commissions for sales.

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.

×