A very basic visual overview of the JS Battle Architecture
The rest of the team was at this point busy with work/family/life, and I was in my last semester of my Masters degree this Fall and simultaneously running a small software consulting business. As a result, none of us had any time to write out an overview.
How JS Battle Works
During the development process, we liked to describe JS Battle internally as having 3 main parts:
- The front-end (Backbone)
- The back-end (Node, Express, Mongo)
- The "way-back"-end (Node, Docker, Mongo)
Really, the "way-back"-end consists of "worker" scripts that run daily on a predetermined schedule.
To get more into the details, I'll break down the Front-End/Back-End, then talk about the "Way-Back"-End:
Front-End / Back-End
- Logging in: When you "log-in", we ask GitHub to check your credentials, then save your "logged-in" status to the session. Most of the details here get handled by Passport.
- Viewing battles: All game data is stored in our Mongo database. When you click the "play" button (or drag the slider), the front-end queries the server (API) for that specific game and turn. Once it returns, our Backbone code sees that it has new info, and updates the game view. The result is that you can watch the battle(s) unfold, as well as jump to a specific turn, rewind, etc. (Editorial comment: Writing the pausing/unpausing, etc. logic was kind of a pain in the butt.)
- Viewing the leaderboard: This is very similar to the logic for watching the battles, but far less complicated. When you select a new item in the leaderboard dropdown menus, the front-end hits an endpoint on our Express API asking for that specific leaderboard, and the leaderboard view on the front-end (again, implemented in Backbone) updates when the request comes back.
The way-back-end, (again, just the name for our collection of "Cronned" worker scripts), had one main job: run the daily battles.
Getting user code:
Using GitHub meant that it was easy to fork our provided code, so anyone could get up-and-running ASAP. It also meant that beginning programmers could get exposure to git and GitHub (with help from the instructions and links we provide as part of our site).
To actually get everyone's code, we simply wrote a Node script that queries our current list of users in our database, then attempts to grab all the hero-starter code from those users' public repositories. This script runs shortly before we start running the day's battles.
(It should be noted that JS Battle has NO access whatsoever to any non-public GitHub stuff. It doesn't ever even see your password. When you log into JS Battle, you're actually logging into GitHub--at the end of the process, we simply ask GitHub, "did they look OK to you?" and GitHub says "yep, they're legit", or "no, they're not".)
Running the battles:
This is the big one, and the lynchpin upon which everything else relies. If we can't run our daily battles, then we don't really have a site.
One huge problem with running a bunch of user-submitted code is that, unless we take precautions, it would be very easy for clever users to cheat (change their health, the outcome of the game, etc). Even more clever users could even (potentially) hack into our virtual machine.
The key part of our solution to address this is Docker. In a nutshell, Docker allows you to spin-up a lot of very lightweight virtual machines, and helps out by making it difficult for code within those virtual machines to "escape".
Here's the process:
- We already have local copies of every users' most recent hero code (see the previous step).
- So, we start out each day by randomly assigning every registered user to a different battle.
- Then, we run each battle in sequence:
- At the start of a battle, we spin-up a lot of Docker virtual machines, one for each user. Then, we communicate with these virtual machines via POST requests. The main game-runner sends in the current state of the game, and the user-submitted code sends back where they want their hero to move next (eg. "North", "South", "East", or "West"). If no response is received (which can happen if there is an error in the user code, or if the user code is taking too long, etc), then the game engine interprets the move as "Stay".
- The advantage of doing things this way is that the user code can do whatever it wants to the game state that it gets sent, but it can only influence the "master" game state (located in the game engine) by sending back a direction when it's that user's turn.
- At the end of each game, the Docker VMs are killed, each user's stats are updated (for users involved in the most recent game) and the next batch of user code is loaded up before starting the next battle.
- After every battle is finished, the game engine kicks off a script that updates the leaderboard.
And that's JS Battle in a nutshell. Obviously, there are a lot of sticky details that go into all this, especially on the security side of things (Docker alone isn't the end of it), but if you made it all the way to the end of this, you have a pretty decent idea of how all the parts fit together.