DEV Community

Cover image for WILDCASE: The World Is the Case File — An Outdoor Mystery Game That Gets You Off Your Screen
Himanshu Gangwar
Himanshu Gangwar

Posted on

WILDCASE: The World Is the Case File — An Outdoor Mystery Game That Gets You Off Your Screen

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

WILDCASE: The World Is the Case File 🔍🌳

What I Built

WILDCASE is an outdoor mystery-investigation game that turns a walk in the park into a detective case. You receive a classified dossier, put your phone in your pocket, and go outside. You hunt for real physical evidence (a metal gate, weathered bark, brick masonry, a circular fastener), take a 2-5 second scan to verify it, eliminate suspects, and finally name the culprit.

Most "get outside" apps pull you away from your phone and still end up glued to a screen. WILDCASE inverts that. The phone is an intermittent field instrument; the world is the game board.

How it gets people off the screen:

  • Field Mode: an ultra-minimal dark HUD with a single breathing sonar indicator. It is built to be ignored, so the phone stays in your pocket.
  • Away Ratio: using the Page Visibility API, WILDCASE measures the share of your session spent away from the screen and shows it in your final Field Report (for example, 87.4% Away).
  • Real objects as clues: progress depends on noticing materials, textures and shapes in your actual surroundings.

Who it's for: students and commuters, families on weekend walks, casual mystery fans, and anyone who wants a reason to look up from their screen.

The Player Journey

┌──────────────┐   ┌───────────────┐   ┌────────────────┐   ┌───────────────────┐
│ CASE BRIEFING│──▶│ PHONE IN      │──▶│ WALK OUTDOORS  │──▶│ OBSERVE SURROUND- │
│ (dossier)    │   │ POCKET        │   │ (Field Mode)   │   │ INGS              │
└──────────────┘   └───────────────┘   └────────────────┘   └─────────┬─────────┘
                                                                      │
                                                                      ▼
┌──────────────┐   ┌───────────────┐   ┌────────────────┐   ┌───────────────────┐
│ FIELD REPORT │◀──│ VERDICT       │◀──│ ACCUSATION     │◀──│ DISCOVER PHYSICAL │
│ (Away Ratio) │   │               │   │                │   │ EVIDENCE          │
└──────────────┘   └───────────────┘   └───────▲────────┘   └─────────┬─────────┘
                                               │                      │
                                       ┌───────┴────────┐   ┌─────────▼─────────┐
                                       │ NEW LEAD       │◀──│ BRIEF SCAN (2-5s) │
                                       │ UNLOCKED       │   │ + AI INTERPRETATION│
                                       └────────────────┘   └───────────────────┘
Enter fullscreen mode Exit fullscreen mode

Demo

🔗 Live demo: https://wildcase-web.onrender.com

It is a PWA, so open it on your phone and add it to your home screen. It also works fully offline.

Code

GitHub logo Himanshu0250 / WILDCASE

AI-powered outdoor mystery investigation game — the world is the case file.

WILDCASE

"The world is the case file."
An outdoor mystery investigation game engineered to get players off screens and into the physical world.

Hacktoberfest 2026 License: MIT Gemma 2 PWA Ready


Live Demo

[click here] https://wildcase-web.onrender.com

What is WILDCASE?

WILDCASE is an outdoor detective adventure where the player's physical environment is the game board and their mobile device is an intermittent field sensor instrument.

Instead of sitting in front of a screen, players receive a classified mystery dossier, put their phone away in their pocket, walk outdoors through parks or streets, observe real physical objects (metal gates, weathered bark, brick masonry, circular fasteners), verify them on-device, eliminate suspects, and deduce the true culprit.

CASE BRIEFING ➔ PUT PHONE IN POCKET ➔ WALK OUTDOORS ➔ OBSERVE SURROUNDINGS
      ➔ DISCOVER PHYSICAL EVIDENCE ➔ BRIEF SCAN (2-5s) ➔ AI INTERPRETATION
            ➔ UNLOCK NEW LEAD ➔ PHONE AWAY ➔ ACCUSATION ➔ VERDICT ➔ FIELD REPORT

The Architectural Invariant

$$\text{Device Senses Descriptors} \longrightarrow…

The repo is a pnpm monorepo (apps/, packages/, docs/, fixtures/, scripts/), MIT licensed, with documentation covering the vision pipeline, evidence system, case design, privacy and offline behavior. Run it locally with pnpm install && pnpm dev. It needs no API keys to start.

How I Built It

WILDCASE is built on one architectural rule:

Device senses descriptors → Core engine matches predicates → AI interprets context.

Layer Responsibility Tech
On-device vision Extracts statistical descriptors (metal, rough, vertical) from a short camera scan. No photo ever leaves the device. HTML5 Canvas, Sobel gradients
Deterministic core (@wildcase/core) Holds the immutable ground truth, unlocks clues progressively, and eliminates suspects by evaluating predicates. TypeScript state machine
AI Game Master Interprets verified evidence and writes atmospheric dispatches. It narrates; it never decides what is true. Gemma 2 (open-weight) via Mastra workflows
API Case delivery and orchestration. Hono
Client Installable, offline-first app with local persistence. React 19, Dexie (IndexedDB), Service Worker
Audio Procedural shutter clicks and stamp impacts, plus optional voiceover. Web Audio, ElevenLabs / Web Speech

System Architecture and Trust Boundaries

The key design decision is who is allowed to decide what. Pixels never leave the device, the engine alone decides truth, and the AI only writes the story.

 PHYSICAL WORLD
      │  camera frames (never uploaded)
      ▼
┌──────────────────────────── DEVICE (PWA / React 19) ────────────────────────────┐
│                                                                                 │
│  ┌────────────────────────┐        ┌───────────────────────────┐                │
│  │ On-device Vision       │        │ Page Visibility Tracker   │                │
│  │ Canvas → color,        │        │ screen time vs away time  │                │
│  │ roughness, Sobel edges │        └─────────────┬─────────────┘                │
│  └───────────┬────────────┘                      │                              │
│              │ descriptors only                  │                              │
│              ▼                                   ▼                              │
│  ┌────────────────────────────────────────────────────────────┐                 │
│  │            Investigation State Machine (@wildcase/core)    │                 │
│  │   predicate matching • clue unlocking • suspect alibis     │                 │
│  └───────────┬───────────────────────────────┬────────────────┘                 │
│              ▼                               ▼                                  │
│  ┌────────────────────────┐     ┌──────────────────────────┐                    │
│  │ Dexie / IndexedDB      │     │ Deterministic Fallback   │                    │
│  │ offline cache + sync   │     │ Narrator (no network)    │                    │
│  └────────────────────────┘     └──────────────────────────┘                    │
└──────────────────────────────────────┬──────────────────────────────────────────┘
                                       │ verified facts + case text only (online)
                                       ▼
                         ┌───────────────────────────┐
                         │ WILDCASE API  (Hono)      │
                         └─────────────┬─────────────┘
                                       ▼
                         ┌───────────────────────────┐
                         │ Mastra Workflow Engine    │
                         └─────────────┬─────────────┘
                                       ▼
                         ┌───────────────────────────┐
                         │ Gemma 2 (open-weight)     │
                         │ → dramatic dispatch text  │
                         └───────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Evidence Verification Flow

What happens in the 2-5 seconds when a player scans an object:

        ┌────────────────────┐
        │  Player scans item │
        └─────────┬──────────┘
                  ▼
        ┌────────────────────┐   no    ┌──────────────────────────┐
        │ Frame quality OK?  │────────▶│ "Hold steady / more light"│──┐
        │ (blur, brightness) │         └──────────────────────────┘  │ retry
        └─────────┬──────────┘                                       │
                  │ yes                                              │
                  ▼                                                  │
        ┌────────────────────┐                                       │
        │ Extract descriptors│◀──────────────────────────────────────┘
        │ metal / rough /    │
        │ vertical ...       │
        └─────────┬──────────┘
                  ▼
        ┌────────────────────┐   no    ┌──────────────────────────┐
        │ Predicate matches  │────────▶│ Evidence rejected,       │
        │ a clue in the case?│         │ no state change          │
        └─────────┬──────────┘         └──────────────────────────┘
                  │ yes
                  ▼
        ┌────────────────────┐
        │ Core eliminates    │
        │ suspects, unlocks  │
        │ next lead          │
        └─────────┬──────────┘
                  ▼
        ┌────────────────────┐  online   ┌──────────────────────────┐
        │ Narrate result     │──────────▶│ Gemma 2 via Mastra       │
        │                    │           └──────────────────────────┘
        │                    │  offline  ┌──────────────────────────┐
        │                    │──────────▶│ Deterministic narrator   │
        └────────────────────┘           └──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key Code

