Building a web-based multiplayer game with Google Firebase
Two weeks ago I decided to build rummikub-app.web.app a web-based version of a classic board/tile game. If you haven't played before, it's like a combination of rummy (collect "sets" and win when you have no cards left) and poker (players share cards on the table). I've wanted to learn Google Firebase, so what better opportunity to learn new technologies than a quarantine? ¯\_(ツ)_/¯
Goals
Technologies Used
Vue JS
Vue.js is a popular javascript framework. I already know it well, and didn't want to introduce too many new technologies at once (i.e. React.js).
Vuetify
Vuetify.js is a "Material Design" framework for Vue.js.
I use older, deprecated version (v1.5) on nerdydata.com, and decided this would be a good chance to learn their new v2 framework.
Firebase Realtime Database
Truly a magical database product, which deserves its own write-up. Benefits include:
Firebase Functions
Same as Google Cloud Functions with a different UI. I needed some server-side logic to prevent cheating and keep some game-play secret (i.e. dealing tiles, validating/saving the board).
+1 to not paying for a server, setting up an API or docker image, manage authentication, etc.
Firebase Authentication
I've never used this service before. After 10 minutes I had "Sign in with Google" working.
Again +1 to not having to set up a server, a database to store users, configure OAuth credentials, handle redirect flow, etc.
Just call a single method to log in (and a cloud function to validate and save the data to the Realtime Database.)
Firebase Hosting
It's free static website hosting - save yourself $5/mo. in site hosting fees - all by typing:
firebase deploy --only hosting
In theory, you don't even need to buy a domain, and can use the free "<name>.web.app".
Building the Game
1. Authentication
I started with authentication because (1) to test how to read/write user data in Firebase Realtime Database, and (2) if I couldn't get authentication working easily, I didn't want to have to create a server, and set up a database to read/write user data, and would have cut my losses early on.
Just enable the Google provider in the Authentication Console, and add two lines of code to the app to handle all login/redirect requirements
2. Game creation, and Game joining
Next up was adding two buttons to "create" and "join" games, and update each game to keep track of its players. I wanted game sharing to be easy, and found a fun library to generate unique, short, and human-readable names to help with game sharing.
On web, simply copy+paste the URL, and on mobile-web I used `navigator.share` to use the native share dialog:
3. Designing the "tile", "board", "pool", and "racks"
With users and games set up, next step was to create (1) the face-down "pool" of tiles for players to draw from, (2) each player's private "rack" where tiles are dealt, (3) the shared "board" that players can see and interact with, and (4) the small-but-important "tile".
The "Tile" class only needed a few properties. An ID (since there are two of each color/number combination), a number (or zero if a joker), and a color.
Recommended by LinkedIn
How tiles were formed into "groups" was less straight-forward.
Attempt #1 - create a grid of x,y coordinates that tiles can occupy. I realized early enough that this would be complex to validate/add/remove tiles from groups, and I'd be stuck with this structure if I ever wanted to change the grid-dimensions of the board.
Attempt #2 - create "tile groups" which more closely aligns with actual gameplay vs a fixed grid, and I could store and recreate the board using arrays of tile groups: [[1,2,3], [5,5,5]]
4. Ending a Player's Turn
"...it was at this moment..." I realized I picked a difficult game to build:
The result was to send up a list of the player's "moves" from their turn; storing each move's Tile ID and destination (which group and position within the group), and then replay the moves to validate and store the new board's state.
5. Everyone loves drag-and-drop
I had a working game 🎉 but usability was lacking: click on a tile and click on a destination to move it. I knew critics would demand "drag-and-drop".
After some failed attempts at making my own draggable directive, I decided to install Shopify's (not actively maintained?) Draggable Library.
After a few failed attempts (admittedly, by my own mistakes) I tested out a few other popular libraries, failed again, and eventually got Shopify Draggable (mostly) working.
I had a working game (again!) 🎉 spruced up the design a bit, added a homepage to explain how to play, a quick dialog for players to share games with friends, made sure things were responsive for mobile, and deployed! All in about 2 weekends.
Challenges and Time Sinks
Firebase, specifically Realtime Database, has changed the way I think of database storage and client-server communication. I'm already overwhelmed by all the (mostly useless) product ideas that come to mind 😇.
While there's enough learnings from Firebase Realtime Database alone to warrant its own write-up, here were the two biggest areas that took longer than expected:
Shared Typescript Modules with Firebase Functions
I ran into lots of issues sharing common Typescript code (like the Tile and TileGroup classes) between Vue and Firebase Functions. Firebase requires all referenced code to be in its directory for when it builds and deploys.
While I eventually figured out how to use the "composite" option in Typescript to designate a module as "shareable", I couldn't find a way to deploy shared functions referenced outside of the Functions directory, and don't think Firebase Functions can handle this case easily.
My solution: `cp -r ./shared/src ./functions/src/shared` - copy it all manually before releasing.
/functions (all Firebase Functions code must be in this directory)
- tsconfig.json
- src/
- lib/
/shared (common modules)
- tsconfig.json
- src/
- lib/
/src (Vue source)
- tsconfig.json
- views/
- App.vue
Firebase "request" vs "callable" functions
There are two types of Firebase Functions: "request" functions which are described in the "getting started" docs, and "callable" functions. I changed all Firebase Functions to use the "callable" type - a much better approach for apps which automatically handle authentication, and just easier to implement in general.
Firebase database rules
Firebase's Realtime Database Rules seemed counterintuitive at first, but it's really well thought out, and removes the need for a server to proxy and authorize database operations.
My advice here is to (RTFM) make sure that each key in the collection is something you have access to from the app and/or would want to filter/query by (i.e the user's UID, the game's ID, etc.)
Using their simple JSON rule builder, I can give access to the client (public website code) and enforce what each user can ".read", ".write" based on their identity.
In no other database would you ever allow a client (website/app) direct access to read or write from your database without some api server in-between. They handle all the validation and enforcement automatically, and default to restricted access when you don't specify a rule.
With that JSON rule schema, I enforce the following rules for requests made from the browser:
Firebase's documentation says it best: "While it [cascading rules logic] may not seem immediately intuitive, this is a powerful part of the rules language and allows for very complex access privileges to be implemented with minimal effort."
--