Case Study

Broken Oaths

A persistent hex-globe 4X. Twelve days. The one I actually wanted.

A massively multiplayer 4X strategy game on a hex globe that keeps turning while you sleep. Lose a war and you do not die: your conqueror makes you a vassal, and you pay tribute, fight their wars, bank gold, and plot rebellion. Built in a 12-day sprint in July 2026 on a Goldberg polyhedron mesh, a per-world GenServer, and 279 executable BDD specs. 108,734 lines of Elixir. Seven production releases in eight days. I typed 0.25 percent of the characters. This is the most complicated thing the harness has produced, and it is also a game I had been planning for years, which is why my fingerprints are all over it.

12
Sprint Days
108,734
Lines of Elixir
279
BDD Spex
0.25%
Characters Typed by John

What is Broken Oaths?

A browser-based persistent 4X built in Phoenix LiveView. The world is a Goldberg polyhedron generated from seeded 3D Perlin noise, so the hex grid closes cleanly around a sphere with pentagons at the poles. Each world runs as its own GenServer and keeps ticking whether or not you are logged in: movement resolves every tick, the economy every tenth. The mechanic the game is named after is what happens after you lose. Conquest makes you a vassal rather than a corpse, and vassalage comes with Oath Strain, Protection Pacts, tribute, a gold bank, stewardship, and the Pact of Broken Oaths that lets vassals coordinate a revolt. Three personas scoped it, all of them variations on a casual browser-strategy player.

Phoenix LiveView

Server-authoritative rendering; the globe ships as a canvas impostor with a vector near mode and zero DOM tiles

GenServer per world

Each world is a supervised process holding live simulation state, rehydratable from Postgres

Spex

279 executable BDD spec files driving the same LiveView surfaces a player drives

Kamal + Hetzner

Fleet deploy to UAT and production, boot-time migrations, TLS via kamal-proxy

Google OAuth + Resend

Sign-in and transactional mail, both scaffolded by the harness in one evening

Aseprite + Nano Banana

709 game art PNGs from 13 Aseprite sources and a generated-texture CLI

Key Features

  • Seamless hex globe: Goldberg polyhedron mesh, 3D Perlin terrain, orthographic projection, drag-rotate and wheel zoom
  • Persistent simulation: movement every tick, economy every tenth tick, running whether you are online or not
  • Feudal vassalage: tribute, gold bank, stewardship, levies, garrisons, Honor consequences for refusal
  • Rebellion: Oath Strain, Protection Pacts, Declare Independence, heir reconciliation, coordinated multi-vassal revolt
  • Combat with melee and ranged strength split, sieges, city defense, fortify stances, and a naval Galley
  • Cities with production queues, growth, worked tiles, yields, wonders, and buildings
  • Research tree from Stone Age through Bronze, gated strategic resources, worker chop and road orders
  • Barbarian camps with a hunting AI, spawn cadence, and placement rules that keep them off claimed tiles
  • Per-world chat with blocking and profanity filtering, alliances, cooperative combat with bounty split by damage dealt
  • Fog of war and per-player exploration, with the camera keyed to the explored set

User Stories

A representative slice of the stories that drove this build. Stories that ran the Three Amigos protocol expose their actual persona, rules, scenarios, and resolved questions inline. The records shown are excerpted from the harness; the full set lives in the project.

52
Stories
387
Acceptance Criteria
52
With Three Amigos

Open Source Repository

The repo is public. The architecture decision records, the 279 spec files, the QA briefs with their screenshot evidence, and the design documents that preceded the feudal system are all in there, including the commits that got rolled back.

View on GitHub
Elixir / Phoenix
Language
275
Commits
171
Components
227
Subagents Dispatched

The Dev Story

The Good, The Bad, and The Ugly

The first commit is dated March 19 and the last is still landing, but citing a four-month span would be dishonest. 253 of the 275 commits landed in July, across 12 distinct days. This is a 12-day sprint with a four-month exploratory prologue. It is also the first build in this series where I was a heavy participant on purpose, because I wanted to play the result. That shows up in the numbers, and the numbers are better read as a measure of how much I cared than as a measure of what the harness needed.

A Playable Persistent 4X in Twelve Days

108,734 lines of Elixir across 184 modules, 1,630 tracked files, 709 pieces of game art, and seven tagged releases from v0.2.0 to v0.3.1, all inside a 12-day window with two days off in the middle. July 13 alone carried 44 commits. July 14 produced roughly 13,000 lines of Elixir and ran the entire harness lifecycle twice, morning and evening: architecture, then red specs, then implementation, then a UI panel, then around again.

Risk-First Sequencing Was the Right Call

The unproven part got the whole day and the well-understood part got an evening. July 13 was spent entirely on the renderer, which is the thing that could have killed the project. Accounts, invitations, integrations, authorization, Google OAuth, a Kamal fleet deploy kit for two environments, and Resend mail all landed that same evening, with commit c728292 carrying 3,503 insertions of generated platform in one go. Both environments were verified live by the end of it.

Specs Genuinely Came First, Repeatedly

