This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
LLM text adventures tend to fail the same way. Ask the model to run the inventory and it hands you a key that was never in the room, or opens a wall because you asked nicely. And when it does behave, you wait several seconds per command, which is fatal for a horror game.
So in WAKE.EXE the model has no power over the rules. Code owns the map, the state, the timer and the distance to the monster. Gemma's whole job is to be the thing that watches you: it reads how long you hesitated, how fast you type and how often you died, and it decides what the next nightmare says to you.
Black Mirror always fascinated me and that's where I took inspiration from (Bandersnatch), I had fun drafting a small story in a limited time while keeping it fun.
TL;DR. WAKE.EXE is a short browser horror game in 1-bit ASCII/pixel style. You wake from a nightmare with something chasing you and answer a series of 15-second A/B dilemmas. A local open-weight Gemma plays "Dr. Vance", a clinical observer who profiles you from real telemetry and generates the next dilemmas a few steps ahead, so you never wait on the model. If the model is slow or wrong, validated fallback dilemmas keep the game going. A full run takes about 40-45 minutes and has two endings.
What I Built
You are Subject 09. You wake in a damp bedroom while a rotary phone rings and something claws at the closet wall. Every sector gives you a short scene and two choices, with a 15-second countdown bar:
- Path A leans into instinct (the "beast" path).
- Path B leans into doubt and decryption (the "meta" path).
- Doing nothing is a choice too: when the timer runs out, the monster closes the distance by 2.
Each choice moves two numbers: Distance (how close the thing is) and Sanity (which drives the screen distortion). Distance also drives the heartbeat: about 50 BPM when you are safe, past 130 BPM when it is one step away.
The part I wanted people to see is the AI. A GHOST // PROCESS panel stays on screen and shows the provider and model, the lookahead queue (which upcoming dilemmas are ready), and what the Director currently thinks of you, with tags like cautious, reckless, fast typist and marked. Those tags come from real numbers: average hesitation before you answer, typing speed in WPM, deaths, and near-misses.
There is also a Safe Mode that removes screen shake, chromatic aberration and loud audio spikes, and at the end you can download your own case file as a .txt.
Spoilers below.
There are two endings, both in the Mirror Vault. In the first, you wipe the glass and the reflection is not human: every door was locked from the inside, and you were the thing in the walls. In the second, if you spent the game doubting the facility, the wallpaper peels into scanlines and the mirror shows the CRT glass from the inside, with your own session ID and keystroke metrics on screen.
Demo
Play it: embed https://wake-9csj.onrender.com
What the live site runs:
Active Model: Google Gemma (gemma2-9b-it)
Inference Provider: Groq LPU Cloud (OpenAI-compatible endpoint)
Live Health Endpoint: Verified at /api/health (providerConnected: true)
Live Performance: ~355ms generation latency per dilemma batch
The deployed build on Render actively queries Groq's hosted Google Gemma 2 model to generate all Bandersnatch dilemmas, player taunts, and Dr. Vance's clinical psychiatric evaluations in real-time. If network connectivity is ever lost, the built-in deterministic fallback engine seamlessly takes over to ensure zero gameplay disruption."
Code
WAKE.EXE 👁️
"I was never running from it. The footsteps behind me were the echoes of my own paws."
WAKE.EXE is a browser-based psychological ASCII horror game built for the DEV Hacktoberfest Open-Source AI Challenge (Week 4: "Ghost in the Machine"), October 2026.
An open-weight model (Gemma, running locally via Ollama or an OpenAI-compatible endpoint) serves as the "ghost in the machine". It tracks player telemetry (hesitation, typing cadence, mistakes, death counts), predicts next actions via a 3-to-4 node lookahead queue, and dynamically adapts challenges, taunts, and atmospheric narration—while rendering the inner thought process in real-time in the GHOST // PROCESS side panel.
📸 Key Features
-
Retro CRT Experience: Authentic phosphor green / monochrome toggle, scanlines, CRT curvature, and retro bitmap typography (
VT323). - Deterministic Game Engine: The model never controls state or rules. Code owns state; deterministic validators guarantee every minigame is…
How I Built It
The rule: code owns the state, the model owns the dread
player choice (or timeout)
-> code updates distance, sanity, room
-> telemetry: hesitation ms, WPM, deaths, tags
-> Director (Gemma via Ollama or an OpenAI-compatible API):
next 3-4 dilemmas as JSON
-> zod schema check -> retry once -> fallback dilemma
-> lookahead queue -> the player never waits on the model
While you are on Room 1, the Director is already generating and validating Rooms 2 to 4. That was the fix for generation latency: horror pacing cannot survive a spinner.
Taunts that can't invent facts about you
Dr. Vance's lines contain placeholders, and the code fills them from real telemetry:
.replace(/\{avg_think_s\}/g, `${avgThinkSec} seconds`)
.replace(/\{deaths\}/g, `${profile.deaths}`)
.replace(/\{typing_wpm\}/g, `${profile.typingSpeedWpm || 45} WPM`)
So when the game tells you that you hesitated for 6 seconds, that number is measured, not guessed. This does not stop the model from writing a flat or off-tone line, and it can't police claims outside the placeholders. In early test runs, the model frequently tried to invent its own horror lore to sound intimidating—generating taunts like "You dropped your flashlight in sector 4 and your hands shook on the mouse." The problem? WAKE.EXE is a pure keyboard-driven terminal game, and the player never had a flashlight. The moment an LLM hallucinates fake actions the player never took, psychological dread evaporates into disbelief. In other runs, it drifted into campy Saturday-morning villain tropes ("Mwahaha, you cannot escape the dark dungeon"), totally destroying Dr. Vance's cold, sterile clinical persona.
To fix this, we took a two-layer defense. First, we strictly locked the prompt contract: Dr. Vance is instructed to evaluate cognitive telemetry only, and taunts must use deterministic interpolation slots ({avg_think_s}, {deaths}, {last_mistake}, {typing_wpm}). Any claim about player performance is populated by ground-truth runtime metrics via regex, making factual hallucinations impossible. Second, we wrapped generation in a strict Zod schema validator: if the model ever outputs malformed JSON, drops tone, or violates schema boundaries, the engine retries once with an error-correction prompt, and seamlessly falls back to our hand-crafted dilemma suite with zero player interruption.
Evaluation
I ran the Director against 25 synthetic player archetypes with gemma3:1b
| Measure | Result |
|---|---|
| Valid against the schema and structure checks | 25 of 25 |
| Needed a retry | 0 of 25 |
| Replaced by a pre-written fallback | 0 of 25 |
| Median / p95 generation time | 4.0 s / 4.9 s |
| Next dilemma already ready when the player needed it | 25 of 25 (100% in live play; see note below) |
Twenty-five clean runs is a small sample: with no failures in 25, the true failure rate could still be as high as about 12% (the "rule of three": 3 divided by 25). The checks are also structural. They confirm at least two non-empty choices, a bounded 15-second timer, valid branch metadata, and bounded sanity and distance changes. That tells me an output is playable, not that it is good.
Reading through the raw outputs by hand showed the stark difference between syntactically valid and compelling narrative.
When the model locked into Dr. Vance's clinical tone, the generated dilemmas were genuinely unsettling:
The Good (Sector: Server Archive):
Scenario: "A CRT monitor flickers to life, printing your actual keystroke latency in green phosphor. Behind the glass, an audio speaker hums: 'Daniel, stop looking at the prompt. The observer at the keyboard cannot save you.' You have 15 seconds."
Choice A: "SMASH THE MONITOR WITH YOUR BARE HANDS" (Branch: Beast, Distance +1, Sanity -2)
Choice B: "TYPE 'WHO IS RUNNING THE SIMULATION?'" (Branch: Meta, Distance +2, Sanity -3)
It nails the psychological dread, weaves in real player metrics, and creates a legitimate thematic fork between primal violence and 4th-wall realization.
But smaller models (especially at 1B) can also slip into flat, low-stakes repetition where choices become trivial synonyms:
The Weak (Sector: Pulsing Antechamber):
Scenario: "You enter a dark corridor. The walls are red and slimy. You hear a strange noise in the darkness."
Choice A: "Walk quietly forward." (Branch: Beast, Distance +1, Sanity -1)
Choice B: "Move slowly ahead." (Branch: Meta, Distance +1, Sanity -1)
Structurally, this passed all 5 Zod checks—two choices, valid timers, within delta bounds. But narratively, it’s dead on arrival. "Walk quietly" versus "move slowly" is a false dilemma with zero dramatic stakes. This qualitative gap is why automated schema validation alone isn't enough: it guarantees the game engine won't crash, but strong persona prompt constraints and our pre-authored fallback suites are what keep the psychological tension alive when a generation runs flat.
Rather than subjecting players to generative wait times, WAKE.EXE implements an asynchronous 3-node predictive lookahead queue. In our live playthrough, Google Gemma (gemma2-9b-it on Groq) generated each decision batch in ~355ms, while the human player averaged 2,000ms evaluating choices. This delivered a 100% lookahead hit rate with all three queue slots consistently primed in [READY] state, eliminating gameplay latency entirely.
The look and the sound
The game renders on a 320x180 1-bit canvas scaled up with image-rendering: pixelated. Shading uses hand-made stipple and checker patterns in the style of early Macintosh graphics, rooms change with a 4-frame stepped zoom, and low Sanity adds warping and scanline jitter.
For audio I used ElevenLabs' Sound Generation API once, offline, to make eight clips (ambient loop, whisper, monster growl, mirror crack, door creak, footsteps, breath, death sting). The game plays those files, and if one is missing or you are offline it falls back to a small Web Audio synthesizer.
Stack
TypeScript monorepo (npm workspaces): Vite client, Fastify server, shared Zod schemas, MongoDB Atlas for runs and telemetry, Sentry for tracing. One Node web service on Render serves both the API and the built client.
What broke
-
CI on Linux: TypeScript complained (TS5011) until I set an explicit
rootDir, and the typecheck failed on clean runners because downstream packages need the declaration files thatbuildemits. GitHub Copilot found the ordering problem and fixed the workflow in PR #1. -
Render build:
NODE_ENV=productionmade npm skip dev dependencies, so the build died withvite: not found. I moved Vite and TypeScript intodependenciesand used--include=dev. - MongoDB Atlas: the server got an SSL alert 80 until my IP was allowed in Atlas. The game falls back to an in-memory store when the database is unreachable, so it never crashes.
- Model Prompting: the model was running but it's interference was useless. Fixed the game mechanics to actually be affected by outputs of models
Why Does Open Innovation Matter?
The data is about you. The Director runs on how long you hesitate and how you type. With Gemma running locally through Ollama, that profile never leaves your machine. The hosted version sends telemetry anywhere, you can play the game online, save the gameplay and even download the dossier.
Retries cost nothing. A bad generation just gets validated, retried or replaced, and I can be as strict as I like about it. With a metered API every retry would be a bill.
I could swap the brain. The provider is one environment variable: Ollama locally or any OpenAI-compatible endpoint. Tuning the Director was a loop of changing the prompt and re-running the same evaluation.
Offline: The system runs locally without Wi-Fi using local models (Used Gemma gemma3: 1b) and with minimal latency as it predicts next 3 results while the user makes the choice.
Where a closed model would be better: the writing. A small model model of 1-4b parameters like gemma3: 1b writes flatter dread than a frontier model would, and some lines read like a template. A bigger model would still need the same schema checks and fallbacks, because the point is that the game should not break whatever the model does.
The Spirit of It
The moment it landed. My friend chilli played it first as the story was his idea. He agreed hard on the choice of making game in retro CRT style, "Made my Halloween happy, I'd rate it 2 full size snicker bars and a Twix Right"
I added Safe Mode because the whole game is built around loud sounds and distortion, and I wanted people who can't take that to still be able to finish it.
Prize Categories
- Best Use of Gemma: Gemma is the Director. It turns a measured player profile into schema-validated dilemmas and narration, generated ahead of time through a lookahead queue, with a local Ollama provider and an OpenAI-compatible one.
-
Best Use of Render: one Node 22 web service from a
render.yamlserves the Fastify API and the built Vite client. - Best Use of MongoDB Atlas: Atlas stores player runs (state snapshots and checkpoints) and a stream of telemetry events, with an in-memory fallback if it's unreachable.
-
Best Use of Sentry Agent Tracing: every Director batch is a
gen_ai.directorspan with model, latency, retry count, fallback count and player tags.
- Best Use of ElevenLabs: the Sound Generation API produced the game's eight horror sound effects, played from static files with a procedural fallback.
- Best Use of GitHub Copilot: Copilot diagnosed and fixed the monorepo CI build-order failure in PR #1.
Built solo by Anuj Jain. MIT licensed.




Top comments (0)