DEV Community

Cover image for Ghost in the Machine: reviving a buried 1985, where the die decides and Gemma 7B speaks last
Antonio Pasolini
Antonio Pasolini

Posted on

Ghost in the Machine: reviving a buried 1985, where the die decides and Gemma 7B speaks last

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

Retro-Ghost-85 is a digital board game about the year 1985 — specifically, about the year a teenager learns to write games on a machine that was never designed to be kind.

Forty squares. Four acts. One six-sided die. One to four players in a single browser, with local CPU opponents filling the empty seats. You start with a BASIC listing copied out of a magazine and you end at the day your own game appears in a catalog. In between: the typo you cannot find on line 340, the cassette that refuses to load, the floppy drive that writes when it should read, the modem handshake that sounds like a machine clearing its throat, the BBS at two in the morning, the local user group, the publisher's deadline.

The theme this week is Ghost in the Machine, and I read it literally. The ghost is the machine itself: the personality that emerges from a 1985 home computer when you spend enough nights with it. It is not supernatural. It is the feeling that the thing on the desk is answering you.

The twist is architectural, and it is the whole point of the project:

The die decides. The AI narrates.

Local TypeScript rolls the die, moves the tokens, applies the consequences and decides who wins. The model touches none of it. Each square already has its own authored story. Gemma enters at the moments that matter most — the trophies marking Acts II, III and IV, and the Endgame. It receives selected moments from the player's actual journey and adds one short closing line, in character, like the Master at a table. The AI is there for the milestones; it never takes the die out of anyone's hand.

That constraint is not a limitation I worked around. It is the design.

Demo

Live at jrpasolini.tech/retro-ghost-85 (mirror: retro-ghost-85.onrender.com).

English is the default; the USA / BR switch changes language mid-match without touching the board, the pieces or the random state. A match can be exported to a readable Markdown file and loaded back in the other language — the JSON block is the source of truth, the prose is presentation.

Retro-Ghost-85 — opening screen

The board during the prologue

Code

Retro-Ghost-85

English (default) · Português brasileiro

A retro board game about making your first computer game in 1985: BASIC listings, cassette backups, floppy disks, stubborn sprites, BBS messages, and finally seeing your name in a catalog.

40 squares · 1–4 local players · Four Acts · One Master storyteller

Play on the project site · Render deployment

The die decides. The AI narrates.

The browser rolls one six-sided die, moves tokens, applies consequences, controls local CPU turns, and determines the winner. Gemma never decides gameplay. The first player to reach square 40 wins; project progress is recorded but is not a condition for winning.

The first player to enter each later Act earns that Act's trophy. The trophy records that player's historical journey at the moment of the achievement. Each journey combines approved story beats with a brief closing comment from Google Gemma 7B, served through Cloudflare Workers…

npm test runs 99 tests; npm run build type-checks, bundles and verifies that no root-absolute asset leaked into the subpath build.

How I Built It

The open-weight model. Narration and voice run in a Cloudflare Worker. The model is @cf/google/gemma-7b-it-lora — Gemma 7B, open weights, served through Cloudflare Workers AI, overridable at runtime with a NARRATION_MODEL variable. GET /health reports the model actually deployed, so the claim is checkable rather than asserted:

{"ok":true,"model":"@cf/google/gemma-7b-it-lora",
 "tts":"eleven_multilingual_v2",
 "prompt":"v9-gemma-brief-closing-comment",
 "voiceConfigured":true}
Enter fullscreen mode Exit fullscreen mode

Where Gemma sits in the narration. Every square has an authored beat — written in Portuguese first, then translated — because a board game needs a canonical story that does not change between two matches. Gemma is present at the pivotal moments: the trophies for Acts II, III and IV, and the Endgame. At those milestones, the Worker sends the model selected beats from the journey the player actually lived through, and Gemma 7B adds the Master's closing line — a short remark addressed to that player, referring to what just happened, ideally twelve to twenty-four words. The fixed story gives each match continuity; the AI gives its turning points a personal voice.

So the model performs a relative part of the narration, inside limits that keep it honest: one inference call per uncached request, max_tokens: 96, temperature 0.3, a twenty-second deadline with an AbortSignal, and a hard ceiling of forty words. If the answer is empty, too long, malformed, or mentions numbers or rules it was not given, the Worker rejects it and returns the authored beats alone. The game never depends on inference, and a cache hit costs no neurons at all.

The bug that taught me the most. My first prompt put the instructions in a separate system message. Four consecutive calls returned 72–80 words each, exhausted the 96-token output budget and failed my length validation. I initially thought Gemma 7B simply would not follow the constraint. After reading Cloudflare's prompting documentation, I tested a different structure: a single user message with instructions first, the player's journey second, and a short response cue at the end. The next call returned a 27-token comment in 2.9 seconds. That result mattered for this deployment; it does not mean Gemma 7B or Workers AI forbids a system role. Cloudflare documents scoped prompts using both system and user. A supported message format and a prompt that behaves reliably for a specific task are not always the same thing.

That is the kind of problem you solve by testing prompt behavior instead of assuming one recipe works for every model. The NARRATION_MODEL variable keeps model selection separate from the game's rules; any replacement model still needs its prompt and latency checked.

Voice. POST /tts turns the composed text into audio through the ElevenLabs API (eleven_multilingual_v2, a single configured voice), cached in Workers KV by a key that includes text, language, voice, model and parameters.

