I Made a Video Game (?!)

I Made a Video Game (?!)

Self-hosted · Personal project · 2026

Warhammer 40,000 Digital Tabletop

While a parent’s first choice is always going to be to have theirs children outside playing, I think a close second is going to be a board game. I get the struggle: when nintendo hit in the mid 80s, I no longer spent 16 hours a day outside. I mean, did you play Techmobowl? Anyway….
My kids have been interested in Warhammer for some time now, both playing the board game and painting the (overly-expensive) characters. One problem: the board game is hard. As a kid, I played Dungeons and Dragons through much of middle school – I think this is more complicated than that. So what did I do? I combined interests from several different areas to come up with an assistant. I have been spending a lot of time with Anthropic lately, so I wanted to continue to take Fable out for a spin (extended usage over July 4th weekend!). I had also been experimenting with coding agents via a self-hosted Agent Zero installation (more on that in an upcoming blog).

React + TypeScript
Python / FastAPI
PostgreSQL 17
WebSockets
Docker + Caddy
Forgejo CI/CD
Self-hosted on my Open Media Vault Server

By the numbers

6phases shipped & live-verified
23factions ingested
995unit datasheets
2,565weapons
1,624abilities
46backend tests green

What it does today

A complete, winnable two-player game — every rule below is
enforced server-side, not suggested.

True-scale battlefield

Inch-accurate 2D board rendered from real terrain layouts. True-scale base
sizes, live distance measurement, wheel zoom to 8× and drag-pan — tablet-friendly.

True Line of Sight

Real sight-line tracing against resolved terrain footprints (Obscuring rules,
graze handling). An interactive LoS mode draws green/red sight lines per model:
“4/10 models visible.”

Movement, enforced

Normal / Advance / Fall Back with the actual movement stat, one move per unit
per turn, server-rolled Advance dice that lock (no re-roll fishing), and 1"
engagement-range rules.

Full combat resolution

Shooting (range + LoS gating, weapon keywords like Rapid Fire, Heavy, Lethal
Hits), 2D6 charges, and the fight phase with pile-in, alternating activations,
and consolidation. Casualties are real — models die.

Objectives & victory

Objective Control math (including battle-shocked units dropping to OC 0),
Take-and-Hold primary scoring with caps, and a round-5 victory screen with
winner, margin, and kill tiebreak.

Live multiplayer

Each player joins from their own device; WebSocket broadcasts keep every
board in sync. A per-game admin can undo any action — powered by an
append-only action log with full state snapshots.

Army builder

Points-validated list building with real loadout options, driven by the
community 40kdc-data engine — plus accounts, roles, and an admin dashboard.

Battle report

After the game, a generated recap reads the action log back as a story:
round-by-round beats, the turning point, and an MVP unit.

Screenshots

Captured from the live deployment.

LoS mode
True Line of Sight — per-model sight lines traced against terrain.
Deployment

Deployment — true-scale tokens, zone legality checked by the server.
Shooting result

Shooting — attacks → hits → wounds → saves → slain, with weapon keywords.
Objectives

Objective control — 3" rings tinted by controller, live OC totals.
Victory screen

Victory — final standings with VP breakdown and battle report.
Army builder
Army builder — points-validated lists with real loadout options.

Architecture

Three services behind a reverse proxy, one shared Postgres, everything
Dockerized on a home server. The server is the single source of truth — clients
can only request actions.

web — Caddy + React SPA
Serves the frontend; proxies /api and /ws to the backend, /roster to the
roster service.
backend — FastAPI
Auth, games, the rules engine, geometry/LoS engine, combat resolver,
scoring, WebSocket fan-out, admin undo.
roster — Node
Wraps the community 40kdc-data engine: list building, points validation,
loadouts, and pre-made map/terrain geometry.
PostgreSQL 17
Reference data (factions → weapons), users, army lists, and all live game
state — snapshotted per game so rules-data refreshes never corrupt a match.

CI/CD: push to main → a Forgejo Actions runner builds
three images, pushes them to a self-hosted registry, and redeploys the stack. A
second workflow regenerates and reloads the full 40k reference dataset on demand.

Engineering highlights

The referee pattern

Every mutation flows through server-side validation with exact-number
rejections (“that spot is 8.2" away, this unit moves 6"”). The
frontend can’t cheat — and neither will the AI opponent, which will use the
same API.

Undo as a first-class invariant

Every mutating endpoint writes an action-log entry carrying a full
prev snapshot and broadcasts after commit. Admin undo replays those
snapshots — including gnarly cases like restoring positions after casualty
undos rebuilt models with new ids.

Server-rolled, locked dice

Advance rolls, 2D6 charges, and battle-shock tests are rolled once
server-side and persisted — retrying a request can never re-roll a die.

Geometry engine

Base-edge distance math shared across movement, engagement range, LoS, and
objective control; rigid-cluster unit moves; polygon sight-line tracing against
resolved terrain (unit-tested).

Snapshot-per-game data model

Games snapshot stats, weapons, movement, and objectives at creation, with
idempotent startup migrations + backfills so in-flight games survive schema
evolution and dataset refreshes.

Self-hosted end to end

Forgejo (repo, issues, CI runner, container registry), OMV Compose
deployment, LAN Postgres — no cloud dependencies, and the roadmap keeps it that
way (local Ollama for AI, local TTS for narration).

Roadmap

  • Phases 0–2 — Foundations done

    Docker/CI skeleton, full 40k data ingestion, accounts, admin dashboard,
    points-validated army builder.

  • Phase 3 — Battlefield & geometry done

    True-scale board, deployment, movement, and full True Line of Sight.

  • Phase 4 — Turn engine done

    Phase/turn state machine, WebSocket live sync, admin undo.

  • Phase 5 — Combat done

    Shooting, charge, fight, command & battle-shock — full dice resolution.

  • Phase 6 — Scoring & victory done

    Objective control, primary scoring, victory screen, battle report.

  • Phase 7 — Breadth next

    Per-model loadouts, cover, stratagems & CP spending, an LLM-assisted
    pipeline that turns ability text into enforced effects, missions &
    secondaries, kid-friendly “why can’t I do this?” UX.

  • Phase 8 — AI opponent planned

    A heuristic decision engine (candidate generation + exact expected-value
    math) playing through the same validated API as humans, with difficulty tiers,
    self-play tuning tracked in MLflow, and a local-LLM strategy/personality layer.

  • Phase 9 — Voice & narration planned

    Local TTS narrates the battle — phases, results, and the AI’s table-talk.

Tags
, ,

Add a comment

*Please complete all fields correctly

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Blogs