DEV Community

adricasi
adricasi

Posted on

TCGrass: A real-world Pokédex powered by Gemma 4

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

TCGrass banner:

TCGrass is a collectible card game where the cards only exist outside. Touch grass to discover ’em all

You open the app, point your phone at a real flower, tree or animal and press Capture. An open-weight Gemma 4 vision model looks at the photo and works out which card in the deck it matches. The server then rolls a rarity (Common, Rare, Epic or Legendary) and a hand-illustrated card lands in your binder. Then you put the phone back in your pocket and keep walking.

A dandelion on the way to work, the oak in the park, the neighbour's dog: each one is a card, and a lucky Epic roll turns an ordinary walk into a good pull.

How it gets people off the screen:

  • 📷 Live camera only. There's no photo upload, and Gemma is told to reject photos of screens and printed pictures. You can't play from your sofa scrolling Google Images.
  • ⏳ A 5-hour cooldown per card. Photographing your own dog ten times doesn't pay. To grow your collection you have to go somewhere new and look around, or trade.
  • 📖 A binder with empty slots. The first deck, Everyday Nature, has 15 cards: 5 trees, 5 flowers and 5 animals you can find in any town. Missing cards show as silhouettes, which is a strong reason to go for another walk.
  • 🤝 Friends and trades. Add friends by code, look inside their binders and offer one of your cards for one of theirs. It's a reason to go out together.
  • ⚡ The screen is the shortest part. Point, shoot, see the reveal, done. Everything else happens outside.

Who it's for: anyone who needs a small nudge to go outside. Kids and parents on a walk, friends who want a reason to meet up, people who'd never call themselves "nature people" but love collecting things. And because a deck is just a YAML file and some images, schools, hiking clubs and towns can fork it and make a deck for their own place (more on that below).

Demo

QR code that opens TCGrass at tcgrass.duckdns.org. Scan it with your phone camera to play: free, no app store, no account.

Play TCGrass now

Open it on your phone, allow the camera and go photograph something green. It's an installable PWA, so you can add it to your home screen on Android and iOS. There's no app store, account or email.

In the video:

  1. Snap it: the phone sees a dog, and one tap sends the photo to Gemma.
  2. It becomes a card: Gemma identifies it as Dog, the server rolls Rare, and the card is revealed with a flavor line written by Gemma.
  3. Fill your binder: 15 numbered slots, with silhouettes for what you haven't found yet.
  4. Bring your friends: add Trail Buddy by friend code and peek inside their binder.
  5. Trade: offer the Dog for Trail Buddy's Epic Lavender. They accept, and the swap happens atomically.

Four TCGrass screens: a Legendary Oak card being revealed; the binder showing 6 of 15 cards found, with empty slots for the rest; a friend's binder with

Fun fact: the demo video itself was made with open-source AI too. The narration is Kokoro-82M (Apache-2.0) running locally on my laptop, the screen recording was automated with Playwright, and the soundtrack was composed in code with the Web Audio API.

Taking It Outside

Code

TCGrass: photograph real plants and animals to earn illustrated cards, then trade them with friends

TCGrass

A collectible card game you can only play outside. Photograph a real flower, tree or animal, and an open-weight Gemma model turns it into an illustrated card for your binder. Then trade your finds with friends.

Tests Security License: MIT
Powered by Gemma Deployed on DigitalOcean Hacktoberfest 2026 #hf26challenge Touch Grass

Play now → tcgrass.duckdns.org · How it works · Gemma · DigitalOcean · Run it yourself · Make your own deck · Contributing

QR code that opens TCGrass at tcgrass.duckdns.org. Scan it with your phone camera to play: free, no app store, no account

Contents

Why TCGrass

Collecting cards is fun; scrolling past them is not. TCGrass flips it around: the cards live on your phone, but you can only find them outdoors. A dandelion on the way to work, the oak in the park, the neighbour's cat. Each one is a card, and a rare roll makes an…

