DEV Community

Mark Cantimbuhan
Mark Cantimbuhan

Posted on

GPlay: turning dead phone time into playground time with local Gemma

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

GPlay answers one question fast: "We've got five people, a ball, and a patch of grass — what can we actually play, right now?"

You tell it how many people you have, what's in your bag, and where you are. It filters a library of real-world games down to the handful that genuinely fit those constraints, then uses Gemma (open weights, running locally through Ollama) to write a short, tailored playbook — setup, how to play, how to win — adapted to your group size, equipment, and space.

The goal is to make the screen the shortest part of the experience. Open GPlay, get a pick, read three bullets, walk outside.

It has three views:

  • Home / Coach — describe your group ("4 kids and 2 adults, one frisbee, small yard") and it suggests playable game cards you can tap for rules. Ask it something unrelated and it politely refuses and steers back to games.
  • Games Catalog — the full local library with search and area filters. Every card opens streamed, adapted rules.
  • Scoreboard — pick any game and log scores, with a ranked table per game.

Demo

Run it yourself in two commands:

# local Ollama: ollama serve && ollama pull gemma4:latest
docker compose up --build
Enter fullscreen mode Exit fullscreen mode

then open http://localhost:5173.

Code

GPlay

An open-source, local-first web app for the Hacktoberfest "Touch Grass" challenge Tell it how many people you have, what equipment is on hand, and where you are — it instantly filters matching physical games and uses Google's open-weight Gemma model (via Ollama) to stream tailored rules, setups, and adaptations as Markdown over SSE.

Zero cloud APIs, zero API fees, works fully offline.

Architecture

[ React + Tailwind ] --REST/SSE--> [ FastAPI ] --filter--> [ SQLite games DB ]
                                          \
                                           '--local HTTP--> [ Ollama: gemma4:latest ]

Quick start

Docker

# Uses Ollama installed on your machine (ollama serve + ollama pull gemma4:latest)
docker compose up --build

# No local Ollama? Run everything in containers instead:
docker compose --profile containerized-ollama up --build
Enter fullscreen mode Exit fullscreen mode

Serves the frontend at http://localhost:5173. OLLAMA_URL/GPLAY_MODEL env vars override the defaults (http://host.docker.internal:11434 / gemma4:latest).

Manual

1. Backend

cd server
uv venv && uv pip
…
Enter fullscreen mode Exit fullscreen mode

How I Built It

Stack: React 19 + Vite + Tailwind CSS on the front end, FastAPI on the back end, SQLite for the seeded 29-game catalog, and Ollama serving gemma4:latest for every bit of language generation.

Architecture:

[ React + Tailwind ] --REST/SSE--> [ FastAPI ] --filter--> [ SQLite games DB ]
                                       \
                                        '--local HTTP--> [ Ollama: gemma4:latest ]
Enter fullscreen mode Exit fullscreen mode

A few decisions worth calling out:

The model only does what it is good at. "Which of these 29 games fit 4 people with one frisbee on grass?" is a deterministic filter — so it is plain Python/SQL, not a prompt. Gemma is handed only the games that already match and asked to adapt them. That keeps results fast, predictable, and cheap, and it means a small local model behaves well.

Everything streams. Rules and chat are streamed token-by-token from FastAPI as Server-Sent Events and rendered as Markdown with react-markdown, so the playbook starts appearing immediately instead of after a long wait.

The chat markers never leak. The Coach prompt is restricted to physical games only. When it wants to show game cards it emits a GAMES_PICK: id,id,id line, which the backend strips out line-by-line mid-stream and converts into a structured {"games": [...]} event. The user sees prose plus cards; the plumbing stays invisible.

The model can invent new games — under constraints. A separate endpoint has Gemma return strict JSON for a brand-new game, then validates it before storing: equipment and areas must be from the allowed sets, player counts are clamped, and the slug is de-duplicated. Bad model output is rejected, not saved.

The AI is optional by design. If Ollama or the model isn't running, /api/rules and /api/chat fall back to deterministic, built-in rules, streamed with the same token-by-token behaviour. The app works end-to-end offline — the AI is an upgrade, not a dependency.

Why Does Open Innovation Matter?

This is an app you're supposed to use outside — in a yard, a park, a campsite, the backcountry. That makes the open approach the whole point:

  • It runs with no internet. Inference happens on the machine in front of you. No signal required, no round-trip to a server that might be slow or down. A bird-call-style "works on the trail with no bars" promise, applied to play.
  • No data leaves your machine. A group's location, ages, and habits stay local. There is no vendor seeing which games a school picked or where a run club meets.
  • No API keys, no per-request cost. Open weights mean the app costs nothing to run. A teacher, a parent, or a run-club organiser can clone it and use it without a billing account.
  • The model is swappable. GPLAY_MODEL is an env var. Point it at a different Ollama model, a fine-tune, or a smaller model for a weak laptop — nothing else changes. A closed API would lock both the model and the terms.

Where open beat closed here: GPlay works on a phone in a backyard with no cell service, adapts everything to a group's real situation without sending that situation anywhere, and stays free forever to the people who'd benefit most from it.

Prize Categories

  • Best Use of Gemma — the project's language generation is Gemma via local Ollama: adapted rules, the Coach chat, and constrained new-game generation all run through it.

Team

Solo — @justcallmemarko.

Top comments (0)