In this chapter … I further develop the Player class by adding a function to calculate attack accuracy based on any current player conditions, including weapon increments. I demonstrate tuple deconstruction for the purpose of keeping code a little more concise and readable. Read more of the Rogue C# series here.
The game experience is really starting to feel like the classic Rogue for me. I’m getting closer to the point where I can add a SQLite database to hold the game data but I needed to get some bugs and issues out of the way first. I also needed to clarify the player’s inventory limits so I addressed that this week.

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.
Attack accuracy
I mentioned in the last chapter that I needed to take weapon accuracy bonuses into account. It’s probably not going to be the last thing. Both Attack() overloads currently calculate HitChance based on things like armor and player experience and if the attacker is confused. In preparation for playing around with the odds, I decided to refactor this into a new function in the Character class.
public int Accuracy(Character Attacker, Character Defender)
{
int hitChance = 0;
if (Attacker is Player)
{
Player attack = (Player)Attacker;
Monster defend = (Monster)Defender;
// Chance of landing a punch: 50% + (5% * attacker min hit points) - (5% * defender's total protecton)
// + (5% * weapon AccIncrement)
hitChance = 50 + (5 * attack.ExperienceLevel()) - (5 * defend.ArmorClass) +
(5 * ((attack.Wielding != null) ? attack.Wielding.Increment : 0));
}
else
{
Player defend = (Player)Defender;
Monster attack = (Monster)Attacker;
// Chance of landing a punch: 50% + (5% * attacker min hit points) - (5% * defender's total protecton)
hitChance = 50 + (attack.MinStartingHP * 5) - (defend.TotalProtection() * 5);
}
// If the attacker is confused, decrease the chance to 25%.
if (Attacker.Confused > 0)
hitChance = (int)(hitChance * 0.25);
return hitChance;
}
This reduces the calculation in both overloads to a single line. In the code above, you can see that the player’s weapon now adds a 5% chance for every increment degree.
hitChance = Attacker.Accuracy(Attacker, Defender);
Charged (zap) attacks from staves and wands should always hit. Hulk Mode (for testing) already overrides the rest of the accuracy calculation, so it was simple to add charged attacks to that in the Attack() method. Now, if Hulk Mode is active or the attach is charged, the attack will always connect. Otherwise, it goes by random chance within the calculation.
hitSuccess = (HulkMode || Charged) ? true : rand.Next(1, 101) <= hitChance;
Picking up inventory
Currently, the Game.MovePlayer() method detects inventory on the map when the player steps on it and calls Game.AddInventory() to determine if it can be added to the player’s inventory. It looks at whether the item is groupable and searches the player’s current inventory for the item. If there’s no grouped slot the item can be added to, it goes by the INVENTORY_LIMIT constant of 20 for the number of slots allowed.
One problem is that it’s still looking at the player inventory by item name instead of ID and increment so that needs to change. I also saw an opportunity to relieve some of the Game class bloat and move AddInventory() to the Player class. This means it becomes a public method and will need to accept the inventory item as a parameter. The Player.SearchInventory() function also needed to be changed to accept a PriorityID instead of the name.
public Inventory? SearchInventory(Inventory.InvTemplateID ItemID)
{
return (from Inventory item in CharacterInventory
where item.PriorityId == ItemID
select item).FirstOrDefault();
}
In the process of reviewing the AddInventory() method, I questioned the use of the Inventory class Amount property. The main point of it was to make it easier to drop groupable items on the map but there’s really no reason for it and it just creates confusion in the code when converting between a single Inventory item with an Amount of 5 and the five discrete items that are required in the player’s inventory. It’s only being referenced four places in the code. It needs to go.
That meant writing some loops and other code to handle batches of groupable items rather than just setting the amount. It also meant that the MapLevel.DetectInventory() function needed to be changed to return a List of Inventory items instead of a single item and the six references to that had to adjust. It took awhile to work out all the bugs, including the annoying kind where I’d just used the wrong comparison operator, but I finally got it working.
So, the process starts in MapLevel.AddInventory() where the mapping process adds items to the map’s inventory list.
if (itemSpace != null && invItem != null)
{
// For ammunition that's groupable, decide how many items are in the batch.
if (invItem.ItemCategory == Inventory.InvCategory.Ammunition
&& invItem.IsGroupable)
{
itemCount = rand.Next(1, MAX_AMMO_BATCH + 1);
for (int x = 0; x <= itemCount; x++)
{
Inventory newItem = new Inventory(invItem);
newItem.Location = itemSpace;
MapInventory.Add(newItem);
}
}
else
{
MapInventory.Add(invItem);
}
}
Ammunition is the only thing that gets placed in groups now so, for example, if there’s more than one arrow a loop just keeps creating new items.
As for the new Player.AddInventory() function, I’m going to admit that I over-engerineered it a bit but if I’m going to have a function that accepts a LIst<Inventory> that could potentially contain any mix of inventory objects, I’m going to make it robust.
Sure, the List should contain multiples of the same type of object but sometimes you have to protect not only from user errors but also whatever silly thing another developer might do with the functions the code provides. In this case, it could be me having a brain fart six months from now. The fact that it’s only going to be called from one location right now is really not reassuring.
public List<(bool Added, string Message, Inventory item)> AddInventory(List<Inventory> Items)
Yes, it returns a list of tuples with a boolean indicating if the item was added, a results message and the inventory reference itself. Then, for every item in the list …
- Add any gold to the player’s purse.
- For everything else, the item can be added if a.) it’s groupable and the player already has something with the same PriorityID or b.) creating a new slot for the item would not exceed the limit of 20 slots.
- Add the item to the player’s inventory if possible and add a tuple to the return list with True, the message showing that it was added and the reference to the item itself.
- If the item cannot be added, add (False, “There is no space for …”, <item>) to the return list.
Back in the Game.MovePlayer() method, when the player steps on some inventory, it gets passed to the new function.
addInventory = CurrentPlayer.AddInventory(invFound);
// In this case, either everything will be added or nothing.
// Iterating to test for add is simple enough here ...
foreach (var (added, _, item) in addInventory)
{
if (added)
CurrentMap.MapInventory.Remove(item);
}
// ... but just look at the first item for the messages.
if (addInventory[0].Added)
{
if (addInventory.Count > 1)
UpdateStatus($"You picked up {ListingDescription(addInventory.Count, addInventory[0].item)}.", false);
else
UpdateStatus(addInventory[0].Message, false);
}
else
UpdateStatus("You don't have room in your inventory for this item.", false);
The foreach statement at the top is using tuple deconstruction to make the code a little more concise and only reference the parts of the tuple it needs. As the comments indicate, this call to the function should result in everything or nothing being added so the code just takes the first message returned as representative of the rest.
The one piece left is if the player wields a groupable item like a crossbow bolt. It’s not a great idea since they’re not incredibly effective but you never know. That means the item has to come out of the group and be treated separately so it can get separate increments, etc.. That can’t happen if creating a separate inventory listing would put the player’s inventory over the limit, though.
The Game.Wield() method gets an update.
if ((items[0].IsGroupable && slotsAvail) || !items[0].IsGroupable)
{
items[0].IsGroupable = false;
CurrentPlayer.Wielding = items[0];
UpdateStatus($"You are now wielding {ListingDescription(1, items[0])}.", false);
}
else
{
UpdateStatus($"You're carrying too much to wield that right now.", false);
}
Other fixes
- When the player starts fainting for lack of food, the frequency of fainting was so high that it was hard for the player to even grab something to eat. I reduced the FAINT_PCT constant from 33 to 5 which gives the player a 5% chance of fainting on any move and is still inconvenient.
- The mace wasn’t being marked as the wielded weapon at the start of the game. I realized this was the result of my fix from last chapter. I was creating an extra object where I didn’t need to.
- A keypress issue when displaying the inventory screen using ‘i’ would cause other keypresses, like ‘w’ for wield, to register when they shouldn’t. A minor fix to KeyHandler() fixed this.
- The game was advancing the player’s experience too quickly. Every time, the player defeated a monster, another one would respawn on the same level, so the player could stay on the low, easy levels for a long time and boost their experience before hitting the harder monsters. I turned off monster respawning and that seemed to bring it in line with the original game.
- During my changes to the addition of player inventory, I made InventoryDisplay() and ListingDescription() functions in the Inventory class static. This simplified the code in the Game class by removing a lot of instance references.