Gray Horizon development log

The Rules That Built GrayHorizon

Before Gray Horizon had a world, an economy or a single player, it had a set of rules about what it was allowed to become.

The very first test world

Long before Gray Horizon became the browser game I am building today, I played eRepublik. I didn’t stay for very long, and I can’t say I understood much of it either, but it fascinated me. There was something very different about a game where the countries were filled with actual people. Politics existed because people wanted political power. Newspapers existed because somebody had something to say. Wars mattered because there was another group of players on the other side. Years later, I stumbled into that world again through WarEra. I saw a Reddit post from people from my country essentially calling for reinforcements.

They needed more people to join and help, and I thought: sure, why not? This time I stayed. And eventually I realized something slightly strange:

I didn’t really fall in love with WarEra. I fell in love with the people playing WarEra. (no homo)

The game gave us the map and most of the mechanics, but the community made it’s own rules about how it would govern the country. The arguments. The elections. The alliances. The newspapers. People taking things far more seriously than I thought possible, its only a game after all. That distinction eventually became one of the most important ideas behind Gray Horizon.

Gray Horizon existed before Gray Horizon The name itself actually predates the game I am making now. My first Gray Horizon was a single-player grand strategy project in Unity. It shared some of the same DNA: countries, geography, politics, strategy and a world that could change around the player. But it was fundamentally a different kind of game. WarEra made me reconsider that. The more I played, the more I found myself thinking about things I wished existed. Why does this system stop here? Why can’t players influence this? Why is this predetermined? Why isn’t geography more important? Why does the economy work like that? And, probably most dangerously for the amount of free time I would have over the following weeks: How would I do it? I’m one of those people who has a hard time seeing something that could be improved without immediately wanting to improve it. So the single-player Gray Horizon gradually stopped making sense to me. What interested me wasn’t simulating a society full of convincing computer-controlled people. It was giving real people enough power that they could build one themselves. That led to the current idea: modern Earth, and thousands of potential players entering it not as omniscient rulers, but simply as people, whether they choose to be themselves or take on an entirely different persona. it’s up to them, and I think that is the beauty of such games. That was the point where Gray Horizon stopped being a traditional grand strategy game. These ideas made me realize that these kinds of games aren’t traditional strategy games. Gray Horizon to me became something closer to a social platform with a geopolitical simulation wrapped around it.

And before I built it, I wanted rules. A complicated game gets rules whether you write them down or not. The dangerous part is letting all of them live only in your head. I already had most of Gray Horizon mentally designed, but “I know what I mean” isn’t very useful once a project starts becoming complicated. So I sat down and wrote a Game Design Document. And then I kept writing. The first version that entered the project repository was already 5,184 lines long, divided into 28 parts, 166 sections and three appendices. It contained 19 explicit Design Laws, an entire list of things that should not be built, and a ten-phase plan for turning the design into a functioning game.

The first lines of the original Gray Horizon design document: the title, "Unified Game Design Document & Implementation Constitution", version 1.0, design epoch August 2026, then the start of section 2, Design Laws, with Law 1 "The player is a citizen first", Law 2 "One human, one citizen" and Law 3 "The world is permanent". The file's line numbers run down the margin.
The opening lines of the original design document and the start of its Design Laws, as they were in the very first version of the file. The numbers in the margin are the document's own line numbers.

In normal-document terms, it was well over a hundred pages. It took hours. And, looking back, writing it was probably one of the most valuable things I did for the project. My thought process was simple: I spend time writing and designing now, therefore I avoid a headache later, and I was right, it did help me. I never expected the GDD to predict the future. What I wanted was to force myself to answer the annoying questions early, before every answer turned into code.

There is a huge difference between: “The game will have politics.”

and: “What exactly is the player in this world?”

The second question changes everything about how you think games are made. So I tried to work from principles downward. You can think about the original design in roughly four layers:

Principles -> systems -> rules -> numbers

The further down that chain you go, the less sacred something should become. “The player is a citizen” is a principle. “Countries may have different government structures” is a system. “An election works in this particular way” is a rule. “An election lasts exactly X days” is a number.

