DEV Community

Aditya Sorathiya
Aditya Sorathiya

Posted on

Your Screen Can Have One Job: Get You Outside

I Built a Game That Wants You to Stop Playing It

We've spent the last decade getting really, really good at making apps that keep you staring at your phone.

Infinite scroll, autoplay, streaks that punish you for leaving. Everything is optimized for time-on-screen.

So I had a dumb thought: what if I built one that does the opposite?

The phone gives you the quest, but you have to look away from the phone to finish it. If you keep staring at the screen, you lose.

That's how NatureMate — Touch Grass Edition started.

Live demo: https://naturemate.netlify.app
GitHub: https://github.com/sorathiya903/nature-mate/

The weird idea

The core loop is intentionally backwards.

Most games are: screen -> eyes on screen -> reward.

NatureMate is: screen -> eyes OFF screen -> reward.

I open the app and it says:

Find a bench outside.

That's it. That's the quest.

I can't tap the bench. I can't buy the bench. I have to physically get up, go outside, and find a real bench in the real world. The phone becomes the quest giver, and the street becomes the game board.

It's a small shift, but it changes how you walk. You start noticing things. "Oh, there's a fire hydrant. I didn't know we had one on this street."

So what do you actually do?

It’s stupidly simple, which is why it works on mobile.

  1. You get one quest: Find a bicycle outside.
  2. You go outside.
  3. You point your camera at a bicycle.
  4. You press Capture & Verify.
  5. On-device AI checks that one frame and says yes or no.

If it says yes:

✓ THING FOUND! bicycle detected — 84% — +50 XP +16 🌱

You get XP, coins, your streak goes up, and that object is gone forever. You’ll never get “bicycle” again in this run.

If it says no:

Not quite... I couldn't confidently find a bicycle.

No life lost. You just try again from a better angle.

That’s the whole game. One quest at a time. 43 possible things to find. No repeats.

The interesting technical part

I didn't want to send camera frames to a server. That feels creepy for a game that asks you to point your camera at the real world, and it would be laggy outdoors.

Everything runs in the browser with Transformers.js.

The model is Xenova/yolos-tiny — a tiny YOLOS object detection model quantized to q8 so it actually loads on a phone. It's about 24MB, cached after the first load.

detector = await pipeline("object-detection", "Xenova/yolos-tiny", {
 dtype: "q8",
 progress_callback: handleProgress
});

Enter fullscreen mode Exit fullscreen mode

The most important rule I set for myself:

CAMERA PREVIEW ≠ AI INFERENCE

The video element is just a preview. No detection runs while you’re walking around. Only when you press Capture does it do this:

canvas.drawImage(video, 0, 0, w, h);
const dataUrl = canvas.toDataURL("image/jpeg", 0.65);
const results = await detector(dataUrl, { threshold: 0.20 });

Enter fullscreen mode Exit fullscreen mode

One button press = one frame = one inference. That’s it. It keeps the phone cool, saves battery, and feels intentional — you’re choosing to ask the AI “is this it?”

YOLOS is trained on COCO, so it can actually detect 80 real things. I limited the game to 43 that make sense outdoors and that the model can reliably see: bench, bicycle, car, person, dog, chair, bottle, potted plant, bus, truck, motorcycle, bird, cat, backpack, umbrella, frisbee, skateboard, traffic light, stop sign, kite... you get the idea.

I really wanted “tree” but COCO doesn’t have tree. So potted plant is the closest we get. I kept the display names separate from detector labels so I don’t lie about what the AI is checking:

{ id: "potted plant", detectorLabel: "potted plant", title: "plant" }

Enter fullscreen mode Exit fullscreen mode

If the model says bench with >30% confidence, you pass. If not, you don’t. No faking.

Why lives and no repeats?

Early versions felt like a demo, not a game. You could just spam “skip” until you got an easy one.

So I added constraints that make it feel like an adventure:

  • 3 lives. Shown as SVG hearts. Skipping a quest costs exactly one life. Failing detection costs zero.
  • No repeats, ever. Found it? It goes into foundObjects. Skipped it? Into skippedObjects. Both go into usedObjects. The next quest is always OBJECTS.filter(o => !usedObjects.has(o.id)). Guarantee.
  • 43 unique targets. So you can actually “complete” it. Progress is 12 / 43 FOUND, not an endless loop.
  • XP, coins, streak. Small dopamine hits, but the real reward is you went outside.

The no-repeats rule changed everything. It stopped being “find a chair again” and started feeling like a collection journal. You’re not grinding, you’re clearing the map.

Touch Grass Run

This is my favorite mode.

You press 🏃 Touch Grass Run and normal mode pauses completely. Separate state, separate timer, no contamination.

You get 6 random targets and 10 minutes:

10:00 — Find: bench, car, dog, bottle, kite, bus

Same camera, but now it’s checking against the Run target, not your main quest. One capture can knock out multiple Run items if they’re in the same photo. Find all 6 before the timer hits zero and you get a little arcade-style win screen.

When Run ends — whether you win, time out, or hit Exit — your normal adventure is restored exactly where you left it. No lives lost, no objects marked as found unless you actually found them in normal mode.

Building that isolation was the hardest bug hunt. At first Run was modifying foundObjects and people would lose quests permanently. Now it’s state.runMode boolean: if true, Capture goes to the Run path, Skip is disabled.

Building it wasn’t completely straightforward

A few things that actually bit me:

1. Mobile browsers are picky. iOS Safari will kill your camera stream if you don’t playsinline and muted, and it hates multiple getUserMedia calls. One stream, reuse it.

2. Model loading UX. On 4G, 24MB feels big. I had to build a real progress bar from progress_callback — loaded/total per file — instead of fake percentages. The steps “Loading AI engine → Downloading model → Caching → Preparing camera → Adventure ready” actually reflect real events.

3. Stale state. Capture is async. If you generate a new quest while inference is running, you could compare results against the new quest. Fix: capture the quest at the start:


const questAtCapture = state.quest;
//... await detector()
if (state.quest !== questAtCapture) return; // game was reset

Enter fullscreen mode Exit fullscreen mode

4. Busy lock. Without a busy flag, people double-tap Capture and get double XP. Now it’s if(busy) return + try/finally so the button never stays stuck.

None of these are groundbreaking ML problems. They’re just the unsexy mobile web problems that make a demo feel like a real product.

The part I like most

Most apps compete to keep you.

NatureMate uses 30 seconds of your attention to get rid of you.

The best session isn’t “I played for 2 hours.” It’s:

“I opened it, got ‘Find a bird’, went to the balcony, actually saw a bird, and closed the app.”

The longer you play, the less you should look at this page. That’s the point.

I built it because I wanted a reason to walk that wasn’t “close your rings” or “10k steps.” A little quest that makes you look up.

Try it

If you’re curious:

Live demo: https://naturemate.netlify.app
Repo: https://github.com/sorathiya903/nature-mate/

No account, no backend, no upload. Just open it on your phone, give it camera permission, and give it a quest.

Go outside. See if the AI agrees with you.

Top comments (0)