feat: refactor to SI Quiz with Space Invaders arcade interface

- Updated Dockerfile to Python 3.11, selective COPY for cleaner image
- Expanded .gitignore with Python/OS patterns
- Added config.json, questions.json, env_example, and static assets
- Updated templates (host, player, scores) with Space Invaders UI
- Rewrote README for SI Quiz branding and setup docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 01:45:04 +00:00
co-authored by Claude Sonnet 4.6
parent d8f4cc97c6
commit 3539b1f75f
13 changed files with 451 additions and 408 deletions
+132 -222
View File
@@ -1,247 +1,157 @@
# 🌐 NI Quiz
# 🛸 SI Quiz — Space Invaders 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.
A multiplayer trivia game with a Space Invaders-style arcade twist. Players join from their phones, answer questions by shooting the correct answer with their spaceship, and compete on a live leaderboard — all projected from the host's screen.
**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)
```
Built with FastAPI, WebSockets, SQLite, and Docker. No external services required.
---
## 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
- 🎮 Space Invaders mechanic — players shoot answers, not just tap buttons
- 📱 Mobile-friendly player view — join via QR code, no install needed
- 🖥️ Host panel — controls game flow, shows live answer count and ranking
- 🌐 Bilingual UI — English / Español toggle on the join screen
- 🎨 Theme switcher — Dark / Light / High Contrast (great for projection)
- 📊 Score history — persistent leaderboard across sessions
- 🏢 Department grouping — track scores by team
- ⚙️ Fully configurable — questions, org name, departments, time limit via JSON
---
## 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
## Quick Start
```bash
# First deploy
cd /opt/ni-quiz
docker compose up -d --build
docker network connect web_web-net ni-quiz-ni-quiz-1
git clone https://github.com/carlitosbond/si-quiz.git
cd si-quiz
# Subsequent deploys (network defined in docker-compose.yml)
docker compose down
docker compose up -d --build
# 1. Edit your questions and branding
cp config.json config.json # already provided, edit as needed
cp questions.json questions.json # already provided, customize freely
# Restart only (no rebuild — e.g. after editing main.py)
docker compose restart ni-quiz
# 2. Start
docker compose up -d
# Backup DB
docker compose cp ni-quiz:/data/quiz.db ./quiz-backup-$(date +%Y%m%d).db
# Host panel: http://localhost:8000/host
# Players join: http://localhost:8000
```
---
## 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.
### `config.json` — branding and game settings
### Auto-reset timeout
Edit in `main.py` (`ConnectionManager.disconnect_host`):
```python
self._reset_task = asyncio.create_task(self._auto_reset(delay=300)) # seconds
```json
{
"app_name": "SI Quiz",
"app_subtitle": "Space Invaders Quiz",
"org_name": "Your Team",
"departments": ["Engineering", "Operations", "IT", "Sales", "Finance", "HR", "Other"],
"time_limit_seconds": 15,
"questions_file": "questions.json"
}
```
### 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 |
| Field | Description |
|---|---|
| Players | https://ni-quiz.carloselugo.com |
| Host / Projector | https://ni-quiz.carloselugo.com/host |
| Score History | https://ni-quiz.carloselugo.com/scores |
| `app_name` | Displayed in browser title and UI headers |
| `app_subtitle` | Tagline shown on the join screen |
| `org_name` | Optional org/team name shown in the host panel |
| `departments` | List shown in the player join dropdown |
| `time_limit_seconds` | Seconds per question (default: 15) |
### `questions.json` — your question bank
Each question follows this format:
```json
{
"q": "Question text here?",
"options": ["Answer A", "Answer B", "Answer C", "Answer D"],
"correct": 0,
"fun_fact": "Explanation shown after the answer is revealed."
}
```
- `correct` is the **zero-based index** of the correct answer (0 = A, 1 = B, 2 = C, 3 = D)
- Questions are shuffled each session
- No minimum or maximum — add as many as you want
---
## Scoring
| Component | Points |
|---|---|
| Correct answer | 200 pts base |
| Speed bonus | Up to +800 pts (scales with remaining time) |
| Wrong / no answer | 0 pts |
| **Max per question** | **1,000 pts** |
---
## Reverse Proxy (Caddy)
If you're running behind Caddy, **do not** add `encode gzip` — it breaks the WebSocket upgrade.
```
si-quiz.yourdomain.com {
reverse_proxy si-quiz:8000
}
```
Connect the container to your Caddy network:
```yaml
# In docker-compose.yml, under the si-quiz service:
networks:
- si-quiz-net
- web-net # your Caddy shared network
```
---
## Environment Variables
Copy `.env.example` to `.env` to override defaults:
| Variable | Default | Description |
|---|---|---|
| `DB_PATH` | `/data/si-quiz.db` | SQLite database path (inside container) |
| `CONFIG_PATH` | `config.json` | Path to config file |
| `QUESTIONS_PATH` | `questions.json` | Path to questions file |
---
## Project Structure
```
si-quiz/
├── main.py # FastAPI backend + WebSocket game logic
├── config.json # Branding, departments, time limit
├── questions.json # Your question bank
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env.example
├── templates/
│ ├── player.html # Mobile player view (Space Invaders game)
│ ├── host.html # Host control panel
│ └── scores.html # Score history viewer
└── static/ # CSS, JS assets (add music.mp3 here if desired)
```
---
## Background Music (optional)
The player view supports background music. Add a file named `music.mp3` to the `static/` folder — it will play automatically when a player joins and stop at the podium screen. Volume is set to 20%.
Music is **not included** in this repo. Bring your own royalty-free track.
---
## License
MIT — use it, fork it, adapt it for your team.