Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 56 additions & 248 deletions .omc/project-memory.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ instructions.

See [CONTRIBUTING.md](CONTRIBUTING.md) for full setup, running, and testing instructions.

FINDING: Server cannot be built and started w/o installing ".[dev]" dependencies!
FINDING: python risus.py does not work w/o installing ".[client]" dependencies!

```bash
# Start the stack
podman-compose up -d # or: docker compose up -d
Expand Down
17 changes: 17 additions & 0 deletions docs/architecture/component-communication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Component Communication Architecture

Risus CLI — multiplayer battle tracker. Pure CLI client + FastAPI server over WebSocket and REST.

This document is an index. Each diagram lives in its own file.

---

## Diagrams

| Diagram | Description |
|---------|-------------|
| [System Overview](system-overview.md) | All client and server components with their internal wiring, cross-boundary WebSocket and REST connections, and the three PostgreSQL tables. |
| [WebSocket Message Protocol](websocket-protocol.md) | All 7 client→server command types and 6 server→client frame types, annotated with lock requirements, broadcast scope, and which commands produce which responses. |
| [Key Interaction Flows](interaction-flows.md) | Four sequence diagrams: the lock/edit/unlock happy path, concurrent edit rejection, automatic lock release on client disconnect, and battle save/load. |
| [REST Endpoints](rest-endpoints.md) | The three HTTP endpoints (`/healthz`, `/state`, `/saves`), the CLI code paths that call them, and their PostgreSQL backing tables. |
| [Threading Model](threading-model.md) | How the CLI splits work across a synchronous main thread and a background asyncio thread, connected via two queues and a thread-safe state mirror with an update event. |
90 changes: 90 additions & 0 deletions docs/architecture/interaction-flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Key Interaction Flows

Four sequence diagrams covering the most important runtime scenarios. The first shows the happy path: a client acquires a lock, edits a player, then releases the lock, with broadcasts propagating to other clients at each step. The second shows concurrent conflict: a second client attempts an edit on an already-locked player and receives a private error. The third shows automatic cleanup: when a client disconnects the server releases all its held locks and broadcasts presence/lock updates. The fourth shows persistence: a client saves the current battle state to a named snapshot or restores one, replacing all live player data.

---

## Lock → Edit → Unlock

```mermaid
sequenceDiagram
actor A as Client A
participant S as Server (ws.py + commands.py)
participant L as LockManager
participant D as Database
actor B as Client B

A->>S: lock {player_name}
S->>L: acquire(player_name, A)
L-->>S: granted
S-->>A: lock_acquired (caller)
S-->>B: lock_acquired (broadcast)

A->>S: switch_cliche {player_name, cliche, dice}
S->>L: check holder == A
L-->>S: ok
S->>D: UPDATE players
S-->>A: state (broadcast)
S-->>B: state (broadcast)

A->>S: unlock {player_name}
S->>L: release(player_name)
S-->>A: lock_released (broadcast)
S-->>B: lock_released (broadcast)
```

## Concurrent Edit — Lock Denied

```mermaid
sequenceDiagram
actor A as Client A (holds lock)
participant S as Server
participant L as LockManager
actor B as Client B

Note over A,L: A already holds lock on "Aragorn"

B->>S: switch_cliche {player_name: "Aragorn", ...}
S->>L: check holder
L-->>S: locked by A
S-->>B: error "lock required" (caller only)
```

## Client Disconnect — Auto Release

```mermaid
sequenceDiagram
participant S as Server (ws.py)
participant L as LockManager
participant D as Database
actor X as Client (disconnects)
actor O as Other Clients

X--xS: WebSocket closed
S->>L: release_all(client_name)
loop for each released lock
S-->>O: lock_released (broadcast)
end
S->>D: update presence
S-->>O: presence (broadcast)
```

## Save / Load

```mermaid
sequenceDiagram
actor C as Client
participant S as Server
participant D as Database

C->>S: save {save_name}
S->>D: INSERT/UPSERT saves (JSONB snapshot)
D-->>S: ok
S-->>C: (no broadcast — silent)

