Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

DEAL-R

Event-sourced No-Limit Texas Hold’em engine and server: a deterministic, test-heavy core (engine/) with FastAPI adapters (server/), PostgreSQL (or SQLite) persistence, JWT auth, WebSockets for live tables, and a small static web UI.


Table of contents

  1. Overview
  2. Features
  3. Technology stack
  4. Repository layout
  5. Core concepts
  6. Prerequisites
  7. Installation
  8. Configuration
  9. Running locally
  10. Docker
  11. Testing & quality
  12. REST API
  13. WebSocket protocol
  14. Web UI (web/)
  15. CLI tools
  16. Determinism & replay
  17. Security
  18. Continuous integration
  19. Troubleshooting
  20. Documentation
  21. License

Overview

DEAL-R implements a play-money poker table where:

  • All meaningful state changes flow through a pure reducer: next_state(state, command) → (new_state, events[]).
  • Events are append-only in the persistence layer with optimistic concurrency (expected_version).
  • Clients send commands (sit, stand, act, start hand); the server validates, persists events, and broadcasts derived public state.
  • Hole cards are never leaked to non-owners in the intended production posture (see threat model docs).

Features

AreaCapability
EngineNLHE rules: legality, side pots, auto-advance, showdown evaluation, hand strength ranking
Event sourcingCommand idempotency, versioned streams, deterministic replay via apply_event
ServerFastAPI REST + JWT auth + WebSockets + Prometheus-friendly metrics hooks
PersistenceSQLAlchemy models, event store abstraction (PostgreSQL in CI/production path; SQLite default for dev)
QualityUnit tests, integration tests with Postgres, property-based invariant tests (Hypothesis), mypy on engine + server
UXStatic web/ assets: login, home, poker table, daily roulette

Technology stack

  • Python 3.11+ (CI tests 3.11 and 3.12)
  • FastAPI, Pydantic v2, Uvicorn
  • SQLAlchemy 2.x, PostgreSQL (recommended) / SQLite (default URL)
  • python-jose + bcrypt for JWT and password hashing (see server/services/auth.py)
  • Hypothesis for property testing
  • Ruff, Black, mypy (with sqlalchemy.ext.mypy.plugin), pre-commit

Repository layout

DEAL-R/
├── engine/ # Pure game logic — no IO, no networking
│ ├── domain/ # GameState, commands, events, Card, Deck, types
│ ├── rules/ # Legality, side pots, invariants
│ ├── reducer/ # next_state, apply_event, auto-advance
│ └── eval/ # Hand evaluation / pot splitting helpers
├── server/ # FastAPI application
│ ├── api/ # REST routers, WebSocket router, schemas
│ ├── persistence/ # Event store, ORM models
│ ├── services/ # Table service, auth, analytics, etc.
│ ├── middleware/ # Logging, rate limiting
│ ├── config.py # Pydantic settings / env
│ └── main.py # App factory, static mounts, health
├── web/ # Static HTML/JS/CSS client (session.js, table.js, …)
├── tools/ # replay_cli, hand history export
├── tests/
│ ├── unit/ # Engine + server units
│ ├── integration/ # DB + API + websocket tests
│ └── property/ # Hypothesis invariant tests
├── docs/ # Architecture, threat model, ADRs
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md # (this file)

Core concepts

Commands vs events

  • Commands (engine/domain/commands.py) express intent: SitDown, StandUp, StartHand, Act, RevealSeed.
  • Events (engine/domain/events.py) capture facts that happened: PlayerSatDown, ActionApplied, StreetDealt, ShowdownResolved, HandEnded, etc.
  • The reducer validates commands against GameState and emits zero or more events; apply_event folds events into state for replay.

Why event sourcing?

  • Auditability: the event log is the authoritative history.
  • Deterministic replay: same initial state + same events ⇒ same final state (modulo deliberate non-determinism boundaries, which this project avoids in the engine).
  • Concurrency story: optimistic locking on (hand_id/stream_id, version) detects conflicting writers.

Table streams vs hand streams

Runtime code uses identifiers such as:

  • table-{table_id} for table-level lifecycle (sit/stand outside a specific hand persistence path in some flows)
  • Individual hand_id values once a hand is started (StartHand)

The exact composition is implemented in TableService; when extending persistence, preserve version monotonicity per stream.


Prerequisites

  • Python 3.11 or newer
  • PostgreSQL 15+ (optional locally; Docker Compose supplies one)
  • Docker / Docker Compose (recommended for Postgres and image builds)

Installation

git clone <your-fork-url> DEAL-R
cd DEAL-R
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -e ".[dev]"
pre-commit install # optional but recommended

Development dependencies ([dev]) include: pytest, hypothesis, ruff, black, mypy, types-python-jose, pre-commit.


Configuration

Environment variables are read via pydantic-settings (server/config.py). Typical .env:

VariableDefaultMeaning
DATABASE_URLsqlite:///./poker.dbSQLAlchemy URL (PostgreSQL DSN for real deployments)
JWT_SECRET_KEY(dev placeholder)Must be rotated in production
JWT_ALGORITHMHS256JWT alg
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Session lifetime
RATE_LIMIT_ENABLEDtrueToggle simple rate limiting
RATE_LIMIT_PER_MINUTE60Requests/min per IP/key
LOG_LEVELINFOStructlog threshold
LOG_FORMATjsonjson or text
SNAPSHOT_INTERVAL100Snapshot cadence hook (see event store)

Running locally

PostgreSQL via Compose

