Skip to content
obilodeauPublic

About

Game Board for NorthSec's Hacker Jeopardy since 2017

Topics

Resources

Stars

14 stars

Watchers

4 watching

Forks

Repository files navigation

Ceopardy

The Hacker Jeopardy Game Board we use at NorthSec.

Screenshots

This is what the crowd sees:

The Viewer Interface Displaying the Game Board

When a clue is displayed:

The Viewer Interface Displaying a Clue

This is the host interface, how you control the game:

The Host Interface

Note that there are two drawers that can be opened by clicking on the brown arrows at the top and at the bottom of the screen. The top drawer contains the functions to change team names and, online, to hand out buzzer links. The bottom drawer provides functions to display a custom message on the board or to pause a game.

Teams buzz in with keys 1, 2, 3... (USB buzzers that type a digit), or from a web page in online mode. A team marked Bad can't buzz again on that clue and its panel turns red; move its slider back to undo.

Architecture

Starting with v0.5, Ceopardy is split in two parts:

  • A Python/Flask back-end that exposes a small REST API (/api/v1/...) and broadcasts state changes over a single Socket.IO namespace (/game).
  • A Vite + Vue 3 + TypeScript front-end (in frontend/) that powers the crowd-facing viewer, the host UI, and the start screen.

Ceopardy is designed for single-operator local-network use: the server binds to 127.0.0.1 and there is no authentication on the host UI. If you need to expose it on a LAN, put your own reverse proxy (and auth) in front.

The one exception is the players' buzzer port, 5001, in online mode. It is a separate server that serves only the buzzer page and is meant to be reachable from the internet; see Remote buzzers.

Running Ceopardy (operators)

For people who just want to host a game.

Install pipx, then install the latest release wheel (requires curl and jq):

pipx install "$(curl -fsSL https://api.github.com/repos/obilodeau/ceopardy/releases/latest | jq -r '.assets[] | select(.name | endswith(".whl")) | .browser_download_url')"

Or pin a specific version from the releases page:

pipx install https://github.com/obilodeau/ceopardy/releases/download/v0.8.0/ceopardy-0.8.0-py3-none-any.whl

Then scaffold a per-game directory and start the server:

mkdir my-game && cd my-game
ceopardy init               # writes data/ + game-media/ starter content
# edit data/Questions.cp and data/1st.round to set up your game
ceopardy serve              # starts the server on http://127.0.0.1:5000/
ceopardy serve --debug      # add verbose logging + auto-reload
ceopardy serve --online     # sounds on the viewer, web buzzers for players

Open the two URLs ceopardy serve prints:

Online events

Normally the buzzers, daily double, timeout and waiting music play on the host machine, which is what you want when the crowd is in the room with a projector. When you run the game online you share the viewer tab instead, so the sounds have to come from there: start the server with ceopardy serve --online.

The viewer then shows a "Click to enable sound" screen once — browsers do not let a page play audio until someone has clicked in it. Click it while you set up the screen share and it won't come back. Remember to share the tab with audio so your audience actually hears it.

Remote buzzers

Online, players buzz from a web page. Each team gets a secret link, any number of players can share it, and the server judges who was first, correcting for each player's network latency. Ceopardy opens the tunnel to players itself, through ngrok:

  1. Once: create a free account on ngrok and put your authtoken in your shell profile, export NGROK_AUTHTOKEN=... (or run ngrok config add-authtoken if you have the ngrok agent). Optionally set NGROK_DOMAIN to the free static domain from your ngrok dashboard, so the links keep working across a restart.
  2. ceopardy serve --online. The banner shows the buzzer URL.
  3. On the host, open the top drawer and click the copy icon next to each team. Send each link only to that team: anyone with it plays as that team. The key icon replaces a leaked link; the person icon counts who has joined.

Players click through ngrok's "You are about to visit" page once.

To use another tunnel (Cloudflare Tunnel, Tailscale Funnel...), point it at http://127.0.0.1:5001 and pass its address with --buzzer-url; --no-tunnel keeps the links local. Never tunnel port 5000: it is the host's interface.

ceopardy init never overwrites existing files; it's safe to re-run. The SQLite database, round files, and uploaded media all resolve relative to the directory you run ceopardy from, so keep one directory per game.

Note: Ceopardy persists transactions to a SQLite database as the host submits points, so a crash doesn't lose the game state. The flipside is that games must be finalized (click "Game over") before a new one can be started in the same directory.

Hacking on Ceopardy (developers)

You need Python 3.11+, pip, virtualenv, and Node.js (LTS).

git clone https://github.com/obilodeau/ceopardy.git
cd ceopardy
make venv                          # creates .venv/ + installs deps
source .venv/bin/activate          # bash/zsh
source .venv/bin/activate.fish     # fish
make init                          # seeds data/ + game-media/
make run                           # starts Flask (:5000) + Vite (:5173)
CEOPARDY_ONLINE=1 make run         # same, with online mode on

Then open http://localhost:5173/ — Vite hot-reloads the UI and proxies /api and /socket.io to Flask on :5000. In dev, always use the Vite URL (:5173); the Flask port serves the built SPA which gets stale.

For the web buzzers, build the team links on Vite too:

CEOPARDY_ONLINE=1 CEOPARDY_BUZZER_URL=http://localhost:5173/buzzer.html make run

As with ceopardy serve, an ngrok authtoken in the environment opens a real tunnel to :5001, which serves the built buzzer page, unless CEOPARDY_BUZZER_URL is set. CEOPARDY_TUNNEL=0 keeps the links local. Never tunnel :5173: Vite proxies /api to the host.

Optional: dev container

The repo ships a dev container (Python 3.11, Node 20, the GitHub CLI, and Claude Code) for VS Code. Copy .devcontainer/devcontainer.env.example to .devcontainer/devcontainer.env (gitignored) and fill in what you need, then run Dev Containers: Reopen in Container.

Variable What it unlocks
GITHUB_TOKEN git push/pull over HTTPS and gh; fine-grained, scoped to this repo
GIT_USER_NAME committing at all — without it git says Author identity unknown
GIT_USER_EMAIL same
CLAUDE_CODE_OAUTH_TOKEN Claude Code, signed in across rebuilds; mint it on the host with claude setup-token

Docker reads that file verbatim: no quotes, no spaces around =, and no trailing comments — they'd end up inside the value.

The container builds and runs without any of them; each one just unlocks the matching row. Nothing under ~/ is persisted, which is why the identity lives here rather than in a ~/.gitconfig — see .devcontainer/README.md for the token's scopes, why that is, and what the container sets up.

Optional: direnv

If you use direnv, the repo ships an .envrc that auto-activates .venv on cd. Run make venv first (direnv won't), then direnv allow.

Before committing

Run the full CI suite — same checks GitHub Actions runs:

make ci      # ruff lint + format check + prettier + vue-tsc + pytest

To auto-fix Python formatting first:

make format

See AGENTS.md for the conventions the codebase follows.

Building a wheel locally

make build reproduces the release path (frontend bundle + sdist + wheel):

make build
pipx install --force dist/ceopardy-*.whl   # test the wheel end-to-end

To make a release

Push a valid version tag. Release notes are automatically generated from merged PRs, see .github/release.yml.

About

Game Board for NorthSec's Hacker Jeopardy since 2017

Topics

Resources

Stars

14 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages