Shipping a web game as its own Android app, from code we already had

How Brain Defense went from a canvas game on braineos.com to its own Play Store app, reusing the game's rules byte for byte and rebuilding only what the phone needed.

Photo by CALIN STAN on Unsplash

Brain Defense is now on Google Play. It’s the Braineos Tower Defense game, where you answer flashcard questions to earn the gold you spend on towers, packaged as its own Android app. It has its own listing and icon, but it signs in to the same Braineos account and plays the same decks. Here’s how we built it without writing a second game.

A variant, not a new app

Our mobile code already builds more than one app. Braineos, CertBrain and Finance Brain are variants of a single Expo codebase, each with its own name, package id, signing key and navigation, sharing one API client and sign-in flow. Brain Defense became another variant with its own package, com.braindefense.app, and a minimal nav: a deck picker and the game. It needed no new backend endpoints. The game already asks the server for a deck’s questions, and the server already checks who may study which deck.

The rules moved byte for byte

The web game keeps its rules apart from its drawing: the engine, waves, pathing, question queue and save format are plain TypeScript with no browser APIs. We copied those files across unchanged, with all three levels, and checked them with cmp. Their tests came too, including a headless bot that plays whole games to check the balance. So the Android game launched with exactly the tuning the web version had already been tested with, rather than a re-guessed copy.

Only two pieces changed. Saved best waves moved from localStorage to React Native’s AsyncStorage, which made those functions async. And the keyboard shortcuts were dropped, because phones don’t have keys.

Drawing and sound had to be rebuilt

The web game draws on an HTML canvas. On Android it draws with Skia, a native 2D graphics library that React Native can use. We ported the drawing code to Skia’s imperative canvas API. We checked every call against the installed package’s own type definitions, because some documentation described APIs that don’t exist in the version we use.

Sound was harder. On the web, every effect is synthesised live with Web Audio oscillators, and React Native has no equivalent. So a script renders each of the 14 sounds offline, using the same synthesis maths, into short audio clips that ship with the app. We compared the clips’ spectra and peak levels with the original parameters, not just by ear.

The bugs only a phone found

Two problems only showed up once the game ran on Android:

  • A crash as soon as play started. Skia’s native font binding skips the typeface argument only when it’s left out, not when it’s passed as undefined, so the game threw “Value is undefined, expected an Object” on its render thread. The fix asks Skia for a real system font with matchFont, built once instead of every frame.
  • A map that showed only one corner. A browser stretches a canvas to fit its box; Skia’s picture view doesn’t. The game records each frame at a fixed 800 by 600, so on a phone only the corner near the origin was visible: a tower or two and a sliver of road. The fix scales the drawing by the view’s measured size. While we were there, the map and status bar now stay fixed during play and only the panels below them scroll.

What’s next

Brain Defense is Android only for now. The same variant setup should build for iOS, but it hasn’t been built or tested there yet.

Planning an app?

Start a project