Rogue C# – “A mango a day …”

The search for resources that you need to even survive is a part of many games and Rogue is no different. Food is one of the most important items that you look for on the map and, without it, you eventually weaken, start fainting and die. So, it’s appropriate that food be the first item that we implement in our roguelike game. In this chapter, I’ll demonstrate how the player’s level of hunger can be managed and write the function that will allow the player to consume food and keep going.

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.

A Programmable Feast

As I said in the last chapter, the inventory in a roguelike has a complete cycle just like real-life inventory, from specifications and production through use and disposal. The specification for inventory in our game includes references to delegate functions which will be run when the player actually uses an item such as a scroll or a weapon.

private static List<Inventory> invItems = new List<Inventory>()
{
    new Inventory(InvCategory.Food, "some food", "some food", 
        "rations of food", '♣', 95, Player.ConsumeFood),
    new Inventory(InvCategory.Food, "a mango", "a mango", 
        "mangoes", '♣', 95, Player.ConsumeFood)
};

The constructor used above accepts arguments for up to three Func<> delegate functions, Main, Throw and Zap, but the ConsumeFood() function here is the default function that will be run for most items. The class property defines it as accepting a Player object reference and a nullable Inventory reference and returning a boolean.

public Func<Player, Inventory?, bool>? MainFunction { get; set; }

The challenge when setting this up was that the function itself either needed to be static, which meant it wouldn’t have access to properties associated with the actual class instance, or the Inventory class needed a reference to other objects like the current player which seemed like an unnecessary connection. I looked into setting up class events and delegates but that was a lot code that would have to be customized for each item and I really liked the simplicity of using Func<>.

Finally, I decided to make the delegate functions static and change the required function signature to include a reference to Player and Inventory objects. This might mean that the Inventory class will get another Func<> property or two if it’s really necessary but it keeps things simple. So, the object templates above refer to a new static function in the Player class called ConsumeFood().

public static bool ConsumeFood(Player currentPlayer, 
    Inventory? inventoryItem = null)

Now, when the player hits the ‘e’ key, I want the game to do the following:

  • Check to see if the player has anything they can eat.
  • If they do, use the item selected by the play to call the static delegate function.
  • The delegate function will adjust the player’s hunger level and extend their life and the item will disappear from their inventory.
  • If not, notify the player.

If you’ve read the previous chapters, the pattern is pretty familiar by now. The Game.KeyHandler() method now has an option for the ‘e’ key and it will send the code to a new function.

case KEY_E:     // Eat
    startTurn = true;
    Eat(null);
    break;

The new Eat() function is similar to the DropInventory() function from last chapter. If the Inventory screen is not showing, it will load it, set itself as the function in the class’s ReturnFunction property and exit. Otherwise, it will select the inventory item the user chose and call its MainFunction delegate.

private bool Eat(char? ListItem)
{
    bool retValue = false;
    List<Inventory> items;

    if (!InvDisplay)
    {
        // Verify the player has something they can eat.
        items = (from inv in CurrentPlayer.PlayerInventory
                    where inv.ItemCategory == Inventory.InvCategory.Food
                    select inv).ToList();

        if (items.Count > 0)
        {
            // If there's something edible, show the inventory
            // and let the player select it.  Set to return and exit.
            DisplayInventory();
            cStatus = "Please select something to eat.";
            ReturnFunction = Eat;
        }
        else
            // Otherwise, they'll be hungry for awhile.
            cStatus = "You don't have anything to eat.";
    }
    else
    {
        // Get the selected item.
        items = (from InventoryLine in 
             Inventory.InventoryDisplay(CurrentPlayer.PlayerInventory)
                    where InventoryLine.ID == ListItem
                    select InventoryLine.InvItem).ToList();

        if (items.Count > 0)
        {
            // Call the appropriate delegate and remove the item
            // from inventory.
            items[0].MainFunction(CurrentPlayer, items[0]);
            CurrentPlayer.PlayerInventory.Remove(items[0]);
            RestoreMap();
            retValue = true;
        }
        else
        {
            // Process non-existent option.
            cStatus = "Please select something to eat.";
            RestoreMap();
            retValue = false;
        }
    }

    return retValue;
}

Now, control is passed to the delegate function specified by the item, the Player.ConsumeFood() function, which has access to the inventory item and the current player object.

Except that as I started programming the actual ConsumeFood() function, I realized there was no specific need for it when consuming food. The program needs to add a specific number of turns to the player’s current HungerTurn value and insert into the Player.HungerTurns property. The ConsumeFood() function doesn’t have access to the Game.CurrentTurn property and there’s no way for it to accept it and still match the delegate signature required by the Inventory class. After that, all it was left to do was set one Player property. In this case, it’s just as easy to let the Game.Eat() function do it all.