If playtesting later tells me that X is terrible, changing it doesn’t cost me anything meaningful. It’s a simple change. If I suddenly decide that players should actually control entire countries from the sky, I’ve changed the game.

Four stacked layers, marked hardest to change at the top and expected to change at the bottom. Principle: the player is a citizen; change it and it is a different game. System: countries can differ in government; change it and whole systems move. Rule: an election follows a set process; change it and a few screens change. Number: each election stage lasts 24 hours, the first design document's value, already changed.
Changes get cheaper the lower you go. The 24 hours per election stage is the real number from the original document, and it's already been changed since.

Rule 1: You are a citizen

The first design law was very simple: The player is a citizen first.

That sounds almost trivial until you follow it through. In Europa Universalis, you are France. You are the country itself. France’s treasury is your treasury. France’s armies are your armies. France’s diplomacy is your diplomacy. The state has one intelligence controlling it: you. Gray Horizon cannot work like that. If you are Serbian, Serbia is not you. You are one Serbian. Maybe you work in a factory. Maybe you own the factory. Maybe you write a newspaper complaining about the government. Maybe you join a political party and eventually become part of that government. Maybe you leave Serbia entirely. The important part is that none of those roles are guaranteed. The country belongs to the people currently participating in it. That one decision creates most of the game. You need employment because a citizen needs somewhere to work. You need companies because somebody has to employ them. You need journalism because citizens need a way to speak publicly. You need elections because somebody has to receive political authority. You need military organisations because a war cannot just be “the Serbia player clicked Attack.”

Design Law 1, "You are one citizen, not a country", with six branches beneath it. You work, so there are jobs and wages. You own, so there are companies. You write, so there are newspapers. You vote and run, so there are parties and elections. You fight, so there are military units. You live somewhere, so there is citizenship, and travel.
One design law, and six systems that follow from it. All six are in the original design document.

A lot of Gray Horizon’s complexity isn’t there because I wanted a giant feature checklist. It follows from asking: If the player is really one person inside this world, who actually does this? That is a useful game-design trick in general. A good foundational rule should generate consequences. If a “design principle” never forces you to make a difficult decision later, it probably isn’t much of a principle. The community is the content This is probably the idea I care about most. I don’t think a game like Gray Horizon survives because I can continuously manufacture enough content for everyone. I couldn’t. No developer could. Instead, the systems have to make players interesting to one another. That is why I sometimes think about Gray Horizon more like a social platform than a traditional game.

Not because I want to build Facebook with tanks. Please, no.

I mean it in the sense that Facebook doesn’t have to design the conversation you’re going to have tomorrow. It gives people structures for forming relationships and communicating, and the people provide the content. Gray Horizon needs to work in a similar way. I can create elections. I cannot create a political rivalry. I can create newspapers. I cannot create a scandal. I can create military units. I cannot create the group of people who become famous for fighting together. I can create diplomacy. I cannot write the history of two countries that spend six months alternately fighting and cooperating with each other. That part belongs to the players. Which means that a mechanic is most valuable when it gives people another meaningful reason to interact. This also gives me a useful filter when deciding whether a feature deserves to exist. Does this create an interesting decision? Does it connect players? Does it create something other players can react to?

If not, maybe it is technically impressive but socially dead. That distinction has already killed more than a few ideas.

Rule: money cannot buy power

There was another rule I considered non-negotiable from the beginning: Gray Horizon cannot be pay-to-win. This matters even more in a social game than it does in a normal multiplayer game. I couldn’t imagine spending weeks, months even, grinding only to be beaten because the enemy has deeper pockets. Personally, it infuriates me. Don’t get me wrong, plenty of games outside this niche do it, and quite successfully, but I care about this game.

So then you ask: how does the game actually make money? By offering you simple incentives to do so, only if you want to, the game isn’t a gacha and it never will be, it will not force your hand, ever. This is my philosophy.

That is partly a monetisation decision, but it’s also a simulation rule. Once real money can manufacture in-world power, every other economic decision I make about making the game balanced and fair becomes fake, because people literally spawn money out of thin air, that is not fair, even less balanced.