MIT-licensed. Each deck has its own licence (the Everyday Nature data is Apache-2.0).

How I Built It

Architecture

📱 Phone (PWA)
   │  HTTPS, one JPEG frame (≤ 1024 px), held in memory only
   ▼
Caddy (Let's Encrypt) ──▶ API (Hono + zod)
                            │  system prompt = deck catalogue + photo
                            ├──────────────▶ Gemma 4 (DO serverless · vLLM · Ollama)
                            │  ◀── {"card_id", "confidence", "is_photo_of_screen", "flavor_line"}
                            │
                            │  one IMMEDIATE SQLite transaction:
                            │  cooldown check → crypto rarity roll → mint
                            ▼
                         SQLite (on a DigitalOcean Volume)
Enter fullscreen mode Exit fullscreen mode

Gemma is the part that looks at the world

Every capture is one Gemma vision call. Gemma decides what you photographed. The server decides everything else.

1. The deck is the prompt. The system prompt is built from the active deck's YAML: its instructions plus one line per card (id — name — identify_hint). Here's a card from Everyday Nature:

- id: oak
  name: Oak
  category: tree
  identify_hint: Broadleaf tree with lobed leaves and/or acorns.
Enter fullscreen mode Exit fullscreen mode

Because recognition comes from data, a forked deck changes what Gemma recognises without a single code change.

2. One OpenAI-compatible call. The photo goes as an image part, and the answer comes back as JSON:

const body = JSON.stringify({
  model: config.inference.model,          // gemma-4-31B-it or gemma4:e4b
  temperature: 0.2,
  response_format: { type: "json_object" },
  messages: [
    { role: "system", content: systemPrompt },   // built from the deck
    { role: "user", content: [{ type: "image_url",
        image_url: { url: `data:${photo.mimeType};base64,${toBase64(photo.bytes)}` } }] },
  ],
});

const response = await fetch(`${config.inference.baseUrl}/chat/completions`, {
  method: "POST", headers, body,
  redirect: "error",                       // a redirect would resend the photo elsewhere
  signal: AbortSignal.timeout(30_000),
});
Enter fullscreen mode Exit fullscreen mode

Gemma answers with this shape:

{ "card_id": "dog", "confidence": 0.94, "is_photo_of_screen": false,
  "flavor_line": "A loyal companion always ready for a walk in the great outdoors." }
Enter fullscreen mode Exit fullscreen mode

3. Model output is untrusted. I treat Gemma's answer like user input. zod restricts card_id to the catalogue ids, so a hallucinated card can never be minted:

export function modelAnswerSchema(catalogueIds: readonly string[]) {
  return z.object({
    card_id: z.enum([NO_MATCH, ...catalogueIds]),
    confidence: z.number().min(0).max(1),
    is_photo_of_screen: z.boolean(),
    flavor_line: z.string().nullish(),
  });
}
Enter fullscreen mode Exit fullscreen mode

After validation there's a fixed decision order: invalid → no card; screen → "Photograph the real thing!"; none → no card; confidence below 0.5 → no card; otherwise → card. Flavor lines are stripped of control and bidi characters, cut to 140 characters and rendered as plain text, because other players see them.

4. Gemma never picks the rarity. That would be an invitation to prompt-inject your way to a Legendary. Rarity is rolled with crypto.randomInt from the deck's published odds (70 / 22 / 7 / 1), inside the same SQLite transaction as the cooldown check and the mint:

export function rollRarity(odds: RarityOdds, random = randomInt): Rarity {
  let roll = random(100);
  for (const rarity of RARITIES) {
    if (roll < odds[rarity]) return rarity;
    roll -= odds[rarity];
  }
  throw new RangeError("Rarity odds do not sum to 100");
}
Enter fullscreen mode Exit fullscreen mode

5. I measured it before building on it. The first task was a spike: scripts/spike-identify.ts runs a folder of labelled photos (catalogue cards, non-catalogue subjects and photos of screens) against any endpoint and reports accuracy, parse rate and latency.

Model and host Correct Parseable Latency p50 / p95
Gemma 4 E4B (gemma4:e4b) on Ollama, laptop 18/18 (100 %), twice 100 % 6.0 s / 14.8 s
Gemma 4 31B (gemma-4-31B-it) on DO serverless, through the live app 18/18 (100 %) 100 % 3.1 s / 3.8 s (tap to result)

I tested DigitalOcean through the live app, feeding each photo into the phone camera and pressing Capture, so it covers the whole path a player uses. All 18 test photos came out right: 13 photos of cards in the deck (12 different cards, two of them lavender), 3 subjects that aren't in the deck (a car, a lamp post and a seagull) and 2 photos of screens. On a deck of 15 very different subjects, both models are at the ceiling; the 31B would only pull ahead on harder decks, like 50 look-alike wildflowers.

The spike also caught a bug early: my first prompt matched the car, the lamp post and the seagull to cards. Two rules fixed it: "only the main subject counts" and "a similar but different kind is none". (The test photos are AI-generated, so real-world accuracy may be a bit lower.)

Privacy: photos are never stored

  • The PWA grabs one frame from the live camera, scales it to at most 1024 px and re-encodes it as JPEG, which also strips EXIF and GPS data.
  • The API checks the real file type from its magic bytes, holds the photo in memory for one request and drops it in a finally block. It's never written to disk, the database or the logs, and a test checks the database, its WAL file and the log file for image data.
  • There are no accounts and no email. A player is a random secret kept on the device and stored on the server only as a SHA-256 hash.

Fair play and a bounded bill

A public app that pays per model call has to protect its budget:

  • At most 5 Gemma calls in flight, and 10 photos per minute per player.
  • Hard daily caps: 3,000 model calls per UTC day overall (about $1/day at most) and 30 per player, reserved atomically in SQLite before the call. Setting the limit to 0 is a kill switch.
  • Atomic trades: a card in an open offer is locked, and the swap happens in one transaction, so a card can't be traded twice.

Built spec-first with an agent

I designed and built TCGrass in about a week, spec-first, with Claude Code as my coding agent and Gemini as a second reviewer. I reviewed every change, which came with tests. Three of the sessions are linked below, from the idea to atomic card trades.

Card art

The 15 illustrations were generated with Google's Gemini app from one shared style_prompt (a watercolour field-guide plate) plus a per-card art_prompt. They're labelled as AI-generated in the deck's licence field and here.

Deployed on DigitalOcean

The whole stack runs on DigitalOcean:

Product How TCGrass uses it
Serverless inference Gemma 4 31B (gemma-4-31B-it) identifies every capture through the OpenAI-compatible endpoint https://inference.do-ai.run/v1. Pay per token, about $0.0003 per capture
Droplet (Ubuntu 24.04, 1 GB) Docker Compose with Caddy (automatic HTTPS) and the API. The container is read-only, runs as non-root and its port isn't published
Volume (10 GB) SQLite in WAL mode, JSON logs and 7 daily backups, so the data survives rebuilding the Droplet
Reserved IP + Cloud Firewall A stable address. Only ports 80/443 are public, and SSH is open only from my IP
Container Registry GitHub Actions builds the image (with SBOM and provenance) and pushes it to DOCR. A systemd agent on the Droplet pulls each release by digest, runs a health check and rolls back automatically if it fails. GitHub never connects to the Droplet
Monitoring + billing alert A second guard on spending, after the app's own daily caps

Why serverless inference rather than a GPU Droplet? I priced it out in ADR 0006. An always-on RTX 4000 Ada GPU Droplet costs about $555/month whether anyone plays or not, and only breaks even above roughly 1.8 million captures a month. Serverless Gemma costs $0 when nobody is playing. With the Droplet and Volume at about $7/month, TCGrass can stay online as a public game after the challenge. The code still supports vLLM on a GPU Droplet for short private sessions: you just change one environment variable.

Why Does Open Innovation Matter?

Open weights are what make TCGrass portable, private when you need it, and affordable.

1. Swap the model, keep the game. Every way of running Gemma speaks the same OpenAI-compatible API, so moving TCGrass between them is configuration, not code:

Where Gemma runs INFERENCE_BASE_URL INFERENCE_MODEL
DigitalOcean serverless https://inference.do-ai.run/v1 gemma-4-31B-it
Your laptop (Ollama) http://localhost:11434/v1 gemma4:e4b
Your own GPU (vLLM) http://<host>:8000/v1 the served Gemma name

I used this freedom during the build. I developed and validated the game against Gemma 4 E4B on my laptop for free, then moved production to Gemma 4 31B on DigitalOcean by changing three environment variables, and re-ran the same spike to confirm accuracy. With a closed API I'd have been locked into one vendor's model, price and uptime from day one.

2. Your photos go only where you send them. Photos of someone's garden, street or pet are personal. Self-host with Ollama or vLLM and a photo never leaves your network: the game works with no third party at all. On the public instance, photos go to one provider under its stated terms, and TCGrass itself keeps none.

3. It's cheap enough to stay free. At about $0.0003 per capture with hard daily caps, a free public game is sustainable. For a school, club or family, an old laptop running Ollama costs nothing per photo.

4. The game content is open too. A deck is plain YAML plus an image folder. Because recognition is driven by the deck's own identify_hints, anyone can fork Everyday Nature into My Campus, Our Allotment or The Lakeside Trail, run the spike against their own Gemma, and play, without touching the code. Open models make this work: you can test your deck against the exact same weights you'll run in production, as often as you like, for free.

5. Even the demo is open. The narration in the video is Kokoro-82M (Apache-2.0) running locally, with no API key and no cost per line.

Did I give anything up by going open? Not on this deck. A frontier closed model might pull ahead on a much harder one, like 50 look-alike wildflowers, but here Gemma scored 100 % on my laptop and on DigitalOcean alike. Open gave me things a closed API couldn't: the same model running offline on a laptop or in the cloud, photos that never have to leave your own machine, and a cost of $0 when nobody is playing.

My Agent Session

I didn't save every session, but these three cover the key steps, from the first idea to the core of the game:

1. Ideating: from "Touch Grass" to a trading card game, choosing Gemma and DigitalOcean.

Ideating "Touch Grass TCG" for the Hacktoberfest Open-Source AI Challenge (Gemma + DigitalOcean)
You

/ideate

Agent

Where are you starting from (Explore / Shape / Compare)? Main goal? Time per week?

You

Idea for this challenge: Hacktoberfest Open-Source AI Challenge Week 1, theme 'Touch Grass' — build something with open-source AI that gets people off the screen and into the world. Tracks: Best Use of Gemma, Best Use of DigitalOcean. Goal: portfolio / learning / Hacktoberfest challenge. Time: 14h total, it finishes next Sunday the 11th.

Agent

Active, ~4.5 days left. Writing quality is weighted most, so plan ~8h build, ~2h outdoors, ~4h write-up. Which outdoor activities do you do, what hardware, which track, which stack?

You

Gardening, hiking/walking, birding. Cloud GPU is fine. Gemma + DigitalOcean. PWA (TypeScript).

Agent

Proposed 5 ideas: (1) Touch Grass Quests — Gemma-generated nature scavenger hunts with vision verification; (2) Walk Debrief; (3) This Week in the Garden (frost dates); (4) Leaf Doctor; (5) Offline bird-call ID (high risk). Recommended #1.

You

Touch Grass Quests, but as an outdoor TCG: Gemma vision verification gives the user a trading card they can exchange with other users, with common/rare/epic rarities and drop ratios.

Agent

Scope questions: trading, offline, accounts, card art.

You

Trading as stretch goal, online only, anonymous device + display name, photo + CSS rarity frame.

Agent

Corrected one pre-mortem claim (DigitalOcean is a featured prize category). Scored 47->51/80, verdict Pivot then Go, with an MVP timeline and kill criteria.

2 messages not shown
You

We need to find other strengths to provide a more creative solution.

Agent

Proposed mechanics: A) open forkable expansions as YAML rulesets; B) weather sets the odds (daily bounties); C) pocket mode + pack opening at home; D) travel log on traded cards; E) living cards that evolve by season; F) field duels; G) narrator voice with open TTS.

