r/SideProject • • 7d ago

Quiz Ninja

Enable HLS to view with audio, or disable this notification

Hey r/SideProject. Started this in May as "can I make trivia feel live without needing a host and a Zoom call", and five months later it's a thing people actually play: **[quiz.ninja](https://quiz.ninja)\*\*

**What it is**

Real-time multiplayer trivia in the browser. No signup to play, no app install, no host. You land on the page and you're answering a question in about ten seconds.

Six ways to play:

- **Battle Royale**: a drop every 5 minutes, everyone online jumps in together, warm-up rounds then the cut, last one standing wins

- **Quick Match**: 1v1 against whoever is in the queue, speed counts

- **The Climb**: one shared ladder for everyone, a new question every 20 seconds, climb as far as you can before you miss (featured this week, 3× XP)

- **Daily Kata**: five short games a day on a fixed set of topics, same for everyone, one leaderboard per day

- **Solo Dojo**: practice at your own pace

- **Rooms**: private room code for friends, pub-quiz style

About 12,800 questions across 71 categories. Scoring rewards speed, so it's less "who knows the most" and more "who commits fastest". Full English and Lithuanian (I'm from Lithuania, so that was non-negotiable).

**Stack, for the curious**

- Backend: Rust + Axum + tokio, Postgres via sqlx, WebSockets for everything in-game. Hosted on Railway.

- Web: React + Vite on Vercel.

- Mobile: Expo / React Native, iOS app currently sitting in App Store review.

- Analytics: PostHog, self-hosted reverse proxy so ad blockers don't eat the funnel.

- Email: went SendGrid → Brevo → Amazon SES in one month. Don't ask.

Rust for a game server was the best decision in the project. One small instance handles every live mode, and I have not had a single "the server fell over" night.

**Things I got wrong**

- Built a dark neon "ninja" look first. Lived with it for months before admitting it looked like a tech demo, not something you open at lunch. Rebuilt the whole UI bright and friendly (think Duolingo, not Tron). Painful, and the name stayed, the ninja visuals went.

- Put the landing page before the game. Wrong order. Now the first screen IS a game, and that page turns roughly twice as many visitors into players as the old hero section did.

- Almost built an admin panel. Static config plus a migration is fine when the team is one person.

- Ran a small batch of paid ads before checking the UTM tags. Learned exactly nothing from it.

**Things that surprised me**

- The questions are the whole product. I generate them with LLMs in batches, then run every batch through fact-checking, dedup against the full corpus and a screen for swapped names and dates. That pipeline has eaten more hours than the backend.

- Multiplayer edge cases are endless: host closes the tab mid-game, reconnects on a phone, two people answering in the same millisecond, clock drift. Each one became a test.

- Guests matter. Most people never sign up, so guests now earn badges too, and can claim them if they create an account within a day.

**Where it's at**

It's live, it's free, there are no ads and I'm not planning any. Right now I'm trying to find out whether this is a game people come back to daily, or a thing you try once. The Daily Kata and The Climb are the bets on "daily".

**What I'd love from you**

- Play one Battle Royale drop or a Quick Match and tell me where it felt confusing or slow.

- Did the first screen make sense without any explanation?

- Anyone who has launched a multiplayer side project: how did you get the first few hundred regulars?

Happy to answer anything about the Rust/WebSocket side or the question pipeline.

0 Upvotes

12 comments sorted by

0

u/[deleted] 7d ago

[removed] — view removed comment

1

u/Perfect-Marsupial562 7d ago

Honest answer: I don't compensate for latency. I make the server the only clock, then score things so lag can't decide much.

- The server stamps the moment it broadcasts a question and the moment each answer packet lands. Clients send an option index, nothing else. No client timestamps, because that's the first thing anyone forges.

- Your countdown starts when the question reaches you, so on a bad connection your timer starts late while the server's is already running. That's the penalty: your one-way latency, no more. On a 20 s clock, 100 ms is half a percent of the window.

- Rooms and Quick Match don't score speed at all, flat points per correct answer. A round ends when everyone has answered (plus a 300 ms wait for in-flight packets) or when the clock runs out.

- Battle Royale is where speed is points. Elimination is a percentile cull per round, so you're measured against the field's median, not against the one person on fibre. The 1v1 final tiebreak sums answer times, and a miss counts as the full clock.

- The Climb runs on a shared schedule, so there the client syncs to the server: every frame carries the server's clock, the client keeps the offset, and your deadline is a server timestamp rather than "20 s from arrival". The server accepts answers up to 300 ms past the deadline as network slack, and the client locks the buttons 100 ms before it so you never tap into a "too late".

- First answer per player counts. Anything arriving after the round closed is dropped.

Real RTT compensation would mean pinging and subtracting half the round trip from your time. I haven't, because it's a knob you can game (pad your pings, gain time) and the honest gain is tens of milliseconds. If a leaderboard ever gets decided by latency, I'll revisit.

0

u/[deleted] 7d ago

[removed] — view removed comment

1

u/Perfect-Marsupial562 7d ago

"does the 1v1 final ever feel unfair" way too little data to make a call honestly, although I should be able to get that from the game stats.

The finalist must go toe-to-toe the whole 3-8 minutes game to make the latency a deciding factor, espcially since the correct answer never elliminates you (unless your answer did not register cause of the latency when you answer the very last moment). With the current (very small) player volume I do not consider that an issue.

0

u/[deleted] 7d ago

[removed] — view removed comment

1

u/Perfect-Marsupial562 7d ago

Not per matchup, no. What actually lands in the DB today: one median answer time and accuracy per Battle Royale drop for the whole human field, and per player per game just score, rank, answered, correct. Per-answer times are only stored for the daily mode. The engine has every answer's elapsed ms in memory at game end (it uses them for the final tiebreak and for badges) and then throws them away, and nothing measures RTT at all.

So if it ever did matter, I couldn't prove it from the logs. The fix is small: per-player p50/p90 answer time and the finalists' summed times on the game row, plus one RTT sample per socket at join so I can correlate the two. On the list, but not before there are enough finals to read anything from.

1

u/YusukeLandingBoost 7d ago

The guest badges are a nice touch. Do they survive across visits without signing up, or is the one-day claim window the only way to keep them?

2

u/Perfect-Marsupial562 7d ago

They survive. Your browser keeps a random guest key, and the server stores your badges and counters under a hash of it, so you keep them across visits as long as you use the same browser and don't clear storage. The server only drops a guest after 180 days without a game.

The one-day window is about the account, not the badges: you can claim them into an account that's less than 24 hours old, whenever you decide to make one. It's there so people don't farm badges as guests and dump them into an old account.

0

u/[deleted] 7d ago

[removed] — view removed comment

1

u/Perfect-Marsupial562 7d ago

its 10 minutes now but the plan is reducing to 5, the problem with 5 is that the games will overalp as the average BR can take up to 8 minutes under right conditions and the engine does not supprt that yet. It's on the backlog!