Grassling

Grassling

Grassling: An AI Pet That Actually Wants You to Leave Your Screen This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass. What I Built I built Grassling, a small Android virtual pet with one unusual goal: it wants you to put your phone down and go outside. I discovered this Hacktoberfest challenge a little later than I would have liked, so Grassling was built under a pretty tight time constraint. That forced me to focus on the core idea first: get a real Android app working, track movement, build a simple game loop, and run a local language model directly on the phone. There are still rough edges, especially around dialogue quality and polish, but the core system works and gave me a chance to experiment with something I had wanted to try for a while: on-device AI on Android. The idea started from the "Touch Grass" theme itself. Instead of building another AI application designed to keep someone chatting with a screen, I wanted the AI to actively encourage the opposite behavior. Grassling has an energy level that slowly increases over time. As that energy rises, its mood changes: Sleeping Normal Anxious Exploding with rage The main way to calm it down is simple: walk. The application tracks the user's steps using Android's step counter. Walking reduces Grassling's energy, while completing outdoor quests reduces it even further. Some current quests are things like: Walk 750 steps Take a 10-minute walk Find something blue outside Find three different plants Spend five minutes outdoors Walk 500 steps without using your phone There is intentionally not much to do by staring at the app. You open it, check on Grassling, accept a quest... ...and then the useful thing to do is leave. Where the AI comes in Grassling runs Gemma 3 270M locally on the Android device. Gemma gives the character its personality. It generates short dialogue based on Grassling's current state and reacts when the user accepts a quest. An anxious Grassling should not sound like a sleeping Grassling, and an extremely energetic Grassling should feel noticeably more desperate to get outside. One important detail is that Gemma currently does not generate the quests themselves. The quests come from a predefined pool and are randomly selected. I deliberately kept the game rules deterministic while using the model for the part where generative AI is actually useful: personality and variation. That separation also makes the application much easier to reason about. The AI can say something weird. It cannot randomly give itself 500 energy points. Because I was working with limited time, this separation also helped a lot. I could focus on making the game loop reliable without depending on the language model for critical application behavior. Code The project is open source: 👉 https://github.com/gbmao/Grassling The Android application is primarily built with: Kotlin Jetpack Compose Android step-counter sensors Foreground Services WorkManager Kotlin Coroutines / Flow C++17 JNI llama.cpp Gemma 3 270M IT Q8_0 / GGUF The application supports arm64-v8a and x86_64. One architectural decision I particularly liked was keeping the game logic separate from the language model. Conceptually, the flow looks like this: Android UI │ ├── Step Counter │ │ │ └── Energy / Mood │ ├── Quest System │ └── Local AI │ └── JNI │ └── llama.cpp │ └── Gemma Enter fullscreen mode Exit fullscreen mode Gemma receives information about the current state. It generates dialogue. But it does not control the state itself. That means energy calculation, walking progress, quest completion, and persistence remain deterministic Kotlin code. How I Built It The application started with the basic virtual-pet loop. Grassling has an energy value from 0 to 100. The value increases automatically over time, reaching maximum energy in roughly 24 hours if the user does nothing. Walking decreases it. Currently, every 11 new steps remove one energy point, while completing a quest provides an additional energy reduction. From there, I added Android's TYPE_STEP_COUNTER sensor and a foreground service so the app could keep tracking walking without requiring the main screen to remain open. Then came the part I was most curious about: running the language model directly on Android. Since I discovered the challenge late, I did not have much time to slowly prototype each part. A lot of the project became a process of: get the smallest version working test it on a real phone find what breaks fix it move to the next part That made the project a little rough around the edges, but it was also a very practical way to learn. Gemma + llama.cpp I used a quantized Gemma 3 270M IT Q8_0 model in GGUF format. Instead of calling a cloud endpoint, Grassling talks to llama.cpp through JNI: Kotlin ↓ JNI ↓ C++ ↓ llama.cpp ↓ Gemma GGUF Enter fullscreen mode Exit fullscreen mode The model is not included inside the APK. On first launch, the app downloads it using WorkManager into the application's private storage. The current model is around 278 MiB. After downloading it, Grassling validates: expected file size GGUF header SHA-256 checksum Only after validation is the model loaded. Once that initial download has completed, inference itself happens locally. No OpenAI API. No Gemini API. No server running the model somewhere else. The phone is actually doing the inference. Running Gemma on a real phone I tested the local inference directly on a Samsung Galaxy A54. For the current quantized model, I measured approximately: Metric Result Time to first token ~212 ms Prompt processing ~222 tokens/s Token generation ~27.5 tokens/s Native model loading ~759 ms That performance was much better than I originally expected from local inference on a mid-range Android phone. The harder problem turned out not to be speed. It was quality. I ran 20 simulated quest-acceptance interactions across the different moods. Every request returned a response, but the quality was inconsistent. Some responses reflected the mood well. Others were generic, repeated part of the quest, or produced something like: "Please allow me to proceed." That taught me something useful very quickly: getting a model to run locally is one problem. Getting a tiny model to consistently produce good character dialogue is another. Given more time, this is probably the part I would iterate on the most. Working with a tiny model Using a 270M-parameter model forced me to think differently about prompting. I cannot simply throw a huge system prompt at it and expect a perfect character. The prompts have to be small and focused. Grassling is given information such as: Mood: ANXIOUS Energy: 75 Quest accepted: Walk 750 steps Enter fullscreen mode Exit fullscreen mode and asked for a very short reaction. Generation is intentionally capped at a small number of tokens. That helped latency, but it also exposed the limitations of the model. Prompt wording has a surprisingly large effect on whether the result feels like a character reaction or just a paraphrase of the quest. That became one of the most interesting parts of the project. The screen should be the shortest part A lot of software optimizes for engagement. More screen time. More notifications. More things to tap. For this challenge I wanted to experiment with the inverse. Grassling is useful precisely when you stop interacting with Grassling. Accept the mission. Put the phone in your pocket. Go walk. Come back later. For once, "user engagement" means getting the user away from the UI. Why Does Open Innovation Matter? Local AI is not just a technical detail in Grassling. It changes what the application can be. With a traditional closed AI API, every interaction would look something like: Phone ↓ Internet ↓ Company API ↓ Model ↓ Internet ↓ Phone Enter fullscreen mode Exit fullscreen mode That creates several problems for this particular idea. First, Grassling is supposed to encourage outdoor activity. Outdoor activity is exactly where connectivity can become unreliable. A character that suddenly loses its personality because there is no signal would defeat part of the idea. With Gemma and llama.cpp, the important path is: Phone ↓ Gemma Enter fullscreen mode Exit fullscreen mode After the initial model download, inference does not depend on a remote AI service. There is also no per-message API cost. And the user's game state does not need to be sent to a third-party inference server just to generate a sentence from a virtual pet. But the biggest advantage for me as a developer was experimentation. I can: change the prompt change inference parameters inspect performance replace the quantization try another compatible model benchmark everything locally inspect llama.cpp change the native integration understand exactly where the model is running The model is not a black box behind an HTTP endpoint. I am still learning how local inference works, and being able to inspect and modify the entire stack made this project much more interesting than simply calling an API. The short deadline actually made this even more noticeable. Because the whole stack was accessible, I could make changes quickly, test them directly on the device, and understand what was happening when something did not work. Open innovation made it possible for someone building an Android project at home to put a real language model inside a phone and experiment with the whole pipeline. That is pretty amazing. Prize Categories Best Use of Gemma Grassling uses Gemma 3 270M IT Q8_0 as the local language model responsible for the virtual pet's dialogue and quest-acceptance reactions. Gemma runs directly on Android through llama.cpp and JNI rather than through a hosted model API. The project also includes real-device benchmarks and experiments around the strengths and limitations of running a small Gemma model locally.

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.