Hex Population by Regional Capacity

Was re-reading S. John Ross’ essay, “Medieval Demographics Made Easy”, and I wanted to figure out my own take with less number-fidgeting and more discrete quantities. Then, instead of generating an entire region at once, we can take it piecewise and maybe even do some procedural generation on the fly during a hex crawl.

Settlements & Base Population

Let’s start with basic types of settlements, and some base population numbers that we can modify as we please later. Why not like below, assigning the below types as well as a range somewhere between 1 and 20? Then we can roll d20 for what type of settlement we have.

d20 Settlement Base Population
1-10 N/A 1
11-14 Hamlet 10
15-17 Village 100
18-19 Town 1,000
20 City 10,000

A six-mile hex has an area of ~31 square miles. Finding the weighted average of the table above, I’ve determined that a random hex will have a (base) population density of 620 persons per hex or 20 persons per square mile. That’s quite low, but we can use the base quantities above in combination with another factor—such as the carrying capacity of the region.

Regional Carrying Capacity

Below, I’ve come up with five degrees of carrying capacity corresponding to the basic die sizes (except for 1d20).

Description SPD/sq mi
SPD/hex Capacity Die
Devoid 50 1550 d4
Sparse 70 2170 d6
Moderate 90 2790 d8
Dense 110 3410 d10
Sprawling 130 4030 d12

We can roll the region’s carrying capacity die and multiply that by the settlement’s base population. This means that the average general population density can be multiplied by the average die roll of the region in order to find the region’s specific population density (SPD). In the case of a dense region with a capacity die of d10 (which I originally worked with because it’s convenient), the region would have an average specific population density of 110 persons per square mile or 3410 persons per hex.

Since Ross says that an average medieval region would have a population density of 75 persons per square mile, we might find d6 to be the most apt capacity die—which is pretty convenient! The distribution of cities, towns, and villages is less accurate, as are the populations of individual settlements, but I would just rather use d20 instead of percentile dice. Maybe it can be helped by some sort of arbitrary rule, like if you roll a settlement of the same type as an adjacent one then you should make it of the next lower type.


Here’s the overall procedure:

  1. Determine the region’s carrying capacity, and find its corresponding capacity die.
  2. Roll for the hex’s settlement type, and find its corresponding base population.
  3. Multiply the hex’s base population by a roll of its region’s capacity die to find that hex’s total population.

Let’s say we have a hex in a sparse region, and it turns out to have a village. We multiply the village’s base population of 100 by the region’s capacity die of d6, which turns up a 3. This means our village has a population of 300 persons! Quick and easy.

I would like to apply population results above to something like the supply value system that Ross describes in his essay, to find how likely it is to find such-and-such in a given settlement. However, I think it's possible to do so without using hard numbers (maybe rendering the specific population values unnecessary). For example, say: every hamlet has a cobbler; every village has a blacksmith; every town has a tavern; and every city has a librarian. Stuff like that.

If you want to simulate a map, try the Python script below! It's generated as a simple grid, each row in a line, but in practice I think we would want to start from an origin hex and fill adjacent hexes. In that case, though, maybe we don't want to determine each hex in isolation, but instead to use something like a wave collapse function—which wouldn't be difficult if you restrict the number of possible states and transitions. Maybe a second post?

import random
import math

def get_population():

    roll = random.randint(1, 20)

    if roll >= 20:
        return 4

    if roll >= 18:
        return 3

    if roll >= 15:
        return 2

    if roll >= 11:
        return 1

    return 0

total_pop = 0
capacity = 6
types = {}

for i in range(0, 10):

    row = '  ' if i % 2 == 0 else ''

    for j in range(0, 10):

        local_pop = get_population()

        row += '{}   '.format(local_pop if local_pop > 0 else '-')

        total_pop += math.pow(10, local_pop) * random.randint(1,capacity)
        types[local_pop] = types[local_pop] + 1 if local_pop in types else 1


print('Average Hex Population: {}'.format(total_pop / 100))
print('Average Hex Pop. Density: {}'.format(total_pop / 3100))
print('Empty Hexes: {}'.format(types[0] if 0 in types else 0))
print('Hamlets: {}'.format(types[1] if 1 in types else 0))
print('Villages: {}'.format(types[2] if 2 in types else 0))
print('Towns: {}'.format(types[3] if 3 in types else 0))
print('Cities: {}'.format(types[4] if 4 in types else 0))


Popular posts from this blog

Plagiarism in Unconquered (2022)

OSR Rules Families

Bite-Sized Dungeons