From c7f1b5ebe8f32fe045b25709aaa2b52e2aef082c Mon Sep 17 00:00:00 2001 From: Carlos Lugo Date: Wed, 13 May 2026 12:15:23 +0000 Subject: [PATCH] Add README.md --- README.md | 275 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..96547af --- /dev/null +++ b/README.md @@ -0,0 +1,275 @@ +# Route Rush + +Multiplayer network routing game built for team events. Players race to find the most efficient path through a 4×4 network grid before time runs out. Built with FastAPI, WebSockets, and vanilla JS — no dependencies on the client side. + +--- + +## Overview + +Each round, all players receive the same grid with a start node and a destination. Links between nodes carry modifiers (normal, slow, high-latency, or down). Players trace a route by tapping nodes on their mobile device and submit before the timer expires. Points are awarded based on whether the route is valid, how fast it was submitted, and how efficient the path was. + +Designed for 2–4 players. Runs as a Docker container proxied through the Pathfinder nginx stack. + +--- + +## URLs + +| Purpose | URL | +|---|---| +| Players (mobile) | `https://pathfinder.libertypr.com/route-rush/` | +| Host / Projector | `https://pathfinder.libertypr.com/route-rush/host` | +| Score History | `https://pathfinder.libertypr.com/route-rush/scores` | + +--- + +## Stack + +| Component | Technology | +|---|---| +| Backend | Python 3.12, FastAPI, Uvicorn | +| Realtime | WebSockets (native FastAPI) | +| Persistence | SQLite via Docker volume | +| Frontend | Vanilla JS, HTML5 Canvas | +| Proxy | Pathfinder nginx (`pathfinder-nginx`) | +| Container | Docker Compose, `ipfix-stack_ipfix-net` | + +--- + +## File Structure + +``` +/opt/route-rush/ +├── main.py # FastAPI app — game logic, grid generation, WebSockets, SQLite +├── requirements.txt +├── Dockerfile +├── docker-compose.yml +└── templates/ + ├── player.html # Mobile game interface (HTML5 Canvas grid) + ├── host.html # Host/projector control panel with QR code + └── scores.html # Score history viewer +``` + +--- + +## Docker Compose + +```yaml +services: + route-rush: + build: . + container_name: route-rush + ports: + - "8001:8001" + volumes: + - rr-data:/data + environment: + - DB_PATH=/data/rr.db + restart: unless-stopped + networks: + - ipfix-net + +volumes: + rr-data: + +networks: + ipfix-net: + name: ipfix-stack_ipfix-net + external: true +``` + +--- + +## Nginx Proxy (Pathfinder default.conf) + +Add to both the HTTP (port 80) and HTTPS (port 443) server blocks: + +```nginx +location = /route-rush { + return 301 /route-rush/; +} + +location /route-rush/ws/ { + proxy_pass http://route-rush:8001/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; +} + +location /route-rush/ { + proxy_pass http://route-rush:8001/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_read_timeout 300s; + proxy_send_timeout 300s; +} +``` + +After editing, reload nginx without downtime: + +```bash +docker exec pathfinder-nginx nginx -t && docker exec pathfinder-nginx nginx -s reload +``` + +> If nginx ignores the changes, force a full recreate: +> ```bash +> cd /opt/pathfinder && docker compose up -d --force-recreate pathfinder-nginx +> ``` + +--- + +## Deploy + +```bash +# First deploy +cd /opt/route-rush +docker compose up -d --build + +# After code changes +docker compose up -d --build + +# View logs +docker logs route-rush --tail 50 -f +``` + +--- + +## Game Flow + +``` +Host opens /host → Players scan QR or open URL → Players enter name + dept + ↓ +Host configures rounds (3/5/7) and time per round (30/45/60s) +Host clicks START GAME + ↓ +[Each round] + Grid is generated → broadcast to all players + Players trace route by tapping nodes → submit before timer expires + Timer expires or all players submit → Host clicks END ROUND + Results screen: all paths revealed on grid, leaderboard updated + ↓ +After final round → Podium screen with confetti +``` + +--- + +## Scoring + +| Condition | Points | +|---|---| +| Route reaches destination | 1000 base | +| Speed bonus (fastest = +500, slowest = +0) | 0 – 500 | +| Each extra hop over minimum (efficiency penalty) | −50 per hop | +| Route did not reach destination | 0 | +| Path through a down link | 0 | +| Time expired, no submission | 0 | + +Formula: `points = 1000 + 500 × (time_remaining / time_limit) − max(0, hop_cost − 3) × 50` + +--- + +## Grid Modifiers + +| Modifier | Color | Effect | +|---|---|---| +| Normal | Cyan | Counts as 1 hop | +| Slow (2×) | Orange | Counts as 2 hops (efficiency penalty) | +| Boost (½×) | Green | Counts as 0.5 hops | +| Down (✕) | Red dashed | Cannot be traversed — route invalid if used | + +**Rules:** +- The start node is always pre-selected. Players begin tracing from the first adjacent node. +- Backtracking is supported — tap the previous node to undo the last step. +- Loops are not allowed (a node can only appear once in the path). +- The start node always has at least one non-blocked neighbor (enforced at grid generation). + +--- + +## API Endpoints + +| Method | Path | Description | +|---|---|---| +| GET | `/` | Player game page | +| GET | `/host` | Host control panel | +| GET | `/scores` | Score history page | +| GET | `/api/scores` | JSON — all sessions and scores | +| DELETE | `/api/scores/clear` | Wipe all sessions and scores | +| WS | `/ws/player/{id}` | Player WebSocket connection | +| WS | `/ws/host` | Host WebSocket connection | + +--- + +## WebSocket Events + +### Player → Server + +| Event | Payload | Description | +|---|---|---| +| `join` | `{name, display, dept}` | Register player | +| `submit_path` | `{path: [nodeIds]}` | Submit route for scoring | + +### Server → Player + +| Event | Payload | Description | +|---|---|---| +| `joined` | `{name, display, dept}` | Confirmed join | +| `phase` | `{phase}` | Phase change (lobby/playing/results) | +| `round_start` | `{round, total_rounds, grid, time_limit}` | New round begins | +| `path_result` | `{points, total, reason, elapsed}` | Score for submitted path | +| `round_results` | `{grid, submissions, leaderboard, is_final}` | Round ended | +| `error` | `{msg}` | Error (game full, invalid name) | + +### Host → Server + +| Event | Payload | Description | +|---|---|---| +| `start_round` | `{total_rounds?, time_limit?}` | Start next round | +| `end_round` | — | End current round and reveal results | +| `reset` | — | Reset game to lobby | + +--- + +## SQLite Schema + +```sql +CREATE TABLE sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + started_at TEXT NOT NULL, + ended_at TEXT, + rounds INTEGER DEFAULT 0 +); + +CREATE TABLE scores ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER REFERENCES sessions(id), + player TEXT NOT NULL, + score INTEGER DEFAULT 0, + rank INTEGER, + dept TEXT NOT NULL DEFAULT '', + finished_at TEXT NOT NULL +); +``` + +Scores persist in Docker volume `rr-data` and survive container rebuilds. + +--- + +## Known Issues / Lessons Learned + +- **`crypto.randomUUID()` not available over HTTP** — the player ID uses `Math.random().toString(36)` as a fallback since the game runs over HTTP internally. `crypto.randomUUID()` requires HTTPS or localhost. +- **No gzip on WebSocket proxies** — Caddy/nginx `encode gzip` or `gzip on` breaks WebSocket upgrades. The route-rush proxy block intentionally omits compression. +- **nginx bind mount is read-only** — after editing `default.conf`, nginx must be force-recreated (`--force-recreate`), not just reloaded, for changes to take effect from disk. +- **Player limit is 4** — enforced server-side. A 5th connection attempt returns an error message. + +--- + +## Scaling Notes + +Current limit is 4 players. To scale to larger groups (e.g. 16 for a full team event), the recommended approach is manual brackets: 4 groups of 4 play simultaneously, winners advance to a final round. The host manages bracket progression manually between sessions. + +To raise the hard limit, change `game.players >= 4` in `main.py` and update the lobby display in `host.html`. \ No newline at end of file