Checkout our video from Capstone Demo Day at Fullstack Academy:
In this post, I cover the "what, why, and how" of my capstone project at Fullstack Academy and share a little about what I learned from the experience.
For the final project at Fullstack Academy, you are assigned to a small team and tasked with collaboratively deciding on a software product to create and building within the span of two and a half weeks. My two teammates and I decided to create a web app version of a fun and fast-paced multiplayer card game called Nertz (aka Hell, Pounce, Peanuts, Racing Demon, and Squinch).
What Is Nertz?
Nertz is sometimes described as a hybrid of Solitaire and Speed (you may know Speed as "Spit"). Everyone has their own deck of cards set up in a solitaire fashion in front of them, but with one additional 13-card stack called the "Nertz pile". When you deplete your Nertz pile, you can call "Nertz" and end the round. Calling Nertz doesn't necessarily mean you win, however. The way you win is to have the most points, and you get points by getting your cards out onto the shared stacks area by playing a variation of Speed with everyone. The solitaire portion simply provides a constrained mechanism for gaining access to different cards in your deck. Any card on the top of your solitiare piles, your three-card draw, or your Nertz pile is eligible to be placed on a shared Speed stack or a solitaire pile.
The shared stacks area in Nertz is like Speed in that there is no taking turns and players race to shed a designated stack by playing on each other's cards. It's unlike Speed in that you can only play your card on another card of the same suit and only in strict ascending order (e.g. a 5 of hearts can only ever go on a 4 of hearts). Speed lets you play a card if it is either one above or one below the topmost card in the shared stack regardless of suit.
Once someone calls "Nertz!", gameplay stops and all the cards in the shared area are separated by player (every player has a uniquely identifiable deck) and counted. You get one point for each card you got onto the shared area. You also lose points for any cards left in your Nertz pile. Some people play where you detract one point for each card, some do two. It's whatever you agree on.
You can see a bit of what real-life Nertz gameplay looks like in this video
Why We Decided to Make NertzIO
Deciding on what to make was pretty tough. We brainstormed and debated for about a day and half before we settled on Nertz. Some other ideas we considered were an "opinionated budgeting app" that attempts to help solve the pandemic financial mismanagement of most Americans, a grocery-hacking app using crowd-sourced data to help users save time and money on grocery shopping, and a Harry Potter-themed augmented reality app that enables you to fight off dementors while waiting around at soul-suckingly boring locations (think DMV).
We decided against augmented reality because of our time constraints. It seemed too risky to try to take on learning all the frameworks and technologies surrounding AR when we need to have a fully functioning app in two weeks. We decided against the budgeting and grocery apps because, while they were definitely good ideas, neither of them seemed to pose a particularly unique or difficult programming challenge that we felt would be all that impressive or fun to work on. When you boil it down, all of their functionality primarily revolves around basic RESTful create, read, update, destroy (CRUD) operations. Building out a CRUD app full of features can certainly be a challenging task, especially under time constraints, but the challenge would be more about the quantity of work rather than the quality of the problems we needed solve. We've been implementing all kinds of CRUD apps throughout our Fullstack education, so doing another one just didn't seem that appealing.
Ultimately, we chose Nertz because we thought that a real-time multiplayer game would pose some interesting programming and architecture challenges we hadn't faced before, like implementing a very low-latency experience for players' moves to be rapidly persisted and displayed in the UI, keeping multiple users' front-end states in sync with each other and the database at all times, and implementing a drag and drop interface.
How We Built NertzIO
There were several considerations that went into how we made NertzIO. Some of the biggest ones included the following:
- Our very short time constraint (approx. 2.5 weeks)
- Our current skill set (learning new tech takes precious time)
- The requirements for a minimum viable product, like:
- Creating a rapid real-time experience
- Synchronizing front-end state across multiple clients
- Maintaining a single, persistent source of truth for front-end state
- Achieving the lowest latency experience for users
- Implementing a drag and drop interface that behaved intuitively
Our Initial Approach
We spent most of our time at Fullstack learning a tech stack consisting of Node, Express, React, Redux, and PostgreSQL, so that's where we started architecturally. We also had a little experience creating real-time apps using web sockets with Socket.io, so we were aware that we would almost certainly need to incorporate that into our stack in order to enable real-time updates for all users in a particular game instance. More important than that, we knew we needed to keep all players' front-end states perfectly synchronized. We needed to make it impossible for any player to ever end up in a situation where the state of the game on their browser is different from another player's.
The way figured we'd achieve this was by having clients connect over websockets through a shared key that corresponded to a game instance in the databse, and then essentially treat the database and like a Redux store and treat clients like components that are "connected" (a la
react-redux) to the store. Any client (like a component) could "dispatch" new state to the database over websockets, and then only when the database successfully updates will all the connected clients (including the one that dispatched the update) receive the new state of the game in their (actual) Redux stores, and thus render it out for all players to view.
So basically we would be creating a one-way flow of data that takes some payload from a client-side event, emits an event to the socket-connected Node server to update the database with the payload, then (after successfully writing to the database) emit a broadcast to all connected clients with a payload representing the new state, then socket listeners on all the clients dispatch an update to the Redux store based on the event name and payload, and then the components subscribed to the store will receive the new state of the game, and all players' will see the same update at the same time. Then the process starts again with the next user action.
That's a lot to follow isn't it?
We were pretty confident this architecture would work, but one concern we had was whether we should use Postgres (a relational database with NoSQL capabilities) or a straight document-based NoSQL database like MongoDB. It was our understanding that a document-based JSON data store like Mongo could potentially read and write more quickly than Postgres, and thus help us achieve less latency in each round trip that has to be made to update the game, which is extremely important when creating a real-time multiplayer experience. On top of that, being able to have our game data be the same structure in both the databse and the client's redux store would offer a lot of convenience for us as programmers trying to think and reason about what's going on in the front and back end application state.
So we thought we might incorporate Mongo in rather than Postgres. It would certainly be a time expense learning the API, but we hoped it would be doable
Lastly we needed to figure out how the heck to implement a drag and drop interface. None of us had any experience with this before, so we did some research and explored various libraries that had their own way of achieving drag and drop functionality. Some had such complicated and convoluted API that were incredibly daunting. Thankfully we ended up (fortuitously) finding and landing on Dan Abramov's (the author of Redux and many other React-related tools) React DnD library, which provided the perfect and minimal solution we needed to cleanly make our React components draggable and drop-receivable and enhanced with drag-related event handling so we could do something (like dispatch an update to the databse) depending on the element being dragged and the destination it's being dropped on. The library utiltizes the browser's built-in HTML5 Drag and Drop Web API under the hood so it has great cross-browser compatibility, and it offers an incredibly clean and concise API for wrapping your plain React component with a draggable higher order component. I can't recommend ReactDnD highly enough. It's really the perfect solution for drag and drop in React.
Our Final Approach
Now, as we began thinking about how to integrate our UI, web sockets, and database in a way that kept our front-end code clean and semantic, we began to realize that we would need to implement a fairly extensive library of server-side and client-side websocket event handling that wires up nicely to our database object relational mapping API and gives us a clean client-side API to make calls to update the database. We knew it would take several days to knock out and we didn't have several days to spare.
That's when we decided to try Google's Firebase, a real-time NoSQL database with a client-side SDK that offers a really great API for sending updates to the database and setting up event listeners to handle different update events on the client. This saved us an immense amount of time because Firebase offered everything we would've had to build ourselves.
So ultimately, we ended up builing NertzIO on four main technologies:
- Firebase NoSQL Real-time Database
- ReactDND (utilizing the HTML5 Drag and Drop Web API)
...eventually I'll update this post with addtional implementation details and reflections on the project.