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:
@@ -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.
|
||||
Reference in New Issue
Block a user