A customizable Jeopardy game that runs entirely in the browser. No build step, no server, no dependencies — just push it to GitHub Pages and play. Questions and answers live in a single JSON file you can edit.
- JSON-driven — edit questions.json to make your own game
- Built-in question editor — build a board right in the page and download it
as
questions.json, no hand-editing required - Players & scoring — add up to 8 players, award or deduct points per clue, click any score to fix it manually
- Daily Doubles — mark any clue with
"dailyDouble": truefor a wager round - Final Jeopardy — optional final round with per-player wagers
- Saves your game — state is kept in
localStorage, so a refresh won't lose scores or the board - Works offline too — open
index.htmlstraight from disk and it falls back to the built-in sample game
-
Create a new repository on GitHub and push these files to it:
git init git add . git commit -m "feat: jeopardy game" git branch -M main git remote add origin https://github.com/YOUR-USERNAME/YOUR-REPO.git git push -u origin main
-
On GitHub, go to Settings → Pages.
-
Under Build and deployment, set Source to
Deploy from a branch, pick themainbranch and the/ (root)folder, then save. -
After a minute, your game is live at
https://YOUR-USERNAME.github.io/YOUR-REPO/.
Click Question Editor (top right, or from the start screen). It opens prefilled with whatever game is currently loaded. From there you can:
- rename the game and categories, edit clues, answers, and values
- add or remove categories (up to 8) and clues (up to 8 per category)
- tick DD on any clue to make it a Daily Double
- toggle Final Jeopardy on or off
Your draft auto-saves in the browser as you type, so it survives a refresh. When you're done:
- Download JSON validates the draft and saves it as
questions.json— commit it to the repo (replacing the existing file) to make it the default game for everyone. - Use in game loads it into the current session immediately, no download needed.
Edit questions.json. The shape is:
{
"title": "My Custom Game",
"categories": [
{
"name": "Category Name",
"clues": [
{ "value": 200, "clue": "The host reads this.", "answer": "What is the answer?" },
{ "value": 400, "clue": "Another clue.", "answer": "Another answer.", "dailyDouble": true }
]
}
],
"finalJeopardy": {
"category": "Final Category",
"clue": "The final clue.",
"answer": "The final answer."
}
}Rules:
| Field | Required | Notes |
|---|---|---|
title |
no | Shown on the start screen and header |
categories |
yes | 1–8 categories |
categories[].name |
yes | Column header |
categories[].clues |
yes | 1–8 clues per category (5 is classic) |
clues[].value |
yes | Positive number — point value of the tile |
clues[].clue |
yes | What the host reads aloud |
clues[].answer |
yes | Revealed when the host clicks "Reveal Answer" |
clues[].dailyDouble |
no | true turns the tile into a Daily Double |
clues[].image |
no | Photo question — shown above the clue text (see below) |
clues[].imageAlt |
no | Screen-reader description of the clue image |
clues[].answerImage |
no | Second image revealed with the answer |
finalJeopardy |
no | Omit it entirely to skip the final round |
finalJeopardy.image |
no | Image shown with the final clue (never during wagers) |
Categories don't need the same number of clues — uneven columns are fine.
Any clue (and Final Jeopardy) can carry an image. Two ways to add one:
- In the editor — each clue has a 📷 control: paste an image URL, or
Choose file… to embed a photo straight into the JSON (it's downscaled
and compressed in your browser, so the downloaded
questions.jsonis fully self-contained — no image hosting needed). An "embedded images" meter warns you before browser storage limits become a problem; prefer URL images for image-heavy boards. - In the JSON — set
imageto anhttps://URL, a repo-relative path (e.g.images/mystery.jpgcommitted next toindex.html), or adata:image/...URI.
Board tiles stay classic dollar amounts; the image appears when the clue opens (for Daily Doubles, after the wager is locked). Buzzer phones don't receive images — players look at the host screen. Note: images loaded from external URLs reveal viewers' IP addresses to that host, as with any web image; embedded or repo-hosted images don't.
Troubleshooting embeds: transparent PNGs are flattened onto a white background (JPEG has no transparency). Very large phone photos or HEIC files (iPhone's default) may not embed on every device — if you see an error, use a smaller JPEG/PNG or paste an image URL. iPhone: set the camera to Settings → Camera → Formats → Most Compatible to shoot JPEGs.
If a photo comes out as black bands instead of the picture, the device ran out of memory processing it; the app now retries with a browser-side downscale, so just add it again. If it still can't, it will say so and you can use a smaller copy (a screenshot of the photo works) or paste an image URL.
If a photo doesn't show up at all: work down the triage list in
docs/photo-clue-verification.md — the usual
causes are a stale cached page (hard-refresh), a link to the page a picture
sits on rather than the image itself, a host that blocks hotlinking (embed the
file instead of linking it), or the gold "can't save your game" banner, which
means browser storage is full of embedded photos and a refresh would lose them.
That file also defines the 26 success states the feature is checked against;
run them from tests/photo-harness.html over a local server.
Tip: after editing
questions.json, also updatejs/data.jsif you want the same questions when openingindex.htmldirectly from disk (it's the fallback used when the JSON can't be fetched). On GitHub Pages onlyquestions.jsonmatters.
Two more ways to use custom questions:
- Upload a file — on the start screen, click "Load custom questions (.json)" and pick any JSON file from your computer.
- Link to a URL — append
?game=URLto the page address to fetch questions from any JSON URL (it must allow cross-origin requests, e.g. a GitHub Gist raw URL):https://you.github.io/repo/?game=https://gist.githubusercontent.com/...
- Add players on the start screen and hit Start Game.
- Click a dollar amount to open the clue. Read it out.
- Click Reveal Answer, then mark each player ✓ (adds points, closes the clue) or ✗ (deducts points; other players can still answer).
- No one got it — close ends the clue with no score change.
- Daily Doubles ask who's answering and their wager before showing the clue.
- When the board is cleared, the game offers Final Jeopardy: lock in per-player wagers, reveal the clue and answer, and judge each player.
- New Game (top right) returns to the start screen with the same players, scores reset and a fresh board.
Scoring follows house rules, not TV rules — e.g. players at $0 or less can still play Final Jeopardy (with a $0 wager).
Turn phones into real buzzers. It's completely optional — if you never open a
room, the game behaves exactly as above and makes no network calls beyond
loading questions.json.
- On the start screen, under Players, click Open buzzer room. You get a big 4-letter room code and a join link.
- Each player opens the same site URL on their phone and taps
Join a buzzer room (under Players on the start screen), then types the
4-letter code — or uses the direct
?room=CODEjoin link, which prefills it. They enter their name and get a full-screen buzzer button. Their name links to a scoreboard player (a new one is added automatically if the name is new). - The moment you open a regular clue, every phone's buzzer turns red ("Wait for it…") — and it's live, just like the show: tapping while it's red locks that player out of the clue ("Too soon!"), and the buzz bar shows you who jumped the gun. Finish reading, then click Arm buzzers — or just press the Spacebar (it toggles arm/disarm, no mouse hunt) — and the surviving buzzers flip green ("BUZZ!").
- The first green-light tap wins; you see their name with ✓ / ✗ buttons right there (same scoring as usual). ✗ locks that player out and re-arms the rest; ✓ closes the clue. Early and wrong-answer lockouts both clear on the next clue. Daily Doubles and Final Jeopardy never show a buzz bar (no trap there), and Space only does anything while that bar is live.
- On the board screen a small
CODE · n 🔔chip in the top bar toggles the room panel (join link, connected players, kick, close). New Game keeps the room open. If you refresh, the room auto-reopens with the same code so phones reconnect on their own.
- Needs internet. Signaling uses the free public PeerJS cloud broker
(pinned build
peerjs@1.5.5from cdnjs, loaded lazily only when you first open a room or a phone visits?room=). After that, phone ↔ host traffic is peer-to-peer WebRTC. No game data ever touches any server — only the room-code handshake goes through the broker. ?room=CODEtakes precedence over?game=URL: a page opened with?room=is always the player buzzer, never the host game.- Troubleshooting: strict corporate/school networks sometimes block WebRTC — buzzers won't connect there. If the room code collides on open, the app regenerates and retries automatically. Offline? The feature is simply unavailable; the game itself is unaffected.
- Early buzzes are host-judged by arrival: a tap that reaches the host while the buzzer is still red locks that player out of the clue; a tap that arrives after you arm counts, even if their screen hadn't flipped green yet (network skew forgives knife-edge timing). There's no timed penalty window like the TV show's — going early costs you the whole clue.
Connected phones double as contestant podiums. Mixed mode is fully supported — any player without a connected phone keeps the normal host-driven flow, and the host can always override a phone player (handy if a battery dies mid-Final).
- Daily Double. When you open a Daily Double and the player picked in the "Who's answering?" dropdown has a connected phone, that phone shows a wager pad (with the legal range) and the splash notes they're wagering on their phone. Their submitted wager locks the Daily Double exactly as a typed wager would; an out-of-range wager is bounced back to the phone with the reason. Changing the dropdown re-prompts the newly selected player. The manual wager box stays usable the whole time — typing and locking it yourself wins.
- Secret Final wagers. Every connected player wagers on their own phone.
Their wager arrives pre-filled on the host's wager list but masked
(shown as dots,
🔒 from phone) so nothing leaks on a projector — a genuine secret wager, unlike the manual boxes. An Unlock button hands the input back to you for a manual override. Players without phones use the normal editable boxes. - Typed Final answers. After you lock the wagers, phones show the Final clue and a text box. Answers land in the host's judge rows verbatim, in quotes, for you to read and rule on with the usual ✓ / ✗ — the app never auto-checks an answer, never auto-scores, and never advances on its own; you drive the pace. An "Answers in: n/m" line just tells you how many are in. Players may edit and resubmit until you reveal. As you rule each verdict, that player's phone shows their result and new score.
- Final Jeopardy is one-shot. Once you have judged Final, it can't be replayed — the Final Jeopardy button and end-of-board banner route to the standings instead, so wagers are never applied to scores twice. (Backing out of Final before judging anything is still fine and re-prompts phones.)
index.html page structure
css/styles.css all styling
css/buzzer.css buzzer host panel + player phone screen styles
js/app.js game logic (vanilla JS, no dependencies)
js/editor.js in-page question editor
js/data.js built-in sample game (offline fallback)
js/buzzer-protocol.js pure buzzer core (room codes, validation, reducers)
js/buzzer-host.js host side: PeerJS load, room lifecycle, buzzer UI
js/buzzer-wagers.js host side: phone Daily-Double + Final wagers & answers
js/buzzer-player.js player side: phone join, buzzer, wager & answer screens
questions.json the questions GitHub Pages serves — edit this one
tests/ node:test unit tests + in-browser harnesses (buzzer, photo clues)
The buzzer feature loads PeerJS lazily from a pinned, SRI-verified cdnjs URL
(peerjs@1.5.5) only when a room is opened or a phone joins — the core game
never requests it.