How the Google Doodle team used the Realtime Database to build a multiplayer game

Did you play the interactive Google Doodle for the Mexican card game “Lotería” last December? If not, you can still check it out here. Lotería is a multiplayer bingo-style game that supports up to five players per room. In the digitized Doodle version, you can play with random people around the world or create a private game with your friends.

The Lotería game was developed in JavaScript primarily by one engineer over a few months, and the game’s usage jumped from no players to millions literally overnight. The multiplayer component that supported it all was built entirely with the Firebase Realtime Database, which meant we could spend more time developing a great user experience, and less time worrying about infrastructure. Let’s look into how we did it!

Gameplay

Lotería is a card game similar to bingo, where each player has their own board. A dealer calls out cards, and players check their own boards for a match. If they have the card, they mark it⁠—traditionally with a pinto bean. When a player matches the win pattern for that game, such as a full row or column, they call out, “Lotería!”. The first person to call “Lotería!” wins.

Loteria game animation
Loteria game animation
There are two parts of the game that need to happen in real-time for all players. The first is dealing out the cards, and the second is when somebody calls, “Lotería!”. For the dealer, we randomly choose one of the players to act as the game host and run the dealer logic. On a repeating interval, the dealer randomly selects a new card from the deck and writes it to a location in the database that all the other players are listening to. We also include the time that the deal started, and the milliseconds until the next deal. Each client can use these numbers to show an animated timer. So the data looks like this:

/games/{gameId}/deal = {card, time, countDown}

To detect the winner, all players listen to another database location at /games/{gameId}/winner. When a player clicks “Lotería!” and has a valid winning pattern on their board, we write their ID to that location.

Often, there’s a close tie, so we use Reference.transaction to ensure that only the first player to call, “Lotería!” is declared the winner. We do this by making sure that /games/{gameId}/winner doesn’t already have an existing value before we write anything to that location.

Matchmaking

To create random public games, we utilized Cloud Functions for Firebase in conjunction with the Realtime Database.

When a player selects “Random Match,” their player ID is written to the location /queue/. A Cloud Function is set up to trigger when a new entry is created, and it checks whether we have enough players to start a game. If so, it creates the game at /games/_{gameId}_, and writes the gameId to each player’s entry, like so: /players/_{playerId}_/gameId = _{gameId}_

Players listen to gameId on their own database entries, so they know when to transition to the gameplay screen.

matchmaking
matchmaking

Since there are a lot of players connecting simultaneously, and the Cloud Function is asynchronous, we once again used a transaction to ensure that players are removed from the queue and placed into games atomically. Making one game at a time atomically could create a bottleneck, so instead we create as many games as possible in one transaction.

Scaling

Interactive Google Doodles get millions of players, so we needed to make sure that the database could handle the load. To do this, we sharded the database into multiple instances. Each instance can handle a limited amount of traffic, so we estimated how much traffic we expected to have. To do this, we wrote a load testing bot that could be run repeatedly and measured how much load this generated on a single instance. Then, we extrapolated that number to how many players we expected, which then gave us the number of instances we might need.

However, there is a downside to having too many instances: if players are spread too thinly, it might take longer for enough to connect to one instance to start a game. To solve this problem, we created an entry in the default database instance called “shards.” This was a number that could be adjusted from 1 to the maximum number of shards we created. Clients read this value and only used shards in that range. Since the Doodle was only live on the Google homepage for a short time, we updated the shards value manually through the Firebase Console, setting it lower when there was less traffic, and higher when there were spikes. Naturally, this process could be automated for a longer-running application.

More time on gameplay, less time on infrastructure

Lotería is a game that has a lot of significance for many members of the team, and we were proud to make an experience that shares our love of the game with the rest of the world. Building our game with help from the Realtime Database meant that we could get our backend up and running quickly, without having to worry about scaling. We hope to use it again in future Doodles and encourage you to give it a try for powering your own multiplayer game.