The map isn’t decoration

Another original design law said that the map is the primary interface.

Greatly inspired by how Paradox games work, the map being the main interface the players interact with, came very naturally to me. I really couldn’t imagine it any other way.

I know this may be a point of contention for some people, because they absolutely don’t like looking at maps, but I do, I’m even half convinced to go back to regular maps instead of GPS for navigation, if only I weren’t lazy for modern convenience :D

It’s extremely easy to get this wrong. If you approach a browser game like a normal web project, you end up building a SaaS website and calling it a game. It doesn’t work like that. This is a game that works in your browser, and as a developer, you have to treat it like that. Pretty soon you have a perfectly competent web application with a map buried inside it. Gray Horizon would later fall into exactly that trap anyway. More than once.

But having the rule written down gave me something important to argue against myself.

This may work, but is it still the game I intended to make?

We will get back to that in another devlog.

The first evening
The first version of the game: a dark web page titled "Test Continent", a menu down the left reading World, Citizen, Country and Activity, six coloured rectangles in the middle standing in for provinces, and a white "World briefing" panel on the right listing three fictional countries.
Today
The game today: a world map fills the whole screen, countries in different colours, a menu down the left with Map, Today, Inventory, Work, Market, Chat, Press and more, and a "Next step" card at the bottom suggesting where to earn a first wage.
On the first evening the map was a small picture on a page called “Test Continent”, with three made-up countries. Today the map fills the screen and everything else sits around it.

Sometimes good design starts with “no” One of my favourite parts of the original GDD wasn’t the feature list. It was the Do-Not-Build list. Before deciding everything Gray Horizon needed, I also wrote down things I specifically did not want. The early document explicitly ruled out ideas including national currencies, delivery timers, RPG-style character stats and abstract “diplomacy mana.” It even ruled out unnecessary infrastructure complexity such as jumping to microservices and Kubernetes just because the project might someday become large.

Some of those deserve explanation.

Let’s take Diplomacy mana for example.

A lot of strategy games need abstractions because one person controls an entire state and the game has to limit how many things that person can do. So perhaps signing a treaty costs 50 Diplomatic Influence. Perfectly reasonable for that kind of game.

It would make very little sense in Gray Horizon. If Serbia wants a treaty with Greece, the interesting constraint is not whether Serbia has accumulated enough blue diplomacy points. The constraint should be that Greece has players on the other side who have to agree. The difficulty comes from other people sitting on the other side of the same decision. That, to me, is much more interesting.

Or consider delivery timers. A market can make geography matter by forcing somebody to wait twelve real hours for their purchase to arrive. But waiting isn’t necessarily strategy. If geography can instead affect shipping cost, available routes, tariffs, blockades and access to markets, then distance still matters without turning the game into a parcel tracking simulator, which most of us do in real life, and no one finds it particularly interesting.

The distinction I kept coming back to was: Does this create a decision, or merely a delay? That question is useful.

How that works in the game today

When you buy something on the market, it’s yours as soon as the trade goes through. You don’t wait for anything.

Distance still matters, it just shows up in the price. For every seller, the game works out what their goods would cost you delivered to where you are: their price, the freight along the cheapest route, your country’s import tariff if the goods cross a border, and a small market fee. Your order gets filled by whoever comes out cheapest after all that, and that isn’t always whoever has the lowest price tag.

Say you want 100 iron ore. One seller is 200 km away and asks 4.00 credits each, another is 2,500 km away and asks 3.80. Freight on raw goods is 0.12 credits per unit for every 1,000 km, so the nearby ore ends up costing you about 4.02 and the “cheaper” one about 4.10 (both also pay the same half a percent market fee). So you buy from your neighbour, and you never had to work any of that out yourself.

The freight bit is short enough that I can just show you the game’s code for it. Quantity times weight times distance times a rate, and it never goes below one cent:

logisticsFee, packages/game-rules/src/economy.ts

return Math.max(
  1,
  Math.round(
    (quantity *
      unitWeightMilli *
      effectiveRouteKm *
      BALANCE.economy.logisticsRatePerMilliWeightKm) /
      1_000_000,
  ),
);

