Rogue C# – Turns and Cheatcodes
Role-playing games are often turn-based; your character performs an action and then all the other characters and opponents get their turn. In Rogue, your character’s need to eat and the length of some spells are based on a number of turns. For this reason, managing the turns is essential to the game and that’s what we’re looking at in this chapter. While testing this, I also found a problem with the map creation process and I decided to add a couple of cheats to the game to help me debug that.
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.
Counting the Turns
The project’s Game class is responsible for managing all actions in the game and its KeyHandler() method receives all key presses and initiates responses so that seemed like great place to start managing the turns. Not all player actions start a turn; things like viewing inventory, for example, are just for reference, so whether or not to start a turn has to depend on the key pressed.
For this reason, Game.KeyHandler() gets a new boolean variable.
bool startTurn = false;
Then, we can simply set the variable to True for any action that requires it.
switch (KeyVal)
{
case KEY_S:
startTurn = true;
SearchForHidden();
break;
default:
break;
}
After carrying out any player action needed, the method will then complete the current turn by initiating any other actions for monsters, etc.. Right now, it just needs to increment the CurrentTurn property of the Game object.
if (startTurn)
{
// Perform whatever actions needed to complete turn
// (i.e. monster moves)
// Increment current turn number
CurrentTurn++;
}
For some reason, I setup the CurrentTurn property as read-only so I had to add a set accessor to it. I think I was making everything read-only by default until it needed to be otherwise.
For now, I’m adding the CurrentTurn value to the game’s player stats display at the bottom of the screen for reference. It might not need to be a permanent thing but it certainly helps now.
public string StatsDisplay
{
get { return $"Level: {CurrentLevel} Gold: {CurrentPlayer.Gold} " +
$"Turn: {CurrentTurn} "; }
}
It works great except we get into a question about requirements when I notice that running into a wall repeatedly increments the turn. In other words, if the player is next to a wall and holds down the direction key to try to move beyond it, the turn will continue to increment – quickly. I don’t know that it makes a real difference for game play but I don’t think pounding ineffectively on a wall should really count as full turn so I went with the original’s behavior.
The fix for this was pretty simple. I changed the MoveCharacter() method into a boolean function and had it return True only if it could actually move the character.
public bool MoveCharacter(Player player, MapLevel.Direction direct)
In the process, I noticed that the function was performing a couple of actions like discovering surrounding spaces and responding to gold and other items whether or not the character actually moved. It didn’t make a difference to the game but it was unnecessary and inefficient so I made those dependent on the character moving.
Finally, in the KeyHandler() method, when the player presses a movement, key, the startTurn variable will be set to the return value of MoveCharacter().
case KEY_WEST:
startTurn = MoveCharacter(CurrentPlayer,
MapLevel.Direction.West);
break;
Cheat codes
Implementing turns was just too easy so I had to find a bug to fix. While testing, I encountered a map with only two rooms and no way to a staircase. That’s a gamebreaker. I thought something was either wrong with the VerifyMap() function or with the search function. I didn’t have a way to see the full map at that point so debugging was a pain as I had to set a breakpoint and inspect a lot of properties for each map space involved.
My first step was to create a simplified version of the MapText() function that would make everything visible and show all hidden characters like doorways. I named it MapCheck() and can call it after setting a breakpoint to see how the map actually exists.
In order to duplicate the error, I also needed a way to cycle through a lot of maps until I found another map that was faulty. I figured that was as good a time as any to create a developer mode for the game.
I wanted to use specific keys without eliminating them as possible game controls. The KeyDown event of the program’s DungeonMain form also enables the CTRL key to be detected so I updated Game.KeyHandler() to send it to the KeyHandler which gets a new definition and a new section of code.
currentGame.KeyHandler(e.KeyValue, e.Shift, e.Control);
public void KeyHandler(int KeyVal, bool Shift, bool Control)
if (Control)
{
switch (KeyVal)
{
case KEY_D:
DevMode = !DevMode;
cStatus = DevMode ?
"Developer Mode ON" : "Developer Mode OFF";
break;
default:
break;
}
}
The Game class needs a couple new class-level items.
private const int KEY_D = 68;
private const int KEY_N = 78;
public bool DevMode { get; set; }
Now, when I run the program, I can just use CTRL-D to turn Developer Mode on which shows the entire map and then turn it off again.
Now we need a way to cycle through a bunch of maps without changing the level or anything else.
I defined CTRL-N to change out the map if the program is in DevMode.
case KEY_N:
if (DevMode)
ReplaceMap();
break;
Then the ReplaceMap() method can simply change it out and place the player on it.
private void ReplaceMap()
{
// Dev mode only - replace the map for testing.
CurrentMap = new MapLevel();
CurrentPlayer.Location =
CurrentMap.PlaceMapCharacter(Player.CHARACTER, true);
}
Now the program can turn on all the lights and quickly switch out the map as many times as needed. If I find a map that doesn’t work, it will be that much easier to break into the program and debug it.
MapLevel Revisited
After that, it didn’t take me long to find the problem.
Immediately, I can tell there’s a basic problem with the algorithm for the VerifyMap() function that I should have anticipated. The function currently checks for rows and columns in the array that only have blank spaces. That’s not the case here but there’s still an isolated section so we need a better test.
The main problem is actually with the algorithm behind the HallwayGeneration() method. For each room exit defined on the map, the method would search for available hallways to connect to and use the first one it found. With nine rooms, this still allowed for the possibility that a room, or pair of rooms, would be isolated, even with the VerifyMap() function which couldn’t see the problem.
I thought about doing a second pass through the list of exits after the first group of hallways had been dug to give each exit another chance to connect, even if it meant making a second connection. This would make more passages and reduce the chance of isolation. Then I realized that the simplest solution was to do it on the first pass. I removed the switch statement I was using and let each exit create hallways in as many directions as it could.
surroundingChars = SearchAllDirections(hallwaySpace.X, hallwaySpace.Y);
if ((surroundingChars[hallDirection] != null &&
surroundingChars[hallDirection].MapCharacter == HALLWAY))
{
DrawHallway(hallwaySpace,
surroundingChars[hallDirection], hallDirection);
hallwayDug = true;
}
if ((surroundingChars[direction90] != null &&
surroundingChars[direction90].MapCharacter == HALLWAY))
{
DrawHallway(hallwaySpace,
surroundingChars[direction90], direction90);
hallwayDug = true;
}
if ((surroundingChars[direction270] != null &&
surroundingChars[direction270].MapCharacter == HALLWAY))
{
DrawHallway(hallwaySpace,
surroundingChars[direction270], direction270);
hallwayDug = true;
}
If it can’t connect to anything, the method still adds another space and then tries again on the next pass of the WHILE loop. This seems to work; it probably leaves the VerifyMap() function without anything to do.
It looks kind of busy but we can just write the backstory for the game around it …
These dark dungeons have existed for centuries, constructed by an unknown civilization and then abandoned. Now they are home to every kind of monster imaginable and some best not imagined. These creatures, in their mindless rage, have smashed, dug and even eaten an endless series of twisting tunnels between the dungeon rooms. As you descend through the levels in your search of the Amulet, it is imperative that you not get lost in this byzantine maze as so many unfortunate travelers have been lost before you …
Yeah, that will work. Legend creation is a give and take process.
If I really wanted to reduce the complexity a bit, I could probably base each hallway connection on a call to the Random class set the percentage constant to 97 or above. Too low and it risks leaving rooms isolated.
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.
0