← Work
Case study · Game

Tiny Tactics

Tiny Tactics is a free turn-based tactics game you play in the browser — a square grid, cities to capture, factories to build from, and a rock-paper-scissors web of 18 units, set on a 3D toy-box diorama. It has voiced story campaigns, a computer opponent with a personality, a map editor, and online multiplayer with a public lobby, spectators, correspondence games and Glicko-2 ladders. Under it all is one pure, deterministic rules engine that runs in the browser, on the server and inside the AI. I built it solo.

Tiny Tactics
~2 weeks
From the first commit to a live game with campaigns, online play and rated ladders
1 engine
The same deterministic rules in the browser, the match server and the AI
50+ missions
Across five voiced story campaigns, plus 32 skirmish maps
Solo
Designed, built, and shipped single-handedly

Overview

Tiny Tactics is a turn-based tactics game in the style of Advance Wars, played for free in any browser with no account and no download. Two to four armies share a square grid; you capture cities for income, build units at factories, airports and ports, and win by taking the enemy HQ or wiping out their army. The depth comes from the matchups — 18 units across ground, air and sea, each strong against some and helpless against others — and from terrain, fuel, ammo and each commander's special power. The commanders, maps, story and art are all original; mechanics are fair game, Nintendo's IP is not.

It's a game, but it's also the most complete piece of software I've built in a short time. In about two weeks it went from an empty repository to a live product: a 3D board, five story campaigns with voiced cutscenes, a computer opponent, a map editor, a second real-time game mode, and a full online layer with accounts, lobbies, spectators, async play and leaderboards. This page is about the architecture that made that pace possible without the whole thing collapsing under its own weight.

A Tiny Tactics match in progress — red and blue toy armies facing each other across a river on a 3D island diorama, with the unit info card open

One deterministic engine

Everything rests on a single rule: all game logic lives in a pure TypeScript package with no DOM, no Workers and no browser dependencies. It exposes essentially one function — applyCommand(state, command) → { state, events } — with a seeded RNG, integer-only math and no Math.random anywhere. Given the same starting state and the same list of commands, it produces the same game, byte for byte, on any machine.

That one decision pays for most of the rest of the project. Clients send intents ("move unit 12 to (4,7), attack unit 31"), never state, and the server replays them through the same engine to validate them, so nobody can cheat by editing their board. The command log is the replay format, so replays, "since you last looked" catch-up animations and spectating all come for free. The renderer only animates engine events, so the 3D board, the battle cutscenes and the flat PNG snapshots are all just different views of the same truth. Optional mechanics — commanders, weather, fog of war — are composable rule modules toggled per match; a game without commanders simply has no commanders field and plays exactly as it did before.

Online play on Durable Objects

Multiplayer runs on Cloudflare. Each match is one Durable Object holding the game state and an append-only command log in its own SQLite database, talking to players over hibernating WebSockets — so an idle match, or a correspondence game waiting three days for a move, costs nothing. A single Worker serves the game, the REST API and the WebSocket upgrades; D1 holds users, the match index and ratings, R2 holds replays and uploaded maps.

On top of that sit the features you'd expect from a real platform: rooms by link, a public lobby with search and filters, spectators on a delay, chat with an emotes-only mode, and rematches. Accounts (Discord, Google, or a six-digit email code — no passwords) unlock rated games with Glicko-2 ratings, global and per-country leaderboards, and seasons. Correspondence play lets each player move whenever they like within a per-turn limit, with web push to say it's your move and a board snapshot rendered on a Worker to show what changed. Every view the server sends is built per recipient, which is what made fog of war a contained change rather than a rewrite: the server simply stops telling you about units you can't see.

The Tiny Tactics online hub — create a room, join by code, and a public list of live games to watch, filterable by size, players and clock

A computer opponent with a personality

The AI is its own pure package that sees only what its player sees — a per-seat view without the RNG seed, so it can't peek at luck rolls or through fog. It builds a strategic picture each turn (a threat map of every enemy's reach, distance fields per movement type, capture assignments, HQ defence), then scores every unit's possible actions, picks one, lets the engine resolve it, and re-plans. Hard adds lookahead on top of the same scoring. Because it only ever emits ordinary commands, it needs no special engine paths, and its games replay like anyone else's.

The goal was fun before strength: each commander passes its own weights, so one plays aggressive and another greedy, and Easy's mistakes are designed to look like human ones rather than random noise. Since the package is pure TypeScript, the same code runs in a Web Worker in the browser or on a dedicated Node server for online games. A self-play arena runs hundreds of AI-vs-AI games across the map pool to tune difficulty and catch balance problems before players do.

Content at the speed of code

A tactics game lives or dies on content, and that's where most of a solo developer's time would normally disappear. Tiny Tactics has five story campaigns with more than fifty missions — a boot-camp tutorial, a toy-museum WWII wing, an ancient-history hall, a modern war of a scrappy meadow country holding its capital, and a parody naval campaign — plus 32 skirmish maps, a map editor, and Lane Rush, a separate real-time mode with its own engine.

To make that feasible, content is data and assets are pipelines. Missions are written in a small scenario DSL with objectives, triggers and dialogue scenes. Commander portraits — six expressions per character — and over 170 voice lines are generated from documented prompts and scripts, so any of them can be regenerated or extended consistently. Cutscenes are played out by the game's own runtime on the real board rather than as pre-rendered video. The public site at tinytactics.app is prerendered with TanStack Start and pulls its unit sheets, damage charts, commander kits and map previews straight from the engine at build time, so the guide can never drift from the game.

A Tiny Tactics battle cutscene — a squad of red toy tanks firing on blue tanks in a forest, with each side's HP bar and a −70% damage readout

How it was built

I built Tiny Tactics on my own, working the way I now work on most projects: I own the design, the architecture and every decision, and AI coding agents do much of the implementation in parallel branches against a written plan. That only works if the codebase is built to be worked on that way — a pure engine with a single entry point, rules as isolated modules, a design document per system, and a test suite and typecheck that every change has to pass. Those constraints are what let several features land in a day without each one quietly breaking the last. It's the same approach I bring to client work: decide carefully, keep the architecture strict, and let the tooling carry the volume.