Everyone planning their first pub quiz makes the same mistake. They count the questions, multiply by "about a minute", and announce a finish time. Then the night overruns by forty minutes, the kitchen has closed, and half the room left before the results.
The error is not in the multiplication. It is that answering a question is the cheapest part of asking one. You read it out. Someone at the back says "what?". You read it again. A table argues. You wait. Then you read the answers out at the end of the round, and that takes as long as a small round by itself.
We built a round planner whose entire value is doing that arithmetic honestly. There is no clever code in it, and that is the point of this post: sometimes the feature is that somebody sat down and worked out the real numbers.
The model
type RoundType = {
key: string
label: string
/** Minutes per question, including reading it twice and settling it. */
minutesPerQuestion: number
/** Fixed overhead: handing out a picture sheet, explaining the rules. */
overheadMinutes: number
note: string
}
Two numbers per round type, and the comments on both are load-bearing, because "minutes per question" is ambiguous until you say what is inside it.
{ key: 'standard', minutesPerQuestion: 1.1, overheadMinutes: 2 },
{ key: 'quickfire', minutesPerQuestion: 0.6, overheadMinutes: 1.5 },
{ key: 'music', minutesPerQuestion: 1.3, overheadMinutes: 3 },
{ key: 'picture', minutesPerQuestion: 0.8, overheadMinutes: 4 },
{ key: 'list', minutesPerQuestion: 1.6, overheadMinutes: 3 },
Read those as a set and they tell you things that are not obvious:
- A picture round is the fastest to answer and the slowest to set up. Four minutes of overhead to hand sheets out and collect them, then 0.8 a question. This is why it belongs across the break, where the overhead is free.
- A music round is slow per question because clips get replayed and the room gets loud, and it carries three minutes of overhead for the same reason.
- A connections or list round is the most expensive thing on the menu at 1.6 a question, because tables talk it through. That discussion is the point of the round and also its cost.
- Quickfire is genuinely half the price of a standard round, because there are no options to read out.
Then the two costs nobody budgets for at all:
const WELCOME_MINUTES = 5
const RESULTS_MINUTES = 10
Ten minutes for results. Every first-time host writes down zero. In practice you are reading out positions, handling a challenge, finding out the team that won has gone outside for a cigarette, and handing over a prize.
The figures are deliberately on the generous side, and the reasoning is in the file:
A quiz that finishes early is a quiz people come back to, and one that overruns by forty minutes is not.
An estimator's bias should point at the outcome the user can recover from. Early is recoverable. Late is a lost Tuesday.
Output a schedule, not a total
The mistake I nearly made was rendering one number: "your quiz will take 2 hr 14 min". Useful once. Useless while planning, because it does not tell you which round to cut.
So the planner emits a running order with a wall clock beside every item:
20:00 Welcome and rules
20:05 Round 1: Standard round
20:18 Round 2: Music round
20:34 Break
20:49 Round 3: Standard round
...
22:31 Results and prizes
Two small formatters, and nothing else:
function formatClock(startMinutes: number, offset: number): string {
const total = Math.round(startMinutes + offset)
const hours = Math.floor(total / 60) % 24
const minutes = total % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
}
function formatMinutes(total: number): string {
const rounded = Math.round(total)
const hours = Math.floor(rounded / 60)
const minutes = rounded % 60
if (hours === 0) return `${minutes} min`
return `${hours} hr ${minutes.toString().padStart(2, '0')} min`
}
The % 24 is the only trap in the whole file, and it is there because a quiz starting at 21:30 with seven rounds genuinely crosses midnight. Without it you get 24:10, which is a time that does not exist and makes the tool look like it cannot count.
The accumulation is a single pass with a running offset, so every item knows both its own duration and its start. That is what makes "cut round 5" instantly legible: every clock time after it moves, on screen, as you delete it.
Parse defensively, because the input is a string
const startMinutes = useMemo(() => {
const [hours, minutes] = startTime.split(':').map(Number)
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return 20 * 60
return hours * 60 + minutes
}, [startTime])
A <input type="time"> can be empty, and on a few browsers it can hand you a partial value mid-typing. Number('') is 0 and Number(undefined) is NaN, and one NaN in the start time makes every clock in the schedule read NaN:NaN. The fallback to 20:00 means a half-typed time shows a plausible schedule rather than a broken one.
Put the domain knowledge in the strings
Each round type carries a note, and each schedule item a detail line:
note: 'Clips need playing and often replaying, and the room gets loud.'
detail: 'Put the halfway standings up. This is where the bar makes its money.'
That last one is for the venue, not the quizmaster, and it is the single most useful sentence in the tool. The break is not dead time to be minimised, it is the reason a pub is hosting a quiz at all. A planner that encouraged you to shorten it would be optimising the wrong thing, so the tool says so out loud.
If your tool encodes expertise, put the expertise in the interface. Nobody reads the docs for a calculator.
Have a play
pub-trivia.app/tools/round-planner starts with a realistic six round night. Add a list round and watch the finish time move by more than you expected, which is exactly the lesson the tool exists to teach. There are guides behind it for the non-arithmetic half of the problem, and the app itself, free tier, no card, if you would like the scoring to stop being a job for a pen.
Top comments (0)