<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Forrest Miller</title>
    <description>The latest articles on Forem by Forrest Miller (@forrestmiller).</description>
    <link>https://forem.com/forrestmiller</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3872475%2F1b38c5a4-9313-4bc3-8bfd-a21db422888e.jpg</url>
      <title>Forem: Forrest Miller</title>
      <link>https://forem.com/forrestmiller</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/forrestmiller"/>
    <language>en</language>
    <item>
      <title>Building a Virtual Team Activity That Works in Any Browser</title>
      <dc:creator>Forrest Miller</dc:creator>
      <pubDate>Tue, 21 Apr 2026 18:11:49 +0000</pubDate>
      <link>https://forem.com/forrestmiller/building-a-virtual-team-activity-that-works-in-any-browser-57ea</link>
      <guid>https://forem.com/forrestmiller/building-a-virtual-team-activity-that-works-in-any-browser-57ea</guid>
      <description>&lt;h1&gt;
  
  
  Building a Virtual Team Activity That Works in Any Browser
&lt;/h1&gt;

&lt;p&gt;Our team is remote. Fully distributed, five time zones, the usual. Every few weeks someone on the People team asks if we can "do something fun together." The options are always the same: Jackbox (someone can't install it), Kahoot (half the team groans), or a virtual escape room that costs $25/person and requires a purchase order.&lt;/p&gt;

&lt;p&gt;So we built something instead. A multiplayer bingo game that works in any browser, needs zero installs, and starts with a shared link. Here's how it works under the hood, and why the constraints mattered more than the features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraint That Shaped Everything
&lt;/h2&gt;

&lt;p&gt;The hardest part of virtual team activities isn't the game itself. It's the 15 minutes before the game where someone can't install the app, someone else needs to create an account, and the IT person on the call says the domain is blocked by the corporate firewall.&lt;/p&gt;

&lt;p&gt;We set one rule: &lt;strong&gt;it has to work the moment someone clicks a link.&lt;/strong&gt; No app stores. No sign-up forms. No browser extensions. If it doesn't work in the browser you already have open, it's not worth building.&lt;/p&gt;

&lt;p&gt;That constraint eliminated most architectures. Native apps were out. Anything requiring authentication was out. Heavy client-side frameworks that break on corporate-managed Chrome installations were out.&lt;/p&gt;

&lt;p&gt;What's left? A server-rendered page with a WebSocket connection for real-time gameplay.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Real-Time Part Works
&lt;/h2&gt;

&lt;p&gt;Every bingo game is a room. When a host creates a game, the server generates a room code and a shareable link. Players click the link, land on the game page, and a WebSocket connection opens automatically.&lt;/p&gt;

&lt;p&gt;Here's what travels over the wire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Player joins&lt;/strong&gt; → server sends them a uniquely shuffled board (same word pool, different arrangement per player)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Player claims a cell&lt;/strong&gt; → broadcast to all players in the room&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Player reacts with an emoji&lt;/strong&gt; → broadcast immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone gets bingo&lt;/strong&gt; → server validates the pattern and announces it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important design choice: the server is authoritative. Clients don't decide if they got bingo. They send their board state, the server checks it, and confirms or rejects. This prevents the inevitable "I definitely had bingo first" argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client A clicks cell → WebSocket → Server validates → Broadcasts to Room
                                                    ↓
                                            Client B sees claim
                                            Client C sees claim
                                            Host sees claim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use WebSocket heartbeats to track who's still connected. If someone's browser goes to sleep (common on phones during Zoom calls), the connection drops and the player shows as inactive. When they come back, they reconnect and the server replays any missed state.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Content Generation: The Part That Saves 20 Minutes
&lt;/h2&gt;

&lt;p&gt;The second friction point with team bingo is content. Someone has to write 25 squares. For a "things that happen on Zoom calls" card, that means opening a Google Doc, brainstorming with the team, arguing about whether "someone's kid walks in" is too obvious, and eventually giving up at 18 squares.&lt;/p&gt;

&lt;p&gt;We built an AI content pipeline that takes a topic description and generates a full set of bingo clues in seconds. The model gets the topic, the grid size (3×3, 4×4, or 5×5), and returns clues that are specific enough to be funny but broad enough that multiple people can claim them.&lt;/p&gt;

&lt;p&gt;The generation is streaming — clues appear one by one as the model produces them. Users can swap individual clues they don't like, which triggers a single-clue regeneration rather than rebuilding the whole card. This keeps the human in the loop without making them do the tedious part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "No Account" Is a Feature, Not a Missing Feature
&lt;/h2&gt;

&lt;p&gt;We went back and forth on this. Accounts enable saved games, player history, leaderboards across sessions. But accounts also mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone in IT has to approve a new SaaS tool&lt;/li&gt;
&lt;li&gt;GDPR compliance for European team members&lt;/li&gt;
&lt;li&gt;Password reset emails when someone forgets their login before the next game&lt;/li&gt;
&lt;li&gt;One more entry in the company's vendor management spreadsheet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a team activity that happens every few weeks, the overhead of accounts kills adoption. So we made accounts optional. You can create one to save your cards, but players never need one. The host shares a link, people click it, they play.&lt;/p&gt;

&lt;p&gt;This is the same reason the game works without a native app. Every friction point — every "download this first" or "create an account to continue" — loses a percentage of your team. By the time you've lost 3 people to setup friction, the game isn't fun anymore because the room is too small.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Printing Path (For When WiFi Fails)
&lt;/h2&gt;

&lt;p&gt;Not every team activity happens on a video call. Some offices do in-person team lunches. Some teams meet up at offsites. We added PDF generation that produces up to 200 uniquely shuffled boards — same content, different cell arrangements — that you can print and play with markers.&lt;/p&gt;

&lt;p&gt;The PDF generation runs server-side. The client sends the card content and grid size, the server generates the PDFs with randomized layouts, and returns a downloadable file. No client-side PDF libraries, no browser compatibility issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Distribution beats features.&lt;/strong&gt; A game that works instantly via link beats a game with 50 features that takes 10 minutes to set up. We could add tournament brackets, persistent leaderboards, team scoring. But none of that matters if three people can't join.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The host experience is the bottleneck.&lt;/strong&gt; Players just click a link. The host has to create the game, pick a topic, configure the grid, and share the link. Every second of host setup time reduces the chance they'll do it again next month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Corporate networks are hostile to fun.&lt;/strong&gt; Firewalls block unknown domains. Chrome extensions get disabled by policy. App installations require admin approval. Building for the browser — specifically, building something that works on a locked-down corporate Chromebook — is the only reliable path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. WebSocket reliability matters more than WebSocket performance.&lt;/strong&gt; Nobody cares if a cell claim takes 50ms or 200ms to propagate. They care a lot if the connection drops during the game and they lose their progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It With Your Team
&lt;/h2&gt;

&lt;p&gt;If you want to run this with your own remote team, &lt;a href="https://bingwow.com/for/remote-teams" rel="noopener noreferrer"&gt;BingWow&lt;/a&gt; is the tool we built. Completely free, no account needed for players, works in any browser. Type a topic, share the link in Slack or Zoom chat, play for 10 minutes.&lt;/p&gt;

&lt;p&gt;The best team activities are the ones that actually happen. And they only happen when the setup cost is close to zero.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>remote</category>
      <category>websockets</category>
      <category>teambuilding</category>
    </item>
    <item>
      <title>How We Generate Bingo Cards with AI in 10 Seconds</title>
      <dc:creator>Forrest Miller</dc:creator>
      <pubDate>Mon, 13 Apr 2026 18:14:15 +0000</pubDate>
      <link>https://forem.com/forrestmiller/how-we-generate-bingo-cards-with-ai-in-10-seconds-1ke</link>
      <guid>https://forem.com/forrestmiller/how-we-generate-bingo-cards-with-ai-in-10-seconds-1ke</guid>
      <description>&lt;p&gt;Type "Super Bowl party" into &lt;a href="https://bingwow.com/create" rel="noopener noreferrer"&gt;BingWow&lt;/a&gt; and you get 25 themed bingo clues in about 10 seconds. Clues stream in one at a time while you watch. You can swap any clue with an AI-generated replacement, adjust the tone (playful vs. realistic), or edit the text directly.&lt;/p&gt;

&lt;p&gt;Here's how the generation pipeline works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Streaming Approach
&lt;/h2&gt;

&lt;p&gt;We use Google's Gemini API with server-sent events (SSE). The prompt includes the topic, desired tone, grid size, and any existing clues to avoid duplicates. Clues stream to the client as they're generated -- the board fills in progressively instead of making the user wait for all 25.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/templates/generate-clues
→ SSE stream of clues
→ Client appends each clue to the board in real time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tone control gives users three options: Playful (puns, exaggeration, humor), Balanced (mix of funny and realistic), and Realistic (things that actually happen). The prompt engineering for each tone took more iteration than any other part of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just GPT-4?
&lt;/h2&gt;

&lt;p&gt;We use Gemini for clue generation and Claude for content moderation. The split is intentional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; (clue generation): Faster streaming, cheaper per token, good at creative list generation. The clues are short (under 50 characters each), so the model's creative writing quality matters less than speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; (moderation): Better at nuanced content review. When a user-created card goes through moderation, Claude classifies it as KEEP, DELETE, or IMPROVE (rewrite + publish). The IMPROVE path rewrites inappropriate clues while preserving the card's theme.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Semantic Deduplication
&lt;/h2&gt;

&lt;p&gt;A common failure mode: the AI generates "Someone scores a touchdown" and "A touchdown is scored." Same clue, different words. We run semantic dedup after generation -- computing embedding similarity between all clue pairs and replacing duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background Image Generation
&lt;/h2&gt;

&lt;p&gt;When you create a card, we also generate a background image via Replicate's FLUX Schnell model. The image goes through a text detection step (Claude Haiku vision) -- if readable text is detected in the image, we discard it and assign a fallback background. Text in background images makes clue text unreadable.&lt;/p&gt;

&lt;p&gt;The full pipeline (clue generation + background image) completes in under 15 seconds. The image usually finishes before the clues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Moderation Pipeline
&lt;/h2&gt;

&lt;p&gt;Every user-created card runs through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic checks&lt;/strong&gt; (blocked words, spam patterns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI review&lt;/strong&gt; (Claude classifies KEEP/DELETE/IMPROVE)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-categorization&lt;/strong&gt; (assigns the card to the right topic category)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Icon assignment&lt;/strong&gt; (matches a Noun Project icon via embedding similarity)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cards are playable immediately (even before moderation completes). The moderation runs in &lt;code&gt;after()&lt;/code&gt; so it doesn't block the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Create a card on any topic in 10 seconds: &lt;a href="https://bingwow.com/create" rel="noopener noreferrer"&gt;bingwow.com/create&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Browse 1,000+ pre-made cards: &lt;a href="https://bingwow.com/cards" rel="noopener noreferrer"&gt;bingwow.com/cards&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything is free -- no signup, no ads, no limits.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>gemini</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Real-Time Multiplayer Bingo with Next.js and Ably</title>
      <dc:creator>Forrest Miller</dc:creator>
      <pubDate>Mon, 13 Apr 2026 17:30:33 +0000</pubDate>
      <link>https://forem.com/forrestmiller/building-real-time-multiplayer-bingo-with-nextjs-and-ably-3j0b</link>
      <guid>https://forem.com/forrestmiller/building-real-time-multiplayer-bingo-with-nextjs-and-ably-3j0b</guid>
      <description>&lt;p&gt;I built &lt;a href="https://bingwow.com" rel="noopener noreferrer"&gt;BingWow&lt;/a&gt;, a free multiplayer bingo platform. Players open a link, get a uniquely shuffled board, and play together in real time. No app download, no account creation.&lt;/p&gt;

&lt;p&gt;This post covers the interesting technical decisions: real-time sync, optimistic claims, server-side bingo detection, and why each player needs a different board.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Traditional bingo generators give everyone the same card. In person that works because a caller draws numbers. Online, there's no caller -- players mark cells when they spot something happening (during a TV show, a meeting, a baby shower). If everyone has the same arrangement, the first person to mark a cell wins every time.&lt;/p&gt;

&lt;p&gt;Each player needs the same &lt;em&gt;clues&lt;/em&gt; in different &lt;em&gt;positions&lt;/em&gt;. And claiming a cell needs to be instant (optimistic) while still being authoritative (server-verified).&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; with App Router (React 19)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; PostgreSQL for rooms, players, boards, claims&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ably&lt;/strong&gt; for real-time events (claims, bingo, round transitions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Board Generation
&lt;/h3&gt;

&lt;p&gt;Every board is deterministic. Given a room seed and a player ID, the board is reproducible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;playerSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomSeed&lt;/span&gt; &lt;span class="nx"&gt;XOR&lt;/span&gt; &lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="nx"&gt;playerRng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerSeed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fisherYatesShuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonFreePositions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;playerRng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The room seed determines &lt;em&gt;which&lt;/em&gt; clues appear. The player seed determines &lt;em&gt;where&lt;/em&gt; they go. Late joiners regenerate the exact same board by using the same seeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wildcard Mode
&lt;/h3&gt;

&lt;p&gt;In rounds 2+, each player gets ~2/3 shared clues and ~1/3 unique clues. This creates boards that overlap enough for shared moments but diverge enough that bingo timing varies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimistic Claims
&lt;/h3&gt;

&lt;p&gt;Tapping a cell marks it instantly. The server call is fire-and-forget for non-bingo claims:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Player taps cell&lt;/li&gt;
&lt;li&gt;UI shows claimed immediately (optimistic)&lt;/li&gt;
&lt;li&gt;POST fires to server (no await)&lt;/li&gt;
&lt;li&gt;Server rejects? Next &lt;code&gt;fetchGameState&lt;/code&gt; reconciles&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For bingo-completing claims, the POST is awaited. The server's &lt;code&gt;claim_and_process&lt;/code&gt; PostgreSQL function atomically inserts the claim, checks all winning lines, and updates the room status in one transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Sync
&lt;/h3&gt;

&lt;p&gt;Ably handles event broadcast. The server publishes; clients subscribe. Events: &lt;code&gt;claim&lt;/code&gt;, &lt;code&gt;bingo&lt;/code&gt;, &lt;code&gt;new-round&lt;/code&gt;, &lt;code&gt;player-joined&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt;. If Ably disconnects, clients call &lt;code&gt;fetchGameState&lt;/code&gt; on reconnect to reconcile any missed events.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;The claim system works but the fire-and-forget architecture means silent failures. If a claim POST fails (network error), the player sees a claimed cell that the server doesn't know about. It self-heals on the next state fetch, but there's a visible "unclaim" moment that confuses users.&lt;/p&gt;

&lt;p&gt;If I rebuilt it, I'd use a write-ahead approach: persist claims locally first, then sync. But for a free tool with casual gameplay, the current approach is good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The whole thing is free -- no ads, no paid tier, no signup required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a card: &lt;a href="https://bingwow.com/create" rel="noopener noreferrer"&gt;bingwow.com/create&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Browse 1,000+ cards: &lt;a href="https://bingwow.com/cards" rel="noopener noreferrer"&gt;bingwow.com/cards&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;For teachers: &lt;a href="https://bingwow.com/for/teachers" rel="noopener noreferrer"&gt;bingwow.com/for/teachers&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Built a Real-Time Multiplayer Bingo Engine with Next.js, Supabase, and Ably</title>
      <dc:creator>Forrest Miller</dc:creator>
      <pubDate>Fri, 10 Apr 2026 21:10:09 +0000</pubDate>
      <link>https://forem.com/forrestmiller/i-built-a-real-time-multiplayer-bingo-engine-winextjs-webdev-javascript-reactth-nextjs-2og1</link>
      <guid>https://forem.com/forrestmiller/i-built-a-real-time-multiplayer-bingo-engine-winextjs-webdev-javascript-reactth-nextjs-2og1</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;BingWow&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://bingwow.com&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; is a free multiplayer bingo platform. You type a topic, AI generates a card, and up to 20 people play together in the browser. No downloads, no accounts. Teachers use it for classroom review games, event planners use it for baby showers and team building, and watch party hosts use it for live TV events.

Here's how the real-time multiplayer works under the hood.

&lt;span class="gu"&gt;## The Stack&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Next.js 16**&lt;/span&gt; (App Router) on Vercel
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Supabase**&lt;/span&gt; (PostgreSQL + Auth + Storage)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Ably**&lt;/span&gt; for real-time pub/sub
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Tailwind v4**&lt;/span&gt; for styling

&lt;span class="gu"&gt;## The Hard Problem: Simultaneous Bingo Claims&lt;/span&gt;

Most of the game is simple. A player taps a cell, the client marks it optimistically, and a fire-and-forget POST goes to the server. No waiting, no blocking.

The moment it gets complicated is bingo. When a claim completes a line, you need to atomically determine who won. Two players might complete their lines within milliseconds of each other. The server has to pick one winner and broadcast the result to everyone.

We solve this with a Supabase RPC function called &lt;span class="sb"&gt;`claim_and_process`&lt;/span&gt;. It runs in a single database transaction: insert the claim, check if it completes a line, and if so mark the round winner. The atomicity of the transaction means two simultaneous bingo claims can't both win.

&lt;span class="gu"&gt;## Board Generation: Deterministic Randomness&lt;/span&gt;

Every player needs a unique board, but late joiners need to reconstruct the exact same board a player would have gotten had they joined at the start. We solve this with seeded PRNGs.

The room gets a random seed at creation. Each player's board is derived from &lt;span class="sb"&gt;`(roomSeed XOR hash(playerId))`&lt;/span&gt;. The hash function is FNV-1a, the PRNG is Mulberry32. Both are deterministic: same inputs, same board, every time.

This means we never store boards in the database. Any client can reconstruct any player's board from the seed and player ID. Late joiners derive their board locally and overlay the current claim state from a single &lt;span class="sb"&gt;`game/state`&lt;/span&gt; API call.

&lt;span class="gu"&gt;## Wildcard Mode: Shared Clues with Individual Variation&lt;/span&gt;

In wildcard mode (always on for online play), players share about 2/3 of their clues with 1/3 unique per player. The room RNG selects a "core" set, and each player's own RNG picks their variable clues from the remaining pool.

This creates the right balance: enough overlap that calling clues is meaningful, enough variation that copying someone else's board doesn't work.

&lt;span class="gu"&gt;## Real-Time Events via Ably&lt;/span&gt;

Every room subscribes to an Ably channel &lt;span class="sb"&gt;`room:{code}`&lt;/span&gt;. Events include &lt;span class="sb"&gt;`claim`&lt;/span&gt;, &lt;span class="sb"&gt;`bingo`&lt;/span&gt;, &lt;span class="sb"&gt;`new-round`&lt;/span&gt;, &lt;span class="sb"&gt;`player-joined`&lt;/span&gt;, &lt;span class="sb"&gt;`chat`&lt;/span&gt;, and &lt;span class="sb"&gt;`unclaim`&lt;/span&gt;. Claims are fire-and-forget from the server -- the DB is the source of truth, and Ably events are convenience notifications.

On reconnect after a disconnect, the client calls &lt;span class="sb"&gt;`fetchGameState`&lt;/span&gt; to reconcile any missed events. A heartbeat POST every 2 minutes keeps &lt;span class="sb"&gt;`last_active_at`&lt;/span&gt; fresh for the expiry cron.

&lt;span class="gu"&gt;## AI Card Generation&lt;/span&gt;

When a user types a topic on &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;bingwow.com/create&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://bingwow.com/create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;, we stream clues from Gemini via SSE. The user sees clues appear one by one as the AI generates them. They can edit any clue inline, change the grid size (3x3, 4x4, 5x5), or regenerate with a different tone.

Background images are generated in parallel via Replicate's FLUX Schnell model (~2 seconds), then screened by Claude Haiku for text artifacts before being attached to the card.

&lt;span class="gu"&gt;## What I'd Do Differently&lt;/span&gt;

&lt;span class="gs"&gt;**Skip the custom PRNG.**&lt;/span&gt; Mulberry32 was fun to implement but a simple &lt;span class="sb"&gt;`crypto.getRandomValues`&lt;/span&gt; with a seed would have been simpler. The deterministic requirement is real, but there are libraries for this.

&lt;span class="gs"&gt;**Use server-sent events for game state instead of polling + Ably.**&lt;/span&gt; Ably works great but adds a dependency. SSE from a Next.js route handler could handle the claim/bingo broadcasts with one fewer service.

&lt;span class="gu"&gt;## Try It&lt;/span&gt;

The whole thing is free at &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;bingwow.com&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://bingwow.com&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;. Teachers can find classroom-specific guides at &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;bingwow.com/for/teachers&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://bingwow.com/for/teachers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;. The card creator is at &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;bingwow.com/create&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://bingwow.com/create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>gamedev</category>
      <category>nextjs</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