The PLAY button in a trophy dialog requests the voice; the cassette button downloads the same MP3. Replay and download never re-synthesize inside a session. The six soundtrack tracks were generated in ElevenLabs Music — one per act, plus opening and ending.

The pipeline. Quality assurance was part of my commit routine, not a final checkbox before submitting. On every push to master, GitHub Actions runs the core verification workflow: npm test (a suite of 99 tests), documentation and license checks, and the TypeScript/Vite production build, including a check that no absolute asset path would break the subpath build. Two additional workflows run when relevant project files change, testing browser behavior in English and Portuguese with Playwright, and gameplay across responsive layouts. These automated checks catch regressions early; they are not presented as mandatory branch-protection rules. No test claims to prove story quality: the model's integration contract, output limits, cache behavior, deadline and fallback are exercised with simulated providers, without making paid inference calls.

A deliberate nod to older Git conventions. Even the repository branch is part of the aesthetic: I intentionally kept master instead of the now more common main. Git itself came much later than 1985, of course, but the name is another small tribute to an earlier era of software development.

Presentation and hosting. Vite, TypeScript and the DOM — no framework. The static build is served by both Render and the .tech domain, and the board is a single image with positioned hotspots, which is why the whole game runs on a phone.

Why Does Open Innovation Matter?

Three principles, each grounded in this project.

1. More people deserve a way in. Open innovation matters because the chance to build should not belong only to people with privileged access to tools or platforms. Open-source code lets students, hobbyists and independent developers read how something works, learn from it, modify it and share their own improvements. Open-weight models widen who can experiment with AI, without a single hosted provider being the only route. None of this makes hardware, inference or maintenance magically free; licenses and technical requirements still matter. But access to the building blocks is a powerful beginning.

2. Freedom is the ability to choose. Proprietary models can be excellent — and, for some tasks, better than open-weight alternatives. Recognizing that does not diminish the value of open innovation. For Retro-Ghost-85, I tested Gemma 4, found its latency unsuitable for this short dialog, and chose Gemma 7B instead. The NARRATION_MODEL setting let me change the deployed model without rewriting the TypeScript game engine; I still had to adapt and test the prompt. Gemma's open weights also preserve options beyond this particular hosted API, subject to its license and the resources available. The point is not that one kind of model must defeat the other. The point is that a developer gets to decide.

3. What we build should be understandable and reusable. The forty squares, rules and authored story beats live in the project's source code, which I am publishing alongside this article. Readers can inspect where the die is rolled, how the winner is chosen, which story beats reach the Worker, and what happens if inference fails. If Workers AI becomes unavailable, the board still plays and the written stories still appear; only the personalized closing line goes quiet. Others can study, contribute to or adapt the original code within its license. Openness does not erase every dependency. It makes it easier for the next person to pick up the work and carry it somewhere new.

A teenager in 1985 might have learned to program by copying a BASIC listing from a magazine and changing one line to see what happened. I like the idea that, in 2026, someone can still begin by opening the code, asking questions, and making it their own.

The Spirit of It

The part I did not expect: people who were there.

I built Retro-Ghost-85 for the feeling of a specific decade, but the first reactions came from people naming their own machine — the MSX in the living room, the cousin who lent the cartridge, the listing typed in from a magazine and never quite working. The game does not emulate hardware and distributes no ROMs or BIOS. What it rebuilds is the ritual: the patience, the transcription, the machine that only speaks when you get every character right.

That is the ghost. Not a spirit haunting a computer — the memory of everyone who ever stayed up late teaching one to talk.

Prize Categories

Each entry below names a category exactly as the challenge lists it, and says only what the deployed project can demonstrate.

  • Best Use of Gemma — Gemma 7B (@cf/google/gemma-7b-it-lora, open weights, served by Cloudflare Workers AI) composes the Master's closing line at the Act II–IV trophies and the Endgame, inside hard token, length and deadline limits, with an authored-text fallback when it cannot.
  • Best Use of ElevenLabs — six soundtrack tracks created in ElevenLabs Music — one per act, plus opening and ending — and on-demand narration through the ElevenLabs TTS API (eleven_multilingual_v2, one configured voice), with a versioned audio cache in Workers KV.
  • Best Use of Render — the static frontend is deployed and served by Render at retro-ghost-85.onrender.com, alongside the .tech domain.
  • Best Use of a .tech Domain — the project's public identity is jrpasolini.tech/retro-ghost-85/; the subpath build is verified against root-absolute asset leaks so the game works from a directory, not just a bare domain.
  • Best Use of GitHub Copilot — Copilot Pro through the VS Code extension. Its contribution is visible in the repository itself, without publishing private prompts or session transcripts. It helped ship the in-board GAME LOG (commit 5b1c3ec, 118 lines added across src/main.ts and src/style.css): a scrollable event log positioned inside the board artwork, with board-relative placement, the newest entry highlighted, role="log" and aria-live for screen readers, and reset at the start of a match. It also helped resolve a merge conflict in src/main.ts by keeping origin/master canonical, verifying file equality by hash before running the test suite, and refining the layout of aside.inspector and .audio-console. The cited GAME LOG commit includes a Co-authored-by: Copilot trailer; the evidence is the committed result, not a private conversation log.

No claims are made about categories that the deployed project cannot demonstrate.

Top comments (0)