You

A, B, C; D/E/F/G planned for later. Minimal QR trading in the MVP.

You

It should be an instant reward: take the photo, get the card with its rarity. Also a daily pack of cards from other players to trade, with online trading instead of in person.

You

No QR trading: one online trading logic, used from a friends section and the daily trading pack. The requester continues; the other user gets a notification and accepts or declines; on accept the trade happens at that moment. Cards are not the user's photos: Gemma identifies the photo and awards the matching catalogue card with a rolled rarity. Start with an easy expansion: trees, domestic animals, flowers.

Agent

Closed-catalogue identification makes Gemma more reliable, makes duplicates meaningful for trading, and lets photos be deleted right after identification (strong privacy 'why open'). Decisions: rarity rolled per capture, in-app inbox with badge, any valid photo counts.

You

Redefine the card art: OpenMoji is too basic, it should be an illustrated image.

Agent

Gemma does not generate images; options: open image model generated once, vintage public-domain plates, or per-capture generation. Style chosen: watercolour field guide; same image per rarity with CSS frame effects.

You

Maybe the forge is not the core, so make it simple: I will generate and add the images manually and with that you will create the cards.

Agent

Next: tonight's checks (serverless image curl, GPU Droplet, 20 photos through gemma4 against the draft YAML, 3 test images, recruit players), then new-project + a short /spec.

