← 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
Crawlers in-game wide shot
Floor 3, dungeon view with two-party expedition.

INTRO

A solo dungeon crawler built to test one idea: that a server-authoritative architecture is worth it from the start, even for a single-player game. When I added real-time co-op later, it slotted in with no rewrite and no parallel codepath.

The scope was deliberately broad: procedural generation, D&D-adjacent combat resolution, real-time state sync, and a persistent shared world, backed by 161 server tests. I wanted a project whose decisions I could defend, not just screenshot.

01

SECTION

The architectural bet

Single-player games usually keep gameplay logic on the client because it's the least work. I put it on the server. 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, so the client never receives data the player shouldn't have. The React and Pixi.js client sends intent and renders state diffs over SignalR.

That paid off when co-op arrived. The same room broadcast that drove single-player now fanned out to several connections, and the gameplay layer barely changed. Fog of war was already per-player, the animation pipeline already consumed structured combat events, and persistence for corpses and world stats was already isolated. Multiplayer mostly dropped into gaps the design had left for it.

Lobby screen with 4-player room
Code-based lobbies, 1-4 players.
Per-player fog-of-war filtered server-side
Each player only receives tiles they can see.
02

SECTION

Procedural generation that's testable

Floors come from BSP partitioning, deterministic per seed. The Generation project is a pure function of (seed, config) with no I/O, no DI, and no hidden state. That's what let 161 server tests cover BSP layout, field-of-view, movement legality, engagement, combat, item interactions, and descent end to end.

Combat uses a `ScriptedDice` test double, so outcomes that are random in play become fixed on demand, and a flaky test points to a real bug. Because the same seed produces the same dungeon every time, bug reports reproduce exactly and runs are shareable.

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

SECTION

Combat as an event stream

Combat could have been a number change on the server and a re-render on the client. Instead the server emits a structured event stream — Hit, Crit, Miss, Fumble, Heal — each carrying target, magnitude, and timing, and the client plays it back as an animation queue.

Hits lunge and flash red. Crits add camera shake. Misses sidestep. Heals pulse green. Killing-blow sprites hold off on destruction until pending animations drain, so the hit that earned the kill lands before anything dies. The server still owns every outcome; the client only owns how it feels.

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

SECTION

Persistence at the right layer

EF Core and Postgres handle run history, player identity, corpses, and aggregate world stats. Configuration is environment-driven through `ConnectionStrings__DefaultConnection` and friends, and if no database is present in dev, every persistence service falls back to a Null implementation and the game still runs. Postgres is optional in development and required in production.

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

OUTCOME

Live at crawlers.brac.dev: single-player core, visual polish, combat juice, real-time co-op for 1–4 players with code-based lobbies and shared fog of war, and a persistent world. Every one of those expansions fit the architecture the first version already had.

05

EXHIBITS

CAPTURE / 01 OF 03

Spectator mode with cross-floor camera follow
Spectator mode after a party wipe.

CAPTURE / 02 OF 03

Run summary screen showing dice rolls and combat log
Run summary — every roll logged, every outcome traceable.

CAPTURE / 03 OF 03

Mobile touch controls with D-pad and combat buttons
Touch controls auto-appear on coarse pointers.