The poker engine is built using event sourcing architecture, ensuring deterministic replay and complete auditability. The system is divided into two main layers:
- Engine Layer (Pure, IO-free)
- Server Layer (IO, persistence, networking)
The engine is pure - no database, no network, no side effects. It consists of:
-
Domain Models (
engine/domain/): Core business entitiesstate.py: GameState, PlayerState, Pottypes.py: Card, Deck, Money, SeatIdcommands.py: Command types (SitDown, Act, StartHand, etc.)events.py: Domain events (HandStarted, ActionApplied, etc.)
-
Rules (
engine/rules/): Business logiclegality.py: Betting legality checks, round completionsidepots.py: Side pot calculationinvariants.py: State invariant validation
-
Reducer (
engine/reducer/): State transitionsreducer.py:next_state(state, command) -> (new_state, events[])autoadvance.py: Automatic game progression
-
Evaluation (
engine/eval/): Hand evaluationevaluator.py: Poker hand ranking and pot splitting
The server provides IO adapters:
-
Persistence (
server/persistence/): Event storeevent_store.py: Append-only event log with optimistic concurrencymodels.py: SQLAlchemy models
-
API (
server/api/): HTTP and WebSocket endpointsrest.py: REST API (table snapshot, hand events)ws.py: WebSocket API (real-time updates)schemas.py: Pydantic request/response models
-
Services (
server/services/): Business orchestrationtable_service.py: Command processing, event persistenceauth.py: Authentication (stub for play-money)
Client → Command → TableService → Reducer → Events → EventStore → Broadcast
- Client sends command with
idempotency_keyandexpected_version - TableService checks idempotency (duplicate commands ignored)
- Reducer processes command, emits events
- Events appended to event store with optimistic concurrency
- Events broadcast to all connected clients
State is never stored directly. Instead:
- Load events from event store
- Replay events in order:
state = fold(events, initial_state) - Result is identical to reducer-produced state (deterministic)
Given:
- Initial state
- Seed commit/reveal
- Event log
The final state is guaranteed identical across replays.
┌─────────┐
│ Client │
└────┬────┘
│ Command (idempotency_key, expected_version)
▼
┌─────────────────┐
│ TableService │
│ - Check idemp. │
│ - Get state │
└────┬────────────┘
│
▼
┌─────────────────┐
│ Reducer │
│ next_state() │
└────┬────────────┘
│ (new_state, events)
▼
┌─────────────────┐
│ EventStore │
│ append_events() │
│ (optimistic CC) │
└────┬────────────┘
│
▼
┌─────────────────┐
│ Broadcast │
│ (WebSocket) │
└─────────────────┘
┌──────────────┐
│ EventStore │
│ get_events() │
└──────┬───────┘
│
▼
┌──────────────┐
│ Replay Loop │
│ for event: │
│ state = │
│ apply_event│
│ (state, │
│ event) │
└──────┬───────┘
│
▼
┌──────────────┐
│ Final State │
└──────────────┘
- Pure Engine: Engine has no dependencies on server layer
- Event Sourcing: All state changes are events
- Optimistic Concurrency: Version-based conflict detection
- Idempotent Commands: Duplicate commands safely ignored
- Deterministic: Same inputs → same outputs
- Language: Python 3.11+
- Web Framework: FastAPI
- Database: PostgreSQL (SQLite for development)
- ORM: SQLAlchemy 2.0
- Validation: Pydantic v2
- Testing: pytest + Hypothesis