Files
ni-quiz/README.md
T
2026-05-12 03:10:52 +00:00

247 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🌐 NI Quiz
Multiplayer quiz game — self-hosted, no app install required. Built for the Network Intelligence team's show-and-tell event at LCPR. Players join from their mobile browser, answer questions via a Space Invaders arcade interface, and scores are tracked across sessions.
**Live at:** `https://ni-quiz.carloselugo.com`
---
## Stack
| Layer | Tech |
|---|---|
| Backend | Python · FastAPI · WebSockets |
| Storage | SQLite (Docker volume) |
| Frontend | Vanilla HTML/CSS/JS · Canvas 2D |
| Container | Docker Compose |
| Reverse proxy | Caddy (shared `web_web-net`) |
| Host | labmini-01 · Dell Optiplex · Ubuntu |
---
## Architecture
```
Player (mobile browser)
↓ HTTPS / WSS
Caddy (ni-quiz.carloselugo.com)
↓ reverse_proxy :8000
ni-quiz container (FastAPI)
SQLite (/data/quiz.db) — Docker volume quiz-data
```
All game state lives in memory on the server. SQLite persists session scores across restarts.
---
## File Structure
```
/opt/ni-quiz/
├── main.py # FastAPI app — game logic, WebSockets, SQLite
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── templates/
│ ├── player.html # Mobile arcade game (Space Invaders)
│ ├── host.html # Projector/host control panel
│ └── scores.html # Score history — global, by dept, by session
└── static/ # Static assets (empty — all inline)
```
---
## Features
### Player (`/`)
- Language selector — English / Español
- Registration — first name, last name, department
- Space Invaders arcade — 4 lanes, one per answer; ship at bottom fires at the correct invader
- 8-bit sound effects via Web Audio API
- Particle explosions on hit
- Score flash after each answer — points earned + running total
- Podium screen — top 10 with medals at game end
- Exit button — leaves session cleanly from lobby or podium
### Host Panel (`/host`) — projector view
- Password-protected — blocks access before WebSocket connects
- QR code auto-generated with the player join URL
- Live lobby — player count + name chips update in real time
- Live leaderboard during questions — who answered, time taken, correct/wrong
- Full top 10 leaderboard after each question with department
- Game controls — Start, Next Question, Show Results, Reset
- Clear Scores — wipes all SQLite history with confirmation
- Auto-reset — if host disconnects mid-game, server resets automatically after 5 minutes if no host reconnects
### Score History (`/scores`)
- Top 5 cards — all-time best scores across all sessions
- **Global tab** — full player ranking by best historical score, with department and session count
- **By department tab** — each department ranked internally, departments sorted by their top player
- **Sessions tab** — collapsible session list with full rankings and department per player
---
## Scoring System
```
points = 200 + 800 × (time_remaining / time_limit)
```
| Condition | Points |
|---|---|
| Correct, fastest possible | 1000 |
| Correct, just before timeout | 200 |
| Wrong answer | 0 |
| No answer / timeout | 0 |
Time limit: **20 seconds** per question.
---
## SQLite Schema
```sql
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL,
ended_at TEXT,
total_players INTEGER DEFAULT 0
);
CREATE TABLE scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL REFERENCES sessions(id),
player TEXT NOT NULL,
score INTEGER NOT NULL DEFAULT 0,
rank INTEGER,
dept TEXT NOT NULL DEFAULT '',
finished_at TEXT NOT NULL
);
```
Scores are saved when the host advances past the last question. The `dept` column is added automatically via migration on startup if it doesn't exist (safe for existing DBs).
---
## API Endpoints
| Method | Path | Description |
|---|---|---|
| `GET` | `/` | Player game page |
| `GET` | `/host` | Host control panel (password protected) |
| `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 |
| `WS` | `/ws/host` | Host WebSocket |
---
## Docker Compose
```yaml
services:
ni-quiz:
build: .
volumes:
- quiz-data:/data
environment:
- DB_PATH=/data/quiz.db
restart: unless-stopped
networks:
- web-net
volumes:
quiz-data:
networks:
web-net:
external: true
name: web_web-net
```
---
## Caddyfile Block
```caddy
ni-quiz.carloselugo.com {
import security_headers
reverse_proxy ni-quiz:8000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
log {
output file /var/log/caddy/ni-quiz.log
format json
}
}
```
> No `encode gzip` — gzip breaks WebSocket upgrades.
> No `internal_only` — players need public access from mobile devices.
---
## Deploy
```bash
# First deploy
cd /opt/ni-quiz
docker compose up -d --build
docker network connect web_web-net ni-quiz-ni-quiz-1
# Subsequent deploys (network defined in docker-compose.yml)
docker compose down
docker compose up -d --build
# Restart only (no rebuild — e.g. after editing main.py)
docker compose restart ni-quiz
# Backup DB
docker compose cp ni-quiz:/data/quiz.db ./quiz-backup-$(date +%Y%m%d).db
```
---
## Configuration
### Host password
Edit in `templates/host.html`:
```js
const HOST_PASSWORD = 'ni2026'; // ← change this
```
The password is checked client-side and stored in `sessionStorage` — valid for the browser tab session only.
### Auto-reset timeout
Edit in `main.py` (`ConnectionManager.disconnect_host`):
```python
self._reset_task = asyncio.create_task(self._auto_reset(delay=300)) # seconds
```
### Question time limit
Edit in `main.py` (two places must match):
```python
TIME_LIMIT = 20 # server-side validation
"time_limit": 20, # sent to client for countdown display
```
---
## Known Issues / Notes
- **Network on rebuild** — `docker compose up --build` recreates the container and drops it from `web_web-net`. Fix: define the network in `docker-compose.yml` (already done), or manually reconnect with `docker network connect web_web-net ni-quiz-ni-quiz-1`.
- **No gzip on WS proxies** — Caddy's `encode gzip` breaks WebSocket upgrades. Always omit for WS-heavy services.
- **Host password is client-side** — suitable for access control at an internal event. For stricter security, move auth to the backend with `fastapi.security`.
- **Single game instance** — all game state is a single global `GameState` object. Concurrent games are not supported.
---
## URLs
| | URL |
|---|---|
| Players | https://ni-quiz.carloselugo.com |
| Host / Projector | https://ni-quiz.carloselugo.com/host |
| Score History | https://ni-quiz.carloselugo.com/scores |