Making a golfing roguelike in one week


I recently took part in the 2024 7DRL Challenge, a one week jam dedicated to creating a roguelike, or roguelike adjacent game, where I made Par for the Corpse, a golfing-roguelike combo where I tried to include staples of both genres.

For the golf piece, it’s all about trying to hit your ball to the hole in as few of swings as possible. For the roguelike, levels are procedurally generated, there’s an XP and upgrade system, and once you’ve hit the ball, you have to run to it via a grid and turn-based movement system while skeletons are trying to attack you! You can fight them off easily enough with a melee attack using your club, but that will count as an additional swing, or you could try to hit them with your ball on your turn so that you can work on your overarching goal at the same time, but at the risk of maybe having a less than optimal shot. It’s not much, but it does introduce a strategic element to the game that I think meshes with the golf aspect pretty well.

The results are still like a month out, so today, I’m just going to briefly talk about a few elements of this game’s development that I hope you’ll find interesting.

Generating level

The first element of the game I’d like to discuss is how the levels are generated, as this was actually something I was concerned about when I started the game. While I’m not the first to do a procedurally generated golf game, there isn’t a lot online about how others have done it, so I was going in blind on this one. I started at the high level by thinking about how the holes on a mini golf course are designed, which can oftentimes simply be stretches of straight green connected at various angles to other stretches of green with objects contained within.

That’s a pretty good fit for a grid-based system, even if it does mean some of the curvier or whackier designs have to be dropped. So I got to work on procedurally creating dungeons that had a general shape and feel of mini golf. At first, I was going to use pre-generated pieces that would snap together based on the design goals for each level, but I decided to go fully procedural to create a little bit more variety to the design.

For the actual generation, I first do a very coarse random walk to carve out the general shape of the level, and by coarse I do mean coarse. Starting out, only three steps are required, but this number slowly increases as the game goes on so that the levels become bigger. Each step represents a number of tiles that cycle up and down as you play so that you’re getting varying amounts of space as you go, but its typically anywhere from a 3x3 to 5x5 set of tiles per step.

After the level shape is determined, I place the hole and the player somewhere within the two steps furthest from each other and begin to populate the level by iterating over it, placing enemies and obstacles into the space, until the desired level of difficulty is reached. I’ll talk about how objects get placed in the level in just a moment, but since the generation is driven by the desired difficulty, let’s first discuss how I determine how hard a level is.

To determine the level of difficulty, I repeatedly use Godot’s AStarGrid2D class to calculate the path along the grid from the player to the hole and compare that distance to the distance that the player could hit the ball along a straight line. Obviously, the path calculated is not the same the ball would actually have to go, so these distances don’t line up exactly, but I’ve found that this metric works pretty well as a baseline. I then add one stroke per enemy, one additional stroke as a safety measure, and end up at the final par for the level. So as an example, a level where the path is three strokes in length and has two enemies in it will have a par of 3 strokes + 2 enemies + 1 buffer = 6 par, which I think actually works fairly well as I end up with a par that is a little generous for skilled players but can still be challenging to meet for someone getting the hang of things.

So now that I can calculate how hard a level is, how does the generator determine where, what, and how much to place in the level to meet those goals? Well, there are two options for making a level harder: adding walls and adding enemies. The decision between which to do is random, and this decision is just looped over and over until the desired difficulty is reached or the generator has tried and failed too many times to make the level more difficult while still remaining valid. A wall is placed directly in the path of the player to ensure it throws a wrench in their shots, while enemiees can be placed in or around the path by a few tiles since they can move into the way as you play. Each loop, I ensure that the path to the goal isn’t blocked, and that’s that.

At this point, the level has been generated, but I noticed that the levels could still have a bit of a uniform flatness that I didn’t love. It was obvious that enemies and walls had only been placed directly in your way, as any part of the level not on the direct path to the hole would be barren.

To solve this problem, I do one last step during generation and add some chunks of wall to random locations in the level. The size is random, but it’s typically more than just a single tile so that it feels less like random tiles thrown about and more like actual details to the level’s shape. This creates more visual variety, more potential opportunities for a bad shot to go wrong, ups the difficulty just slightly, and fixes the problem of there only being detail along the obvious path to the goal.

And that’s level generation. Obviously, the levels are still quite spartan due to the limited number of features, but I do think they work well enough, and they’re better than I was worried I was going to end up with when I first started diving into level generation. Plus, I don’t feel as though my system is too hacked together, nor is it too confined to a specific style, as I can see ways of expanding on and improving this generator without having to start over from scratch.

The ball

So with levels out of the way, let’s talk about the ball, which surprisingly provided a number of challenges with this game, the first of which was just the question of how it should move. Originally, I was going to make the movement of the ball be tile based just like everything else in the game in order to stick to conventions of the genre, with your shot angles snapped to eight directions along the grid, but this method didn’t really feel good as it made it harder to line up the shots you really wanted to make and there were some edge cases to resolving paths that didn’t have a satisfying solution, so I decided to let the ball move freely around the level even if the player and enemies were still stuck to the grid. But with this setup, how could you be sure where you needed to move to in order to pick up the ball for your next shot? It was a common occurrence that the ball would end up on the edge of multiple cells, but the player was stuck to the grid and only one cell was the “right” one. There was the potential for them to waste multiple steps trying to get to the right cell.

The solution I went for was to just be generous with where you can play the ball from. While there is the one correct cell the ball occupies, I also do a distance check from the player to the ball so that if you’re close enough to its location, you can pick it up wherever you are. It’s a simple fix, but I personally never had an issue with selecting the wrong cell after implementing it while also never accidentally picking up the ball when I didn’t mean to.

Another question that had to be answered was now that the ball could move freely, should it be physically based? When the ball was tile-based, I would just interpolate its position from cell to cell without any sort of physics calculations, using a Curve object to simulate its velocity slowing down as it reached its destination. Now, though, it could have more varied and complex paths, so did I need a physics body to help make it feel better? Honestly? Maybe. But in the interest of time, I instead opted to just keep the interpolation technique I already had in place, but feed it world positions instead of cell positions. It doesn’t feel perfect, but it works well enough for a one week game.

To get the bounces and path predictions right, though, I do still use some physics. When the player is aiming their shots, I send a raycast out from the ball and bounce it around the level accordingly until the ray has traveled the length of the shot at its current strength. These points are then passed into the movement interpolation code when a shot is made. The only downside to this technique is that it’s not very friendly to dynamic levels, as the state of everything has to be known ahead of time. Since skeletons are killed in one hit, it’s easy enough to bounce off of them once and then ignore them in future ray bounces, but this technique wouldn’t work as well if there were dynamic obstacles to the levels, so this is definitely a case of “good enough” for a jam, but would need to be reworked if I were to expand on this idea.

Closeout

And that’s the main items worth discussing, I think. I tried to keep my scope, and therefore time commitment, smaller with this jam than usual, so while I am happy with what I’ve made, there are certainly some obvious areas for improvement. Most importantly, it turns out that it is possible for the player to end up in a softlock on certain levels, so that’s not great. Plus, I think there’s still a lot of room for exploration in how these two separate genres can be merged together (I’ve certainly got a number of ideas for it!), but that’ll be a task for another time.