Multiplayer BS Bingo Web App

It’s fair to say that there is no shortage of bullshit. Sometimes it may even be hard to escape it. You should still try though.

I built a real-time multiplayer bullshit bingo web app for when there is no way around it. It’s a chance to still have some fun, even while knee-deep in it.

You can play it right here, no login required:

How it works

Choose a public template consisting of 25+ words and phrases or create your own, then hit Play.

Bullshit bingo, if you don’t already know it, works just like regular bingo. You get your card with a (usually 5x5) grid of words or phrases. For a marketing meeting, your card could look like this:

When you hear the buzzword, click on it. Once you get a full row, horizontally, vertically, or diagonally, you’ve got a bingo!

Below your card is a list of all players and a representation of their cards. Cards may contain the same buzzwords, but they are randomly shuffled for each player.

Use cases

The obvious ones are presentations, meetings, and other occasions where you expect a ton of buzzwords. But that’s just the tip of the turd. Bullshit is to be found everywhere.

My favorite use case so far is public transport. Don’t get me wrong, I’m all for it. And playing a game of whose commute was more interesting makes it a lot more enjoyable:

Games have no time limit so you and your friends can play one over the course of a year or more.

The Tech

The web app is built with Svelte, which might not be as mature as react, but feels less verbose.

The API is built in Rust. Apart from the boring CRUD stuff, there is a WebSocket connection for sending and receiving game updates in real-time. Postgres holds the game’s state and card templates. The Rust service subscribes to Postgres notifications, which is quite convenient when using sqlx as your DB client:

let mut listener = PgListener::connect_with(pool).await?;

    .listen_all(vec!["fields_update", "players_update"])

loop {
    let notification = listener.recv().await?;
    let game_update: PgGameUpdateNotification = serde_json::from_str(notification.payload())?;
    // do something with game_update

The corresponding part in SQL that sets up the notifier looks like this:

create or replace function game_update_notification ()
  returns trigger
  language plpgsql
as $$
    channel text := tg_argv[0];
    perform (
      with payload(game_id) as (
        select new.game_id
    return null;

create trigger fields_update
after update on bingo.fields
for each row execute procedure game_update_notification('fields_update');

create trigger players_update
after update on bingo.players
for each row execute procedure game_update_notification('players_update');

In this case, the notification’s payload contains just the game’s ID. It’s up to the Rust service to then load more info about that game and send it to the players. But you could query as much data as you want within a notification trigger.

Shoutout to the makers of the sqlx crate! It is by far the most ergonomic way to interact with a DB in Rust and pretty much every other language I use. ORMs can be nice, until you debug whatever complicated query they’ve constructed for you.

ORMs offer typesafety you might say, but so does sqlx! You can write good old SQL with it and its macros will ensure that the types line up nicely. Works like magic.


Here is a demo of it:

If you find a bug, have a feature request, or just want to host it yourself, here is the MIT licensed GitHub repo:

More Posts

Menu Icon