C->>S: load {save_name}
S->>D: SELECT saves WHERE save_name
D-->>S: snapshot data
S->>D: DELETE players, INSERT from snapshot
S-->>C: state (broadcast all)
```
31 changes: 31 additions & 0 deletions docs/architecture/rest-endpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# REST Endpoints

Shows the three HTTP endpoints exposed by `server/rest.py` and the CLI code paths that call them. The client calls `/healthz` for liveness checks, `/state` to load the current player list on startup, and `/saves` to populate the load-battle menu. Each endpoint is backed directly by a PostgreSQL table — `players` for healthz and state, `saves` for the saves listing.

---

```mermaid
graph LR
subgraph CLIENT
UI2["risus.py<br/>Load menu<br/>Health check"]
end

subgraph REST_LAYER["server/rest.py"]
R1["GET /healthz<br/>→ {ok: true}"]
R2["GET /state<br/>→ {type: state, players: [...]}"]
R3["GET /saves<br/>→ [{save_name, saved_at}]"]
end

subgraph DB2["PostgreSQL"]
P["players"]
SV["saves"]
end

UI2 -->|"liveness check"| R1
UI2 -->|"initial state on connect"| R2
UI2 -->|"list saves in menu"| R3

R1 -->|"SELECT 1"| P
R2 --> P
R3 --> SV
```
54 changes: 54 additions & 0 deletions docs/architecture/system-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# System Overview

Shows all major components of the Risus CLI system and how they connect. The CLI client subgraph contains the main loop, WebSocket client, state mirror, and config reader. The FastAPI server subgraph groups connection management, command dispatch, lock management, REST endpoints, DB helpers, and Pydantic models. PostgreSQL sits at the bottom holding three tables. Arrows show data flow: WebSocket frames between client and server, REST calls for initial state and saves listing, and SQL queries to the DB.

---

```mermaid
graph TB
subgraph CLIENT["CLI Client (risus.py + client/)"]
direction TB
UI["risus.py<br/>Main Loop / UI Renderer<br/>(select.select polling)"]
WS_C["ws_client.py<br/>Async WebSocket Client<br/>(background thread)"]
STATE["state.py<br/>ClientState<br/>(thread-safe mirror)"]
CFG["config.py<br/>Config Persistence<br/>(risus.cfg)"]

UI -->|"enqueue outbox"| WS_C
WS_C -->|"enqueue inbox"| UI
WS_C -->|"apply frame / set update_event"| STATE
STATE -->|"snapshot_*()"| UI
CFG -->|"server / name / token"| WS_C
end

subgraph SERVER["FastAPI Server (server/)"]
direction TB
APP["app.py<br/>Lifespan / DI<br/>(DB pool, managers)"]
WS_S["ws.py<br/>ConnectionManager<br/>(presence, broadcast)"]
CMD["commands.py<br/>Message Handlers<br/>(8 command types)"]
LOCKS["locks.py<br/>LockManager<br/>(in-memory, session)"]
REST["rest.py<br/>REST Endpoints"]
DB["db.py<br/>DB Helpers<br/>(asyncpg)"]
MODELS["models.py<br/>Pydantic Models"]

APP -->|"injects"| WS_S
APP -->|"injects"| LOCKS
APP -->|"injects"| DB
WS_S -->|"dispatch"| CMD
CMD -->|"acquire/release"| LOCKS
CMD -->|"CRUD / snapshots"| DB
CMD -->|"broadcast"| WS_S
REST -->|"read"| DB
MODELS -.->|"schema"| CMD
MODELS -.->|"schema"| WS_S
end

subgraph STORAGE["PostgreSQL"]
TBL_P["players<br/>(name PK, cliche, dice, lost_dice)"]
TBL_S["saves<br/>(save_name PK, saved_at, data JSONB)"]
TBL_L["locks<br/>(player_name PK, locked_by, acquired_at)<br/>audit log — truncated on startup"]
end

WS_C -->|"ws[s]://host/ws/{name}?token=…"| WS_S
UI -->|"GET /state · GET /saves · GET /healthz"| REST
DB -->|"queries"| STORAGE
```
38 changes: 38 additions & 0 deletions docs/architecture/threading-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Threading Model

Shows how the CLI client splits work across two threads. The main thread runs a synchronous `select.select()` poll loop that handles user input and terminal rendering. A background thread runs an asyncio event loop managing the WebSocket connection with automatic exponential-backoff reconnect. The two threads communicate through two thread-safe queues (outbox: main→ws, inbox: ws→main) and a `ClientState` object protected by a `threading.Lock`. The `update_event` flag wakes the main loop whenever the server pushes new state.

---

```mermaid
graph TB
subgraph MAIN["Main Thread (sync)"]
LOOP["select.select() poll loop"]
RENDER["Terminal render<br/>print() / clear()"]
INPUT["input() handlers"]
SNAP["state.snapshot_*()"]
end

