Inspiration

I run a one-person studio in Tokyo. I already had a flick-typing game for Japanese players, and I kept noticing the same thing about people learning Japanese: they can read hiragana long before they can type it.

Almost every hiragana app takes romaji or gives you multiple choice. Neither is how anyone actually writes Japanese. On a phone in Japan you use flick input — a 12-key pad where you press か and slide up for く, left for き, down for こ. Forty-six characters, twelve keys, one gesture. Nobody teaches it, and it's the exact skill you need the first time you want to message someone here.

A conveyor-belt sushi bar turned out to be the perfect frame for it. The belt is already a timer. The plate is already a reward. And the word you have to type is sitting right there on the plate, so the picture, the word and the meaning arrive together.

What it does

Sushi rides past. Type its name in hiragana before it rolls out of reach.

  • Six belts. You start with short words (たまご, いか, まぐろ), then longer ones with voiced marks and small kana, then you order (まぐろください), then you talk to the chef and he serves you something for it.
  • A hiragana chart that doesn't lie. A character fills in only when you spell a whole word containing it, correctly, start to finish. Not when you've seen it. Not when you've tapped near it.
  • Targeted practice. Tap any character that keeps tripping you up and drill only the words it appears in.
  • Seven languages — English, Spanish, German, French, Italian, Portuguese (BR), Indonesian. No Japanese needed to start; every prompt shows its meaning in your language.

You can play it right now in a browser. The link above runs the same build — same belts, same chart, same flick keyboard. In-app purchases are Android-only, so the browser version has everything unlocked; the paywall and the consumable revive are in the demo video.

How I built it

Vanilla ES modules and Phaser, no bundler, wrapped with Capacitor for Android. RevenueCat handles the purchases.

The design question I spent the most time on was what to sell. My first answer was the boring one — put the later belts behind a paywall. It worked, and it was the weakest possible version: Endless mode was behind the wall too, which meant a player who never paid had zero contact with monetisation.

So I rebuilt it:

Free White and Blue belts, Endless mode, the hiragana chart
Dojo Member (one-time) Green belt onward, plus targeted practice
Okawari (consumable) One revive in Endless — "one more plate"

Two products, two purchase types, and both free and paying players have a reason to open the store. The ranked belts stay purely skill-based — you can't buy a rank. The revive only exists in the arcade mode, where a revive is something players actually want.

One nice surprise: I didn't need a bundler to use RevenueCat. Reading @revenuecat/purchases-capacitor, its JS layer is a thin Proxy over registerPlugin('Purchases') that special-cases exactly one method. So window.Capacitor.Plugins.Purchases gets you the same object, and the zero-build ES module setup survived intact.

Challenges I ran into

The tutorial didn't teach half of what the first belt needed. Three of the seven White Belt items — たまご, えび, まぐろ — contain a voiced mark. That's roughly 40% of the opening belt requiring a key the tutorial never mentioned. I didn't find this by playing. I found it by counting. The tutorial now has a fourth step built on えび, and the key only lights up at the moment you're stuck on it.

The hiragana chart was dishonest. Twice.

First pass: I counted and found の, ふ, へ, や, よ appeared in no question anywhere in the game. Five squares that could never be filled. I added four chef conversations to cover them.

Second pass, and this is the one that stung: those conversations were in the paid tiers. A free player still couldn't reach any of them. Counting properly this time — "reachable on the free belts" rather than "exists somewhere" — the free ceiling was 32 of 47. I'd moved the hole behind the paywall and called it fixed.

"Does this exist?" and "can this player reach it?" are different questions, and I'd only ever asked the first one.

The real fix was two things. Five new dishes (がり, あじ, はまち, しょうゆ, つけもの) pushed the free ceiling to 36/47 — and incidentally fixed a complaint I'd had for weeks that White Belt was too repetitive. And for the rest: へ and を are particles. They don't occur in the name of a food. Ever. No amount of new sushi will put them on the free belts.

So the chart now shows a small key on characters that only live in the paid layers. Not "you failed to fill this" — "this is what membership opens." A constraint of the language, turned into an honest signpost instead of a broken promise.

Consumables don't have a balance. RevenueCat records that you bought an item, not how many you have left — which is correct, it's not a subscription. But it means the count lives in local storage, and a crash between "payment succeeded" and "credit the player" loses a purchase. On every launch the app now reconciles its local count against the non-subscription transaction history and credits anything it missed.

What I learned

Coverage is something you measure, not something you feel. Every content bug in this project was the same shape: I assumed the material covered something, and it didn't. Each one was found by writing a twenty-line script that counts, and each one now has a checker that fails loudly if it regresses. check_kana_map.py reports both numbers — how many characters exist, and how many a free player can actually reach — because reporting only the first is how I fooled myself the first time.

The boring monetisation answer is usually the wrong one. Gating content is the first idea and the least interesting. The version I shipped gives more away and earns more contact with players, because I asked "who never sees the store?" instead of "what can I lock?"

What's next

Katakana is the obvious next belt — the data model already carries the proper written form for every loanword (さーもん → サーモン), it just needs the content. Audio for every prompt. And iOS, where the same RevenueCat integration should carry over almost unchanged.

Built With

Share this project:

Updates

Submission history