Welcome to Technical Tuesday, a counterpart to Feature Friday. Feature Friday covers what's going into the game each week. Technical Tuesday pulls back the curtain on how the thing actually works, and occasionally, how badly it nearly didn't. The goal is something worth reading whether you write code for a living or just find it mildly interesting that games are (still) built by humans. I'll aim for one a week.
For the first one it felt right to start at the very beginning.
How it started
I grew up playing strategy games. The typical "dad-games" my dad would buy for himself and never touch were a ones i always gravitated towards, and one of those was Patrician 3, a late-medieval trading sim set in Hanseatic League ports. I found it around 2008 and played it on some creaking old business laptop. The only reason it ran at all is that the game came out in 2003 and was not exactly high fidelity gaming. Twenty-three years later it simply doesn't play nicely with modern hardware. After one especially frustrating afternoon trying to get the game working on a new OS install, I made the mistake most programmers make at least once and said to myself: "how hard can it be to build an economy like that myself?" Well... Hard. Very hard. In my moment of frustration I had glossed over the depth that makes Patrician 3 what it is. But I'd already opened a new solution so no going back now.
The plan
The original scope was small. At this point i had not even considered actually making something playable. A while(true) loop. City definitions. A handful of goods. AI merchants that buy below the average price and sell above it. Population figures driving consumption. That's it. Because the intention at that point was just to prove the economy concept could work, I skipped game engines entirely and wrote it in plain .NET. No Unity, no Godot, no additional tooling whatsoever. As a .NET developer by trade, it was the fastest way to get something running. Just a headless console process.
The architecture
The structure that came out of those first weeks is still the skeleton everything is built on today. I modeled it after my experience as a Backend Dev. This resulted in three packages:
Models - pure data structures. Cities, goods, prices, ship states. No logic, just shapes.
Engine - the simulation. Every system that advances the world lives here.
AI - decision logic. Reads state, sends commands back in.

The Engine is essentially an ordered list of systems, each responsible for one slice of the world. A Ship system moves every vessel forward by its speed. A Population system consumes goods and grows cities when conditions are met. A Market system adjusts prices. Each system reads from and writes back into a single shared state frame, mutating it in place. They fire sequentially, one after the other, and when the last one finishes the frame represents one tick of elapsed game time.
If you've worked with ECS or any data-oriented simulation architecture this will look familiar. There's no component registry or entity tagging here - it's closer to a classic game loop than a full ECS. But the separation of data from logic is the same idea, and it carries the same benefits: systems are isolated, testable independently, and trivially reorderable when you discover that prices should adjust after goods are consumed and not before.
AI decisions
The AI side mirrors this structure but with one important difference: behaviours fire in priority order, but rather than stopping at the first match, every behaviour in the list gets to run. Each one can overwrite the command issued by the one before it. The last behaviour that fires gets the last word - which means the highest-priority behaviour runs last and its command is the one that actually goes into the queue. In practice this means a merchant might work through its entire list - deciding to seek a profitable port, then overwriting that with a trade route waypoint, then overwriting that again because a pirate is nearby - and only the final decision survives. If the ship has critical hull damage, the repair behaviour runs last, overwrites everything, and that's the command that gets queued. Lower priorities can't talk their way back in. (Yes, i have optimized this in the meantime. This is the state how it was at the start)

The Command Flood
Each AI merchant runs its full behaviour list independently against the current frame and emits whatever commands it decides on. Critically, no merchant mutates shared state directly - every decision comes out as a command object dropped into a queue. With a few hundred merchants active on a busy map, and multiple behaviours potentially triggering per merchant, that queue can easily run into the thousands of commands per tick.
The Engine doesn't care about the source. It flushes the queue at the start of each tick, processes everything in sequence, and then runs its systems. The frame that comes out the other side reflects every merchant's decisions simultaneously - a reasonable approximation of a world where trade actually happens in parallel, without any of the locking headaches you'd get from true concurrency. All merchants read the same frame; only the Engine writes state.
The full tick loop looks like this:
The key point in all of this is that the Engine is completely headless. It knows nothing about rendering, input, or networking. The frame it emits is just a data snapshot - a full serializable representation of the world at that moment in time. What consumes it is irrelevant to the Engine, which means the same simulation can run behind a dedicated server, a game engine renderer, or a console harness. The presentation layer is entirely a consumer of the simulation.
This is also why save files are straightforward: serialize the frame. And why replay support is viable in principle: store the command log and play it back. The simulation is deterministic given the same starting frame and the same sequence of commands, so both properties come for free.
The console harness
A console application, partly AI-assisted, piped the engine output into something readable: prices in each city, stock levels, ship positions. It was ugly. It was also where the first version of Argosy's economy was balanced and the first AI behaviours were tuned. If a merchant was ignoring a clearly profitable route, I could watch it happen line by line in a terminal feed and go find the behaviour that was misfiring.
Debugging a simulation through Console.WriteLine() is not an experience I'd recommend. It did teach me early that the behaviour priority system needed to be airtight, because watching 300 merchants all make the same obviously wrong decision at once is a very hard to miss way to notice an error.
Sounds simple enough...
So as you can see,the basic foundation of the loop is simple enough. Just 2 blocks talking to each other using a third as their common language. One controls the other via commands which are made from decision based on the state.
So far so good. The issue i encountered was my choice to stick with .Net. Next week we'll look at how Argosy tries its absolute best to avoid interacting with a core functionality of .Net and why Linq, a .net feature i used to LOVE, has become the bane of my existence.
