Categories
alchementalist alchementalist devlog personal blog

Alchementalog #6: Burying Treasure and Stopping Draughts

In which we go through how treasure rooms and trap rooms are decided (and finding where to place those pesky doors)


All Alchementalogs


Table of Contents


The Goal

So, we’ve just gotten done with some clean-up and optimisation of the previous room gen code. Now let’s move on to adding some interactive elements. I think an important part of roguelike/lite proc-gen is the predictable/chaotic balance. We want the player to be able to assess a situation and formulate an effective plan due to familiarity, but we also want to be able to play with that familiarity just enough that they have to continue to think on their feet during the run.

What does this mean for Alchementalist? Well, at least right now, it means I want to be able to sort rooms into particular “buckets”. The buckets will be things like a “trap room”, a “treasure room”, etc. So in this devlog, I’ll be going through the process of figuring out how to sort the rooms we have made into a few of these buckets and tackling the problems we face along the way.


The Center Marks the Spot

The first room type I started to implement was treasure rooms. This should be relatively easy to implement. Firstly, I add a new step to the generation which picks how many treasure rooms there are going to be in total for that level (right now it picks between 2 and 4 rooms to be treasure rooms).

Then I have two “modes” for deciding where to place the treasure in the room (just a simple random roll and a switch statement). The first is a very simple “place a treasure chest on the center cell of the room” (I also delete anything that might be under the chest, like a flavour item or something).


Every Nook and Cranny

The second mode is a little more elaborate. The map generation sometimes leads to a rooms having little “arms” extending off from them, usually one tile wide and a few tiles deep. Here’s an example:

The top extended corridor hasn’t been drilled by the pathfinding algorithm, it’s just a natural little branch of the actual room. So we want to find these areas and place a treasure chest at the dead-end. So how do we find these little nooks?

Well, first I start looping through the map. For each cell, I check the 4 neighbouring cells in the cardinal directions. Once I have a point where there is only one floor section around it (as in, all the surrounding cells but one are walls), I know it’s a dead-end. Now I just have to find out how long the corridor is (this step could be omitted, but I think it’s good if I can have some control over whether chest gets placed in deep nooks or shallow nooks).

So we have a dead end, now we have to start “stepping” out of it. We’ll first move to the neighbouring open cell we found in the previous step, then we’ll check the cardinal directions again (ignoring the direction we came from). If again, we only have one floor cell, we know the corridor is still going, so we move to that new floor cell we found and repeat the process. As we are doing this, we keep track of how many valid movements we made. Once we reach a cell where there are more than one open floor cell around it, we know the nook we are exploring has ended. The number of cells we moved to tells us how deep the nook is. Right now, I’ve got it so that any nook that is deeper than two tiles is a valid point to place a chest. Here’s the code for this entire process:

var treasure_placed = false;
// Loop the map
for (var xx=0;xx<map_width;xx++) {
	for (var yy=0;yy<map_height;yy++) {
		// If the floor map is a wall tile, ignore it
		if (floor_map[xx][yy] == SOLID) {
			continue;
		}
		// sx and sy here are used to keep track of each cell as we begin our explorations
		var sx = xx;
		var sy = yy;
		// dir_count is used to keep track of how many floor cells are around the current one
		var dir_count = 0;
		// dir keeps track of the previous direction we moved in so we can skip it
		var dir = 0;
		// original_dir keeps track of the direction we moved on the very first tile, so we know what angle to make the chest face
		var original_dir = 0;
		// valid is used to keep track of whether the cell we are on is still a valid "nook" cell
		var valid = true;
		// Here we start checking the cardinal directions
		for (var i=0;i<4;i++) {
			var d = i*90;
			var nx = sx+lengthdir_x(1,d);
			var ny = sy+lengthdir_y(1,d);
			// Make sure the neighbouring cells are in the bounds of the map
			if (nx < 0 || nx >= map_width || ny < 0 || ny >= map_height) {
				continue;
			}
			// If they aren't solid, we'll add to dir_count and set the dir variables...If dir_count > 1 we know the neighbouring cell is no longer valid as a nook
			if (floor_map[nx][ny] != SOLID) {
				dir_count++;
				dir = d;
				original_dir = d;
				if (dir_count > 1) {
					// Once we know the cell isn't a valid nook cell we set valid to false and break out of the neighbour checks
					valid = false;
					break;
				}
			}
		}
		// If the first cell checked wasn't valid, we can break out of this whole section and continue on checking the rest of the map
		if (!valid) {
			continue;
		}
		// Otherwise, we start moving along the nook
		if (dir_count == 1) {
			var cx = sx;
			var cy = sy;
			var valid = true;
			// We want to make sure the nooks are at least 3 cells in length, so since we've already checked one cell, we need to check two additional cells, so we repeat the process twice
			repeat(2) {
				// This whole section is basically a repeat of the section above
				cx += lengthdir_x(1,dir);
				cy += lengthdir_y(1,dir);
				var dir_count = 0;
				for (var i=0;i<4;i++) {
					var d = i*90;
					// Here we get the difference between the current direction d and the previous direction dir
					// If the angle difference is 180, we know we are looking back at the direction we came from, so we can skip this check with a continue
					var diff = angle_difference(d,dir);
					if (abs(diff) == 180) {
						continue;
					}
					var nx = cx+lengthdir_x(l,d);
					var ny = cy+lengthdir_y(l,d);
					if (nx < 0 || nx >= map_width || ny < 0 || ny >= map_height) {
						continue;
					}
					if (floor_map[nx][ny] != SOLID) {
						dir_count++;
						dir = d;
						if (dir_count > 1) {
							valid = false;
							break;
						}
					}
				}
				if (dir_count != 1) {
					valid = false;
					break;
				}
			}
			// If we managed to check all three cells and they were all valid, we can place down a treasure chest at the original sx and sy position and mark the room as a treasure room
			if (valid) {
				var _id = floor_map[sx][sy];
				if (!regions[_id].treasure) {
					regions[_id].treasure = true;
					with (instance_create_layer(sx*CELLSIZE+CELLSIZE/2,sy*CELLSIZE+CELLSIZE/2,"treasure",obj_chest)) {
						image_angle = original_dir;
					}
					treasure_number++;
					treasure_placed = true;
					break;
				}
			}
		}
	}
	if (treasure_placed) {
		// Finally, once we've placed a treasure chest, we break out of the entire map checking, as we want to be able to pick the mode of treasure placement again
		break;
	}
}

