Update README.md
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
# Route Rush
|
# 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.
|
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 client-side dependencies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## 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.
|
Each round, all players receive the same grid with a start node and a destination. Links between nodes carry modifiers (normal, slow, boost, 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.
|
Designed for 2–4 players. Runs as a Docker container on `labmini-01`, proxied through Caddy with automatic SSL.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ Designed for 2–4 players. Runs as a Docker container proxied through the Pathf
|
|||||||
|
|
||||||
| Purpose | URL |
|
| Purpose | URL |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Players (mobile) | `https://pathfinder.libertypr.com/route-rush/` |
|
| Players (mobile) | `https://route-rush.carloselugo.com` |
|
||||||
| Host / Projector | `https://pathfinder.libertypr.com/route-rush/host` |
|
| Host / Projector | `https://route-rush.carloselugo.com/host` |
|
||||||
| Score History | `https://pathfinder.libertypr.com/route-rush/scores` |
|
| Score History | `https://route-rush.carloselugo.com/scores` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Designed for 2–4 players. Runs as a Docker container proxied through the Pathf
|
|||||||
| Realtime | WebSockets (native FastAPI) |
|
| Realtime | WebSockets (native FastAPI) |
|
||||||
| Persistence | SQLite via Docker volume |
|
| Persistence | SQLite via Docker volume |
|
||||||
| Frontend | Vanilla JS, HTML5 Canvas |
|
| Frontend | Vanilla JS, HTML5 Canvas |
|
||||||
| Proxy | Pathfinder nginx (`pathfinder-nginx`) |
|
| Proxy | Caddy (automatic SSL, `web_web-net`) |
|
||||||
| Container | Docker Compose, `ipfix-stack_ipfix-net` |
|
| Container | Docker Compose |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,6 +43,7 @@ Designed for 2–4 players. Runs as a Docker container proxied through the Pathf
|
|||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
|
├── static/ # Empty — all CSS/JS is inline in templates
|
||||||
└── templates/
|
└── templates/
|
||||||
├── player.html # Mobile game interface (HTML5 Canvas grid)
|
├── player.html # Mobile game interface (HTML5 Canvas grid)
|
||||||
├── host.html # Host/projector control panel with QR code
|
├── host.html # Host/projector control panel with QR code
|
||||||
@@ -58,69 +59,53 @@ services:
|
|||||||
route-rush:
|
route-rush:
|
||||||
build: .
|
build: .
|
||||||
container_name: route-rush
|
container_name: route-rush
|
||||||
ports:
|
restart: unless-stopped
|
||||||
- "8001:8001"
|
|
||||||
volumes:
|
|
||||||
- rr-data:/data
|
|
||||||
environment:
|
environment:
|
||||||
- DB_PATH=/data/rr.db
|
- DB_PATH=/data/rr.db
|
||||||
restart: unless-stopped
|
volumes:
|
||||||
|
- route_rush_data:/data
|
||||||
networks:
|
networks:
|
||||||
- ipfix-net
|
- web-net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
rr-data:
|
route_rush_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
ipfix-net:
|
web-net:
|
||||||
name: ipfix-stack_ipfix-net
|
|
||||||
external: true
|
external: true
|
||||||
|
name: web_web-net
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> No ports exposed to host — Caddy reaches the container directly over `web_web-net`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nginx Proxy (Pathfinder default.conf)
|
## Caddyfile Block
|
||||||
|
|
||||||
Add to both the HTTP (port 80) and HTTPS (port 443) server blocks:
|
```caddy
|
||||||
|
route-rush.carloselugo.com {
|
||||||
```nginx
|
import security_headers
|
||||||
location = /route-rush {
|
reverse_proxy route-rush:8001 {
|
||||||
return 301 /route-rush/;
|
header_up Host {host}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
}
|
}
|
||||||
|
log {
|
||||||
location /route-rush/ws/ {
|
output file /var/log/caddy/route-rush.log
|
||||||
proxy_pass http://route-rush:8001/ws/;
|
format json
|
||||||
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:
|
> No `encode gzip` — gzip breaks WebSocket upgrades.
|
||||||
|
> No `internal_only` — players need public access from their mobile devices.
|
||||||
|
|
||||||
|
After editing the Caddyfile, force-recreate Caddy (reload is not enough due to bind mount caching):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec pathfinder-nginx nginx -t && docker exec pathfinder-nginx nginx -s reload
|
cd /opt/web
|
||||||
|
docker compose up -d --force-recreate caddy
|
||||||
```
|
```
|
||||||
|
|
||||||
> If nginx ignores the changes, force a full recreate:
|
|
||||||
> ```bash
|
|
||||||
> cd /opt/pathfinder && docker compose up -d --force-recreate pathfinder-nginx
|
|
||||||
> ```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
@@ -148,12 +133,14 @@ Host configures rounds (3/5/7) and time per round (30/45/60s)
|
|||||||
Host clicks START GAME
|
Host clicks START GAME
|
||||||
↓
|
↓
|
||||||
[Each round]
|
[Each round]
|
||||||
Grid is generated → broadcast to all players
|
Grid generated → broadcast to all players
|
||||||
Players trace route by tapping nodes → submit before timer expires
|
Players trace route by tapping nodes → submit before timer expires
|
||||||
Timer expires or all players submit → Host clicks END ROUND
|
Timer expires or all players submit → Host clicks END ROUND
|
||||||
Results screen: all paths revealed on grid, leaderboard updated
|
Results screen: all paths revealed on grid, leaderboard updated
|
||||||
↓
|
↓
|
||||||
After final round → Podium screen with confetti
|
After final round → Podium screen with confetti
|
||||||
|
↓
|
||||||
|
Host clicks PLAY AGAIN → all players kicked back to join screen
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -222,6 +209,7 @@ Formula: `points = 1000 + 500 × (time_remaining / time_limit) − max(0, hop_co
|
|||||||
| `round_start` | `{round, total_rounds, grid, time_limit}` | New round begins |
|
| `round_start` | `{round, total_rounds, grid, time_limit}` | New round begins |
|
||||||
| `path_result` | `{points, total, reason, elapsed}` | Score for submitted path |
|
| `path_result` | `{points, total, reason, elapsed}` | Score for submitted path |
|
||||||
| `round_results` | `{grid, submissions, leaderboard, is_final}` | Round ended |
|
| `round_results` | `{grid, submissions, leaderboard, is_final}` | Round ended |
|
||||||
|
| `kicked` | — | Host reset — return to join screen |
|
||||||
| `error` | `{msg}` | Error (game full, invalid name) |
|
| `error` | `{msg}` | Error (game full, invalid name) |
|
||||||
|
|
||||||
### Host → Server
|
### Host → Server
|
||||||
@@ -230,7 +218,7 @@ Formula: `points = 1000 + 500 × (time_remaining / time_limit) − max(0, hop_co
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `start_round` | `{total_rounds?, time_limit?}` | Start next round |
|
| `start_round` | `{total_rounds?, time_limit?}` | Start next round |
|
||||||
| `end_round` | — | End current round and reveal results |
|
| `end_round` | — | End current round and reveal results |
|
||||||
| `reset` | — | Reset game to lobby |
|
| `reset` | — | Kick all players and reset to lobby |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -255,21 +243,20 @@ CREATE TABLE scores (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
Scores persist in Docker volume `rr-data` and survive container rebuilds.
|
Scores persist in Docker volume `route_rush_data` and survive container rebuilds.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Known Issues / Lessons Learned
|
## 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.
|
- **`static/` directory must exist** — FastAPI's `StaticFiles` mount throws `RuntimeError` if the directory is missing, even if it's empty. Always create it: `mkdir -p /opt/route-rush/static`.
|
||||||
- **No gzip on WebSocket proxies** — Caddy/nginx `encode gzip` or `gzip on` breaks WebSocket upgrades. The route-rush proxy block intentionally omits compression.
|
- **No gzip on WebSocket proxies** — Caddy `encode gzip` breaks WebSocket upgrades. Omit it for any service with WS connections.
|
||||||
- **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.
|
- **Caddy stale bind mount** — after editing the Caddyfile, always use `--force-recreate caddy`, not just reload.
|
||||||
- **Player limit is 4** — enforced server-side. A 5th connection attempt returns an error message.
|
- **Player limit is 4** — enforced server-side. A 5th connection attempt returns an error. To raise the limit, change `len(game.players) >= 4` in `main.py` and update the lobby display in `host.html`.
|
||||||
|
- **PLAY AGAIN kicks all players** — by design. Host reset sends `kicked` to all connected players, clearing their session and returning them to the join screen. Players must re-enter their info for the next game.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Scaling Notes
|
## 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.
|
Current limit is 4 players. For larger groups (e.g. a full team event), the recommended approach is manual brackets: 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`.
|
|
||||||
Reference in New Issue
Block a user