TDD Lesson - Terrain Generation

09 January 2017

Have you ever wondered how the terrain in games like Minecraft is generated? There’s an old algorithm called the Diamond-Square Algorithm that is in common use. I thought it would be fun to implement this algorithm using TDD. Here’s a sample output from the result:

So I thought you might like to see the TDD steps I used to build this algorithm. Before we start, make sure you understand the algorithm by reading the link above. Don’t worry, it’s a very simple algorithm; and the linked article is extremely easy to read.

So how do you test-drive an algorithm like this? At first it might seem simple. For each test, create an small array of doubles, predict the results, and then write the test that compares the predicted results with the output of the algorithm.

There are several problems with this approach. The first is data-overload. Even the smallest array is a 3X3 with 9 different values to check. The next smallest is 5X5 with 25 values. Then 9X9 with 81 values. Trying to write a comprehensive set of tests, even for the 3X3 case, would be tedious at best; and very difficult for someone else to understand.

The second problem is that test-driving from the raw results forces us to write the whole algorithm very early in the testing process. There’s no way to proceed incrementally. You may recall from previous blogs I’ve written that I call this: “Getting Stuck”.

Yet another problem is that those tests don’t prove the the algorithm is working the way it is supposed to. They just prove that the resulting values are right. Now in most cases that’s not a problem. I’m a strong adherent of the Classical School of TDD, and I tend not to use my tests to inspect the internal workings of the software I am writing. The output values are usually good enough. In this case, however, the algorithm is the overriding concern.

Now, being that I am a classicist, I still want to check values. However, the values I want to check are not the output values of the algorithm, but the internal values that the algorithm uses. Specifically the coordinates of the cells that are being manipulated.

Consider: I want to know that the coordinates of the midpoint of the square are being calculated properly. I also want to ensure that the value of the midpoint is set by taking the average of the four corners. So I’m very interested in the calculation of the coordinates of those four corners. On the other hand, once I know that those coordinates are calculated properly, I don’t really care what the values of the cells are, or whether the average is calculated properly. I mean, averages are pretty trivial to test. So I really don’t need to test the average calculation yet.

The Diamond half of the algorithm has similar concerns. I want to make sure that the coordinates of the diamond points are being calculated properly, and that the coordinates for the inputs to those averages are calculated correctly. I also want to make sure that the diamonds at the edges of the array only draw from the three real points; and don’t try to reach outside the array for the fourth.

So, here’s the code for my tests. I’m not going to show you the code for the actual algorithm; because it’s not particularly interesting. Indeed, I suggest you use these tests to drive your own implementation. ;-)

The Code

We begin by using Stephan Bechtold’s wonderful HierarchicalContextRunner to help us organize our tests. If you aren’t familiar with this runner – shame on you. <grin>

Note the String named actions. This is going to be the heart of our strategy.

We start with simple validations. Our setup for this set of tests creates an instance of TerrainInterpolatorSpy, which you’ll see down at the end of the code. This test double is going to load that actions string with the values that we want to test.

This is a rather different slant an a spy test double. Rather than giving us booleans and flags to inspect about the calls of individual functions, this spy is going to load the actions string with the sequence of things that happened as the algorithm proceeded.

The first three tests are just very simple checks to make sure that the algorithm behaves properly when given invalid inputs.

Now, on to the real work. We’re going to look at the coordinate calculations. We start by making sure that the midpoint of a 3X3 square is calculated properly, and that the points passed to the average function are the correct points. Stare at that string below until you understand what it means. The code that builds the action string is in the TerrainInterpolatorSpy below.

Next, we make sure that the diamond points are calculated properly, that the inputs to the average function come from the right locations, and that, since all the diamonds on a 3X3 are on the edges, that the averages are taken from only three points.

OK, now we have to make sure that the square and diamonds are properly repeated, with the right size and locations. At this point we don’t care about the averages anymore because we know the coordinates for the inputs are calculated correctly. All we care about now is that the starting points, and sizes for the squares and diamonds are calculated properly. So we’re going to use a different spy. The TerrainInterpolatorDiamondSquareSpy loads the actions string with only the square and diamond actions, but not the averages.

OK, finally, let’s make sure the random factor and offset are being added properly. The offset is my own creation, so that I can drive the values up or down with a fixed offset. The third argument to the interpolate call is the random amplitude. The fourth is the offset. The TerrainInterpolatorWithFixedRandom test double simply turns off the randomness, and uses the absolute values of the random amplitude instead.

This test took a lot of hand calculation to create. And, of course, I did the calculation wrong, at first. So then I had to check the results from the algorithm, and modify the test.

The TerrainInterpolatorSpy loads the actions variable with the square, diamond, set, and average actions invoked by the algorithm. This test double simply overrides those functions of the main algorithm with others that load the actions variable.