foodValue = rand.Next(Inventory.MIN_FOODVALUE, Inventory.MAX_FOODVALUE + 1);
CurrentPlayer.HungerTurn += foodValue;
CurrentPlayer.HungerState = Player.HungerLevel.Satisfied;
CurrentPlayer.PlayerInventory.Remove(items[0]);
RestoreMap();
retValue = true;

It’s actually a good thing that I made the MainFunction property nullable because the food and mango items no longer need a delegate. I suspect some of the other inventory items are going to be the same way – they’re going to require other inputs than just the Player object and a single Inventory item. I’m not a fan of having the Game class do everything, or passing the Game object with all its information to a function.

At this point, I need to do something that I should have done earlier and that’s to run through the list of inventory items I want to implement and see what they’re actually going to need. It’s not a catastrophe – the adjustment needed here was pretty small and no real time was lost but it’s a good example of why planning needs to happen up front. It’s possible that I just need to adjust the delegate properties in the Inventory class to provide some more flexible parameters.

Check Your Hunger

Our player can now eat food but still doesn’t really need to so that needs to change. In the Game.KeyHandler() method, each key option can start a turn or not. If a turn is started, the method now needs to check the player’s hunger status by calling a new method.

The Player.HungerLevel enumeration numbers the possible states in descending order from Satisfied to Dead. I also realized while coding this that I left the Hungry stage out of the enumeration.

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

The new method can simply decrement the HungerLevel as needed to make the player more hungry.

if (startTurn)
{
    // Perform whatever actions needed to complete turn
    // (i.e. monster moves)
    EvaluatePlayer;

    // Increment current turn number
    CurrentTurn++;
}

private void EvaluatePlayer()
{
    // If the player's scheduled to get hungry on the current turn, update the 
    // properties.
    if(CurrentPlayer.HungerTurn == CurrentTurn)
    { 
        CurrentPlayer.HungerState = CurrentPlayer.HungerState > 0 
            ? CurrentPlayer.HungerState-- : 0;

        // If the player is now hungry, weak or faint, add some turns.
        if (CurrentPlayer.HungerState < Player.HungerLevel.Satisfied 
            && CurrentPlayer.HungerState > Player.HungerLevel.Dead)
        {
            CurrentPlayer.HungerTurn += Player.HUNGER_TURNS;
            cStatus = $"You are starting to feel 
                {CurrentPlayer.HungerState.ToString()}";
        }
    }
}

I named the method EvaluatePlayer() as I’ll probably be expanding it in coming chapters to adjust more player stats as more inventory items and then monsters are introduced.

The code above actually has a small bug that keeps it from working. Notice it’s using the decrement operator (–) to decrement the CurrentPlayer.HungerState property. The problem is that it’s placed after the property value so it’s returning the value of the property before the decrement happens. It should read like this:

        CurrentPlayer.HungerState = CurrentPlayer.HungerState > 0 
            ? --CurrentPlayer.HungerState : 0;

This is an important point to remember when using the increment and decrement operators.

Then, the Game.StatsDisplay property can be updated to warn the player if they’re Hungry, Weak or Faint. The code involved was getting a little cumbersome for a simple property so I turned it into a function.

public string StatsDisplay()
{
    string retValue = "";

    // Assemble stats dipslay for the bottom of the screen.
    retValue = $"Level: {CurrentLevel}   ";
    retValue += $"Gold: {CurrentPlayer.Gold}   ";
    retValue += $"Turn: {CurrentTurn}       ";

    if (CurrentPlayer.HungerState < Player.HungerLevel.Satisfied)
        retValue += $"{CurrentPlayer.HungerState}     ";

    return retValue;
}

This is where it gets real …

That’s great that our player can cycle down through the levels of hunger but we need there to be some consequences if they go too long without food; they need to start fainting and then, Game Over.

Fainting basically means that the player loses a turn or two while the game goes on. The Player class already has an Immobile property to hold the player still until a specified turn and our KeyHandler() method has a turn manager that keeps the action alternating between the player and the rest of the game so all we need is a little more code to put it together.

At the end of KeyHandler() the code, tests if the player’s last action has started a turn to be completed. Now it needs a loop to test if the player is immobile.

