The Meta-Game Begins

The original Hordes of Orcs games were pretty simple things. Everything was available straight away — every map, every tower, every upgrade, every spell. That’s not a bad thing, and I don’t regret it. But it does mean people burned through all of the content pretty quickly.

For this game, I want the experience to last longer. That means two things: there needs to be a meta-game, and it needs to be a lot easier to author new maps. The last six days were largely about both — and about the unanticipated consequences of getting there.

Apparently I’m doing a series of blog posts about this project!

Part 1: Six Days Equals Six Weeks

Part 2: Specialists in the Factory

Part 3: The Invisible Work Matters

Part 4: Progress, 37 Days in

The refactor: making maps easier to author

Before I could think about more content, I had to make content easier to produce. Map authoring is full of friction points, and one of them was how a map specified where orcs come from and where they’re trying to go.

So I wanted to restructure how spawn points and destination points are described in a map. The goal was narrow and concrete: make authoring new maps less tedious and less error-prone, so that “more content” doesn’t translate directly into “more opportunities to introduce game-breaking mistakes.”

There’s still a long way to go on making maps easy to produce, but this immediate goal – the highest-risk change I have planned – was achieved.

The task was more complicated than it might sound, and it had two consequences I didn’t see coming. One of them was a serious bug. The other one I decided to keep.

I will spare you the details of why it was a complicated task but the tl;dr is that it had structural implications for how the pathfinding system works, editor tooling, how waves are defined, and some of the custom editor tooling that exists in the project.

A serious regression

See those red orcs? Those aren’t Fire-immune orcs. It’s not an art choice.

Early on I added a debug indicator: an orc turns red when it has no valid path to its destination. That’s a condition that’s supposed to be impossible. Orcs necessarily start out with a path — the game won’t let you place a tower that would completely wall them off. So if the red ever shows up, something has gone badly wrong upstream. The red is there precisely because I never expect to see it, and want it to be very very obvious in anotherwise chaotic scene that something has gone Very Very Wrong.

The refactor is the first time I’ve seen it since the first week of development. The bug was subtle, and not immediately obvious from play testing: The orcs still spawned with a valid path, but the bug made it possible to wall them off — to seal away the last route to a destination that should never have been seal-able. My play-testing had only checked for whether the guarantee that a path must exist between every source and each of the destinations it can connect to.

Fortunately, it never reached a player-facing build. It got caught at Layer 3 — the “take a step back” layer, where I cut a checkpoint branch off main and open a larger merge PR whose review covers a much wider surface than any single feature PR. Codex flagged the broken pathing just by reading the code, and the fix happened before the change went anywhere near a build someone could actually play. Copilot had failed to observe the issue at any step of the way. Maybe that’s because Copilot isn’t as strong a model, or maybe it’s because the work was broken up into a bunch of different PRs. Whether it was the reviewer having a “big picture” view, or the stronger model, the end result was the system did its job.

A scope miss worth keeping

The second consequence was a little more obvious, and it turned into a feature.

The old design had an explicit mapping of waves to destinations baked in. The refactor abandoned that, which raised an obvious design question: if waves aren’t explicitly mapped to destinations anymore, how does that mapping happen?

Each spawn location gets a list of target destinations, set in the editor. That assigment remains, albeit relocated from the NavSceneController component to the OrcGate component. I made the choice to decouple source assignment from wave definition to make it easier to reuse wave sequences in a variety of maps, and avoid corner-cases around forgetting that a source exists because the information is more diffuse in the structure of a map (I have plans for much more visually complex maps in mind!).

What I settled on, for doing that mapping of orcs to sources: Round-robin assignment of waves to sources, and round-robin assignment of groups to destinations. A wave is made of M groups, and each group is N orcs. If there’s two orc Gates (say, A and B, and each routes to two different targets – W / X, and Y / Z respectively), that would mean: Wave 1 goes to gate A, wave 2 to gate B. Within wave 1, group 1 goes to W, group 2 goes to X, etc. Within wave 2, group 1 goes to Y, group 2 goes to Z, etc.

Claude misunderstood me, fairly substantially. The wave-to-source part came out exactly right: waves round-robin across the source gates as intended. But instead of round-robin assignment of groups to destinations, it wound up doing round-robin assignment of individual orcs. This resulted from a subtle change that came as part of the package of refactors; A gate has 2 squares on which orcs can spawn / route to. Previously, those were treates as independent sources / destinations, and given distinct identifiers. Now, each gate is treated as a single source or destination.

I discovered this when testing out a new, slightly more complex map design. I thought about it, and decided I liked it better than what I’d actually asked for. Instead of clumps of orcs all streaming toward the same destination, you get a wave that fans out — adjacent orcs peeling off toward different targets, the whole crowd dispersing across the map in a way that’s more interesting to defend against. It was an accident, but it was a good accident.

So it stays. For now. I may revisit it, but if I do, it won’t be to “fix” it back — it’ll be to add a flag in the wave config that says whether round-robin assignment happens at the level of the group or the individual orc. The bug becomes an option.

The meta-game infrastructure

This screenshot isn’t much to look at, but it’s the most important thing I built this week.

What it represents is a complete, working pipeline for the meta-game: the player earns soft currency (currently only via the Battle Pass system), spends it on upgrades, and sees those upgrades take effect in-game — backed by a server-authoritative journal of what’s been acquired that synchronizes across every device the player touches (once the ability to connect to accounts is built).

This is the deep one. It touches the client, the server, the schema, and it drags in a whole pile of distributed-systems and security considerations that a single-player tower defense game has no business needing. Until you decide the player’s progress should follow them across devices, that is. The work spanned a bunch of issues and PRs, involved a lot of missed corner-cases, and at one point had me refreshing my memory on exactly what a TOCTOU vulnerability is and why a “check the balance, then deduct it” purchase flow is a textbook invitation to one.

The upgrade side of this builds on infrastructure I put in early: any entity can have attributes, and each attribute carries a stack of modifiers — each one a flat addition, a percentage addition, or a percentage multiplier. That system was designed to solve three problems at once:

  1. Downloadable balance updates that override the built-in data, so I can tune the game without shipping a new build.
  2. Affliction effects — Weaken reducing resistances, Chilled slowing movement, Burning speeding it up (panicked orcs flee).
  3. Upgrades — in this first case, increasing the percentage of money you get back when you sell a tower.

The meta-game purchase flow is the third of those finally wired all the way through. And because the modifier system was already carrying the weight, adding the next upgrade is now largely a matter of content authoring rather than coding. Which is the whole point of building a meta-game in the first place: I’m trying to manufacture longevity, and longevity is content, and content I can author cheaply is content I’ll actually ship.

Where it stands

There’s still real work to do. There’s some missed scope, and a handful of edge-cases I need to work through — the corner-cases on a synchronizing, server-authoritative ledger are exactly the kind of thing that’s easy to get almost right.

But. The loop is closed. I can earn soft currency through the Battle Pass system, spend it on the one upgrade that exists, and watch that upgrade take effect in a real game. Then I can open the admin UI on the server and see the ledger change and the purchase recorded against the player’s account. End to end, client to server and back.

Not bad for six days of effort — six days that also had a lot of other work threaded through them: The milestone currently stands at 133 issues (128 closed, 5 open).

I’m happy with the progress I’m making with the game. Most importantly, I’m finding it fun already.

Comments