docker compose up -d
export DATABASE_URL=postgresql://user:pass@localhost:5432/dbname # match your compose file

API server

uvicorn server.main:app --reload --host 0.0.0.0 --port 8000

Useful URLs:

  • OpenAPI UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • Health: http://localhost:8000/health
  • Static UI entry: http://localhost:8000/web/home.html (if static mount is enabled; see main.py)

Docker

The Dockerfile installs runtime dependencies only (pip install -e ., no [dev]). Health checks use urllib against /health (no extra requests dependency).

Build:

docker build -t deal-r:latest .

Run (example):

docker run --rm -p 8000:8000 \
-e DATABASE_URL=postgresql://… \
-e JWT_SECRET_KEY=… \
deal-r:latest

Testing & quality

# Full suite (includes integration tests; some require DATABASE_URL pointing at Postgres)
pytest
pytest --cov=engine --cov=server --cov-report=term-missing
# Focused subsets
pytest tests/unit/
pytest tests/integration/
pytest tests/property/
# Static checks
ruff check .
black --check .
mypy engine server

Pre-commit mirrors CI style checks locally:

pre-commit run --all-files

REST API summary

All JSON routes live under prefixes defined in routers (auth: /api/v1/auth, game REST: /api/v1). OpenAPI (/docs) is the authoritative list. Common capabilities include:

ConcernNotes
AuthRegister/login returns JWT access_token; use Authorization: Bearer <token>
Player profile/api/v1/players/me style endpoints (see router definitions)
Table snapshotsRead models for spectators/refresh paths
Roulette (demo)Daily spin endpoint persists chips into users.json (dev-oriented)

Consult server/api/rest.py and server/api/auth.py for exact paths and payloads.


WebSocket protocol

Connect to /ws/tables/{table_id} (see server/api/ws.py). Pass JWT as ?token=<jwt> query parameter (browser WebSockets cannot set custom headers easily).

Outbound messages commonly include:

  • type: "state" — table snapshot wrapper with seats, pots, street, version
  • type: "command_accepted" — echo of successful command application with new_version
  • type: "error" — human-readable failures (validation, capacity, etc.)

Inbound commands mirror domain commands roughly as:

{ "type": "sit_down", "data": { "seat_id": 0, "stack": 1000, "player_id": "player_alice" }, "idempotency_key": "", "expected_version": 0 }

Similar shapes exist for act, start_hand, and stand_up. Always send fresh idempotency keys per logical user action.


Web UI (web/)

Static assets demonstrate end-to-end flows:

  • session.js — shared auth/session helpers (fetchWithAuth, login redirect).
  • table.js — WebSocket-driven poker UI.
  • home.js, roulette.js — profile and daily bonus demo.

See web/DESIGN.md for UX notes.


CLI tools

python -m tools.replay_cli <hand-id> [--hash-only]
python -m tools.hh_export <hand-id> --output hand.txt

These assume database connectivity consistent with DATABASE_URL.


Determinism & replay

  1. Deck: Deck.create_shuffled(seed) derives order deterministically from an integer seed.
  2. Hand start: Clients supply a seed_commit string; replay paths derive integer seeds compatibly with runtime dealing (see TableService._replay_events and reducer start-hand flows).
  3. Replay: apply_event in a loop reconstructs table state machine positions from persisted events alone.

Extended discussion: docs/architecture.md.


Security

DEAL-R is designed as server authoritative poker with JWT auth and optimistic concurrency. Threat assumptions and residual risks are documented in docs/threat-model.md. Rotate secrets, tighten CORS, and harden persistence before any real-money adjacent deployment.


Continuous integration

GitHub Actions (.github/workflows/ci.yml):

  1. Lint — Python 3.12, pip install -e ".[dev]", pre-commit run --all-files, mypy engine server.
  2. Test — Matrix 3.11 / 3.12 against PostgreSQL 15, pytest + coverage XML.
  3. Build — Docker image build with GH Actions cache backends.

Codecov upload runs once (Python 3.12 job) to avoid duplicate reports.


Troubleshooting

SymptomThings to check
Version mismatch errorsClient expected_version stale; reload snapshot or websocket state.version.
401 on RESTExpired JWT; re-login. Ensure Authorization: Bearer … header present.
WebSocket instant disconnectMissing/invalid token query param; server logs in server/api/ws.py print decode failures during dev.
Integration tests failExport DATABASE_URL to a reachable Postgres with created DB/user (CI uses poker_test).
SQLite lockedMultiple writers; prefer Postgres for concurrent local dev.
mypy plugin errorsEnsure sqlalchemy 2.x installed; pyproject.toml enables sqlalchemy.ext.mypy.plugin.

Documentation

DocPurpose
docs/architecture.mdSystem design, boundaries between engine and server
docs/state-machine.mdStreets, transitions, table lifecycle
docs/invariants.mdChip conservation, pot correctness, etc.
docs/threat-model.mdSecurity assumptions
docs/adr/0001-event-sourcing.mdADR: why event sourcing
web/DESIGN.mdStatic client design notes

License

MIT


Quick reference card

# Dev install
pip install -e ".[dev]"# Database
docker compose up -d
# Run API
uvicorn server.main:app --reload
# Tests + types
pytest && mypy engine server
# Hooks
pre-commit run --all-files

For questions about extending the engine (new streets, tournament mode, different game variants), start from engine/domain and engine/reducer/reducer.py, then thread changes through server/services/table_service.py and API schemas.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages