Procedural generation in Python

Having tried many times to learn to program, and losing interest in whatever course I was following after about a month, I think I’ve finally found a way to learn that works for me: Project based learning.

I know: Duh.

Up until now the idea of just trying to make something without really knowing what I was doing, and in what order I should approach it, stopped me from trying this approach. Recently though, I just thought - fuck it.

So, I’m attempting to make a sandbox, city-builder game, partly inspired by the time I’ve spent playing Banished recently. I’m slightly obsessed, and have been writing bits and bobs during my lunch breaks, researching various problems when I should be working on other things, and generally spending hours frantically typing things that I only understand in the moment of writing, to be lost from my comprehension as soon as I click ‘save’.

The first really difficult task that I’ve come up against was trying to make a way to procedurally generate maps.

It turns out that this isn’t just me-hard. This is everyone-hard.

I’ve read many articles about Perlin noise, skimmed interviews with the creator of Dwarf Fortress, and watched all the footage on No Man’s Sky I could find.

And this is what I’ve come up with:

According to the linter I have installed, this function is too complex. Pisshh.

So. This is essentially a function I have created to create a layer of ‘noise’, and then to normalise that noise so it all sits between 0–1. I use this elsewhere in my code to assign an elevation to my maps.

Before I explain how this works, it’s worth saying that I have designed the maps in my game to be represented by an array of arrays. The length of each sub array is therefore the width of the map, and the total number of sub arrays is it’s height (which are also the two arguments that this function makes, to ensure that the noise map generated is the same size as the map itself).

generate_noise starts by creating an array of arrays, and filling it with 0s. I don’t technically need to do this, as the way in which this function adds noise is always from the top left of the array, across and down, so could just append. However, this ensures that there’s the right number of elements in the array, and also allows me to tweak the following code so that I add noise from various points.

Following this, it loops back through the array and adds a value between -1000 and +1000 to each element.

The way in which it does this is not completely random. If we went through and randomly assigned a value from this range to each element of the array then the map would be completely mad. As mentioned above, I’m currently using this function to generate a value for the elevation of each cell within my map. If it created noise randomly, then the elevation wouldn’t flow in a natural way, meaning that you could have the highest point and the lowest point next to each other (to take the most extreme example), whihc you would never see in nature. This would be like the deepest part of the ocean being next to mount everest.

I’ve therefore used a series of ‘if statements’ to control the way in which values are added or subtracted.

This first statement refers only to the first value in the array, and doesn’t change it in any way. This function will ‘normalise’ the values generated by this process later, which means the starting point is essentially arbitrary and might as well be 0.

This second if statement refers to the first row of the array only. It alters each value by choosing a number between -1000 and +1000, and then ading this to the value of the previous element of that list. This means that as we add noise, each element in the array has some relationship to the previous one in order to create that sense of ‘flow’ mentioned above.

This elif refers to the first value in each ‘column’ (i.e. the first value in each list within the array). Obviously, there is now previous value, as defined above, and so we cant use that to generate the new value for this elements. Instead, we can only use the value of the item above (i.e. the first value in the previous list).

This final part is a combination of the previous two. All other values need to have reference not only to the previous value on their row, but also to the one directly above it. Again, this is to make sure that these values flow in as natural a way as possible. This part of the code takes the average of the value next to the one it is currently altering, and the one directly above it and then adds or subtracts from that average value.

Whilst this is occurring there is a second set of if statements in this loop, which keep track of the maximum and minimum value in the array:

Once this has been done, the code then loops through the array a final time, and normalises the range of values so that they all sit between 0 and 1. The reason for doing this is so that I know the exact boundaries of the range and can make assumptions based on this. For example, I currently have 3 ‘terrain types’ (water, mountain, and plain), and I use the elevation of the cell to determine which of these each cell should be. (if the value is < .3 it’s water, >.6 it’s mountain, and everything else is plains).

This process ensures that the relationship between the values is retained, but the boundaries are changed.

When I use this to generate a map I get something like this:

(in this, the green is plains, the blue is water and the beige is mountains)

As you can see, this is by no means perfect. Everything always seems to generate in this streaked fashion, and I always get a few random squares here and there. The latter is more to do with how I determine the terrain type of a cell, whereas the former, I think, is to do with how I’ve gone about generating the noise itself.

As far as I can tell other games will have several layers of noise that interrelate (elevation, temperature, precipitation, etc), and they have biomes which are defined by the relationships of these values. They also seem to run simulations of erosion over the map that they originally generate.

For now, this is ok for me, and I can carry on with other elements of the game whilst continuing to research better ways to generate maps!