if (startTurn)
{
    do
    {
        EvaluatePlayer();

        // Perform whatever actions needed to complete turn
        // (i.e. monster moves)

        // Increment current turn number
        CurrentTurn++;

        if (CurrentPlayer.Immobile > 0) {
            CurrentPlayer.Immobile = (CurrentPlayer.Immobile <= CurrentTurn) ? 
                 0 : CurrentPlayer.Immobile;
            if (CurrentPlayer.Immobile == 0) cStatus =  
                 cStatus + " You can move again.";
        }
    } while (CurrentPlayer.Immobile > CurrentTurn);
}

The Do … While loop will keep the turns cycling as long as the player’s Immobile property is greater than the current turn and then reset the Immobile property once the turn is reached. We haven’t programmed in monsters or their moves yet but it means that they will keep moving (and maybe attacking) while the player is rendered immobile. Once the player is released, the code adds a notification to the status that they can move again.

At first, I was going to leave the Game.EvaluatePlayer() method outside of the loop because it’s going to evaluate if the player faints during a move and I didn’t want a situation where they kept fainting again before they ever get a chance to recover. Then I realized that this was causing the code to miss when the player’s HungerTurns property expired, their hunger level wouldn’t be updated and the game would continue on indefinitely. Instead, I changed EvaluatePlayer() to not initiate another fainting spell if the player was already out.

The method also gets another section which refers to two new constants.

private const int FAINT_PCT = 33;
private const int MAX_TURN_LOSS = 5;

if (CurrentPlayer.HungerState == Player.HungerLevel.Faint
    && CurrentPlayer.Immobile == 0)
{
    if(rand.Next(1,101) < FAINT_PCT)
    {
        CurrentPlayer.Immobile = CurrentTurn + 
            rand.Next(1, MAX_TURN_LOSS + 1);
        cStatus = "You fainted from lack of food.";
    }
}

It seemed reasonable to have the player faint on average of 1 out of every 3 moves and they can lose up to 5 moves at a time. That might need to be adjusted but then that’s what so great about constants.

Now that we have your attention …

After the player starts fainting, they have another 150 turns before the lights go out, permanently. Hey, they don’t call it permadeath for nothing. That means when their HungerState property goes to “Dead”, they need to stop moving and the Game Over screen needs to show up.

In previous chapters, I introduced the DevMode and InvDisplay properties of the Game class which are used to decide what will be displayed on the main form. DevMode shows the entire map, even when unexplored and InvDisplay shows the inventory listing. Th R.I.P. screen represents another stage for the game at which button commands will need different responses and it won’t be the last. I don’t want to keep adding properties that are basically used for the same thing.

Instead, the Game class will get an enumeration to replace the DevMode and InvDisplay properties and accommodate a new mode called GameOver. It’s a type of refactoring. Modes for the scoreboard and victory screens will be used later.

public enum DisplayMode {
    DevMode = 0,
    Titles = 1,
    Primary = 2,
    Inventory = 3,
    GameOver = 4,
    Scoreboard = 5,
    Victory = 6
}

public DisplayMode GameMode { get; set; }

The enumeration and property need to replace references to the previous properties and commenting those properties out provides a nice listing of locations in the Error List. Each one of the references is either testing for one of the old boolean properties or setting it so we get a bunch of changes like this.

 if (!InvDisplay)

to

 if (GameMode != DisplayMode.Inventory)

This actually goes back to the question I mentioned earlier of how much planning should be done before coding. Once again, this is something I could have anticipated during the planning stage but the cost of refactoring is still not very high so I’m fine with it this time. It’s still something to remember.

The EvaluatePlayer() method will be the first to know if the player has moved on so, if the player is not fainting, the code checks to see if they’re dead.

else if (CurrentPlayer.HungerState == Player.HungerLevel.Dead)
    GameMode = DisplayMode.GameOver;

Then, the KeyHandler() can route the code to the next step. I added a switch statement based on the GameMode to bring it up to date and it calls a new function, RIPScreen(), which assembles an ASCII graphic from the necessary information in the class.

switch (GameMode)
{
    case DisplayMode.DevMode:
        ScreenDisplay = this.CurrentMap.MapCheck();
        break;
    case DisplayMode.Primary:
        ScreenDisplay = 
            this.CurrentMap.MapText(CurrentPlayer.Location);
        break;
    case DisplayMode.GameOver:
        ScreenDisplay = RIPScreen();
        break;
    default:
        break;
}

Of course, this horrible fate could have been easily avoided if I’d just eaten some of that food that was filling up my inventory. Such a waste …

There are some refinements still needed. The KeyHandler() method needs a new section so it will respond appropriately to this screen and the player stats at the bottom need to disappear on this screen. There also need to be options for a new game and to see the scoreboard when its developed. Overall, though, it brings the game another step forward

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.

×