Enumerations and Dictionaries in C#
In the last chapter, we got the measurements for the rooms on our game map – now, we need to plot them to the array itself. First, we need a little bit of supporting code to establish directions on the map. In this chapter, you’ll see examples of enumerations and dictionaries in C#. You’ll also see a way to check a char value against many others at once.
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.
Enumerations
Immediately after the declaration of the MapLevel class, enter the following code:
private enum Direction{
None = 0,
North = 1,
East = 2,
South = -1
West = -2
}
// Dictionary to hold hallway endings during map generation.
private Dictionary<MapSpace, Direction> deadEnds =
new Dictionary<MapSpace, Direction>();
In C# and other languages, an enumeration is actually a type that holds a small number of values to be used within the code. Enumerations usually have text values as value names with numeric equivalents like the ones you see above. This means that they can be used in comparisons and conversion operations when necessary. In C#, the numeric value must either be an integer or a type that can be converted to an integer, like a char.
Normally, an enum will assign numeric values by default if you let it but, in this case, I wanted specific values for the map directions that this one is handling.
So … why do we have all those separate constants?
We defined a whole mess of constants to hold the room building characters and basic measurements and all those constants are either chars or integers, which can be assigned to enumeration members. Wouldn’t it make sense to have them in a couple of enumerations instead?
You could do this but the issue is that, to actually use the enumeration member as anything other than a member of its type, you have to do a conversion. So, if you wanted to have a RoomCharacters enumeration with the room building characters assigned to the members, you’d have to do something like this to reference it:
levelMap[x, y] = new MapSpace((char)RoomChars.Horizontal, false, false, x, y);
That’s an example of a new MapSpace object being defined with a horizontal character (‘═’) being taken from an enumeration. Notice the conversion to a char type before it. We would have to do those conversions every time the enumeration was referenced. From my understanding, there is no performance issue with this but it would be more tedious in writing and reading the code later and there’s no real benefit. Besides, the way we did it was a great exercise in using constants.
Dictionaries
C# also has a number of collection classes which can hold many values for reference. One of them is the Dictionary which can hold key / value pairs just like a dictionary holds terms and definitions. The key within this collection must be unique so the Dictionary is well suited for a collection of unique items for which you want to have corresponding values.
The Dictionary is also referred to as a generic collection because it can be defined around any pair of types and it will enforce the use of those types in the key / value pairs that it holds. In other words, you cannot define a Dictionary<string, int> and then store a pair made up of date and string values.
The Dictionary shown above is defined to hold pairs in which the key is a MapSpace object and the value is a Direction as defined by our enumeration. We’re going to use this to define the initial hallways coming off the room exits later in this lesson. Each hallway will need a Direction value that will determine where it should go from there and that value will be used in the next step of completing the hallways.
Plotting the Rooms
In the last chapter, we left off in the middle of the MapGeneration() method after it randomly calculated dimensions of the new room and called a new method – RoomGeneration().
The RoomGeneration() method needs to accept the X and Y coordinate of the new room, it’s width and height, so let’s create it in code.
private void RoomGeneration(int westWallX, int northWallY, int roomWidth, int roomHeight)
{
}
Again, this method will have its own algorithm. Let’s take a look at that.
- We have the X and Y array coordinates of the room’s northwest point and the measurements, so now we need to calculate where the east and south walls should be.
- Remember that we’re plotting the room within a specific region of the array so we need to know which region we’re working in so we can determine the exits. This can be calculated from the coordinates we already have.
- Then we’ll need to actually create the walls. Another nested FOR loop will be needed.
- Depending on the region and probability, we need to select walls to contain exits and replace one wall character with an exit.
- Place the corners of the room.
Let’s start the code. Within the new RoomGeneration() method, type the following:
// Create room on map based on inputs
int eastWallX = westWallX + roomWidth; // Calculate room east
int southWallY = northWallY + roomHeight; // Calculate room south
// Regions are defined 1 to 9, L to R, top to bottom.
int regionNumber = GetRegionNumber(westWallX, northWallY);
int doorway = 0, doorCount = 0;
var rand = new Random();
Now we have the east and south walls for handy reference and we’ve initialized a couple of integers for use with the doorways on the last line. Again, whatever grid you’ve made helps for reference.
GetRegionNumber() is another new function that will take the coordinates passed in and determine the region. MapGeneration() probably could have kept a counter variable and passed this in as well but other parts of the code might need to determine the region at some point so we might as well write it.
private int GetRegionNumber(int RoomAnchorX, int RoomAnchorY)
{
// The map is divided into a 3 x 3 grid of 9 equal regions.
// This function returns 1 to 9 to indicate where the region is on the map.
int returnVal;
int regionX = ((int)RoomAnchorX / REGION_WD) + 1;
int regionY = ((int)RoomAnchorY / REGION_HT) + 1;
returnVal = (regionX) + ((regionY - 1) * 3);
return returnVal;
}
Remember, the regions are arranged in a 3 x 3 grid so the function determines where the region resides in this grid by dividing the X and Y coordinates by the region width and height respectively and adding 1. Then it calculates the region number based on which “row” its in and returns it.
Going back to the RoomGeneration() method, let’s build some walls …
// Create horizontal and vertical walls for room.
for (int y = northWallY; y <= southWallY; y++)
{
for (int x = westWallX; x <= eastWallX; x++)
{
if (y == northWallY || y == southWallY)
{
levelMap[x, y] = new MapSpace(HORIZONTAL, false, false, x, y);
}
else if (x == westWallX || x == eastWallX)
{
levelMap[x, y] = new MapSpace(VERTICAL, false, false, x, y);
}
else if (levelMap[x, y] == null)
levelMap[x, y] = new MapSpace(ROOM_INT, false, false, x, y);
}
}
Our nested X and Y loop uses the variable we defined earlier in the following algorithm:
- Iterate through all of the array cells within the boundaries of the room.
- If y is equal to the north or south wall, add a horizontal character.
- if x is equal to the east or west wall, add a vertical character.
- Otherwise, just create a space showing the room’s interior.
The vertical characters will replace the horizontal characters on all four corners of the room but that’s okay; they’ll be replaced with corners later, anyway.
Making some exits
I’m not going to post the entire code for this part since it is a little long and I want you to see if you can write some of it yourself. Remember that it is available on Github.
Looking back at the requirements, we need to make a decision for each wall of each room as to whether there should be an exit. First, no walls facing the outside edge of the map should have exits. Not every one of the remaining walls need exits, just enough of them to ensure that every room can be connected to the rest of the map.
Remember that the RoomGeneration() method is running once for each room on the map.
Each room must have at least one door so I’m going to start with a WHILE loop that will function as long as the doorCount variable initialized earlier = 0. Within that loop, I’m going to put four separate IF statements because I want each of these to run for each room.
while (doorCount == 0) {
// North doorways
if (regionNumber >= 4 && rand.Next(101) <= ROOM_EXIT_PCT)
{
}
// South doorways
if (regionNumber <= 6 && rand.Next(101) <= ROOM_EXIT_PCT)
{
}
// East doorways
if ("147258".Contains(regionNumber.ToString()) &&
rand.Next(101) <= ROOM_EXIT_PCT)
{
}
// West doorways
if ("258369".Contains(regionNumber.ToString()) &&
rand.Next(101) <= ROOM_EXIT_PCT)
{
}
}
Let’s look at the IF statements:
- If the regionNumber is greater than or equal to 4, add an exit on the north wall.
- If the regionNumber is less than or equal to 6, add an exit on the south wall.
Now, it gets a little trickier because I want east facing exits in regions 1, 4, 7, 2, 5 and 8. In C#, strings are actually collections of chars so I assemble the region numbers I want to test into a string, and use the Contains function of the String class to see if it contains the string equivalent of the region number. Then, I do the same for the west facing exits in regions 2, 5, 8, 3, 6 and 9.
Each of these IF statements also uses the Random class to determine if the individual wall should get an exit according to the ROOM_EXIT_PCT percentage constant. This constant has to be pretty high because we’re already skipping eight walls that face the edges of the map.
Now, for the actual doorway code for the north exits:
- Get a random number between the X coordinates for the west and east walls. If west is at 5 and east is at 22, the lower value needs to be 6 and the upper value should be 22 because the function will exclude that upper value anyway and use 21.
- Create a new ROOM_DOOR MapSpace object on the array at those coordinates.
- Subtract 1 from the Y coordinate for the north wall and create a HALLWAY MapSpace at those coordinates on the array so there’s a new hallway to the north of the exit.
- Add that new HALLWAY MapSpace object to the deadEnds Dictionary as a key. We’ll need to pick it back up later. The value paired with the key will be the direction the hallway is heading in.
- Increment the doorCount variable so the WHILE loop will know that at least one door has been created.
It is possible, though unlikely, that all four IF statements will decide not to create an exit, in which case the WHILE loop will repeat but it probably won’t do so more than once.
Now it’s your turn. The algorithm is the same for the rest of the IF statements – only the variables change. Try writing the code yourself by substituting the appropriate directions in each case.
Corners
Finally, the RoomGeneration() method fills in the corners.
After all the rooms have been generated and placed on the map, the process returns to the MapGeneration() method which fills in all the blanks on the array with blank spaces. Otherwise, the array space is null and those can cause errors if we forget to check for them so we might as well fill them in.
This is actually a trade off on my part – I’m choosing code simplicity over efficiency. This code is going through the entire array and creating a blank space for any null array value. Many of these spaces will then be replaced by the HallwayGeneration() method that you see here so the program is doing double work.
Plotting out the rooms was an exact process where we know each coordinate that will be used. The HallwayGeneration() method will use a lot more guesswork to search for clear spaces where it can “dig” corridors and, if we don’t fill in all the spaces first, it will have to continually check for null spaces which could make for tedious code.
For now, I’ll let the program do some extra work but don’t be surprised if I bring this back for some refactoring later because it does bug me.
Adding the Stairway
Now that the rooms are plotted, we can go ahead and add a stairway for the next level. It’s as simple as poking at the map until we find a space that’s inside a room.
private void AddStairway()
{
var rand = new Random();
int x = 1; int y = 1;
// Search the array randomly for an interior room space
// and mark it as a hallway.
while (levelMap[x,y].MapCharacter != ROOM_INT)
{
x = rand.Next(1, MAP_WD);
y = rand.Next(1, MAP_HT);
}
levelMap[x,y] = new MapSpace(STAIRWAY, x, y);
}
This method can be called at the end of the MapGeneration() method.
In the next chapter, we’ll work out a way to display the map and see the progress so far before moving on to the hallways.
Enumerations and the Switch Decision Statement – Microsoft Learning
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