Castellan

A browser-based multiplayer game used to work through real-time state, turn flow, and disconnect handling.

I built Castellan as a personal project because multiplayer games expose problems that ordinary CRUD apps avoid: shared state, WebSocket updates, server-side validation, bot-filled seats, and sessions that need to survive unreliable browsers.

React TypeScript FastAPI WebSocket SQLite Docker
A live browser session showing turn updates over WebSocket.
Personal project · Playable demo · React / TypeScript · FastAPI

What the project does

Castellan runs as a browser game with four seats in a room. A player can start a room, play through turns, and see the match update through WebSocket messages. The interesting part is not the theme of the game. It is the server-side flow behind it: validating actions, keeping clients in sync, and deciding what happens when a player leaves.

Players per room
4
Game actions
WebSocket only
State authority
Server
Frontend
React / TypeScript
Backend
FastAPI (Python)
Runtime
Docker

How actions move through the server

The browser sends an action over WebSocket. The FastAPI backend receives it, checks it against the current room state, and sends the updated state back to the room. The client does not try to resolve the game locally; it renders what the backend sends.

This keeps the game state server-side and makes the browser responsible for presentation, not game truth.

Browser Clients
NetworkService.ts
↓  WebSocket
FastAPI Backend
ws.py
GameStateMachine
game_state_machine.py
update_phase_data()
base_state.py
Event Store  +  Broadcast
SQLite  +  SocketManager
↓  phase_change
Browser Clients

The state machine keeps the game flow in one place.

The game has phases such as waiting, preparation, declaration, turns, scoring, and game over. I used a backend state machine so those phases and transitions are handled in one place instead of being spread across separate handlers.

The same update path also stores the event and broadcasts the new state. That made the code easier to reason about because a state change and a room update are not treated as two unrelated steps.

WAITING PREPARATION ROUND_START DECLARATION TURN TURN_RESULTS SCORING GAME_OVER
TURN_RESULTS → TURN while hands remain
SCORING → PREPARATION next round, no winner yet
SCORING → GAME_OVER end condition met

What happens when someone disconnects

Real-time browser apps have ordinary failure cases: a tab closes, a network drops, or someone leaves during their turn. Castellan handles that as part of the game flow instead of assuming every player stays connected.

When a player disconnects, the server gives them a short window to reconnect. If they do not return, a bot takes over that seat so the remaining players can keep playing.

01 Connection drops — grace period starts
02 No reconnect in time — bot takes over
03 Match continues for remaining players
Connected
↓  disconnect detected
5s grace period
reconnects in time
Resume player
no reconnect after 5s
Bot takeover
Game continues

Bots are part of the fallback path

The bots are rule-based. They are not machine learning, and they do not use an LLM. They exist to fill seats and to let a match continue when a human player is not available.

Bot moves go through the same action path as human moves. Once a bot submits an action, the state machine validates it, updates the room, and broadcasts the result like any other move.

Rule-based bots, not ML. The goal is continuity, not intelligence.

Stack

The stack is intentionally direct: a React browser client, a FastAPI WebSocket backend, Docker runtime, and a state-machine architecture for game flow.

Frontend
React TypeScript
Backend
Python FastAPI WebSockets
Data
SQLite Event Store
Runtime
Docker
Architecture
State Machine Event Broadcasting Session Continuity
Automation
Rule-Based Bots

Live demo

Open the project and play through a room in the browser.