Entered for Best Game, the RevenueCat Design Award, the RevenueCat Peace Prize, HAMM and the Gaming influencer award (Mr Lewis Blogs). I put where to look for each at the bottom.

What it does

Palsta is an allotment with six plots, running on a real clock. Grass comes up in a week. A rowan wants most of a season. An oak takes three months to grow up and then stands there for ten years.

You water, you feed and you decide what goes where. That is the whole list of things you can do.

Most of the time you are looking at the home screen. A widget shows whichever plant most needs you, with its water and food, at anything from one cell up to a big tile. It is the only reminder in the app. It does not buzz and it does not turn red. I did not want to write the push notification that tells you your grass is thirsty, so the information just sits there and you look at it or you do not.

Neighbours come from the server. You cannot pick them and they cannot pick you. In someone else's garden there are two buttons, water and feed, and no third one, because the server will not take any other instruction. If a stranger's plot is dry you can fix it and they never find out who did it. A plant that two different people tend on the same day grows half as fast again, so a stranger watering your oak is worth something and not only a nice gesture.

The species are Finnish and they keep their Finnish names, so one of the ferns is a kotkansiipi, which means eagle's wing.

There is also a herbarium. It keeps the seeds you decide never to plant, and the best plants anyone has grown, scored on how long they lived and how well they were kept, with something for the generations of a bred line. Each is kept at its peak and stays there after it dies. It ranks plants and not people, and an entry shows a garden's colour, which is the only identity anybody has here.

How we built it

Five weeks of evenings, one person. I built the renderer first and the game afterwards, which is probably the wrong way round.

The :garden module is plain Kotlin with an empty dependency list. It takes a seed, a species and a growth number and gives back a Drawing, which is a list of paths with fills and depths. Three different things replay that same drawing: Compose in the app, Java2D in a desktop tool I use to render png sheets of every species without starting an emulator, and android.graphics into a bitmap for the widget (RemoteViews cannot draw, and Glance has no shape primitives). The widget renderer was about sixty lines and no botany got copied.

There are no image files in the app at all. Every plant, the icon and the widget are drawn at runtime from vectors. A birch is generated from its own seed, so your birch is not anybody else's birch. Flora Danica by way of the Voynich manuscript is roughly what I was after, roots included.

The server owns the world and the client writes nothing. An hourly Cloud Function tick walks every garden, the client only sends intents through callable functions, and Firestore rules refuse it everything else. The tick is pure TypeScript with no Firestore, no clock and no randomness of its own, so node --test runs the whole simulation with no Firebase project attached. 50 server tests and 21 app tests.

Purchases go through RevenueCat and the server checks each one. The client sends a receipt, claimPurchase asks RevenueCat whether an unspent transaction exists, and every transaction id is redeemed once, so a crash between paying and getting the seed loses nothing and a retry does not make two.

Challenges we ran into

The shop was broken for two days and told me nothing useful about it. Products were live in Play, RevenueCat was linked, and every row in the app said "not yet". The default offering existed, was marked Current and had no packages in it, so getOfferings() handed back an offering with nothing inside. Products are fetched by id now. I also left out the usual fall-through to offerings.current on purpose, because with a Test Store key that serves RevenueCat's own demo offering, and for a while the shop showed three very convincing $0.99 rows that had nothing to do with this game.

Then the balance nearly killed the idea. Half a day away was enough to come back to a dead garden. I measured it instead of guessing: a rare species in sand died in 1.2 days, and a day's water was enough for about two plants, which quietly made the shop compulsory for ordinary play. Halved the moisture decay and the drought damage. Hunger takes about a fortnight now, and three ordinary plants live on one day's water in every soil in the game.

Trees were still impossible after that, and it took a second measurement to see why: a rare plant drank nearly twice as fast and started losing health at moisture 36 instead of 20, two penalties multiplying against a watering can that adds 30. One watering barely cleared the line, so an oak lived permanently a couple of hours from harm however often I watered it, and my nettles were fine. Rarity belongs in the drying rate, once. Maturity is now what it always claimed to be: a grown tree's roots take away 60% of its thirst and half of its wilting point, so it drinks less than grass, one watering lasts it 17 hours, and abandoned it stands for 20 days - while a seedling oak still dies in 2.7 days dry. Hard to establish, then yours. All of it is tests now, run over every species in every soil, because otherwise the next tweak undoes it and I do not notice for a month.

Accomplishments we're proud of

No push notifications anywhere. A game about waiting normally reaches for them sooner or later, and I wanted to see whether a widget could do the same job. Nothing else in the app asks for your attention either.

"Be nice to each other" is an easy thing to put in a store listing. Here the care endpoint takes water and feed and refuses everything else, so there is no chat to moderate and no name to abuse. Admittedly that is also the lazy way to do anti-harassment, since those features simply never got built.

Money buys slower plants. A paid species takes longer than a free one and is not stronger, because one clock runs every plant in the game. Mutations cannot be bought at any price; they come out of the tick's own roll on a plant you kept healthy past maturity, and they breed true down the generations. The two supplies that cost money are also sold for points you earn by tending other people's gardens, so nobody has to pay to keep three ordinary plants alive.

What we learned

Put it on the real device. Three widget bugs were invisible in code review and obvious in a screenshot: an off-by-one that stripped the labels off any ordinary 2×2 tile, a seedling drawn true to scale that came out as a three-pixel speck, and an adjustViewBounds that pushed a bar off the bottom. Play's deprecated-API report was the same kind of thing. Upgrading androidx.activity looked like the fix and it was not, because the non-deprecated edge-to-edge path does not exist yet, so I deleted the call instead.

What's next

Account linking, so a garden survives a new phone. Species that prefer particular soils, since soil already trades water retention against nutrition. A wider neighbourhood once there are more gardens to go round.

The awards

Five categories, and I would rather say where to look than have anybody hunt for it.

Best Game: the loop in What it does, and the numbers behind it in Challenges. Two verbs on a real clock, and both water and food kill in both directions.

RevenueCat Design Award: the renderer in How we built it. No image files anywhere, and the widget is drawn by the same code as the app, so it is your actual plant and not an icon of one.

RevenueCat Peace Prize: the neighbours and the herbarium. The server refuses every instruction except help, which is a smaller claim than most things entered here, but it is a guarantee rather than a rule, so I think it earns its place.

HAMM: money buys slower plants, the supplies are also sold for points, and the server checks every purchase against RevenueCat before anything is planted.

Gaming influencer award (Mr Lewis Blogs): a mobile game with the nagging taken out. One widget, no notifications, no streaks, no energy timers.

Built With

Share this project:

Updates

Submission history