2. Spec-driven design: from the idea brief to an approved spec (requirements with IDs, design and ADRs) in one session.

TCGrass: from idea brief to approved spec in one session (Claude Code)
You

/spec

Agent

Mode: New, from the idea brief. The brief (Go verdict for the Hacktoberfest 'Touch Grass' challenge) already covers most of Phase 1, so I drafted product.md straight from it: 6 goals, 7 measurable success metrics, scope + build priority, non-goals, constraints, glossary, open questions and assumptions.

Agent

Phase 1 gate, plus three Phase 2 questions folded in: approve scope? Can a card in a pending offer be offered again? Do offers expire? Is the daily trading pack shared or per player?

You

Users should create an account and log in to save their data. Block cards while pending. No expiry. Per-player pack.

Agent

Asked which login method (username+password, Google/GitHub OAuth, magic link, passkeys) with time and security trade-offs; the user picked Google/GitHub and I started editing product.md.

You

I prefer the other option without login, saving local data.

Agent

Reverted to an anonymous player: the device stores only a secret ID; cards, friends and trades stay on the server because trades need one shared record of ownership. Non-goals now exclude any login, recovery and multi-device sync.

You

yes, go to phase 2

Agent

Wrote requirements.md: 13 requirements in EARS form (expansion loading, anonymous identity, capture via Gemma against a closed catalogue, photos never stored, server-side rarity, instant reveal, binder, friends, 1:1 trades with locks and atomic swap, inbox, daily pack, swappable model endpoint, installable PWA) and 11 quantified NFRs (privacy, p95 latency, ≤ $25 cost, data integrity...).

You

Approve. Friends: request + accept. Camera only, no gallery upload.

Agent

Phase 3: three choices with recommendations. Topology: one small DigitalOcean Droplet with Docker Compose (Caddy + Node API + SQLite), inference on a separate env-switchable endpoint, so the GPU can be off while the app stays up. Backend: Hono + zod + better-sqlite3. Frontend: Vite + Preact.

You

1 Droplet + Compose; Hono + zod + better-sqlite3; Vite + Preact.

Agent

Recorded ADRs 0002-0005 and wrote design.md: components, data model (partial unique index enforces the card lock), API table, identification call with zod-validated JSON restricted to catalogue ids, security design (CSP, rate limits, threat table incl. prompt injection via text in photos), testing strategy, cost estimate, traceability. Then tasks.md: walking skeleton first, 15 tasks, with a cut order.

Agent

Phase 5: ran the spec-reviewer subagent. Verdict READY WITH FIXES, 3 blockers: (1) the build is ~9.5 h vs an 8 h budget with no rule for what to cut; (2) the 'photos never leave our servers' claim contradicted the inference paths (third-party serverless, tunnels that terminate TLS, inference-server request logs); (3) iOS home-screen install doesn't share storage with Safari, so a player could silently lose their cards.

Agent

Fixed what needed no decision: hour estimates, 'minimum submittable' line and explicit cut list; a 'photo path per inference option' table plus an AC requiring self-hosted inference servers to run with request logging off and only over private networks or SSH tunnels; an iOS 'install first' hint and a 'profile not found → create new' recovery; a global cap on in-flight model calls; a reminder that a powered-off Droplet still bills, so the GPU Droplet must be destroyed.

