A bingo caller is the machine that draws numbers, says them out loud, and shows them on a board. I needed one for BingWow, a free bingo site. The obvious build is a server that owns the deck and a SpeechSynthesis call for the voice. I shipped neither. Here is why, and what the no-backend version actually looks like.
You can play with the result here: bingwow.com/caller. It runs entirely in the tab.
The state lives in a reducer, not a server
A multiplayer bingo board needs a server, because two players must agree on who claimed what. A caller does not. One person runs it, on one screen, and reads numbers to a room. There is nothing to synchronize.
So the whole game is a useReducer:
interface CallerState {
mode: BallMode; // '30' | '75' | '90'
deck: number[]; // shuffled, pop() to draw
called: BingoBall[];
calledSet: Set<number>; // O(1) "was this called"
isAutoMode: boolean;
roundNumber: number;
}
case 'DRAW': {
if (state.deck.length === 0) return state;
const deck = [...state.deck];
const num = deck.pop()!;
return { ...state, deck, called: [...state.called, makeBall(num, state.mode)] };
}
No persistence, no API route, no database row. Refreshing the page starts a new game, which is the correct behavior for a caller anyway. The only thing that survives a reload is two booleans in localStorage: voice on/off and bingo-lingo on/off. Server cost for the entire feature is zero, and it works on a school projector with flaky wifi.
Why not the Web Speech API
speechSynthesis.speak() is free and one line. I used it first. Three problems killed it:
-
The voices are not the same anywhere. The available
SpeechSynthesisVoiceset depends on OS and browser. The default English voice on Windows Chrome, macOS Safari, and a Chromebook are three different voices with three different cadences. A caller that sounds like a calm host on my laptop sounds like a 1998 GPS on the school's machine. -
It cuts off and queues badly. Rapid
speak()calls during auto-draw drop utterances or stack them. Cancel/restart logic to fix that is its own bug farm. - No personality. Traditional 90-ball bingo has spoken calls — "Legs eleven", "Two fat ladies, eighty-eight". A robot monotone reading "eighty eight" is not that. The call IS the fun.
So the voice is 331 prerecorded MP3s. Every number in every mode, every milestone ("halfway", "almost done"), every traditional 90-ball nickname, welcome and round-transition lines. They are real recorded clips, consistent on every device, and they have the warmth a party game needs. The cost is a one-time generation pass and a few MB of audio served from the CDN; clips preload per mode.
The hard part: syncing voice to the animation
The drawn ball physically flies from a hero position into its cell on the flashboard. The voice has to fire at the moment the ball lands, not when React happens to re-render.
The first version triggered the audio from a useEffect keyed on the called list. It desynced constantly — the effect runs after paint, the animation impact is mid-timeline, and at fast auto-draw the gap compounds. The fix was to stop treating audio as a render side effect and fire it from the animation timeline itself, at the impact keyframe:
runFlyingBallToCell({
/* ... */
onAbsorbed: () => {
setCellRevealed(true);
voiceRef.current?.playBallImpact(ball, mode, lingoEnabled);
},
});
playBallImpact() also cancels any in-flight banter or milestone clip so the number call never collides with "you're halfway there". Auto-draw is gated on the animation completing, not on the audio finishing — audio is fire-and-forget at impact. That one move removed every desync vector at once.
What this gets you
A bingo caller with no server, no signup, no app, that runs on anything with a browser. It does 75-ball (US), 90-ball with the recorded traditional calls, and 30-ball speed bingo. Pair it with free printable cards and the whole game costs nothing — I wrote the full no-equipment walkthrough here: Free online bingo caller guide.
The general lesson is the boring one worth repeating: not every feature that could have a backend needs one, and the platform speech API is a demo, not a product. Prerecorded audio plus a timeline that owns its own timing beat both the server and the SDK here.
Try it: bingwow.com/caller.
Top comments (0)