Politics gets into this too. Countries set their own tariffs by law and can vote in an embargo against another country, and shipping goods through occupied provinces costs more. So a war can hurt trade without any delivery timers, because the routes themselves get worse.

And then there are decisions like “do not start with microservices.” It’s engineering restraint. A system does not become professional because it contains more moving parts. Complexity has to buy something. At the start of a project, Kubernetes would have bought me approximately nothing and charged interest. So it went on the list. A GDD (Game Design Document) should constrain you, not imprison you. There is a danger in writing a document this detailed. Eventually you can start treating your own guesses as the Bible. I never expected Gray Horizon to be built exactly as written. It couldn’t be. Before players exist, you don’t know what players will do. Before an economy exists, you don’t know whether its prices make sense. Before the interface exists, you don’t know which apparently obvious control will confuse everyone. Before a war exists, you don’t know whether starting one is too cheap, too expensive or simply boring. You can only make assumptions. You’ll be wrong about plenty of them, but they should at least be educated assumptions. But still assumptions.

There are two very easy ways to screw this up. One is to write nothing down and tell yourself you’ll figure it out while coding. Then every system gets designed in isolation and eventually they start contradicting each other. The other is worse in a different way: treating the GDD like scripture. If the document says something is fun and the game says it isn’t, the document loses.

And the fact that I was wrong, or rather let’s say the GDD was wrong, only strengthens my opinion. The original world design had roughly 1,250 land provinces. The document even worked through Serbia as an example with seven provinces.

On paper, 1,250 seemed logical. You want the game to have as much detail as possible, right?

And yet, you’d be wrong. A geopolitical strategy game needs to function like a game, not a geography atlas that they make you look at in school. It is extremely easy to fall into this pitfall, you start treating the game as an atlas just containing a game by chance, and the players go away as soon as they arrive. Still, let’s go back to my original example… Too few and conquering one region can mean taking half a country.

So: around 1,250. Still reasonable, right? Then I actually built the world. The first Earth map hit exactly 1,250 provinces. That sounds like success. But it wasn’t.

Real geography, and more importantly administrative geography, was used to divide each country. Roughly, it just made sense, but it was so..so wrong for the game that I had envisioned.

The six steps of the first Earth generator. 1, load real geography from Natural Earth, each file checksummed. 2, choose 213 countries; overseas territories join the country that owns them. 3, cut each into its official regions, 4,616 pieces. 4, share out exactly 1,250 provinces: one each, the rest by land area raised to the power 0.38, Serbia pinned at seven. 5, merge down: join the smallest region to the neighbour it shares the longest border with, and repeat until the country fits. 6, name, connect and count: a merged piece takes its first name plus "Region", 123 sea links join what land cannot, and the run stops unless the total is exactly 1,250. The result, 213 countries and 1,250 provinces, is exactly what the design document asked for. A final panel says no step asked whether a player would recognise these places, or want to fight over them.
What the first Earth generator did, step by step.

You might ask why I built it like that in the first place. It made sense at the time, and honestly a lot of it still does.

The world in Gray Horizon is permanent. A province that exists today has to still be the same province a year from now, because ownership, wars and history all point at it. The easiest way to guarantee that is to not draw the map by hand at all: take a public map anyone can check, run the same steps on it, and you get the exact same provinces every time. Official borders seemed like a good place to start, since they’re real and somebody has already agreed on them. On top of that, it gave me the whole planet in a few hours, with exactly the number the design document wanted.

The notes that came with that first map even said it wasn’t done yet, and that the shapes, names and borders still needed a proper human look before a real world could use them. That look is where it all fell apart.

So what did those steps actually do? The whole method fits in one sentence, which I find kind of funny now.

I like to imagine it as the colonisers drawing the borders in Africa: they divided it all neatly, they probably used rulers, it’s all neat. But is it functional? Hell no, they did not care at all about who lived there or what languages they spoke. They did the borders similarly to how we did it, mechanically. At no point did the guy drawing the borders know or care what a place was called, or whether anyone would ever want to fight over it.

