Inspiration

I wanted a game I could play on a crowded train without looking like I was playing a game. Not "discreet" in the sense of a small window — I wanted the screen to look like work. Like a terminal tailing production logs while an incident is burning.

So the disguise came first, and the game was built to fit inside it. Every design decision after that was judged by one question: would this survive someone glancing over your shoulder?

What it does

DEBUGGER is a match puzzle wearing an incident-response terminal as a costume.

You are an on-call engineer covering 16 data centers. Pick a degraded node on a dot-matrix world map, fly there — travel time is derived from real great-circle distance, so Tokyo to São Paulo actually costs you time — and patch the bugs on site.

The patching is the puzzle: connect three or more matching signatures (0x4F, NULL, [SYS], ROOT) on a 6x6 board. While you work, three resources climb:

  • CPU — above 72%, sectors of the board freeze into 0xDEAD and become unusable
  • MEM — at 65% the minimum chain grows from 3 to 4, at 88% to 5
  • TRACE — accelerates the higher CPU and MEM get. This is the real timer

Clearing tiles pushes them back down, but the recovery is deliberately sub-linear (eff = 3 + (n-3)^0.7), so grinding three-matches can never outrun the load. You have to build long chains and cash them in before the multiplier expires.

Between missions there is fatigue that only recovers in real time, an engineer rank ladder (INTERN → ENGINEER → STAFF → PRINCIPAL → FELLOW → … → SINGULARITY at level 85), and waves that re-infect the whole world at higher severity once you clear it.

How I built it

React Native (Expo) + TypeScript, with the game logic kept in pure functions so it is testable and portable: board generation, gravity, dead-end detection, haversine distance, XP curves, fatigue decay. Reanimated for the drops, chain highlights, screen shake and floating PATCHED popups. expo-haptics for the impact on every clear. The world map is drawn with react-native-svg from a hand-built 5° raster — no map tiles, no network, ~30 lines of longitude ranges per latitude band.

RevenueCat handles the money. There is exactly one product: a non-consumable PRO License that removes ads, mapped to a DEBUGGER Pro entitlement. The app never talks to a store SDK directly — it subscribes to the entitlement, mirrors it into local save state so it works offline, and exposes restore because App Store requires it. Being able to develop the whole purchase flow against RevenueCat's Test Store, before Google's merchant account and product setup had even finished propagating, was the single biggest schedule saver in this project.

Ads are AdMob, opt-in first: rewarded ads are attached to things the player already wants (skip travel time, recover fatigue), plus one dismissible interstitial on the mission-result → map transition, rate-limited and never on the first mission.

Challenges I ran into

Ads almost killed the concept. A banner would have destroyed the entire premise — one strip of colorful advertising and the disguise is over. I cut banners completely and rebuilt the ad strategy around opt-in rewards, so the illusion only breaks at a moment the player chose. The original plan was to force an unskippable ad before returning to the map; that is also an AdMob policy violation, so it became a standard dismissible interstitial with a cooldown.

A native crash with no JS stack. The iOS build died instantly on launch. The crash report pointed at worklets::JSIWorkletsModuleProxy — Reanimated 4 needs the react-native-worklets Babel plugin, and without it the app segfaults before any JS error surfaces.

A bug that only existed on iOS. Dragging across the board always selected the top-left cell. nativeEvent.locationX is relative to the touched view, not the responder — so once a cell became the touch target, my coordinate math was measuring from inside that cell. Setting pointerEvents="none" on every cell fixed it. It never reproduced on web, which is exactly why testing on the real target matters.

Kotlin version hell. The Android build failed in compileReleaseKotlin: AdMob's play-services-ads 25.x is compiled with Kotlin 2.3, Expo SDK 57 ships 2.1, and the metadata is unreadable. Raising Kotlin to 2.3 then broke react-native-safe-area-context instead. The fix was to go the other direction and pin the ads library to a version built against Kotlin 2.1.

Accomplishments that I'm proud of

The disguise holds. The icon is a >_ prompt on black, the launch sequence prints mount logs, and a console feed scrolls fake system output the entire time you play. Nothing on screen says "game."

I'm also happy with how the difficulty is expressed through the fiction rather than a difficulty slider: MEM pressure literally raises the minimum chain length, CPU load literally takes squares off the board. The numbers on the HUD are the mechanics.

What I learned

Verify on the actual target, early. Web and iOS both "worked" while Android was completely unbuilt, and every one of the three hardest bugs in this project was platform-specific and invisible in the other environments.

And on monetization: the constraint that ads must not break the premise produced a better design than "put a banner at the bottom" would have. Opt-in rewards tied to existing friction points beat interruptive ads on this app, and the buy-once PRO license is a cleaner promise than a subscription for something that delivers its value exactly once.

What's next for DEBUGGER

iOS release (Apple developer enrollment is in progress — the RevenueCat and ATT plumbing is already in the build). Then a subscription tier that adds ongoing value instead of removing something: faster fatigue recovery, a daily credit grant, and extra fake dashboards — which, for an app whose whole appeal is looking impressive over your shoulder, is a real feature.

Built With

Share this project:

Updates

Submission history