In this chapter … I add a few more rings to the game and consolidate Player class property management into additional functions. I also show how helper functions can be used within a function to avoid repetition of code and still provide updated values. Read more of the Rogue C# series here.
The evolution of the software continues but evolution can be a messy business. The complexity of even this small codebase means that every change can require changes elsewhere to maintain best practices and consistency. The project is maturing into a more complex system that must be maintained in an orderly way to keep things coherent.

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 AndrewComeau.com. The current code for this project is also available on Github.
Don’t miss the next chapter! Subscribe for free on Substack to be notified when new chapters are posted.
Recent changes
Before taking a break from the game, I added a few new rings and started to find that they were consolidating Player properties in the process.
- For the Ring of Protection, I created the TotalProtection() function which looks for this ring and includes the player’s armor increment in one function that can be used to gauge overall protection against attacks.
- The new DamagePotential() function looks for the Ring of Increase Damage and also factors in the player’s strength through a scale provided by my sources on the game. It returns a tuple with min and max limits on the damage the player can inflict on a monster.
I also reviewed the combat calculations in the Game.Attack() methods and replaced some code with the new functions.
According to the reference I use, the Ring of Regeneration provided one HP of regen per turn, two if two rings were used. I decided to do things a bit differently. The Game.EvaluatePlayer() method used the following to determine if the player should get healing in a turn.
if (CurrentTurn % HEAL_RATE == 0 && CurrentPlayer.HPDamage > 0)
CurrentPlayer.HPDamage -= rand.Next(1, (int)(CurrentPlayer.ExpLevel / 3 + 1));
The HEAL_RATE constant was 12 which defined the number of turns between healing. I took that and put it in the new Player.HealingFactor() function that looks for the Ring of Regeneration and returns their increments along with the above result.
if (CurrentTurn % CurrentPlayer.HealingFactor().Turns == 0 && CurrentPlayer.HPDamage > 0)
CurrentPlayer.HPDamage -= CurrentPlayer.HealingFactor().HP;
The function also looks at the player’s experience level and returns a tuple with the number of hit points to give back and the number of turns between healings.
Code Cleanup
In the Game class, I did some code cleanup by changing the ReturnFunction property to an Action<char> intead of a Func<char, bool> because the return value was never being used.
/// <summary>
/// Delgate used to return to function that enables an inventory item to be used.
/// </summary>
public Action<char?>? ReturnFunction { get; set; }
From my database experience, I know that you don’t save information that you can calculate as needed. I replaced the Player.ExpLevel property with a function that calculated it so the program doesn’t have to take that extra step of updating it.
public int ExperienceLevel()
{
int threshold = 10;
int retValue = 1;
if (this.Experience >= 10)
do
retValue++;
while ((threshold *= 2) <= this.Experience);
return retValue;
}
I wanted to do the same with the NextExpLevelUp property but that creates a circular situation where the function is always recalculating the next level requirement by the current experience so the current experience can never actually cross it. At some point, you have to draw an actual line in the sand.
Tuple<T> vs. Value Tuples
I’d been meaning to change the MapLevel.DiscoverRoom() and LightUpRooom() functions to use LINQ instead of the nested X / Y loop. It’s just neater and probably faster so I did that.
public void DiscoverRoom(int xPos, int yPos)
{
// When the player enters a room, decide if the room should be lighted.
// Decide if room is lighted.
bool roomLights = (rand.Next(1, 101) <= ROOM_LIGHTED);
// Get region limits
(MapSpace TopLeft, MapSpace BottomRight) corners = GetRegionLimits(xPos, yPos);
List<MapSpace> spaces = (from MapSpace space in levelMap
where space.X >= corners.TopLeft.X && space.X <= corners.BottomRight.X
&& space.Y >= corners.TopLeft.Y && space.Y <= corners.BottomRight.Y
&& !space.Discovered
&& space.MapCharacter.DisplayChar != HALLWAY.DisplayChar
select space).ToList();
// For all room spaces in region that have not been discovered,
// set Discovered = True and Lighted according to probability.
// Leave HALLWAY and already discovered spaces alone and just focus on rooms.
foreach (MapSpace space in spaces)
{
space.Discovered = true;
if (!space.Lighted) { space.Lighted = roomLights; }
}
}
In the process, I questioned why the GetRegionLimits() function was returning a Tuple<MapSpace, MapSpace> instead of just a (MapSpace, MapSpace) value tuple. With a little research, I found that the old System.Tuple<T> reference object construction is now considered legacy in C#. Value tuples are preferred for their memory usage, speed and the fact that they allow named properties so I decided to replace them across the code. Mostly, they were being used in the Inventory lists so it was a pretty simple fix.
private (MapSpace TopLeft, MapSpace BottomRight) GetRegionLimits(int xPos, int yPos)
{
// Get a pair of MapSpaces defining the limits of the region based
// on an internal x and y coordinate.
int xTopLeft = (int)((Math.Ceiling((decimal)xPos / REGION_WD)) - 1) * REGION_WD + 1;
int yTopLeft = (int)((Math.Ceiling((decimal)yPos / REGION_HT)) - 1) * REGION_HT + 1;
int xBottomRight = xTopLeft + REGION_WD - 1;
int yBottomRight = yTopLeft + REGION_HT - 1;
return (levelMap[xTopLeft, yTopLeft], levelMap[xBottomRight, yBottomRight]);
}
Discovering helper functions
The MapLevel.SearchDirection() function was pretty repetitive and I wanted to clean it up a bit but I couldn’t think of a way so I asked Google Gemini for suggestions.
How would I store the levelMap reference in the following so I can reference it instead of repeating it four times?
(pasted code)
The recommended solution was to use a helper function.
char currChar() => levelMap[currentX, currentY].MapCharacter.DisplayChar;
switch (direction)
{
case Direction.North:
while (currChar() == empty && currentY > 0)
currentY--;
break;
case Direction.East:
while (currChar() == empty && currentX < MAP_WD)
currentX++;
break;
case Direction.South:
while (currChar() == empty && currentY < MAP_HT)
currentY++;
break;
case Direction.West:
while (currChar() == empty && currentX > 0)
currentX--;
break;
}
This provides a continuously updated version of the space that can be used inside the while loop.
Bug Tracking
In a professional project, I’d actually be using a bug and change tracking system for everything that I’m doing with the program but,
- I’m generating enough documentation with these chapters as it is.
- The sheer volume of extra information would keep me from getting anywhere on the project.
- It feels enough like an actual job as it is.
- I’m not sure how many people would read it anyway.
Nevertheless, I like being through about things so I looked at couple of options and decided to start a Github bug tracking project for the Rogue C# repository. I don’t know if it will turn out to be useful yet but I’ll see where it goes.
Featured image produced with ChatGPT