A few years ago, we created a little card game that poked fun at all the ups and downs of a typical development milestone. Players can enjoy development iterations reduced to simple card draws, with each card designed to spark laughs and conversations about past project experiences. Games are intended for two players, but larger groups can split into two teams, each joining forces to share their project successes and commiserate with their opponent’s pitfalls as they race to complete their milestones.
The game is called Milestone Mayhem, where you aim to complete your “milestone” before your opponent. Milestones are built with points over a series of turns (aka “sprints”), and the first team to 20 points wins! But, just like software projects, things are never as easy as they seem…
The deck contains two types of cards. Momentum cards represent the positives within a sprint, adding points towards your game-winning milestone. Watch out for mayhem cards though! These unforeseen challenges pop up during sprints and reduce momentum while the team deals with them. Drawing too many mayhem cards disrupts your sprint, with all momentum points lost for that turn. Players must balance the risk of committing too much – drawing too many cards with more potential for mayhem – and maintaining enough momentum each turn to win the milestone race.
The game works great for breaking the ice and building teams by highlighting common project pitfalls and allowing players to share similar stories. It’s even more fun when played with all project stakeholders, not just engineers – mayhem comes from all corners of a business after all! Since creating the physical edition, we’ve been waiting for the opportunity to build an online version of the game and recently started on this journey. We are pleased to announce we now have a version of Milestone Mayhem anyone can play on the web, iOS, or Android!
Can’t wait any longer? Play Now!
Technology
What went into producing the digital edition of Milestone Mayhem?
Frontend
We wanted the game to work across web and mobile platforms (with the potential for over-the-air updates on mobile), so our technology stack was focused around React Native for performant mobile support and React Native Web for web compatibility through a single codebase. Type safety is always a priority, especially on new projects, so we started the game as a TypeScript project. We used Expo as the application framework and Expo Application Services to build and publish native Android and iOS applications.
A game wouldn’t be complete without sound and motion to liven things up! We used Audio.Sound from expo-av
to handle sound effects, and we created some motion graphics as LottieFiles, with Lottie React Native used to render the more complex animations in the game. Simpler animations on components like buttons or timers are handled using the React Native Reanimated package.
Backend
It’s possible to play Milestone Mayhem locally against an AI opponent, but the battle to finish a milestone first wouldn’t be the same without playing against other people. We initially chose Supabase for its managed database to link players up to games and share state as games progressed, with updates communicated via Supabase Realtime. During our development, Supabase released their Realtime Broadcast support to general availability, allowing for pub-sub style communication. Milestone Mayhem’s multiplayer communication easily fit the pub-sub pattern, so we replaced the database portion of the game with a simple broadcast capability. Supabase manages the broadcast infrastructure for your live apps but can also run locally, which sped up development immensely.
Pipeline
We orchestrated our CI/CD pipeline through GitHub Actions to build the web version of the game when a PR gets accepted to the main branch and publish it with the github-pages action. This gave us an end-to-end pipeline allowing quick iteration and testing of the fully-built game. EAS builds of the mobile game artifacts were kept separate to manage costs during development iterations, but Expo was still fantastic for its local development and testing of mobile app builds. Whether through a simulator or closer to native through Expo Go, we had complete visibility of all target versions and could test the game on all platforms throughout development.
Game loop
Games are different from regular apps in that they typically use up a big chunk of a system’s resources while players focus exclusively on the action, favoring patterns that get bare-metal performance with the highest possible framerates. Every game has a “game loop” at its core – a simple loop that keeps running as quickly as possible until the user exits the game:
while (gameIsRunning) {
gatherInputEvents();
updateGameState();
renderNextFrame();
}
updateGameState()
will be more complex to handle all the various sections players can navigate within a game – menus, tutorials, lobbies, singleplayer, multiplayer, etc. – but the concept is the same everywhere. For the current place in the game, gather all controller input events (including network events for multiplayer), progress the state of the game from the current state + events, and display the next frame of the game view (possibly synchronized with the display’s refresh rate).
Modern game engines are more sophisticated than this and process things at different rates for consistency and efficiency, but fundamentally this is all a game loop is. However, running endless logic as quickly as possible is not great for web or mobile apps where efficiency is more of a concern. Games should be about having fun after all – not draining your phone’s battery in record time!
Performance issues aside, the game loop pattern is well-established for designing games, so we looked for something that allowed us to represent similar sequential game logic within the context of a web application. We eventually settled on Redux Saga, which fit the bill nicely, allowing sequential logic flow in an otherwise reactive application through its use of the communicating sequential processes pattern.
Milestone Mayhem’s redux-saga game
loop looks like this:
export function* rootSaga(): SagaIterator {
while (true) {
yield take(actions.create);
const server: Task = yield fork(serverSaga);
const { start } = yield race({
start: take(actions.start),
reset: take(actions.reset)
});
if (start) {
yield race([call(gameSaga), take(actions.reset)]);
}
yield cancel(server);
}
}
The loop executes indefinitely and performs high-level orchestration for playing a single game. actions
are our reducer functions within the redux slice that manages the overall game state, and redux-saga
’s take() method will wait until a specific store action is invoked – in this case, create()
. As Milestone Mayhem is just a web app at the end of the day, the outer boilerplate for menus and navigation is handled by React components rather than as part of the game loop. At some point in the app’s navigation, a player will initiate a game (single-player or playing online), which invokes the create()
action on the state store. This invocation triggers the next step in our rootSaga
game loop.
A sub-saga then gets initiated through fork()ing serverSaga
, which handles distinctions between single-player games that jump right into the action, and multiplayer, which has an extra lobby step to connect to games. We then race() between starting the game or resetting (if the user navigates away) and delegate to the gameplay loop represented by gameSaga
if a start()
action gets called. Finally, once the game ends with someone winning or a player resets by navigating out of the game, we cancel our server saga and loop back again to wait for the next game to start.
Have fun!
And that, in a nutshell, is Milestone Mayhem! We hope you found this overview intriguing enough to try out the game! Grab some friends or colleagues to join you in a milestone battle, have a blast playing it, and watch out for those mayhems!