Rogue C# – Verifying the Game Map
Not every programming task can be completed in a single step; sometimes multiple parts of the code have to work together. If you’ve read the previous chapters in this series, the process of creating a roguelike map has probably seemed like a long one but, along the way, you’ve seen demonstrations of many features in C# code. In this chapter, we’ll finish the map generation and look at using the generic List<> class.
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.
Coding Decisions
In building the map, I decided that it was going to be entirely random and you’ve seen how I used the Random class in C# in combination with program constants to generate the rooms. Even pseudo-random chance is inherently quirky, though, and I kept coming up with maps that had one or more rooms completely isolated from the rest of the map because not enough exits had been added to the rooms. This makes the map unplayable.
Normally, the answer would be to improve the algorithm to ensure that everything was connected but the problem wasn’t happening that often and I finally decided to just have the program redo any map that wasn’t usable. In the chapter on the StringBuilder class, you saw how each map is being assembled from a complete array in less than 0.1 milliseconds. In another test, the entire generation of the map, including the population of the array itself, usually took less than 0.25 ms.
With numbers like that, there wasn’t much reason not to redo the map. The next step was to come up with a method that would actually verify if the map was usable.
The Algorithm
At first, I was going to check for both the presence of at least one exit on each room and for connections between all rooms but the RoomGeneration() method already ensures there will be an exit. The next possibility was that a vertical or horizontal group of regions might be isolated or that the entire group of rooms might be omitted because the random numbers had come up just the wrong way. I had to run through hundreds of maps for this to happen but here are a couple of examples from the Output window.
In both cases, the map failure results in blank spaces on the map. In the second screenshot, the absent connection between the first and second rows of rooms leaves an entire row of the array blank. The first screenshot has an entire row of rooms missing. Since the disconnect can happen in either direction, the method needs to look horizontally and vertically.
There’s also the issue that spaces on the edges of the array might be legitimately blank because of the random size of the rooms.
- Move through the array, one row and then one column at a time and ensure that each row / column has at least one character in it.
- Limit the inspection to the minimum space on the map expected to be occupied by rooms and hallways.
The Code
Remember that the code for this project is available on Github.
private bool VerifyMap()
{
// Verify that the generated map is free of isolated rooms or sections.
bool retValue = true;
List<char> dirCheck = new List<char>();
The new function, VerifyMap() returns a boolean value indicating if the map passed or not. We start off by assuming that it will pass. Since we need to check every space in a line on the array, it’s easier start there and test for a failure.
The FOR loop makes full use of the program’s constants in order to define a minimum space that should be populated on the array.
for (int y = REGION_HT - MIN_ROOM_HT; y < (REGION_HT * 2) + MIN_ROOM_HT; y++)
i.e.
for (int y = 8 - 4; y < (8 * 2) + 4; y++) or ...
for (int y = 4 <; y < 20; y++)
It might seem easier just to use the numbers in the last line but the first line shows the complete reasoning behind the loop by giving context to the numbers. This also means that the behavior of the program can still be changed in one place by changing the constants.
We’ve defined a reasonable area of the array that must contain something so now we need to look at each line and see if it does. The dirCheck List will contain a unique list of each char found on the row. The SPACE character is assumed so if there’s more than one unique character in the row, everything is fine.
The list’s Clear() function empties the list for inspection of a new row and then a simple FOR loop based on the array’s width assembles a unique list of the character’s found. The return value is set to the condition that the count is greater than one.
dirCheck.Clear();
for (int x = 0; x <= MAP_WD - 1; x++)
{
if (!dirCheck.Contains(levelMap[x, y].MapCharacter))
dirCheck.Add(levelMap[x, y].MapCharacter);
}
retValue = dirCheck.Count > 1;
if (!retValue) { break; }
If retValue is now False, then the break command breaks the program out of this loop because no more rows need to be checked.
If the return value remains true, however, we need to go on and check the array by columns so there’s a second FOR loop to do this but it’s encased in an IF statement so that it will only run if the code hasn’t found any problems yet. If the map has already failed, there’s no point in doing more work.
if (retValue)
{
for (int x = REGION_WD - MIN_ROOM_WT; x < (REGION_WD * 2) + MIN_ROOM_WT; x++)
Calling the Function
Finally, we need to call the verification from somewhere so let’s go to the MapLevel class constructor.
public MapLevel()
{
// Constructor - generate a new map for this level.
do
{
MapGeneration();
Debug.WriteLine(MapText());
} while (!VerifyMap);
}
Now, the constructor will create a new map and keep creating new ones until it gets one that passes. If the first one is right, there’s no need to do any more. As a bonus, the Debug line here will print any failures to the Output window for verification.
Going Forward
At this point, we’re able to generate a basic randomized roguelike game map which is a big step in the game. Now we need to start populating the map with characters, including the player, and other items and code some behavior into those items. This will require a number of new classes and a lot of logic but now it can be treated as a series of features to be added on one at a time. Now that an essential function is out of the way, the program can grow organically.
In upcoming chapters, we’ll look at creating a Player class for the player’s character and a Game class to manage the game operation itself. You’ll get to see how these classes interact to actually create the game experience.
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