⏴ BB ⏵ INDEX
CASE STUDY·2026·LIVESOLO — DESIGN, ARCHITECTURE, ENGINEERING

Crawlers

Server-authoritative dungeon crawler. Procedural floors, D&D-adjacent combat, real-time multiplayer.

STACK

  • C#
  • ASP.NET Core
  • SignalR
  • Postgres
  • React
  • Pixi.js
▦ IMAGE SLOT

Crawlers in-game wide shot

/work/crawlers/hero.png

1600 × 900 PX

Floor 3, dungeon view with two-party expedition.

INTRO

A solo dungeon crawler built to prove a thesis: a server-authoritative architecture from day one is not over-engineering, even when you're shipping single-player. By the time I added real-time co-op, it slotted in. No rewrite, no parallel codepath.

The technical scope was the point — procedural generation, D&D-adjacent combat resolution, real-time state sync, a persistent shared world. ~21k lines of C# and TypeScript across server and client, 161 server tests. It exists because portfolio projects too often optimize for screenshots instead of decisions worth defending.

01

SECTION

The architectural bet

Single-player games usually keep gameplay logic on the client — it's the path of least resistance. I went the other way. The ASP.NET Core server owns every truth: movement legality, line-of-sight, dice rolls, item drops, even which tiles each player is allowed to see. Fog of war is filtered server-side; the client never receives data the player shouldn't see. The React + Pixi.js client sends intent and renders state diffs over SignalR.

That decision compounded. When I added 1-4 player co-op, the gameplay layer barely changed — the same room broadcast that drove single-player now fanned out to multiple connections. Fog of war was already per-player. The animation pipeline already consumed structured combat events. Persistence (corpses, world stats) was already isolated. The shape of the system absorbed multiplayer instead of resisting it.

▦ IMAGE SLOT

Lobby screen with 4-player room

/work/crawlers/lobby.png

1200 × 675 PX

Code-based lobbies, 1-4 players.
▦ IMAGE SLOT

Per-player fog-of-war filtered server-side

/work/crawlers/fog-of-war.png

1200 × 675 PX

Each player only receives tiles they can see.
02

SECTION

Procedural generation that's testable

Floors are generated by BSP partitioning, deterministic per seed. The Generation project is a pure function of (seed, config) — no I/O, no DI, no hidden state. That's what made the system testable end-to-end: 161 server tests covering BSP layout, field-of-view, movement legality, engagement, combat, item interactions, and descent.

Combat uses a `ScriptedDice` test double, so non-deterministic outcomes are deterministic on demand. A flaky test is a real bug, not noise. And the same seed produces the same dungeon every time, which means bug reports are reproducible and runs are shareable.

▦ IMAGE SLOT

ASCII renderer showing BSP-partitioned floor

/work/crawlers/bsp-floor.png

1400 × 800 PX

Server-side ASCII renderer — same seed, same floor, every time.
03

SECTION

Combat as event stream, not state diff

Combat could have been the boring thing — server changes HP, client re-renders. Instead it emits a structured event stream: Hit, Crit, Miss, Fumble, Heal, each carrying target, magnitude, and timing. The client consumes this stream as an animation queue.

Hits get a lunge plus a red flash. Crits add camera shake. Misses sidestep. Heals pulse green. Killing-blow sprites defer destruction until pending animations drain, so death looks final because the animation that earned the kill finishes first. The result: combat *feel* lives entirely in the client, combat *truth* never leaves the server. Two concerns, cleanly separated.

▶ VIDEO SLOT

Combat sequence with lunge, red flash, and camera shake on crit

/work/crawlers/combat.mp4

1280 × 720 PX

Crit → camera shake. Killing blow defers sprite destruction until anims drain.
04

SECTION

Persistence at the right layer

EF Core + Postgres for run history, player identity, corpses, and aggregate world stats. Configuration is environment-driven (`ConnectionStrings__DefaultConnection` and friends) — if no DB is present in dev, every persistence service falls back to a Null implementation and the game still runs. Postgres is optional in dev, required in prod.

The whole thing ships as a multi-stage Dockerfile + compose stack: server, Postgres, and the Vite client behind a single LAN-bound port. From clone to playable on another machine takes one command.

OUTCOME

Shipped. Single-player core, visual polish, combat juice, real-time multiplayer (1-4 players, code-based lobbies, shared fog of war), and a persistent world all live at crawlers.brac.dev. Every expansion was absorbed by the original architecture — which was the whole point.

05

EXHIBITS

CAPTURE / 01 OF 03

▦ IMAGE SLOT

Spectator mode with cross-floor camera follow

/work/crawlers/gallery-01.png

1200 × 900 PX

Spectator mode after a party wipe.

CAPTURE / 02 OF 03

▦ IMAGE SLOT

Run summary screen showing dice rolls and combat log

/work/crawlers/gallery-02.png

1200 × 900 PX

Run summary — every roll logged, every outcome traceable.

CAPTURE / 03 OF 03

▦ IMAGE SLOT

Mobile touch controls with D-pad and combat buttons

/work/crawlers/gallery-03.png

1200 × 900 PX

Touch controls auto-appear on coarse pointers.