The number on each note came from land area, toned down a bit so that a country a hundred times bigger doesn’t get a hundred times more provinces. Russia came in as 87 pieces and got to keep 78. France came in as 122 pieces and got 9.

Quick note before the code, because it’s easy to get the wrong idea: Gray Horizon isn’t written in Python, the game itself is TypeScript. This was a separate tool that builds the map once, offline, before the game ever touches it, and Python just happens to have the geometry libraries I needed.

If you read code, this is the colonizer with a ruler:

Step 5: tools/world-data/generate_earth.py, lines 177–197

def merge_regions(regions: list[Region], target: int) -> list[Region]:
    build_neighbors(regions)
    active_count = len(regions)
    while active_count > target:
        source_index = min(
            (index for index, region in enumerate(regions) if region.active),
            key=lambda index: (regions[index].area_sq_km, regions[index].source_keys),
        )
        source = regions[source_index]
        candidates = [index for index in source.neighbors if regions[index].active]
        if not candidates:
            candidates = [index for index, region in enumerate(regions) if region.active and index != source_index]
        target_index = min(
            candidates,
            key=lambda index: (
                -source.geometry.boundary.intersection(regions[index].geometry.boundary).length,
                geometry_distance(source.geometry, regions[index].geometry),
                regions[index].area_sq_km,
            ),
        )
        destination = regions[target_index]

In plain English: as long as the country has more pieces than it’s allowed, take the smallest one, find the neighbour it shares the longest border with, and glue them together. The two lines starting with if not candidates are there for islands, so a piece with no land neighbours just gets glued to whatever is closest.

Some countries were cut into far too many pieces. Others had regions that made sense to a statistical office but not to a player looking at a strategy map. Hundreds of names literally ended in “Region.” We therefore reached the number that our “Bible” dictated, and yet, the number was wildly wrong. Over the next roughly day and a half, the world went from 1,250 provinces to 948, then 847, then 741 as the approach changed from “make source geography fit the target” toward deliberately designing a playable board. Because again, you have to treat the map the same way you’d treat a game board. This is one big D&D session, only on real Earth.

The first map
The United Kingdom in the first map, as six coloured provinces named Highland Region, Dumfries and Galloway Region, North Yorkshire Region, Powys Region, Oxfordshire Region and Somerset Region. Powys Region covers Wales, the English Midlands and, across the sea, Northern Ireland, which is labelled Powys Region too. Edinburgh sits in North Yorkshire Region and London in Oxfordshire Region.
Today's map
The United Kingdom in today's map, as six provinces named Scotland, Northern Ireland, Northern England, The Midlands, Wales and Southern England, with Edinburgh, Belfast, Birmingham and London each inside the province a reader would expect.
Britain had six provinces in the first map and has six today. In the first, London was in “Oxfordshire Region”, Edinburgh in “North Yorkshire Region”, and Belfast, Birmingham and Cardiff all in “Powys Region”, which also took in Bermuda and the Cayman Islands.

If you’re wondering how London ended up in a province called Oxfordshire, I re-ran the original script and followed it along. Britain came in as 277 pieces and was allowed 6, so there were 271 glue steps. This is what happened to London:

  1. Camden got glued onto Westminster (step 34).
  2. Westminster, with Camden already in it, went onto Ealing (step 68).
  3. Ealing went onto Surrey (step 188).
  4. Surrey went onto Oxfordshire (step 255).

Oxfordshire was never the smallest piece left, so it never got picked up, and it kept its name. That’s pretty much the whole naming rule: a province made of one piece keeps that piece’s name, and anything glued together gets the name of the piece everything else was glued onto, with “Region” stuck on the end:

def region_name(region: Region, country_name: str, ordinal: int) -> str:
    unique_names = list(dict.fromkeys(name for name in region.names if name and name != "-99"))
    if len(unique_names) == 1:
        return unique_names[0]
    if unique_names:
        return f"{unique_names[0]} Region"
    return country_name if ordinal == 1 else f"{country_name} Region {ordinal}"

Edinburgh went the same way. It went into Midlothian, then into the Scottish Borders, and then in step 271, the very last one for Britain, into North Yorkshire. The two British bases on Cyprus didn’t have any land neighbours, so they went to the closest region, which turned out to be Kent, and Kent later went into Oxfordshire. So yes, a province named after an English county had a piece of Cyprus in it.

This was the only check the script ever did on its finished work:

if len(output_provinces) != TARGET_PROVINCES:
    raise RuntimeError(f"Expected {TARGET_PROVINCES} provinces, generated {len(output_provinces)}")

If it isn’t exactly 1,250, stop. The game’s own test (in TypeScript, like the rest of the game) checked the same thing on the map file:

packages/database/src/earth-dataset.test.ts

const countryIds = new Set(dataset.countries.map(({ id }) => id));
const provinceIds = new Set(dataset.provinces.map(({ id }) => id));
expect(countryIds.size).toBe(213);
expect(provinceIds.size).toBe(1_250);

And not long after that, I removed the global province-count target completely. That became one of the first major lessons of the project: Some numbers should be outputs, not goals. If I decide beforehand that Earth must contain exactly 1,250 provinces, every country becomes a contributor toward satisfying my spreadsheet.

If instead I ask how many provinces Spain needs, how many China needs, how many Serbia needs, how many Switzerland needs, and whether each division creates an interesting piece of geography to play with, then the final number is simply whatever those answers add up to. This sounds so obvious to me now. It did not sound obvious before I had a terrible map proving it.

A bar chart of provinces in each version of the map, starting at zero, with a dashed line at the design document's target of 1,250, marked as dropped on 2 September. Earth Alpha: 1,250, official regions merged until it hit the target. Plan v0.2: 948, a hand-written plan where the data supplies only shapes. Map v0.3: 847, country-by-country plans and merges. Map v0.4: 741, all 196 countries looked at before building. Map v0.5, today, since September: 755, version 0.4 plus a list of edits and no target at all.
How many provinces each version of the map had, counted from that version's own data.
1,250 provinces: Earth Alpha
The Sahara and its neighbours divided into many provinces: Algeria into 15, Libya into 13, Chad into 12, Sudan into 14, with thin seams crossing empty desert.
948 provinces: plan v0.2
The same view with fewer, larger provinces: Algeria in 10, Libya in 7, Chad in 6.
The Sahara, from the Atlantic to the Red Sea, on the day the first map was replaced. Algeria went from 15 provinces to 10, Libya from 13 to 7, Chad from 12 to 6.
847 provinces: map v0.3
The Sahara view again, with Algeria in 6 provinces, Libya in 5 and Chad in 5.
741 provinces: map v0.4
The Sahara view with Algeria in 4 provinces, Libya in 3 and Chad in 3.
The next two passes, a day apart. Algeria went to 6 and then 4, Libya to 5 and then 3, Chad to 5 and then 3.
The same view in today's map, v0.5: Algeria in 4 provinces, Libya in 3, Chad in 3, and Egypt, redrawn since, in 7.
The same view today, in map v0.5. All five pictures are drawn straight from the game's data at each version, and nothing was redrawn for this article.

The point of the rules

Fast forward to a month of development, and plenty of things are different from the original GDD claimed. And that’s perfectly okay.

But the things I actually cared about were still there. You are still a citizen rather than a country. The map is still supposed to be the center of the experience. Players still create the politics, companies, newspapers and conflicts that matter. Paying real money still isn’t supposed to make you stronger. And when a detailed mechanic collides with one of those principles, the mechanic is the thing that has to move. That’s what the GDD ended up being useful for. It told me which things I could happily throw away and which ones would mean I’d started making a different game. It’s a way of deciding which parts of your idea you’re willing to negotiate. I changed the province count, and I happily would change it again. I changed economic assumptions. I changed things I had written with complete confidence earlier. But if Gray Horizon ever becomes a game where the most important participant in a country isn’t its community, or where the map is irrelevant, or where the person who pays the most has the most power, then the problem isn’t balance anymore. I’ve built the wrong game. Shame on me. The first set of rules exists to make that harder to do. As you’ve probably guessed, writing was the easy part. Seeing how it all fits together is the hard part…

More on that in the next devlog. Thanks for reading!