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!
/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.
/games/{gameId}/winner.
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.
Reference.transaction
/games/{gameId}/winner
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}
/queue/
/games/{gameId}
/players/{playerId}/gameId = {gameId}
Players listen to gameId on their own database entries, so they know when to transition to the gameplay screen.
gameId
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.
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.