(That title definitely makes me look smarter than I am)

On my spare time, I like to write code for my forever-work-in-progress traditional roguelike. I don’t expect to finish it, since it’s mostly a training ground for my favorite language/engine of the day, but I still enjoy trying new things. At this moment, I’m using Rust, and was motivated by the Porklike tutorial to implement drawn tiles with bitmasking. The general idea is to have nice-looking walls that form a cohesive structure, instead of a repetitive pattern of the same tile.

A point to consider, though, is that when used naively, a bitmask can reveal information to the player. As an example, this cropped screenshot of Caves of Qud:

The player can’t see behind those two wall tiles. We have the same amount of information for both tiles, yet one is clearly the beginning of a longer wall, while the other is just a pillar. I’m not saying this is a bad thing; maybe it doesn’t matter, maybe it’s even desirable to have this information. Personally I find it removes a bit of exploration and surprise.

A simple solution to this, and the one I chose, is to only apply the bitmasking algorithm on revealed tiles, and assume that non-revealed tiles could be walls; this way, a wall is not revealed to be a pillar until you’ve seen all the floor tiles around it. The small counterpart of this method is that some sprites will suddenly switch (e.g. from “wall part” to “pillar”) according the the tiles you’ve seen or not.

The algorithm I use to calculate the field of view (FoV) is the popular “Symmetric Shadowcasting”. It works really great, but there is a small problem:

This is what the algorithm sees:

On my screenshot, the green tiles show completed walls because they’re surrounded by floor tiles, but the ones in red are incomplete walls, because we technically don’t know what is under the ?.

So, since it’s not a wall, it’s obviously a floor. Is it a bug in the algorithm? No, not really.

A floor tile is considered to be inside the FoV when you can see its center point. This is the symmetric part of the algorithm, whose goal is to make sure that if you can’t see an enemy, then the enemy can’t see you either.

The missing floor happens to be technically right outside the FoV. That’s why the algorithm doesn’t reveal it, so as not to also reveal a potential enemy who couldn’t see you. While it is a desirable property for gameplay reasons, visually it just looks like a random hole in the line of sight.

Let’s adjust the algorithm, and change the scan() function from this:

### fov_compute.py
 
def reveal(tile):
	x, y = quadrant.transform(tile)
	mark_visible(x, y)
 
def scan(row):
	prev_tile = None
	for tile in row.tiles():
 
		# The tile is revealed if it's a wall,
		# or if it's a floor that satisfies the symmetry rule
		if is_wall(tile) or is_symmetric(row, tile):
			reveal(tile)
 
# (more code)

To this:

### fov_compute.py
 
-def reveal(tile):
+def reveal(tile, partial):
	x, y = quadrant.transform(tile)
-	mark_visible(x, y)
+	mark_visible(x, y, partial)
 
def scan(row):
	prev_tile = None
	for tile in row.tiles():
 
		# The tile is revealed if it's a wall,
		# or if it's a floor that satisfies the symmetry rule
-		if is_wall(tile) or is_symmetric(row, tile):
-			reveal(tile)
+		reveal(tile, is_floor(tile) and not is_symmetric(row, tile))
 
# (more code)

Now the mark_visible() callback function will be called with an additional parameter that specifies if the tile is an non-symmetrical positioned floor. You just need to manage the partial case in your callback, and flag such tiles as “visited”, but not “in view”.

The result:

The tile is correctly registered as a floor, it’s displayed as such, and if there’s an enemy on it, they will stay hidden until we’re coming close enough.