Commit 9bcc11e is the clearest instance: 5,624 insertions across 69 files, of which 46 were executable BDD specs and exactly two were implementation. Sieges, vassalization, tribute, banking, and stewardship were fully specified before a line of siege logic existed. The same shape repeats on July 14 (37 red spec files in the ten minutes between 11:17 and 11:27, green by 12:53) and on July 16 (roughly 7,900 lines of red specs for combat and garrisons before any combat code).

The Decomposition Was Clean and Nearly Free

All the rebellion logic went into world_server.ex, and that was the problem. Starting at 18:37 on July 19, eight slices pulled it apart: feudal and rebellion logic first, then combat, cities, unit orders, social and vision, stewardship, turn.ex split into domain-owned tick phases, and the 1,161-line game.ex facade split into per-domain sub-facades. Then ten bounded contexts were promoted out of Game entirely, roughly 200 lines touched per commit with zero net change. The LiveViews followed at 04:10, with play.ex shedding 2,028 lines into components and a view-model.

Cross-Story QA Caught What Per-Story QA Could Not

Batch 5 shipped 147 files at 04:49 on July 18 with story-level QA already green. Four end-to-end journeys, run as a real player rather than as a test, immediately found three bugs the per-story passes had missed. The screenshot filenames are the receipt: j2_05_BUG_city_unreachable.png and j3_09_BUG_over_cap_worked_tiles.png. Fixed, rerun 4/4 clean, then frozen into permanent regression tests. That pattern is now the strongest QA idea in the harness.

Real Playtesters Were in Production by Day Six

On July 20 I stopped building long enough to recruit people: go play my game, use the feedback widget in the lower right, report anything you find. Roughly twenty commits exist because of what came back. Four user-reported bugs forced v0.2.1 out at 12:49 on July 18. Playtest triage then split three ways in seventy minutes across backend, frontend, and mobile rendering, and a single prod patch recorded nine playtest fixes. Individual reports carry ids in the harness; playtest issue 2a9df843 became the player display-name feature.

The Sprint Held Its Shape Under Load

227 subagents were dispatched across the build. 52 stories, 387 acceptance criteria, 285 BDD rules, 145 Three Amigos questions, and 171 architecture components went through the harness. The densest stretch, July 19 to 20, carried 59 commits in 46 hours and included both the largest feature commit of the project and a full architectural teardown, without the test suite going red for long enough to matter.

What Broken Oaths Changed About the Harness

Testing and the Record of Testing Are Not the Same Thing

Twenty stories carry no QA attempt and zero criteria are marked verified, yet the game was live in production with real players filing real bugs. Both facts are true. The harness has no way to represent human playtesting as evidence, so a build that was heavily tested reads as untested in its own database. The next experiment is an intake that treats a triaged player report as a first-class verification record against the story it touches, not as a support ticket that happens to produce a commit.

Nothing Closes the Loop

All 52 stories still sit at in_progress. So do Cleaner CRM's 21. No step in the workflow marks a story done when its QA passes and its code ships to production, which means the harness cannot answer what is finished from its own records. It also never noticed that 20 stories went untested, because it never asked. A completion gate that runs on deploy is the smallest useful fix here.

Story-Level Green Is Not Build-Level Green

Per-story QA is the harness's default unit of verification, and it went green on a 147-file batch with three reachable bugs in it. The fix was to run four end-to-end journeys as a player and then freeze them as permanent regression tests, and it worked immediately. The harness did not ask for that. Cross-story journey QA should be a required gate before a batch ships, not something the operator thinks to do.

The Reporting Layer Needs the Same Bar as the Code

The fixture exporter asserted completion, verification, and passing QA without reading any of them from the database, and it generated the three case studies already published on this site. Those pages need an audit. More generally, any tool that produces public claims about a build has to derive them or refuse to emit them, and that rule now applies to the harness's own marketing surface.

Specs Should Be Anchored to Events, and Review Should Enforce It

The turn-count fragility was found and fixed four separate times across two days. That is a mechanical pattern a review check can catch: a scenario asserting on an absolute tick or turn index is almost always the wrong shape when the cadence is still being tuned. Cheap to detect, and it would have saved four rounds on this build alone.

High Human Input Is Not Automatically a Harness Failure

I typed roughly three times the share here that I did on Cleaner CRM, and the reason is that this is a game I had been planning for years and intended to play myself. The characters went into taste and product judgment: whether the globe felt buttery, whether a world felt like resources everywhere, which of two defensible mechanics this game wanted. Measuring human involvement is useful. Treating a lower number as automatically better is not, and this build is the counterexample.

Twelve sprint days. 275 commits. 108,734 lines of Elixir, 52 stories, 387 acceptance criteria, 285 BDD rules, 279 executable spex, 227 subagents, seven production releases in eight days. I typed 58,535 characters against 23.3 million, which is 0.25 percent. Twenty of the fifty-two stories carry no QA record, zero criteria are marked verified, and the game has been live with real playtesters since day six. It is not finished. The immediate ask is players on the vassalization mechanic, the part the game is named after and the part that most needs other people's hands on it. Past that it keeps moving for a structural reason: Broken Oaths is the most complicated thing this harness has produced, which makes it the test bed for every revision to the harness itself. The case study does not close because the project does not close.