The snippets below are simplified illustrations of the pattern each layer follows, trimmed for readability. The full implementations live in the repo.

1. On-device descriptors: no photo leaves the phone

Each frame is reduced to a few numbers (color, roughness, edge direction) in memory, then discarded.

type Descriptor = "metal" | "rough" | "smooth" | "vertical" | "horizontal";

export function extractDescriptors(img: ImageData): Descriptor[] {
  const { data, width: w, height: h } = img;

  // 1. Grayscale
  const gray = new Float32Array(w * h);
  for (let i = 0; i < w * h; i++) {
    gray[i] = 0.299 * data[i * 4] + 0.587 * data[i * 4 + 1] + 0.114 * data[i * 4 + 2];
  }

  // 2. Sobel gradients: edge strength (roughness) + dominant direction
  let magSum = 0, gxSum = 0, gySum = 0;
  for (let y = 1; y < h - 1; y++) {
    for (let x = 1; x < w - 1; x++) {
      const i = y * w + x;
      const gx =
        -gray[i - w - 1] + gray[i - w + 1] - 2 * gray[i - 1] + 2 * gray[i + 1] - gray[i + w - 1] + gray[i + w + 1];
      const gy =
        -gray[i - w - 1] - 2 * gray[i - w] - gray[i - w + 1] + gray[i + w - 1] + 2 * gray[i + w] + gray[i + w + 1];
      magSum += Math.hypot(gx, gy);
      gxSum += Math.abs(gx);
      gySum += Math.abs(gy);
    }
  }
  const roughness = magSum / ((w - 2) * (h - 2));

  // 3. Map statistics to a small, shared vocabulary
  const out: Descriptor[] = [roughness > 40 ? "rough" : "smooth"];
  if (gxSum > gySum * 1.4) out.push("vertical"); // strong horizontal gradient = vertical edges
  if (gySum > gxSum * 1.4) out.push("horizontal");
  return out;
}
Enter fullscreen mode Exit fullscreen mode

2. The deterministic engine: truth is code, not a prompt

Suspects are eliminated by evaluating predicates against immutable case data. The same input always gives the same result, which is what makes the game testable.

interface Suspect {
  id: string;
  /** Descriptors that must NOT be present at the scene if this suspect is guilty */
  alibiContradicts: Descriptor[];
}

interface Clue {
  id: string;
  requires: Descriptor[]; // predicate: all must be observed
  unlocks: string[];      // next clue ids
  eliminates: string[];   // suspect ids ruled out by this evidence
}

export function applyEvidence(state: CaseState, observed: Descriptor[]): CaseState {
  const matched = state.caseFile.clues.filter(
    (c) => !state.found.has(c.id) && c.requires.every((d) => observed.includes(d)),
  );

  return matched.reduce<CaseState>(
    (s, clue) => ({
      ...s,
      found: new Set(s.found).add(clue.id),
      suspects: s.suspects.filter((x) => !clue.eliminates.includes(x.id)),
      available: [...s.available, ...clue.unlocks],
    }),
    state,
  );
}

export const solved = (s: CaseState) => s.suspects.length === 1;
Enter fullscreen mode Exit fullscreen mode

3. The AI narrates, with a safe fallback

The model receives only verified facts and is told to add atmosphere, not new evidence. If it is unavailable, the game keeps working.

export async function narrate(event: VerifiedEvent, ctx: CaseContext): Promise<string> {
  try {
    const res = await gameMaster.generate({
      system:
        "You are a noir detective narrator. Use ONLY the facts provided. " +
        "Never invent clues, suspects or outcomes. Reply in 2-3 sentences.",
      input: JSON.stringify({ facts: event.facts, tone: ctx.tone }),
      signal: AbortSignal.timeout(4000),
    });
    return res.text;
  } catch {
    // Offline, rate-limited or timed out: deterministic template keeps the game playable
    return fallbackNarrator(event, ctx);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Measuring the Away Ratio

The Page Visibility API turns "did you actually go outside?" into a number.

export function createAwayTracker() {
  let awayMs = 0, screenMs = 0;
  let last = performance.now();
  let hidden = document.hidden;

  const flush = () => {
    const now = performance.now();
    (hidden ? (awayMs += now - last) : (screenMs += now - last));
    last = now;
  };

  document.addEventListener("visibilitychange", () => {
    flush();
    hidden = document.hidden;
  });

  return {
    ratio: () => {
      flush();
      const total = awayMs + screenMs;
      return total ? awayMs / total : 0; // e.g. 0.874 → "87.4% Away"
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Why Does Open Innovation Matter?

An outdoor game has constraints a closed API handles poorly:

  • Connectivity: parks, trails and old neighborhoods often have weak or no signal. Open-weight models and an offline-first architecture mean the experience doesn't depend on a live call to a vendor.
  • Privacy: because the model only sees abstract descriptors and case text, never images or locations, we can make a real zero-surveillance promise. That matters for a camera-based app that kids and families might use.
  • Cost and access: open weights let anyone self-host or run a small model without per-request fees, which matters for a free community game.
  • Community cases: the game's content is data. Because the engine, case format and docs are open (CASE_DESIGN.md), anyone can author a mystery for their own town, school or campus. That is the natural Hacktoberfest contribution path.

A closed API could have written the narration. Open innovation is what lets this run offline, stay private, cost nothing to scale, and be extended by people who live where the game is played.

Prize Categories

WILDCASE enters the categories below. Each one is a load-bearing part of the architecture, not a bolt-on, and each is tied to a specific job in the system.

Category Prize tier Its job in WILDCASE
Best Use of Gemma Featured The open-weight model behind the AI Game Master
Best Use of Render Featured Hosts the live PWA and the Hono API (render.yaml in the repo)
Best Use of Mastra Partner Workflow engine that orchestrates the narration pipeline
Best Use of ElevenLabs Partner Voice for the detective's field dispatches

Use of Gemma

Gemma 2 is the AI Game Master: it turns verified evidence into atmospheric, noir-style dispatches. The design makes an open-weight model the right choice rather than just an acceptable one:

  • It only sees abstract descriptors and case text, never photos or locations, so a self-hosted or locally served Gemma keeps the zero-surveillance promise intact.
  • It narrates, it never judges. The deterministic core decides what is true; Gemma only writes the voice, which keeps the mystery fair and the model swappable.
  • It can be replaced or run offline. Because the model is open-weight and sits behind a narrow interface, the game degrades to deterministic fallback narrators when there is no network.

Use of Render

The live demo runs on Render at https://wildcase-web.onrender.com, defined by a render.yaml blueprint in the repo, with the web app and the Hono API deployed as separate services. The deployment steps and environment reference are documented in docs/PRODUCTION_SETUP.md, so anyone can fork the repo and deploy their own copy.

Use of Mastra

The API hands every verified evidence event to a Mastra workflow, which assembles the grounded prompt, calls Gemma 2, and returns the narration. Using a workflow engine instead of an ad-hoc fetch gives the model call a clear, testable shape: verified facts in, constrained narration out, with a defined failure path to the offline narrator.

Best Use of ElevenLabs

Narration is delivered as voice dispatches, so a player can keep the phone in their pocket and listen to the investigation while walking, which is the core of the "touch grass" idea. When ElevenLabs is unavailable, the app falls back to the browser's Web Speech API, so the experience still works offline.

Top comments (0)