Middle Earth: How to create a Climate System from nothing

In which I refuse to invent weather, and instead steal a whole year of it from 1950.
Regional climate system of Middle-earth based on real 1950 European data
Regional climate mapped to real-world Köppen analogs and 1950 hourly data

The temptation I resisted

The easy way to give a fantasy map “weather” is to make it up. Roll some dice, pick “rainy,” move on. It works, and it feels hollow, because invented weather has no memory: it never knows that the coast is milder than the interior, that mountains steal the rain, that summer nights in the south stay warm.

Since the whole project is built on the idea that Middle-earth is georeferenced onto real European coordinates, I had a better option sitting right there: use real weather.

But which real weather? Modern data is contaminated — cities, highways, the urban heat-island effect, a century of industrial warming. None of that belongs in a Third-Age world of villages and forests. So I went back to 1950: recent enough that good hourly records exist, old enough to be before the boom. A pre-industrial-ish Europe, which is about as close to “Middle-earth weather” as physics will politely allow.

The core idea: every region has a real-world twin

The trick that makes the whole system work is a matchmaking exercise. For every one of the 87 regions of Middle-earth, I picked a real European location whose climate feels like that region should feel, and pinned it down with its Köppen classification and real coordinates.

The Shire is the Cotswolds. Rohan is the Hungarian steppe. Lórien is the primeval Białowieża forest. Mordor is… a volcanic corner of eastern Turkey, because of course it is.

Each region simply points at a climate_zone:

-- regions.climate_zone_id → climate_zones.id
-- climate_zones stores: koppen, analog_location, analog_lat, analog_lon,
--                       desc, temperature pattern, precipitation pattern

Then I downloaded a year and a half of history

For each of those analog locations I pulled hourly 1950 weather (ERA5 / Open-Meteo style) into the climate_data table: temperature at 2 m, relative humidity, precipitation, snowfall, cloud cover, wind speed and direction, surface pressure, soil moisture, evapotranspiration, shortwave radiation.

The result: 744,600 hourly climate rows. An entire simulated year, breathing, for every climate zone on the map.

The time-travel trick

Here’s the part I’m quietly proud of. When you look at the weather in-game “right now,” the system takes today’s date, keeps the month, day and hour, and swaps the year for 1950:

// today → the same moment in 1950
const now = new Date();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day   = String(now.getUTCDate()).padStart(2, '0');
const hour  = String(now.getUTCHours()).padStart(2, '0');
const timestamp1950 = `1950-${month}-${day} ${hour}:00:00`;

So if it’s a drizzly October afternoon where you’re sitting, it’s a drizzly October 1950 afternoon in the Shire too. The same mapping happens on the client (useGlobalClimateTime) and the backend, so the calendar, the map and the story all agree on what the sky is doing. You can also wind the calendar forward or backward to visit other seasons.

There’s even a get_climate_at_point_with_transition() PostGIS function so that borders between climate zones blend instead of snapping — no hard line where “temperate” instantly becomes “desert.”

Every region and its real-world climate twin

This is the heart of the system — the complete matchmaking list. (Middle-earth on the left, the real 1950 weather source on the right.)

Middle-earth regionKöppenReal-world analogCoords (lat, lon)
AndrastCfb-coastalCape Finisterre, Galicia (Spain)42.88, -9.27
AnfalasCsaLanguedoc coast, Hérault (France)43.40, 3.70
AngmarDfcJotunheimen valley (Norway)61.60, 9.00
AnórienCsa-CsbInterior Umbria, Perugia (Italy)43.10, 12.39
BreeCfbCotswolds, Gloucestershire (England)51.83, -1.82
Central Misty MountainsET-mountainGotthard Pass (Switzerland)46.55, 8.57
DaleDfbNowy Targ, Lesser Poland (Poland)49.48, 20.03
Dol GuldurDfbPolesie, Belarus/Ukraine border52.10, 27.50
DorwinionBSk-DfbSea of Azov coast (Ukraine)46.85, 38.00
EregionCfbEifel, Rhineland (Germany)50.37, 6.65
FangornCfb-DfbBayerischer Wald, Bavaria (Germany)49.05, 13.35
ForochelDfc-coldFinnmark coast (Norway)70.66, 25.10
HarondorBShAlmería, Cabo de Gata (Spain)36.77, -2.19
Iron HillsDfcChelyabinsk Oblast, Ural (Russia)54.70, 61.40
LórienCfbBiałowieża Forest (Poland)52.70, 23.86
Mirkwood WildsDfbBieszczady, Poland/Ukraine49.20, 22.50
MordorBWhDoğubayazıt, Eastern Anatolia (Turkey)39.55, 44.08
North Misty MountainsET-mountainJotunheimen (Norway)61.60, 8.30
NurnBSh-BSkLake Urmia basin, NW (Iran)37.50, 45.50
RivendellCfb-mountainEngadine Valley (Switzerland)46.50, 9.83
RohanDfbHortobágy (Hungary)47.60, 21.00
Talath UichelET-DfcInari, Lapland (Finland)68.90, 27.01
The ShireCfbCotswolds, Gloucestershire (England)51.83, -1.82
Woodland RealmDfbBieszczady Forest, Poland/Ukraine49.20, 22.50

(A representative slice of the full 87-region table.)

A few things jump out:

  • Ireland and Britain do a lot of heavy lifting. Most of Arnor, Lindon and the Shire lean on the mild, wet Atlantic fringe (Cfb/Cfc) — exactly the damp, green, “it might rain” mood the north of the map deserves.
  • The south warms up sensibly. Gondor’s coasts borrow Mediterranean Csa/Csb climates from Italy, Spain and southern France.
  • Evil is dry. Mordor (BWh, hot desert), Nurn and the eastern steppes reach all the way to Turkey, Iran and the Ukrainian/Russian plains. The bad neighbourhoods are, geographically, the parched ones.
  • The far north is genuinely Arctic. Forochel and Talath Uichel pull from Norwegian and Finnish Lapland (Dfc/ET), up past 68° latitude.

What this buys the story

Because the weather is real and seasonal, everything downstream inherits that realism for free:

  • A journey in winter through the northern moors is cold and miserable in a way the data itself enforces — I never had to hand-author “it is cold here.”
  • The AI narrator receives real temperature, precipitation and cloud for the exact day and place, so its descriptions of sky and mud are grounded, not generic.
  • Travel cost can react to weather upstream of the route calculation.

Lessons from stealing a year of weather

  • Real data has memory; invented data doesn’t. Coastlines stay mild, interiors swing hard, mountains bite — all for free, because 1950 already knew.
  • Matchmaking is the design work. The code is easy; deciding that Rohan is Hungary and Mordor is eastern Anatolia is the part that gives the world its soul.
  • Blend your borders. Transition zones between climates were the difference between “a map with regions” and “a world.”

Next: with ground and sky in place, I had to let people actually travel across it — without accidentally routing them through Mordor. → Getting directions on Middle Earth (without passing through Mordor)