# How can I generate random lakes and rivers in my game?

I have a 2D block building game and am trying to make randomly generated lakes and rivers. I have looked into the Perlin noise algorithm, but, I couldn't get it to generate random and nice results.

I have been trying to use the python noise library, but, it didn't create maps very randomly.

Is there some seed function I am missing on that library to make it more random? What variable do I change if I want it to go more random? If possible, give me a less technical answer, with less math and more python terms.

The map is a 2D tiled map. Here is some examples of the non-randomness of the other algorithm. The following code was outputted 3 times in a row. I was randomizing the octaves and frequency with something like this: freq = 16.0 * random.randint(1, 500000) * 0.000001 + 0.5

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1
1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1
1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1
1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1
1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1
1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1
1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1


This is the code that produced the output above:

"""Writes a 256x256 grayscale simplex noise texture file in pgm format
(see http://netpbm.sourceforge.net/doc/pgm.html)
"""
# $Id: 2dtexture.py 21 2008-05-21 07:52:29Z casey.duncan$

import sys
from noise import pnoise2
import random
octaves = random.randint(1, 500000) * 0.000001 + 0.5
freq = 16.0 * octaves
for y in range(30):
for x in range(40):
n = int(pnoise2(x/freq, y / freq, 1)*10+3)
if n>=1:
n=1
else:
n=0
print n,
print

• I added a little more, but, what do you need to know. Commented May 14, 2012 at 0:56
• You added good info, but we need a little more. What is "not very randomly"? Any screen shot to us see what is the given result, and what is the wanted result? How did you tried it? Any code for you to show us? What's your context? 2D or 3D? Tiled or polygonal? Sorry if all this is too much, But I'm only trying to help. The -1 wasn't mine, if you don't give info, the question will become a unfit for the site and they'll close it. So again, I'm trying to help. Commented May 14, 2012 at 1:11
• +1, now it's a good question :) I'm not good with perlin noise and all procedural generation, but, are you seeding the random object? If i'm not mistaken, its random.seed() So the system time will be used as seed. And instead of octaves = random.randint(1,500000)*.000001+.5 you can try: octaves = random.random() (it have the same result, you'll get a number between 0 and 1, but its much more possibilities than just 500000 numbers.) Commented May 14, 2012 at 1:27
• Thanks +1 :) :) Tried to plus 1 comment but i can't Commented May 14, 2012 at 1:28

Well, as it seems, you are not seeding the random number generator. In python, it can be easily done with just a random.seed().

And I can see too you're generating a number between 1 and 500000 and making it be between 0 and 1. It's a functional method, but it is capped to just 500000 possibilities. You're better with just using random.random() it already generates a number between 0 and 1, but with much more possibilities! If you still need a number between 0.5 and 1.0, as your code suggests, you could just do: (random.random() * 0.5) + 0.5

Your final code should look as follow:

import sys
from noise import pnoise2
import random
random.seed()
octaves = random.random()
# octaves = (random.random() * 0.5) + 0.5
freq = 16.0 * octaves
for y in range(30):
for x in range(40):
n = int(pnoise2(x/freq, y / freq, 1)*10+3)
if n>=1:
n=1
else:
n=0
print n,
print


That's all!

• I'm not clear how seeding has anything to do with it. Random numbers will still be random if you don't seed, but will repeat. The only symptom of not seeding should be the same random sequence every time you run the program (assuming the same seed is used each time). Commented May 15, 2012 at 20:17
• If you're getting the same output three times in a row as mentioned in your question, it is very likely because you have not seeded the random generator. Seed it and see if it fixes the problem. Commented May 15, 2012 at 20:30
• Ah I hadn't seen the "3 times in a row" bit - was reading, "Is there some seed function I am missing on that library to make it more random" part. Yes seeding for non repeating randomness :) Commented May 15, 2012 at 20:46

This isn't answering your specific programming question, but consider that creating lakes and rivers isn't about randomly placing blobs of water and strips of water between them. It's about terrain height - about depressions (basins) that turn into lakes, and water that flows from higher to lower spots.

If you want a great example of creating lakes and rivers that make sense, you might check out this blog post -> http://simblob.blogspot.com/2010/09/polygon-map-generation-part-1.html It's a good reference for this kind of thing if your goal is reasonably realistic hydrography.