You

DO serverless is OK if the spike picks it (cite DO's terms). Locked cards: hide them. Field test in Europe. Then Gemini review.

Agent

The Gemini script needs a commit, so I made two: the scaffold on main and the spec on a docs/spec branch. Gemini then needed an interactive Google login the sandbox can't do, so the user ran it.

You

Review the tasks' assigned days, I'll start working Friday afternoon.

Agent

Rescheduled everything to Friday afternoon, in the user's time zone. The PDT deadline Sun 23:59 is Mon 08:59 CEST. Friday: walking skeleton (Gemma client + spike, deploy) with a 16:00 checkpoint and a 20:00 kill check. Saturday morning: the minimum entry (capture, reveal, binder). Saturday afternoon: friends and trading. Sunday morning: field test. Sunday afternoon: post and video, submitted by 22:00 CEST.

You

(pastes Gemini spec review: READY WITH FIXES — daily pack can't detect owner changes or persist an empty pack; card-art route hardcoded to .png while placeholders are SVG; capture response schema undefined; friend codes not case-normalised; no way to dismiss the reveal)

Agent

Triage: agreed with all 8 findings. Added a daily_packs table and original_owner_id; the card route now serves PNG or SVG; exact capture response schema; fixed 31-character friend-code alphabet with trim + uppercase; AC-006.5 for dismissing the reveal; replaced ?box=sent with /api/trades/sent.

You

yes, approve the spec and accept the ADRs

You

/project-kit → create all 8

Agent

Created the project kit: a read-only game-invariants-reviewer subagent (photo privacy, model output, server-side rarity, trade state machine, single ownership, player secret); skills tcgrass-add-endpoint, tcgrass-identify-eval, tcgrass-expansion and tcgrass-ops-runbook (prepares infrastructure commands for the human, never runs them); a matching always-on Gemini rule; project rules in AGENTS.md; and permission rules that keep agents away from the author's labelled photos, production data and the infrastructure CLI. The deny rules took effect immediately, so the human now runs the photo spike and the local dev DB moved out of the denied folder.

3. Building the skeleton: from the Gemma identification spike to atomic card trades, with reviews and mutation checks along the way.

TCGrass build: from Gemma spike to atomic card trades, with reviews, mutation checks and a production decision (Claude Code)
You

/next — implement the next spec task (T-001: inference client + identification spike).

Agent

Spec is Approved; T-001 touches >3 files, so I posted a plan first: zod schemas for the expansion file and the model answer, an OpenAI-compatible Gemma client (fetch, 30-s AbortSignal.timeout, JSON output validated against catalogue ids), env config that names bad keys without printing values, and a spike script that turns a folder of labelled photos into an accuracy report.

Agent

Three reviewers (code, security, game invariants) found no blockers. Applied: no redirects (a redirect would resend the photo to another host), https required for remote endpoints, flavor lines cleaned and capped at 140, and a real bug: an empty IDENTIFY_MIN_CONFIDENCE silently became 0, so every match would mint.

You

(pasted spike output) car → dog, lamp post → pine, seagull → pigeon, all at confidence ≥ 0.90.

Agent

The model never says 'none': it forces the nearest card, so the confidence threshold can't catch it. I tightened the prompt (only the main subject counts; look-alike kinds are 'none') and the pigeon hint. Re-run with gemma4:e4b on local Ollama: 18/18 correct twice, 100 % parseable, but p95 14.8 s vs the 8 s target, so the production path stayed open (Q-7).

You

/next (T-002a local skeleton), then T-003 database + seed.

Agent

T-002a: Hono API with CSP and security headers, server-generated request ids, pino logs with a key-name scrubber, a 5 MB limit plus magic-byte check, an in-flight cap of 5; Vite + Preact camera page with on-device downscale. T-003: SQLite (WAL) with all tables, a seed command, card art served by DB lookup only.

Best catch of the security review: a forked expansion could ship images/oak.png as a symlink to the database or .env, and the public card route would have served it. Card images must now be real PNGs inside images/, with a test.

2 messages not shown
You

/next — T-004 anonymous players + auth.

Agent

256-bit secret returned once and stored only as a SHA-256 hash, default-deny bearer auth on every /api route, friend codes with clash retry, 10 sign-ups per IP per hour. Two automated commit reviews followed: the limiter did a full scan per request above 1,000 keys and could be bypassed by rotating IPv6 addresses (now grouped by /64); my first fix then locked out all new players once the table filled, so unseen clients now share an overflow bucket instead.

You

/next — T-005 capture → mint → reveal.

Agent

Rarity rolled server-side with crypto.randomInt from the expansion's odds; mint in an IMMEDIATE transaction; the photo reference dropped in finally. A privacy test scans the real DB file, its WAL and the log file after five different outcomes for the photo bytes; logging the photo on purpose makes it fail.

You

/next — T-006 binder, T-007 friends, T-008 trade offers.

Agent

Binder with silhouettes and per-rarity counts; friend requests with a 10-unknown-codes-per-10-minutes limit (two reviewers found a concurrency race on it; fixed with a re-check after the body is read, proven by a 20-parallel-requests test); trade offers that lock the offered card, backed by a partial unique index.

You
  1. AC-008.10 allow 3 re-sends after cooldown of 24h. 2. add a cap on pending offers per friend to 10.
Agent

Both are spec changes, so I added AC-008.10 and AC-009.8 to requirements.md first, then implemented and mutation-checked them. The author then clarified AC-008.10 (re-send at once up to 3 times, then a 24 h pause) and I revised the spec text, code and tests.

2 messages not shown
You

/next — T-009 accept/decline with atomic swap.

Agent

Accept re-checks both owners, swaps them, marks the offer accepted and fails every other pending offer on either card, all in one IMMEDIATE transaction. A seeded randomized test runs 500 interleaved create/accept/decline/cancel actions among 5 friends and checks after every step: no card created or lost, one owner each, per-player counts unchanged, no double lock, no stale pending offer, terminal states never change.

Prize Categories

  • Best Use of Gemma: Gemma 4 is the core of the game. Every capture is a Gemma vision call: production runs gemma-4-31B-it, local play runs gemma4:e4b, the prompt is built from an open deck, and accuracy was measured on both. How TCGrass uses Gemma
  • Best Use of DigitalOcean: the whole stack runs on DigitalOcean. That's serverless Gemma inference with daily cost caps, plus a Droplet, a Volume, a Reserved IP, a Cloud Firewall, Container Registry with digest-pinned pull deploys and automatic rollback, and monitoring with a billing alert. TCGrass on DigitalOcean

Team Submissions: @adricasi Adrià Casals Espax

Thanks for reading! Now close this tab and go find a Legendary Oak. 🌳

Top comments (4)

Collapse
 
xxejktefwfjk profile image
xxejktefwfjk •

Thấy cái tên "Touch Grass" mà cười, nhưng thực tế bài làm này giải quyết được bài toán thú vị: làm sao đưa LLM ra khỏi hộp chat thành trải nghiệm vật lý, xã hội.

Vài điểm mình thấy值得关注:

Architecture side: Dùng Gemma 4 local/edge thay vì gọi API cloud cho game loop realtime — quyết định đúng. Latency predictable, không lo rate limit khi bunch người cùng chơi outdoor, còn tiết kiệm cost cho hackathon. Mình từng thử chạy Llama 3.1 8B quantized trên RPi 5 cho project tương tự, token throughput ~15 tok/s đủ smooth cho turn-based. Gemma 4 9B trên GPU T4 (DigitalOcean GPU Droplet) thì chắc ngon lành.

Game design: Card game + outdoor = state sync phức tạp. Bạn handle conflict resolution như nào khi 2 player scan QR cùng lúc mà network lag? CRDT hay server-authoritative với optimistic UI?

Prompt engineering: System prompt cho "game master" AI cần chặt chẽ — prevent hallucination rule, enforce JSON schema cho action parsing. Mình hay dùng instructor / outlines để force structured output, giảm parse error xuống <1%.

DigitalOcean App Platform + GPU Droplet: Combo này underrated cho prototype. Spin up GPU instance, deploy container, có managed DB + object storage — không lo infra. Mình dùng cho inference batch job hàng tuần, billing per-second (site: labagent .tech)

Collapse
 
adricasi profile image
adricasi •

Thanks a lot for the thoughtful comment, and for sharing your setup! ~15 tok/s for Llama 3.1 8B on a Raspberry Pi 5 is impressive. Cool to see people pushing open models onto small devices.

A couple of details on how TCGrass ended up:

  • Where Gemma runs: I started with Gemma 4 E4B on Ollama on my laptop, but for the public version I moved to Gemma 4 31B on DigitalOcean’s serverless inference. A GPU Droplet would have cost ~$555/month even with nobody playing, while serverless costs nothing at idle. Since every path speaks the same OpenAI-compatible API, switching back to a local model is just three environment variables.

  • Conflicts: There’s no QR scanning. You photograph a real plant or animal and Gemma matches it to a card. But your question still applies to trades: the server is authoritative, and every capture and trade runs in a single SQLite transaction. A card you’ve offered is locked until the offer is resolved, and accepting a trade re-checks who owns both cards, swaps them, and cancels any other pending offers on them, so the same card can never be traded twice.

  • Structured output: Same instinct as yours! Gemma isn’t a free-form game master here: each capture sends the photo plus the deck’s 15 cards, and Gemma returns one small JSON object with the matching card (or none), its confidence, whether it’s a photo of a screen (so you can’t play from Google Images), and a flavor line. JSON mode plus zod validation only accepts card ids from the deck, so a hallucinated card can never be minted, and the server rolls the rarity, never the model. I’ve used instructor/outlines in other projects, but didn’t need them here: the answer is tiny, every response in my tests parsed, and a bad one just means “no card here, try again”. Do you find constrained decoding noticeably better?

Collapse
 
drakitus98 profile image
Marc •

I’ve got a fruit tree and a cat. Let’s gooo!
Who wants to trade???

Collapse
 
adricasi profile image
adricasi •

Love it! 🙌 Fruit tree + cat is a solid opening hand. Add me, my friend code is EJUUPS4C. Send a request and let’s trade!