@@ -25,138 +25,42 @@ Turn an Android TV / Fire TV into a local party-game console and use phones as w
2525
2626## How It Works
2727
28- TV runs a static file server (default ` :8080 ` ) and a WebSocket game server (default ` :8082 ` ).
29- Phones open the controller page, connect via WebSocket, send actions, and receive state updates.
30-
31- ### System Architecture
32-
3328``` mermaid
34- graph TB
35- subgraph TV["Android TV (Host)"]
36- direction TB
37- HOST["@couch-kit/host<br/><i>React Native / Expo</i>"]
38- PROVIDER["GameHostProvider"]
39- REDUCER_H["createGameReducer<br/><i>Canonical game state</i>"]
40- HTTP["Static File Server<br/><i>:8080</i>"]
41- WS["WebSocket Server<br/><i>:8082</i>"]
42- HOST --> PROVIDER
43- PROVIDER --> REDUCER_H
44- PROVIDER --> HTTP
45- PROVIDER --> WS
46- end
47-
48- subgraph CORE["@couch-kit/core"]
49- TYPES["IGameState · IPlayer · IAction"]
50- PROTOCOL["Protocol Messages<br/><i>JOIN · WELCOME · STATE_UPDATE<br/>ACTION · PING · PONG · ERROR</i>"]
51- FN["createGameReducer<br/>derivePlayerId · middleware"]
52- end
53-
54- subgraph LAN["Local Network (LAN)"]
55- direction LR
56- HTTP_CONN["HTTP — serves controller page"]
57- WS_CONN["WebSocket — real-time game sync"]
29+ graph LR
30+ subgraph TV["📺 Android TV"]
31+ HTTP["HTTP :8080"]
32+ WS["WebSocket :8082"]
5833 end
5934
60- subgraph PHONE1["Phone Browser (Client 1)"]
61- direction TB
62- CLIENT1["@couch-kit/client<br/><i>React / Vite</i>"]
63- HOOK1["useGameClient"]
64- REDUCER_C1["createGameReducer<br/><i>Optimistic local state</i>"]
65- SYNC1["useServerTime<br/><i>NTP-style clock sync</i>"]
66- CLIENT1 --> HOOK1
67- HOOK1 --> REDUCER_C1
68- HOOK1 --> SYNC1
35+ subgraph PHONES["📱 Phones"]
36+ P1["Player 1"]
37+ P2["Player 2"]
6938 end
7039
71- subgraph PHONE2["Phone Browser (Client 2)"]
72- direction TB
73- CLIENT2["@couch-kit/client"]
74- HOOK2["useGameClient"]
75- CLIENT2 --> HOOK2
76- end
77-
78- TV -- "HTTP :8080" --> LAN
79- TV -- "WS :8082" --> LAN
80- LAN -- "GET /index.html" --> PHONE1
81- LAN -- "ws:// bidirectional" --> PHONE1
82- LAN -- "GET /index.html" --> PHONE2
83- LAN -- "ws:// bidirectional" --> PHONE2
84-
85- CORE -. "shared types &<br/>reducer wrapper" .-> TV
86- CORE -. "shared types &<br/>reducer wrapper" .-> PHONE1
87- CORE -. "shared types &<br/>reducer wrapper" .-> PHONE2
88-
89- style TV fill:#1a1a2e,stroke:#e94560,color:#fff
90- style CORE fill:#0f3460,stroke:#16213e,color:#fff
91- style LAN fill:#16213e,stroke:#533483,color:#fff
92- style PHONE1 fill:#1a1a2e,stroke:#00b4d8,color:#fff
93- style PHONE2 fill:#1a1a2e,stroke:#00b4d8,color:#fff
40+ HTTP -- "serves controller page" --> P1 & P2
41+ P1 & P2 -- "actions ➡" --> WS
42+ WS -- "⬅ state updates" --> P1 & P2
9443```
9544
96- ### Protocol Sequence
97-
9845``` mermaid
9946sequenceDiagram
100- participant Phone as Phone Browser
101- participant HTTP as HTTP Server :8080
102- participant WS as WebSocket Server :8082
103- participant Host as Host (GameHostProvider)
104- participant Reducer as createGameReducer
105-
106- Note over Phone,HTTP: 1. Load Controller Page
107- Phone->>HTTP: GET /index.html
108- HTTP-->>Phone: Static web controller (React/Vite app)
109-
110- Note over Phone,WS: 2. Establish Connection
111- Phone->>WS: WebSocket connect to ws://host:8082/ws
112- WS-->>Host: connection event (socketId)
113-
114- Note over Phone,Reducer: 3. Join Handshake
115- Phone->>WS: JOIN { name, avatar, secret }
116- WS->>Host: message event
117- Host->>Host: Validate secret (isValidSecret)
118- Host->>Host: derivePlayerId(secret) via SHA-256
119-
120- Host->>Reducer: dispatch __PLAYER_JOINED__ { id, name, avatar }
121- Reducer-->>Host: New state with player added
122-
123- Host-->>Phone: WELCOME { playerId, state, serverTime }
124-
125- Note over Phone,Reducer: 4. Game Loop (throttled to ~30fps)
126- loop State Sync
127- Phone->>WS: ACTION { type, payload }
128- WS->>Host: message event
129- Host->>Host: Rate limit check (60 actions/sec)
130- Host->>Reducer: dispatch action { ...payload, playerId }
131- Reducer-->>Host: New state
132- Host-->>Phone: STATE_UPDATE { state }
133- end
47+ participant P as 📱 Phone
48+ participant TV as 📺 TV
49+
50+ P->>TV: GET controller page (HTTP)
51+ TV-->>P: Web app
52+
53+ P->>TV: JOIN { name, secret }
54+ TV-->>P: WELCOME { playerId, state }
13455
135- Note over Phone,Host: 5. Time Synchronization
136- loop Clock Sync (every 5s)
137- Host-->>Phone: PING { serverTime }
138- Phone->>WS: PONG { clientTime, serverTime }
56+ loop Game Loop
57+ P->>TV: ACTION { type, payload }
58+ TV-->>P: STATE_UPDATE { state }
13959 end
14060
141- Note over Phone,Reducer: 6. Disconnection & Session Recovery
142- Phone--xWS: Connection lost
143- WS->>Host: disconnect event
144- Host->>Reducer: dispatch __PLAYER_LEFT__ { playerId }
145- Reducer-->>Host: Player marked connected: false
146-
147- Note over Host: 5-min disconnect timeout starts
148-
149- alt Player reconnects within 5 minutes
150- Phone->>WS: WebSocket reconnect
151- Phone->>WS: JOIN { name, avatar, secret } (same secret)
152- Host->>Host: derivePlayerId returns same playerId
153- Host->>Host: Cancel cleanup timer
154- Host->>Reducer: dispatch __PLAYER_RECONNECTED__ { playerId }
155- Reducer-->>Host: Player marked connected: true
156- Host-->>Phone: WELCOME { playerId, state, serverTime }
157- else Timeout expires (5 min)
158- Host->>Reducer: dispatch __PLAYER_REMOVED__ { playerId }
159- Reducer-->>Host: Player permanently removed from state
61+ loop Heartbeat
62+ TV-->>P: PING
63+ P->>TV: PONG
16064 end
16165```
16266
0 commit comments