It seems kinda long, but it’s just a tedious series of checks. There is one problem that I’ve got to fix: since we start looping through the map from left to right, top to bottom, each time it means that the algorithm is going to find nooks on the left-side before it finds nooks on the right hand side. It will only manage to get to the right hand side if there are no valid nook placements on the left hand side. An example of a fix to this would be something like this:

Instead of simply looping through the map looking for a nook, I could add all the regions to a list, select one from random (and delete it from the list) and then loop through the cells in the region looking for a nook. This would remove the sided bias.

Another fix could also be to use a breadth-first search, instead of a map loop, and randomly select the cell it would BFS out from. This would also remove the sided bias. However, that’s a problem for future me.

In any case, we have treasure rooms done. Something I want to implement in the future is adding a trigger to the chest, so that something could happen in the room once the chest is opened.

So let’s have a look at trap rooms. The first thing I think of when I think of a trap room is an “arena” of sorts where you get locked into a room and have to kill a certain number of enemies (or kill a spawner or whatever). What would we need to implement that?


Mellon (Speak Friend and Enter)

We’d need to know the locations of the all the entrances to the room, hereby known as “doors” (lol). But how do we find where the doors are?

My initial impulse when I was tackling this problem was to make it so that corridors mark doors as they are drilling out the walls. Any time it moves from a floor to a wall cell, it should mark that as a door. So I implemented that, but it didn’t turn out well.

There’s a ton of edge cases and weird points where it would place doors in stupid locations. For instance, if the algorithm ran over a pillar tile, which are considered walls, it would place a door there, but of course a pillar in the middle of a room isn’t a good place for a door. I could slowly work my way through all the edge cases and add checks for them, but that felt dumb and labour-intensive.

So I spent some more time thinking about the information I had access too about the map. In order to place the centers of each room properly, I had already built a distance map from the borders. During this process, I was also saving the borders of each room, with these borders each sitting over the wall tiles that surrounded the floor tiles of a region. This was all being done before I created the corridors with A*. Looking over all of this information, I could see a solution for findings doors that might work.


Defined By Our Borders

Since we have the border cells of each room and these were created before the corridors were drilled what that meant is that I would have a border cell that existed over the empty floor cell generated by the corridors. By running a check through all the border cells I could compare them to the floor map and see what the value of the floor map was under each border tile. Most border cells would be above wall tiles in the floor map, but if a corridor had been drilled out, the border cell over the corridor would now be sitting over an empty floor cell in the map. So I knew that was where a door should be placed.

I coded up the system and checked it out and voila, it worked perfectly (well, almost perfectly, there was one edge case I had to check for which led to doors being doubled up in certain scenarios, but this only needed a very simple check to fix).

Here, the areas of the room that are considered doors are highlighted with a green rectangle. Now I had the entrances to a room marked, I could move on to the next step.

First we add all the rooms to a list, then we start randomly selecting from that list (and deleting the entries we select). We run a check on the selected room and compare it’s number of doors and how many cells it has. If it has 2 or less doors and more than 30 cells, it will be selected as a trap room.

If it fails that check, we then see if it’s a treasure room. If it is, it has a 25% chance to also be turned into a trap room.

We continue selecting and checking rooms from the list until we have between 3 and 5 trap rooms. Once we hit the number we want to create, we exit the entire process.


The Way Is Shut

Now whenever we “upgrade” one of the rooms into the specialty rooms, we set a boolean (such as treasure or trap) to true in the data structure holding all the information about the room. The final thing to note is that when we build the floor map, each cell is marked holds the id for the room it belongs to. This means whenever the player walks over a new cell, we can quickly check the id that the floor map holds, use that id to grab the data structure for the room that cell belongs to and check to see if it’s a trap or a treasure room. This allows us to do the “doors shut as soon as you’ve entered”-style traps for the trap rooms without having to do much processing for the cells the player is visiting.

Let’s have a look at the treasure and traps room. In the gif below, the first room is a simple treasure room where the treasure has been placed in the center of the room. The second bigger room is a combined treasure and trap room, where the treasure has been placed in a nook. Since this map generation work is being done in a separate project to the main Alchementalist project, I don’t have a player to run into the trap room, so I substituted a mouse click to activate the traps, which you can see at the end of the gif.

One detail you might notice about the map is that there are these green-blue splotches all over the map. These are part of the ore generation, which will be the topic of the next devlog.

Oh, also, I haven’t been saying this in the previous devlogs, but if anyone reading this has any cool ideas they think might work well in the game (such as new room types or that kind of thing), I’m all ears. Leave a comment and let me know. Until next time!

By RefresherTowel

I'm a solo indie game developer, based in Innisfail, Australia.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s