Initial commit
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
# 🌐 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 |
|
||||
Reference in New Issue
Block a user