It's very simple: if you're getting the same map three times in a row (or more), it's because you didn't randomize the seed.

What does this mean?

Computers are inherently deterministic (non-random), so they simulate randomness. It's actually repeatably random (that's why we call it "pseudo random number generator").

How does this work?

When you create a random number, you have the option of giving it a "seed." The important thing is if you always use the same seed, you will always get the same sequence of random numbers, in the same order. Always. This can be good or bad.

In your case, it looks like you're not seeding the random generator, and by default, you're getting the same seed -- it probably uses some component of your date/time. Hence, I recommend you randomize it.

As Gusatavo mentioned in his answer, you need to call random.seed(). The docs state that "if X [the default parameter] is omitted or None, current system time is used." This should be sufficient.

On the topic of rivers ...

I came across some articles that mentioned modelling erosion from rain and use the resulting map as a base of a water flow map. The original article is gone now, sadly, but I could find a link at Wayback Machine, so added it for reference.

Erosion

...

In most of the world, by far the largest influence on the shape of landforms is fluvial (water-based) erosion. Water flows downhill, carrying sediment along with it, carving out valleys and river basins. This is a massively complex phenomenon, and modelling it correctly is a very active research area, but we can get a long way by sketching a simple version of the process.

We need to start by tracing the routes that water would take over the grid. For each grid point, we say that water flows to its lowest neighbour, and so on down until we reach the edge of the map. This gives a map of water flow.

There's an obvious problem when we reach gridpoints which are lower than all of their neighbours. Do we route the water back uphill? This will probably lead to cycles in the water system, which are trouble. Instead, we want to fill in these gaps (often called sinks or depressions), so that the water always runs downhill all the way to the edge.

It's easy to see how to fill in a single gridpoint, but as the depression gets bigger, and possibly links up with other depressions, the number of possible cases multiplies enormously. Luckily, there's an algorithm for filling depressions, called the Planchon-Darboux algorithm.

The Planchon-Darboux algorithm

The algorithm works by finding the lowest surface with the following two properties:

• The surface is everywhere at least as high as the input surface.
• Every non-edge point has a neighbor which is lower than it.

To calculate this, we start with an infinitely high surface everywhere except on the edge, where we use the original heights. Then, on each iteration, we find points which have a neighbor which is lower than them, and set their height to their original height, or the height of their lowest neighbor (plus a small amount), whichever is higher. We halt when we can go a full iteration without changing any point.

With the water routing calculated, we can work out how much water is flowing through each point. I assume that rainfall is constant across the whole map, and iterate through the points in descending order, passing the rainfall, plus the accumulated water flux, from each point to its 'downhill point'. This gives a map of water flux, which usually converges into a nice branching river structure, with lots of small streams feeding a larger central channel.

Here's an example of the Planchon-Darboux algorithm implemented in C++

void gen::MapGenerator::_fillDepressions() {
NodeMap<double> finalHeightMap(&_vertexMap, _heightMap.max());
dcel::Vertex v;
for (unsigned int i = 0; i < _vertexMap.edge.size(); i++) {
v = _vertexMap.edge[i];
finalHeightMap.set(v, _heightMap(v));
}

double eps = 1e-5;
std::vector<int> *neighbours;
for (;;) {
bool heightUpdated = false;
for (unsigned int i = 0; i < _heightMap.size(); i++) {
if (_heightMap(i) == finalHeightMap(i)) {
continue;
}

neighbours = _neighbourMap.getPointer(i);
for (unsigned int nidx = 0; nidx < neighbours->size(); nidx++) {
double nval = finalHeightMap(neighbours->at(nidx));
if (_heightMap(i) >= nval + eps) {
finalHeightMap.set(i, _heightMap(i));
heightUpdated = true;
break;
}

double hval = nval + eps;
if ((finalHeightMap(i) > hval) && (hval > _heightMap(i))) {
finalHeightMap.set(i, hval);
heightUpdated = true;
}
}
}

if (!heightUpdated) {
break;
}
}

_heightMap = finalHeightMap;
}


Other links and/or approaches that could be interesting: