Gage Langdon Next →
The HA7Z title wordmark.

Game development

HA7Z

A 1-4 player co-op colony-sim and tower-defense game, built by prompting Claude Code almost entirely from my phone.

This page was drafted by Claude.

HA7Z is a downloadable Windows game on itch.io: 1-4 player co-op colony sim crossed with maze-based tower defense. Unity 6, Mirror networking, played over LAN or Tailscale.

The pitch

A machine at the center of a poisoned world keeps you alive, and it wants more colonists. You chop wood, build roads, farm, and expand its field by day. At night, whatever survives out past the edge of that field comes to tear the machine down, and you built the maze that decides whether it reaches it.

A night defense at the LSS, walls and torches ringing the colony, the build palette and colony tier progress visible in the UI.
A full base at night: the LSS at the center, walls and towers on the perimeter, production buildings and roads filling the space between, colonists gathering, producing, refining, and defending all at once.

Why I built it

My wife is into colony sims: Stardew Valley, Valheim, Farthest Frontier, Timberborn. I’m into tower defense and RTS: Warcraft III TD mods, StarCraft, Factorio. A co-op game we could actually play together would have to be both.

I’d built games before, but my multiplayer experience started and ended with WebSockets, nothing like real host-authoritative networking. I’d also never written a shader or built procedural world generation. Normally that’s the kind of unknown list that makes me shelve a side project before starting it. I also didn’t have much free time. Most of the prompting happened at my desktop, but a lot of it was from my phone too, using Claude’s remote control to keep the project moving between Overwatch matches.

Building it with Claude Code

The first real sprint was eight calendar days and around 25 hours of active prompting. What came out of it was a 4-player co-op tower-defense city-builder in Unity, playable over the network, with hundreds of tests passing in under a second. I kept building on it in bursts through mid-June, and the game logic today runs to nearly 600 automated tests.

The game logic lives in a headless C# assembly with zero dependency on Unity or the networking layer, which is what lets it run in plain NUnit rather than inside the editor, the same way it would run in a CI pipeline with no game engine installed at all:

Unity -batchmode -nographics -projectPath . \
      -runTests -testPlatform EditMode \
      -testResults TestResults.xml -logFile -

Close to 600 tests run against it in under a second. That speed is what made the whole workflow possible: Claude edits code, reruns the suite, sees exactly what broke, and fixes it, all in the time it takes a human to glance at a terminal.

That fast suite is only half the story. The other half is a proper Unity MCP server setup, and I’d call it the real powerhouse of this entire project. Once it was wired up correctly, Claude could edit C# files, trigger recompiles inside the actual editor, and read compiler and console errors directly, with no human alt-tabbing between an IDE and Unity to relay a stack trace back and forth by hand. Pair a sub-second test suite with an MCP that gives Claude real hands on the editor, and the loop closes entirely on its own: edit, compile, test, read the failure, fix it, repeat, for as many iterations as it takes. That’s the difference between “AI that writes code you review” and “AI that ships a working feature while you make coffee.” Once Claude Fable came out on top of that setup, whole features started landing in one shot.

I was running 3-6 Claude sessions at a time in separate git worktrees, as often as my token allotment allowed, one implementing a feature, one reviewing the last diff, one profiling performance. My bottleneck stopped being lines of code and became decisions.

The catch with one-shotting features on a higher-end model like Fable or Opus was token burn. A game has a lot of coupled moving parts, and fully understanding the wider system before touching one piece of it costs a lot of tokens fast. I’d consistently max out a 5-hour usage limit in about 2 hours, set a timer on my phone for when it would reset, and come back to work when it went off. That turned out to be a good thing: without it, I’d have spent every non-working hour of the day at my desk instead of eating meals or going outside.

For world generation, I asked Claude to figure out how Factorio does it. It read through more than a dozen of Factorio’s “Friday Facts” developer blogs, including the one where they introduce the noise-expression system, and their API reference, wrote up a short memo on what to borrow and what to skip, then ported the technique to C#. What would have been a few weekends of my own reading and trial and error turned into an afternoon. More on what that research actually changed is below, in its own section, since it’s one of the few systems in the game worth understanding even if you’ve never played anything like it.

The best-preserved part of the whole exercise turned out to be the parts Claude couldn’t do:

Claude can’t tell you what’s fun. A friend played the game and got bored. The missing pieces were basic game-design hygiene: a constant reminder of what’s at stake, the friction that keeps you leaning forward at minute 17. What Claude built me was the average compilation of every game I described to it. Competent, coherent, missing the specific feeling I wanted a player to have. Only I know that feeling.

What’s in the game

The fiction: three thousand years after a war that poisoned the surface, a bunker of survivors wakes up when something strikes their LSS, the machine that both revives colonists from stasis and projects a field that holds back the Haz, the irradiated dead zone outside it. Outside the field the world is grey and lifeless, and the radiation twisted whatever still lives out there into the Ravaged, who come for the LSS at night.

An overhead view of the walled colony, the LSS tower at the center, ringed by autumn-red woodland.
The LSS, walled in, seen from directly above.

By day, you build and expand the field. By night, the maze you built decides who gets through:

The Colony Tech panel: a tree of buildings like Foundry, Smeltery, and Boat, each unlocked with credits earned automatically as the population grows.
Progression credits earn themselves as the colony grows, no research queue to babysit.

The systems that make it unique

None of this requires having played a tower defense game before. What follows is every system worth knowing about, grouped by what part of the game it belongs to.

World

Deterministic, seed-only generation

Every tile’s biome, height, and resources are a pure function of its coordinates and a single world seed number, nothing is stored. That’s also how two players on the same seed see an identical world without the game ever sending map data over the network, each client just regenerates it locally from the same starting number.

The idea behind how that world actually looks came from research into Factorio, a well-known factory-building game famous for procedurally generating enormous, natural-looking maps. The trick isn’t a specific formula, it’s a design choice: instead of an artist drawing coastlines and biome borders by hand, or code rolling dice tile by tile, the generator produces a couple of continuous physical properties across the whole world, mainly elevation, and lets everything else fall out of them, the same way a real landscape is really just the consequence of terrain height rather than something painted on top of it.

HA7Z borrows that trick for water specifically: there’s no separate lake system placing ponds and oceans by hand. The same elevation field just gets read twice, once broadly for continents, once finely for coastlines, and water is wherever the fine reading dips below sea level:

public static WaterKind WaterKindAt(int x, int y, int seed)
{
    if (OriginDistance(x, y, seed) <= GreenwoodOriginRadius + 1f)
        return WaterKind.None;                 // spawn stays dry

    if (LandElevation(x, y, seed) >= SeaLevel)
        return WaterKind.None;                 // above sea level: land

    // Below sea level in the COARSE field too → a real ocean basin.
    // Below sea level only in the fine coastline detail → a small pond.
    return CoarseElevation(x, y, seed) < SeaLevel
        ? WaterKind.Ocean
        : WaterKind.Pond;
}

Reading the same field at two different zoom levels is what tells a sprawling ocean apart from a puddle-sized pond, without either one needing its own separate placement logic. And because water is just the low end of the same field that decides hill height, a coastline slopes down into the sea instead of being a flat shape stamped onto flat ground: raise or lower that one sea-level number and the entire coastline redraws itself correctly, beaches, cliffs, and all. The same field also carries bridges and boats: a bridge stamps a water tile walkable, a boat inverts the rule entirely and only sails on water.

The full-screen map view, showing several differently colored biome regions revealed by fog of war, with monster markers scattered around the colony.
The map view, biomes color-coded, fog of war revealing only what's been explored.

The Haz field

The LSS is indestructible and can’t be demolished, it’s the one fixed point in the game. It projects a circular field that holds back the Haz, an irradiated dead zone the game desaturates to grey outside the field’s edge. Expanding the field costs resources, spent through the LSS’s own terminal, and never shrinks back once you’ve paid for it, so every expansion is a permanent, visible reclaiming of dead ground.

Elevation and cliffs

The world isn’t flat. Gentle rolling hills and dunes are freely walkable, but a height difference of more than 2 tiles between neighbors is a cliff, impassable to colonists, enemies, and the player alike, no jumping. Rare, dramatic mountains spike up to 40 units in the colder and rockier biomes, reading as obvious impassable barriers rather than an invisible wall you bump into by surprise.

Day and night

A full cycle runs day, dusk, night, and dawn, with the sun’s angle, color, and intensity all shifting to match. Night is when the Ravaged attack, so the lighting isn’t just cosmetic, torches and buildings become the only reliable visibility right when visibility matters most.

Wildlife and wild horses

Chickens, rabbits, deer, boar, bears, and wolves roam the biomes they’d realistically live in, cold-weather predators sticking to the cold biomes. Most flee when approached; a few fight back. A rare wild horse occasionally spawns and can be mounted for a faster ride, then dismounted to free-roam wherever you left it.

Economy

Point-to-point logistics

The economy runs on supply routes you draw yourself rather than one shared warehouse everything teleports into. Draw a line from a Sawmill to the LSS in the logistics overlay and a colonist automatically becomes a courier, walking planks back and forth along it. A single building can feed several routes at once, splitting its output 75/25 between the LSS and a Storehouse, say, which means the game has to decide, every time a delivery is needed, which route gets it.

Straight randomness would drift unevenly over a short session, and strict alternation breaks the moment one destination is temporarily full. Instead, each route earns credit equal to its share of the split every time a delivery is needed, and whichever route has banked the most credit wins and pays its credit back, the same smooth weighted round-robin scheduling algorithm load balancers like Nginx use to spread traffic across backend servers, just applied to wheelbarrows instead of web requests:

int credit = (_wrrCredit.TryGetValue(key, out var c) ? c : 0) + w;
_wrrCredit[key] = credit;
if (credit > bestCredit) { bestCredit = credit; bestLink = _pickScratch[j]; }
// ...the winner pays its credit back:
_wrrCredit[chosenKey] -= totalWeight;

A 75/25 split reliably delivers three trips out of every four to the first destination, without ever needing a coin flip, and without ever starving the destination that’s briefly full while its neighbor catches up.

Extractors vs. refineries

A building with a recipe that consumes nothing, a Farm or a Quarry, is an extractor and needs an assigned worker to run at all. A building whose recipe consumes something a courier delivers, a Sawmill turning Wood into Planks, is a refinery and runs automatically the moment its input buffer has enough to work with. The distinction is entirely inferred from whether the recipe has inputs, not a manual flag per building.

Storehouses lock to one material

A Storehouse holds exactly one resource type, decided by whichever material its very first inbound delivery route carries. Once locked, any attempt to route a different material into it is simply invalid, so a Storehouse can’t accidentally become a junk drawer of five different half-stacked goods.

Back-pressure and the ”!” alert

Every producing building has a single-slot output buffer instead of an unlimited one. When that slot is already full of the same good, or full of a different one waiting to be hauled away, the building holds its current cycle at full progress instead of wasting it:

// Logistics back-pressure: output accumulates in a per-building buffer
// (drained by couriers), not a global pool. When that buffer can't take
// another cycle's yield, HOLD: don't consume inputs, don't advance, and
// park progress at full so the cycle fires the instant a courier frees
// space. Raising the flag is what puts the "!" over the building.
if (recipe.Outputs.Length > 0 && !OutputCanHold(b, recipe.Outputs))
{
    b.ProductionPaused = true;
    if (b.ProductionProgressSeconds > recipe.CycleSeconds)
        b.ProductionProgressSeconds = recipe.CycleSeconds;
    return;
}

A pulsing amber ”!” floats above any building in that state, the same signal on both host and client, since it’s derived from data everyone already has rather than a separate network message. A building that holds like this doesn’t lose the work its worker already did, the moment a courier clears the jam it picks up exactly where it left off.

Automatic colonist revival

New colonists arrive on a timer rather than being crafted from materials. Every couple of minutes the LSS thaws a batch from the stasis pods beneath it and fills open beds, capped per cycle so a player with a lot of housing doesn’t get flooded at once, and paused entirely whenever there isn’t enough food or bed space to receive them. There’s no cost beyond that.

Two progression ladders instead of a tech tree

There’s no research queue to sit and wait on. One ladder unlocks buildings as the colony’s population grows, the other as you kill more Ravaged, both climbing on their own with no player decision required. The Colony Tech panel shown earlier in this post (in the What’s in the game section, under Progression) is that population ladder, buildings unlocking as credits accumulate automatically.

Defense and combat

Maze tower defense, for the uninitiated

Most tower defense games hand you a fixed path: the enemy always walks the same route from one side of the map to the other, and the whole game is just deciding which towers to place alongside it. HA7Z doesn’t have a drawn path at all. Enemies always take whatever is currently the shortest walkable route to the LSS, so the “path” is just whatever’s left over once you’ve turned some tiles into walls.

That one difference changes the whole genre. Build a wall in the wrong spot and you haven’t blocked anything, you’ve just handed the enemy a different, equally short way in. Build a long, deliberate corridor of walls with towers lining both sides, and every enemy that spawns has to walk the full length of it under fire before it ever reaches the machine it’s trying to kill. The maze isn’t decoration. It’s the actual difficulty of the game: a puzzle you build once, that the enemies then solve against you, continuously, in real time.

Tower arsenal and biome elements

Every tower fires two damage instances per shot: a flat base amount that always lands in full, plus a bonus amount typed by whichever biome the tower is standing in when it’s built, and only the bonus is affected by a monster’s resistances:

private bool ApplyShotTo(TypedHandle<Monster> target,
                          int baseDamage, int bonusDamage,
                          DamageType biomeElement)
{
    bool died = false;
    if (baseDamage > 0)
        died = _sim.Monsters.ApplyDamage(target, baseDamage);
    if (!died && bonusDamage > 0)
        died = _sim.Monsters.ApplyBonusDamage(target, bonusDamage, biomeElement);
    return died;
}

The base hit is guaranteed damage no monster can shrug off; the bonus hit is the interesting part, its element comes from the ground the tower is built on, so the same Watchtower design hits differently depending on whether you built it in the ice fields or the glass wastes. Each tower archetype (bolt, splash, chain, and others) also ships with several mutually exclusive specializations chosen once at placement and never changed afterward, so where and how you build a tower is a real, permanent decision rather than a stat stick you drop anywhere.

Enemies built to break mazes

Not every Ravaged just walks to the LSS and stops at the first wall. Some, tagged to prefer walls, specifically target the nearest reachable wall instead, but only once the maze has actually sealed them out:

if ((stats.tag & ThreatTag.PrefersWalls) != 0
    && !IsVatReachableViaField(sim, monster, from))
{
    if (TryPickNearestReachableWall(sim, monster, from, path, out var wallHandle))
    {
        targetHandle = wallHandle;
        return true;
    }
}

Others climb short walls outright and only tall walls stop them, and a couple of the toughest simply phase through low walls entirely. A maze that only accounts for the easy enemies falls apart the first night one of these shows up.

The wave director

Ravaged spawn on a timer that shifts with the day and night cycle: a slow trickle by day, roughly one every 25 seconds, and a much tighter cadence at night, starting around one every 6 seconds and tightening by about 10% per day survived down to a floor of 1.5 seconds. Which enemy types show up also shifts over time, starting with the simplest and mixing in tougher ones as the nights add up.

AI and pathfinding

The shared pathfinder

Every enemy, and every colonist walking to a job, is really just asking the same question over and over: what’s the shortest walkable route from here to there? That question gets answered by a breadth-first search, a simple, well-known algorithm that spreads outward one tile at a time, like ripples in a pond, until it bumps into the destination. A wall works because a wall tile is simply marked as not walkable, so the search never spreads through it and has to go the long way around instead. Trimmed to the part that matters:

for (int i = 0; i < 4; i++)
{
    var next = new TileCoord(cur.X + DX[i], cur.Y + DY[i]);
    if (came.ContainsKey(next)) continue;

    bool isTarget = next.Equals(target);
    if (!isTarget && !IsWalkable(sim, next, kind)) continue;

    came[next] = cur;
    queue.Enqueue(next);
}

Nothing about the maze is special-cased into the enemy AI. A monster doesn’t “see” a maze and get confused by it, it just keeps expanding its search outward, tile by tile, until the only unblocked route left traces the long way around whatever you built.

That search running constantly, for every colonist and every enemy, is also where the project’s worst performance bug lived. Around five colonists on the map, the editor would freeze solid. A profiler trace handed to Claude turned up the cause: an AI state that fell into 2,400 of these searches a second, each one allocating a fresh dictionary and queue with no reuse at all. The fix was to stop allocating anything, and to put a hard ceiling on how many times a single colonist could re-search in one game tick:

// Allocated once, cleared and reused on every search instead of
// a fresh Dictionary + Queue every time a colonist replans.
private static readonly Dictionary<TileCoord, TileCoord> _scratchCame
    = new Dictionary<TileCoord, TileCoord>(2048);
private static readonly Queue<TileCoord> _scratchQueue
    = new Queue<TileCoord>(1024);

// A stuck colonist can burn at most 4 searches per tick instead
// of spinning until the frame budget is gone.
const int MaxIterations = 4;

Citizen utility AI

Each colonist picks its own activity every tick from a priority list: critical fatigue forces sleep first, an assigned job comes next, autonomous gathering after that, and idle only when nothing else applies. Nothing needs a manager assigning tasks minute to minute, the priority list handles it on its own.

Auto-assigning idle colonists

There’s no hiring screen. Once a second, the game walks every building that needs staff and pulls colonists out of the idle pool to fill it, production workers first, then couriers for anything with an outgoing logistics route. Condensed to the shape of the real pass (the actual code inlines the roster counts rather than calling out to CountAssigned/CountCouriers):

// Phase 2: staff production buildings (Farms, Sawmills, etc.) from idle.
foreach (var (bHandle, b) in Buildings.All)
{
    var job = CitizenJobInfo.JobForBuilding(b.TypeId);
    if (job == CitizenJob.None) continue;

    int needed = BuildingStats.For(b.TypeId).Recipe?.WorkersRequired ?? 0;
    int have = CountAssigned(bHandle, job);
    while (have < needed)
    {
        if (!TryFindFirstIdle(out _, out var idle)) return;
        idle.Job = job;
        idle.Work = bHandle;
        have++;
    }
}

// Phase 3: staff couriers for anything with an outgoing logistics route.
foreach (var (bHandle, b) in Buildings.All)
{
    if (!Logistics.HasAnyLinkFrom(bHandle)) continue;
    int needed = BuildingStats.CourierCountFor(b.TypeId);
    int have = CountCouriers(bHandle);
    while (have < needed)
    {
        if (!TryFindFirstIdle(out _, out var idle)) return;
        idle.Job = CitizenJob.Courier;
        idle.Work = bHandle;
        have++;
    }
}

A newly revived colonist with nothing assigned is exactly as likely to end up farming as hauling wood between two buildings, whichever vacancy the pass reaches first. Delete a building mid-game and its workers don’t get stuck: they’re released back to idle on the very next pass and picked up wherever the colony needs them next, no dead colonists standing around a hole in the ground.

Multiplayer and technical architecture

Host-authoritative snapshots

The host runs the real simulation; every client is a view into it. Citizens, buildings, monsters, and resources all replicate to connected players 30 times a second, so co-op partners see the same colony update in near real time without ever running their own copy of the sim logic.

One snapshot, two jobs

A single snapshot format covers both multiplayer sync and disk saves. Add a new field to the sim and, in one change, it starts replicating to every connected client and starts surviving a save and reload, instead of needing separate plumbing for each.

Deterministic RNG substreams

Nothing in the simulation calls the engine’s built-in random number generator, which isn’t guaranteed to behave identically across platforms or .NET versions. Every system pulls from its own named substream of a small, self-contained generator instead, xorshift64*, chosen because its entire state is one 64-bit number and its next value is just a few bit shifts away:

public ulong NextUInt64()
{
    ulong x = _state;
    x ^= x >> 12;
    x ^= x << 25;
    x ^= x >> 27;
    _state = x;
    return x * 0x2545F4914F6CDD1Dul;
}

The same seed, run on a different machine, produces the exact same sequence of “random” numbers down to the bit, which is what lets two players’ clients agree on a world, or a combat roll, without ever comparing notes over the network.

Player-facing

Build radius and vehicles

Placement, demolition, and upgrades are only allowed within 6 tiles of the player’s avatar, enforced by a simple distance check rather than letting you build anywhere on the map from a bird’s-eye view. Mount a bicycle, boat, or a wild horse and that same avatar moves faster, the build radius riding along with it.

The LSS Terminal

The LSS is operated through a small retro text console rather than a sprawling menu, with a short list of typed commands:

help · status · field · expand · bunker · revive · colony · defense · clear · exit

Typing expand, for instance, spends the resources and grows the Haz field by a fixed step, the same action the field’s expansion button in the UI triggers.

Click-to-gather

Trees can be chopped by hand: click one, and after 3 to 7 clicks (varied per tree so it doesn’t feel like a fixed counter) it drops 5 Wood. It’s a small thing, but it means a solo player is never fully blocked on colonist labor for the very first resource they need.

The map view

A full-screen map (shown earlier, in the World section above) reveals the world as you explore it, chunk by chunk, rather than all at once. Drag to pan and scroll to zoom, and it stays live: colonists still walk, and monster markers still move, while it’s open.

Alert toasts

Small toasts fade in and out at the top of the screen for anything that needed the player’s attention but didn’t need to interrupt them, “not enough Wood,” “walk closer to build there.” The goal is feedback that doesn’t require opening a menu to understand what just went wrong.

Where it stands

HA7Z is up on itch.io as a playable Windows beta, tagged as a survival city-builder / colony-sim / maze / tower-defense game, and disclosed as AI-assisted in both code and graphics. A solo session already covers the full loop: walk around, chop a tree, build up an economy, and survive the first several nights.

What’s not there yet: real art (the whole game is still placeholder primitives), balance tuning, and a couple of the performance upgrades that matter most at scale, like replacing the per-colonist pathfinding searches with a shared flow field once colony sizes get large. The architecture was built with that swap in mind from early on, it just hasn’t been needed yet.