Back to PortfolioNaji Kanounji
ReactTypeScriptNode.jsExpressSocket.IOTailwind CSS

More Than a Quiz

A live quiz built for one camping trip — four teams answering from their phones, a shared screen as the scoreboard, and a host panel I ran the whole thing from.

Why I built it

We were going camping for four days, 34 people split into four teams, and I wanted to run a quiz. There were plenty of platforms I could have used, but I didn’t want a standard quiz — I wanted teams to attack each other, prepare their own defense in advance, steal points, and earn advantages through physical challenges around the camp. Existing platforms are built around more generic quiz formats, and bending those rules into one would have meant designing the game around the tool. A normal quiz also has a dead spot built in: one team answers while everyone else waits. That wasn’t going to hold up across four nights. So I built the platform around the game instead.

What I built

Each team answered from one phone, playing as one of four competing empires: Romans, Vikings, Egyptians, and Samurai. I also designed a shared map and physical cards around them, so the game had an identity outside the screens.

A shared screen showed the question, timer, and scores, while I ran everything from a host panel on my laptop. Questions ran in two formats so four nights wouldn’t turn into the same fifteen minutes on repeat: one gave a single team first crack, with a steal phase opening if they missed; the other put all four teams on the clock at once, ranked by speed.

Each team could also prepare its own defense questions through a separate authenticated page, so I could quickly pull their set when another team attacked them.

Because teams were writing their own questions, and I was preparing around 150 more myself, I needed a way to review the full set without manually checking 250 questions one by one. I used two agents. One turned a raw idea into a structured question with four options and a correct answer. The second reviewed every question before it entered the game, using Wikipedia as the shared reference source. That second step caught something I probably would have missed by skimming: the generator kept placing the correct answer in the first option. It flagged the pattern, so I reshuffled the set before the trip. It also caught questions that were technically close but too broad. For example, one asked who ruled Japan during the samurai era and answered “the shogun.” The reviewer marked it as mostly correct, but too general for the whole period. I still made the final call on every question.

Between rounds, physical mini-games around the camp earned advantages like 50/50, extra time, or the right to switch a question. Teams weren’t just trying to know more answers, they were deciding which advantages to earn, which questions to save, and when a card was actually worth spending.

Running it live

Running this in front of a group is a different problem than running it alone. So the system ran on one rule: the server owns the truth. Phase, scores, the timer, who’s allowed to answer all of it lives on the server, and every screen just renders the same state

Reconnection was built in from the start: if a phone lost connection or refreshed, it could rejoin using a token stored on the device. If a team switched phones entirely, its code reclaimed the same seat and score. Phones lock and batteries die; that shouldn’t stop a live game. No accounts, no database — software for a camping trip, not a bank.

How it played out

It ran across all four nights and became one of the things people talked about most during the trip. By the second night they weren’t just answering questions — they were saving them, planning who to attack, and competing for cards because they already knew where they’d spend them. I had designed those mechanics before the trip. Seeing people actually plan around them was the part I enjoyed most.

Nobody had asked me to build any of this. I just thought the camping trip needed a better game.

Tech stack

React, TypeScript, Node.js, Express, Socket.IO, and Tailwind, sharing one set of types between client and server so a mismatched field failed to compile instead of failing live. State lived in memory with a snapshot on disk, so a crash wouldn’t wipe four nights of scores.

Screenshots

Map

Map

Control Panel

Game Board

Game Board

Game Board

Game on Phone

Game on Phone

Add Question

Add Question
Cards
Cards
Cards
Cards