diff --git a/docs/services/web-api/architecture.md b/docs/services/web-api/architecture.md index 56508c6..8f7a452 100644 --- a/docs/services/web-api/architecture.md +++ b/docs/services/web-api/architecture.md @@ -1,7 +1,9 @@ # Architecture — web-api & the TV↔phone transport This page documents how a phone drives the TV: the **WebRTC peer-to-peer** -transport (backlog **P1**) and the thin signaling service that sets it up. +transport (backlog **P1**) and the thin signaling service that sets it up. The +same process also hosts a small **stats store** (backlog **P6**) — see +[Stats store](#stats-store-backlog-p6) below. ## Why WebRTC (the decision) @@ -73,22 +75,47 @@ not restructured: | game-launcher-web | `assets/js/players.js` | `PlayerSession` — the **hub/answerer**. Accepts one `RTCPeerConnection` per phone, answers offers, adds each to the roster on channel-open, and re-emits received intents as `intent` events. | | game-launcher-web | `assets/js/app.js` | Routes session `intent` events through the **same `handleIntent()`** the keyboard uses — a phone is just another intent source. | -## Current limitation (backlog P2) +## Stats store (backlog P6) -The launcher launches a game by **full navigation** -(`location.href = ../games//`), which unloads the launcher page and **drops -the peer connection**. So today a phone drives the launcher **menu**, but not a -**running game**. Closing this needs a **persistent shell** — the launcher -hosting games in an iframe / in-page swap so the connection (and the -`PlayerSession`) survives launch, with games subscribing to controller intents. +Alongside signaling the service runs a tiny **SQLite** store (`db.js`, +`better-sqlite3`) for the metrics the console keeps *across devices* — so it can't +live in one browser's `localStorage`. It's a single file +(`web-api/data/spaceconsole.db`, `DB_PATH`-overridable, git-ignored) served on the +**same port** as signaling/app (iOS only lets a page reach its own origin, so the +stats API must be same-origin too — the same constraint that drove single-origin +serving in P5). + +``` +game (iframe) ──sc:gameover──▶ launcher ──POST /api/{play,score,result}──▶ web-api ──▶ SQLite + (adds game id, room, player names) plays · scores · results +``` + +Writes are **fire-and-forget beacons** from the launcher — a stats failure never +stalls gameplay. Games stay network-free: they call a shared `Stats` helper that +just `postMessage`s the launcher. Tables: `plays` (popularity, solo-vs-group), +`scores` (per-game leaderboards), `results` (2-player win/draw). API: + +| Method | Path | Purpose | +| --- | --- | --- | +| POST | `/api/play` | Log a launch. | +| POST | `/api/score` | Record a leaderboard score. | +| POST | `/api/result` | Record a 2-player result. | +| GET | `/api/leaderboard?game=` | Top scores for a game. | +| GET | `/api/stats` | Totals + most-played games. | + +Same-screen 2-player rides the existing transport: two phones join the **same +room**, the launcher gives each a stable **seat** (P1/P2) and tags relayed intents +with it, and a shared `Roster` (`sc:players`) carries player **names** into games. ## Design intents to preserve - **Keep the relay out of the hot path** — it brokers the handshake only; gameplay stays peer-to-peer. -- **No game state / auth / DB in web-api** — it stays a thin matchmaker. +- **Signaling stays a thin, stateless matchmaker** — no game state or auth in the + relay path. The stats store shares the process but is a separate concern, out of + the signaling/gameplay path (and best-effort, so it can never block a game). - **One intent vocabulary** end to end; a phone is just another source feeding - the same `handleIntent()`. + the same `handleIntent()`, now tagged with the sender's seat for 2-player games. ## Deploy diff --git a/docs/todo.md b/docs/todo.md index 8b8ec0b..90ee284 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -84,6 +84,10 @@ title: TODO / Backlog P5 Public multiplayer deploy. Connectivity is solved for dev and verified with a real iPhone as controller. Three fixes made it work: (1) signaling binds IPv4 0.0.0.0 (the default was IPv6-only, unreachable by LAN phones); (2) web-api can serve the app and the WebSocket same-origin on one port (STATIC_DIRnpm run serve:app) — needed because iOS Safari only lets a page reach its own origin host:port, so cross-port signaling and a local TURN are unreachable from a phone; clients now default signaling to the page origin; (3) a public TURN (free-tier) relays when direct P2P fails (iOS mDNS / cross-network). Still needed for an always-on public URL a friend can use off-LAN: host web-api on a public host with wss:// (e.g. Render, or the college box) + our own coturn. GitHub Pages serves the static app but has no signaling/TURN, so Pages alone isn't playable; a dev cloudflared tunnel works only when the host network allows it. See web-api / transport architecture. +
+ P6 + Stats + same-screen multiplayer. Framework shipped. web-api now also runs a small SQLite stats store (better-sqlite3, single file, on the same port as signaling): POST /api/play|score|result and GET /api/leaderboard|stats track which games get played, high scores, and 2-player results. The launcher fires fire-and-forget beacons; games report via a shared Stats helper (sc:gameover postMessage) — no network code in games. Two phones now share one screen from the same room QR: the launcher assigns stable seats (P1/P2), tags each relayed intent with its seat, and a shared Roster feeds player names into games for turn labels ("VET's turn (X)"). Controllers enter a name on join (persisted). A dropped player rejoins mid-game via an in-game QR/roster overlay and keeps their seat (matched by name); the lead player (P1) drives the menu & switches games and is promoted to the next player if they leave; local/remote Back now reliably returns to the game grid with the session intact. First real 2-player game: tic-tac-toe. Remaining: roll real 2-player out to the other multiplayer-capable games (chess, checkers, scrabble, space-tower, tic-tac-boom); single-player games stay solo by decision. See web-api / stats store. +