The fog of war is a feature common to many games that use any kind of map. It’s much more interesting when your player has to gradually discover territory than when it’s presented all at once. You never really know what your opponents are cooking up in those areas that you can’t see.
The classic Rogue RPG uses its own set of rules for this. The map is gradually revealed on each level as the player discovers it but there’s always some mystery about anything that’s sitting on … or lurking about … the map in any rooms other than the one the player is in at the moment. In this chapter we’ll look at the rules for this system and see how to add them to our project.
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.
The Rules
In Rogue, when you first enter a dungeon level, you find yourself in the middle of one room on the map with the rest of the level being blank.
As you travel around the map, it’s gradually revealed to you, one room at a time. Some rooms immediately light up as soon as you enter them and others are “darkened” so you have to discover the perimeter and can only see the spaces immediately around you.
So, according to our original requirements:
Portions of the map will need to be invisible when required. Rooms will need to be defined as fully visible on entrance (lighted) or discoverable as the player moves around them. The map class will need to maintain values for the actual character at a specific location and the character that’s displayed.
We also established that 75% of the rooms would be lighted on entrance. So let’s see how this works out for our program so far.
When a level is newly generated, the Visible property for all spaces can be set to False and remain that way until the player gets within one space of them. When a player enters a room, it has a 75% chance of the lights working in which case everything in it becomes visible. Otherwise, the player has to feel their way around and only sees things like monsters when they get within one space of them. In a non-lighted room, the space needs to be hidden again when the player moves away but hallways, room walls and the stairway need to remain visible.
One challenge with this is that the decision needs to be made the first time a player enters the room and then it needs to stay that way. The lights either work in a room or they don’t – re-entering a room shouldn’t change that. Rooms are not objects in this game with properties that can be changed but the condition needs to be stored somewhere.
Finally, there’s a difference in visibility between the room the player is currently in and all the other rooms. Lighted rooms remain lighted but, once the player is out of room, uncertainty comes into play. Monsters move around on their own and might pick up or drop objects so neither are shown for other rooms until the player gets back to them.
The Algorithm
Remember that the MapSpace class, which holds the actual data for each space on the map, has three character properties and a boolean property.
- The MapCharacter is the actual character that makes up the map itself; doorways, walls, hallways, etc..
- The ListCharacter holds anything that’s sitting on the map but not walking around, like scrolls, potions, etc..
- The DisplayCharacter is any occupant of the map – the player or monsters.
- The Visible property determines if the space itself is visible.
This gives us a few tools to work with. I’m trying not to make the MapSpace class any more complex than necessary but we can see that whether a space is visible and whether it’s been discovered are actually two different questions. The MapSpace class needs another boolean property which will need to be added to the constructors.
public bool Discovered { get; set; }
When the player enters a room, the Discovered property can be set to True for the necessary spaces and the Visible property can remain False for the interior if the room is darkened. The next time the player enters the room, the code will test the Discovered property and not retest for lighting.
The MoveCharacter() method of the Game class will grow some more to handle the first part of this. Each time the player moves, the method can set all spaces around the player as Discovered. For walls and other map features, they’ll be set to Visible. When a player is on a doorway, the room will be set to Discovered if it isn’t already and then a decision will be made about whether it’s lighted. If it is, everything is made Visible. If it’s already discovered, nothing needs to be done.
The MapLevel.MapText() function will need to handle the second half of the job as it’s actually making the decisions about what characters to output to the screen.
- If a MapSpace is set to Visible
- If it’s in the same region as the player, prioritize DisplayCharacter, ItemCharacter, then MapCharacter when deciding which character to output to the screen.
- If it’s in a different region, just show the MapCharacter.
- If a MapSpace is not set to Visible
- If it’s within one space of the character, prioritize DisplayCharacter, ItemCharacter, then MapCharacter.
- Otherwise, just insert a blank space.
The MapLevel class has a GetSurrounding() function that returns the MapSpace objects in all eight directions around a player so we should be able to use that to show the necessary character unless it’s a trap which we’ll add later. A trap becomes visible only when the player steps off of it.
The Code
Let’s start by actually raising the fog of war in the MapLevel class using a LINQ generated list of all the spaces. This can be called from the Game class constructor so the map is immediately hidden. It will also be called from the Game.ChangeLevel() method so that each level is shrouded.
public void ShroudMap()
{
// Raise the fog of war
List<MapSpace> mapSpaces = (from MapSpace space in levelMap
select space).ToList();
foreach (MapSpace space in mapSpaces)
{
space.Discovered = false;
space.Visible = false;
}
}
Game.MoveCharacter calls a new method.
// If this is a doorway, determine if the room is lighted.
if(player.Location.MapCharacter == MapLevel.ROOM_DOOR)
CurrentMap.DiscoverRoom(player.Location.X, player.Location.Y);
I decided to let the MapLevel class actually handle the changing of the MapSpace properties with the DiscoverRoom() method.
public void DiscoverRoom(int xPos, int yPos)
{
// Get region limits
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;
// Decide if room is lighted.
bool roomLights = (rand.Next(101) <= ROOM_LIGHTED);
// For all room spaces in region, set Discovered = True and
// Visible according to probability. Leave HALLWAY spaces alone
// and just focus on rooms.
for (int y = yTopLeft; y <= yBottomRight; y++)
{
for (int x = xTopLeft; x <= xBottomRight; x++)
{
if (!levelMap[x, y].Discovered)
{
if(levelMap[x, y].MapCharacter != HALLWAY)
{
levelMap[x, y].Discovered = true;
levelMap[x, y].Visible = roomLights;
}
}
}
}
}
The new DiscoverRoom() method is also called by the Game class constructor after the player is placed on the map and when changing levels so that the player’s room is activated.
At this point, things look promising when I run the program but only because the room’s lights are on. We still need to set the player and all surrounding spaces to visible in the MapText() method and the MoveCharacter() method still needs to handle the ongoing discovery of new spaces.
There should be a defined list of what map characters enable a space to be made visible so I added it to the MapLevel class and added the necessary constants.
public List<char> MapDiscovery = new List<char>()
{HORIZONTAL, VERTICAL, CORNER_NW, CORNER_SE,
CORNER_NE, CORNER_SW, ROOM_DOOR, HALLWAY, STAIRWAY};
Then the MapLevel class gets a new method to discover the surrounding spaces which can be called from the MoveCharacter() method. It uses the MapDiscovery list we added earlier to decide if specific spaces should be made visible.
public void DiscoverSurrounding(int xPos, int yPos)
{
// Discover new spaces
foreach (MapSpace space in GetSurrounding(xPos, yPos))
{
// Mark the space as discovered.
if (!space.Discovered)
space.Discovered = true;
// If this is a wall, stairway or anything else
// that should remain visible, mark it as visible.
if (!space.Visible)
{
if (MapDiscovery.Contains(space.MapCharacter))
space.Visible = true;
}
}
}
If I run the program, the changes are already working … to a point.
As long as the spaces and regions are set to Visible, it’s showing what it needs but our player is currently wandering around in the dark in the bottom center room because that room is not lighted. Also, that gold should not be showing in the left center room since our player isn’t in there. It’s still a very promising start, though. We just need to add the changes to the MapText() function which now needs to see where the player is on the map. The MapLevel class gets a new function.
public MapSpace FindPlayer()
{
// Return the mapspace containing the player.
MapSpace playerSpace =
(from MapSpace space in levelMap
where space.DisplayCharacter == Player.CHARACTER
select space).First();
return playerSpace;
}
Outputting the Text
Fortunately, the resulting MapText() function wasn’t as long or complex as I thought it might be. You can see it on Github but I’ll go over the algorithm here.
The function now uses the FindPlayer() function from the last section to find the player character on the map and the GetSurrounding() function get a list of the surrounding spaces.
MapText() still prioritizes the DisplayCharacter, the ItemCharacter and then the MapCharacter based on which is available. The function still uses a nested loop to walk through the map array and work with each map space. Just inside that loop, it determines the character to prioritize for each space.
if (levelMap[x, y].DisplayCharacter != null)
priorityChar = levelMap[x, y].DisplayCharacter;
else if (levelMap[x, y].ItemCharacter != null)
priorityChar = levelMap[x, y].ItemCharacter;
else
priorityChar = levelMap[x, y].MapCharacter;
As you see here, the character is now assigned to a variable for reference because I didn’t want to repeat this IF statement for both conditions.
Gold and items should only be visible in the player’s current room so we also need to know if the player is actually inside a room and, if so, which room. (The algorithm above said ‘current region’ but I discovered during coding that this didn’t work.)
After getting the player’s location, the function calls GetRegionNumber() to get the region number for that location and stores it for reference. Then, for each space, it does a comparison.
// Determine if player is actually in the room.
inRoom = (GetRegionNumber(x, y) == playerRegion &&
(playerSpace.MapCharacter == ROOM_DOOR ||
playerSpace.MapCharacter == ROOM_INT));
So, if the two region numbers match and the player is sitting on a doorway or an interior room space, we can know they’re actually in the room and not just in the same region.
Later, during testing, I found that when I moved on top of a staircase, the gold in the room suddenly disappeared and reappeared when I moved off of it. You can actually see this in the video at the end of this chapter. I immediately realized I’d forgotten about the STAIRWAY constant. Later on, traps will also be added inside the room and I don’t want to have to hunt down all code like this so the MapLevel class gets a new class-level List and the code in MapText() gets simpler.
// List of characters that occur inside a room.
public List<char> RoomInterior =
new List<char>(){ROOM_DOOR, ROOM_INT, STAIRWAY};
Then in MapText()...
inRoom = (GetRegionNumber(x, y) == playerRegion &&
(RoomInterior.Contains(playerSpace.MapCharacter)));
Finally, it all comes together in a simple nested IF statement according to the original algorithm.
if (levelMap[x, y].Visible)
{
// If the player is in the room or if the space
// is actually showing the player.
if (inRoom || levelMap[x, y] == playerSpace)
sbReturn.Append(priorityChar);
else
sbReturn.Append(levelMap[x, y].MapCharacter);
}
else
{
// If this space is within one space of the player.
if (surroundingSpaces.Contains(levelMap[x, y]))
sbReturn.Append(priorityChar);
else
sbReturn.Append(' ');
}
Now, let’s see it in action in this promo I made for the series.
Finishing Touches
I actually found this to be the most challenging task since the coding of the hallway functions earlier in the series but that just means that it’s that much more rewarding when it finally does work.
Looking over the first set of requirements, I remembered that I still need to hide some of the room doors and make the player search for them. We’ll look at that and the game’s title screens in the next couple of chapters.