Inspiration

The problem. The business model that won on mobile is the one that interrupts. An ad between levels. A timer that empties while you are not playing. A shop that sells back the difficulty it just added. In that model the puzzle is not the product. It is the thing that holds you still long enough to show you something else.

What that costs. Money, attention, and in places worse than either.

So I went and asked players. I pulled a year of r/AndroidGaming out of the Arctic Shift archive and counted. 198 posts about mobile games, sorted by what they were asking for:

Read those two groups together and the position is not "no advertising". It is "not without asking me", and "not that". The last group is the one that changed the build.

The answer. Rivulet is the game those 198 posts describe. Portrait, one thumb, offline, a board in a minute or two, nothing you can buy that makes a puzzle easier, and an ad only when you press a button that says what it gives you.

What it does

One rule, and the whole game comes out of it. Water leaves the spring and moves to any neighbouring tile at the same height or lower. That is it. Tap a stone to raise it one level; tap it past the top and it drops back to the ground.

Every board is a hillside: a spring at the top, thirsty plants at the bottom, a slope of stone in between, and a tap budget. You are not matching colours or beating a clock. You are working out a shape. Undo and reset cost nothing, so the only thing at risk is the next thirty seconds.

Three ways to play.

  • Daily Pour. One board a day, generated from the date, so everyone who opens the game that morning gets the same hillside.
  • Journey. 24 hand made boards across Meadow, Quarry, Orchard and Marsh. Each region introduces one idea, and Marsh hands you a second spring to reconcile.
  • Quick Pour. Three minutes, and as many generated boards as you can get through. The only mode that watches the clock, and it is the one mode you choose to enter.

The terrace. Every plant you water for the first time blooms in your own terraced garden. It is the progress system, and it cannot be bought past.

Comfort. Portrait only. A left hand mirror flips the board for the other thumb. Haptics and sound each have a switch. No account, no sign in, and every puzzle mode plays with the radio off.

How it is monetised

This is the genre where energy timers and lives counters are normal furniture. So the constraint I set was simple: nothing a player spends, in money or in attention, may change how hard a puzzle is.

Ads, only when asked for.

  • There are exactly two buttons that can play one. A hint, on the board, which reveals the next correct tap. A seed packet in the shop, worth 30 seeds. Both say what they give before anything loads.
  • No interstitials. No banners. Nothing on launch, nothing between boards, nothing when a board beats you.
  • Five a day is the ceiling, the count is on screen, and Settings keeps a ledger of every ad you watched.
  • Ad content is capped at a rating suitable for everyone before the first request is made, in RivuletApplication.onCreate. That line exists because of those ten Reddit posts.

Keeper, one payment. Removes the ad buttons for good, gives 3 free hints a day, and unlocks every cosmetic plus 500 seeds. It is a non-consumable, so there is no subscription behind it and no second bill.

Seeds, 500 for a small one off. Cosmetics only. They buy plants for your terrace and touch nothing else.

Daily Pour, all 24 Journey boards and Quick Pour are free and complete without paying anything.

How Rivulet uses RevenueCat

  • Configured by build type. Debug builds use a Test Store key through PurchasesConfiguration. Release builds use GalaxyConfiguration with GalaxyBillingMode.PRODUCTION for the Samsung Galaxy Store. A build with no key deliberately leaves the SDK unconfigured, and the UI says purchases are not set up rather than unlocking anything by accident.
  • Offering default. A $rc_lifetime package for rivulet_keeper, which grants entitlement keeper, and a seeds_500 package for the consumable rivulet_seeds_500. The purchase screen is RevenueCat's own PaywallDialog, with Restore purchases. No price is hard coded; the button shows whatever the store formats.
  • Unlocking without a restart. A CustomerInfo listener means Keeper takes effect the moment it lands, and the ad buttons disappear under your thumb.
  • Ad events in the same place as revenue. Every ad displayed, reward earned and failed to load goes through Purchases.sharedInstance.adTracker, so ad activity and purchase revenue sit in one dashboard. Details in the Catvertising section.

How I built it

Two modules, and the interesting one has no Android in it. :engine is pure Kotlin: the board model, the flow fill, the star rules, a solver, the seeded daily generator, and the 24 Journey boards as JSON resources. :app is Jetpack Compose, with the hillside painted in Canvas and ViewModels holding StateFlow. Splitting it that way is why the puzzle logic is testable at all.

Every Journey board is solver verified. The solver runs over each board before it ships, so none of the 24 is impossible and none of them claims a par it cannot actually hit. A level designer who tells you the truth is worth more than a level editor.

97 JVM tests. 33 over the engine, 64 over the app, covering the flow fill, the star rules, the generator, the ViewModels, the daily ad cap and Keeper's separate hint allowance. Roborazzi captures host screenshots at 1179 by 2556, which is how the art got reviewed without a device in hand.

The art is drawn, not assembled. No sprite sheet. Every stone, the water and the terrace are drawn into a Compose Canvas, which is what let the stone tones carry the height information instead of a number printed on the tile.

Challenges I ran into

"Or lower" had to become "the same height or lower". The first rule was strictly downhill, which is tidier to explain and dead on arrival to play: flat ground stopped the water cold and every board turned into a staircase. Allowing water across equal heights made the hillsides feel like water and made the puzzles readable. It also meant re-running the solver over every board, because a rule change invalidates every par you have written down.

The first build looked like an app, not a game. Flat lilac squares and glossy buttons. It worked and it was lifeless. The second pass drew stone as stacked blocks with a lit top face, gave the water a crest and a glow, and painted the terrace by hand. Same code underneath.

Being honest about ad rewards. RevenueCat can verify a rewarded ad server side before granting anything, which is the right way to do it. That needs an ad network account link this project does not have yet, so Rivulet grants the reward client side and reports it to RevenueCat as unverified, using the SDK's own trackAdRewardEarnedUnverified call. Saying so is cheaper than being found out.

Accomplishments that I am proud of

  • A monetisation model where every line answers a specific complaint from the research, and none of them costs the player a puzzle.
  • 24 boards that are proven solvable before anyone downloads them.
  • A game that can be finished standing on a train, one handed, with the radio off.
  • It shipped on 17 September 2026 and has 111 downloads.

What I learned

  • Players do not hate ads. They hate being ambushed. Put the ad behind a button that says what it gives, and most of the objection goes away.
  • A solver cannot tell you whether a board is fun. It will tell you within a second when a board is a lie, which turns out to be most of what I needed while building 24 of them.
  • If the rule needs an "except" to be written down, the rule is wrong. That is the whole reason water crosses equal heights.

What is next

  • The Cliffs and Moonpond regions, with one way sluices and dry wells.
  • Server verified ad rewards through RevenueCat once the ad network account is linked, so the reward is granted from the server rather than the device.
  • Quick Pour on the Flex mode layout. Daily Pour and Journey already have it.
  • A side by side board and terrace layout for windows 600dp and wider.

Built With

  • android
  • jetpack-compose
  • jetpack-windowmanager
  • kotlin
  • samsung-galaxy-store
Share this project:

Updates

Submission history