subgraph BG["Background Thread (asyncio)"]
ALOOP["asyncio event loop"]
WSC["WebSocket connection<br/>auto-reconnect<br/>1→2→4→8s backoff"]
RECV["recv task"]
SEND["send task"]
end

subgraph SYNC["Shared (thread-safe)"]
OUTBOX["outbox Queue<br/>(main→ws)"]
INBOX["inbox Queue<br/>(ws→main)"]
CSTT["ClientState<br/>(threading.Lock)<br/>update_event"]
end

INPUT -->|"put"| OUTBOX
OUTBOX -->|"get"| SEND
SEND -->|"ws.send_str"| WSC
WSC -->|"ws.receive_str"| RECV
RECV -->|"put"| INBOX
INBOX -->|"get / apply frame"| CSTT
CSTT -->|"update_event.set()"| LOOP
LOOP -->|"update_event triggered"| SNAP
SNAP --> RENDER
```
38 changes: 38 additions & 0 deletions docs/architecture/websocket-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# WebSocket Message Protocol

Maps every WebSocket message type in both directions. The left subgraph lists the 7 client-to-server commands (add player, edit cliche, reduce dice, lock, unlock, save, load) with their required fields and lock constraints. The right subgraph lists the 6 server-to-client frames (state, presence, lock_acquired, lock_released, lock_denied, error) annotated with whether they broadcast to all clients or go to the caller only. Edges show which command produces which response frame.

---

```mermaid
graph LR
subgraph C2S["Client → Server"]
direction TB
c1["add_player<br/>name · cliche · dice"]
c2["switch_cliche<br/>player_name · cliche · dice<br/>⚠ lock required"]
c3["reduce_dice<br/>player_name · amount · is_dead<br/>⚠ lock required"]
c4["lock<br/>player_name"]
c5["unlock<br/>player_name"]
c6["save<br/>save_name"]
c7["load<br/>save_name"]
end

subgraph S2C["Server → Client"]
direction TB
s1["state<br/>players[]<br/>→ broadcast all<br/>→ triggers UI refresh"]
s2["presence<br/>clients[]<br/>→ broadcast all<br/>→ triggers UI refresh"]
s3["lock_acquired<br/>player_name · locked_by<br/>→ broadcast all<br/>→ triggers UI refresh"]
s4["lock_released<br/>player_name<br/>→ broadcast all<br/>→ triggers UI refresh"]
s5["lock_denied<br/>player_name · locked_by<br/>→ caller only"]
s6["error<br/>message<br/>→ caller only"]
end

c1 -->|"DB insert →"| s1
c2 -->|"lock check / DB update →"| s1
c3 -->|"lock check / DB update/delete →"| s1
c4 -->|"acquire"| s3
c4 -->|"already held"| s5
c5 -->|"release"| s4
c6 -->|"DB save (no broadcast)"| s6
c7 -->|"release all locks / DB replace →"| s1
```
60 changes: 60 additions & 0 deletions docs/features/health-status/prd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Anforderungen
=============

Vorbereitende Gedanken
----------------------

- Wie teilen wir die Health des jeweiligen Spielers mit?

- Wann teilen wir die Health des jeweiligen Spielers mit?

- Wie ändern wir den Health State?

- Wie wird er angezeigt?

- Wie werden die effektiv wirksamen Cliché Würfel angezeigt?

- Welchen Cliché Wert muss der Player eingeben, wenn er Health 3 hat und das
Cliché wechselt - das reduzierte Cliché oder das volle Cliché?
-> A:

Beispiel UI
===========

Gesunder Anfangszustand für 1 Player
------------------------------------

```text
Connected: El_Maestro
Battle state
========================================
NAME HEALTH EFF. DICE CLICHE (DICE)
---------------- --------- --------- ----------------
Evil Wizard 6 3 Magic (3)

1. Add player
2. Switch cliche
3. Reduce dice
4. Save
5. Load
6. Quit
```

Abzug wegen Health Zustand für 1 Player
---------------------------------------

```text
Connected: El_Maestro
Battle state
========================================
NAME HEALTH EFF. DICE CLICHE (DICE)
---------------- --------- --------- ----------------
Evil Wizard 4 2 Magic (3)

1. Add player
2. Switch cliche
3. Reduce dice
4. Save
5. Load
6. Quit
```
Loading