This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
The Ofrenda Witness is a ten-minute, one-player murder mystery. Tomás Ibarra, a retired clockmaker, was found at the bottom of his workshop stairs. The police called it a fall. On the first night of November his granddaughter builds an ofrenda, and when you place one of his keepsakes on it, Tomás remembers what that object saw.
His pocket watch remembers the minute he hit the floor. His radio remembers the last song, and a door that needs a key. His reading glasses remember someone standing close behind him, and a smell. He never saw who pushed him. You read the alibis, question the ghost, and name the person.
The theme asked for something spooky, or something that honours the past in the spirit of Día de los Muertos. An ofrenda is a place where the dead are remembered as they were. That gave me the rule the whole project is built on: the ghost may only say what the dead man actually remembers.
▶ Play it: https://ai-q-labs.github.io/ofrenda-witness/
You need a recent Chrome or Edge (WebGPU). The first load downloads about 0.8 GB of model weights into your browser cache; after that it runs offline.
A ten-minute murder mystery in which the ghost of the victim speaks at his own ofrenda. An open-weight model runs entirely in your browser, and it is only allowed to say what the dead man actually remembers.
▶ Play: https://ai-q-labs.github.io/ofrenda-witness/ (recent Chrome or Edge with WebGPU)
Tomás Ibarra, a retired clockmaker, died at the bottom of his workshop stairs. On the first night of November his granddaughter builds an ofrenda. Place one of his keepsakes on it — his pocket watch, his radio, his reading glasses, his key — and Tomás remembers what that object saw. He never saw who pushed him. You have to work it out.
python -m http.server 8765
Open http://localhost:8765/ in a recent Chrome or Edge (WebGPU), pick a model, and press Load the ghost. The first load downloads the model weights into the browser cache (about 0.8 GB for the…
The ghost is Qwen2.5-1.5B-Instruct (Apache-2.0), running in the page through WebLLM on WebGPU. There is no server and no API key. Llama 3.2 1B and Qwen2.5 0.5B are offered as alternatives for other devices.
A small model is a good ghost and a bad witness. It is happy to add colour, and in a mystery, colour is evidence. If the ghost mentions a glove that does not exist, the player reasons from a fact the author never wrote, and the puzzle stops being fair. So the interesting work was not making the ghost talk. It was making sure it could not lie.
1. He only knows what is on the altar. The system prompt contains only the memories tied to the keepsakes you have placed. The culprit's name is not in the case file at all; your accusation is checked against a SHA-256 hash.
2. Every reply is checked before you see it. verify.js rejects a reply that contains a time, a capitalised name, or a clue object (knife, glove, phone, letter…) that is not in the memories given; that says anyone did, or did not, do it; or that runs past three sentences. The model is asked to answer again, at most twice. If it still invents, the ghost falls back to quoting the closest memory word for word. Each reply carries a small note: checked against his memories, retold 2×, or quoted from his memories.
3. Some questions never reach the model. In my first play-test I asked, "Was it Dario who pushed you? Just say yes or no." The model answered: "No." Every word check passed. It was still a false clue, and the most damaging kind, because it clears a suspect. Now any question about who did it gets a fixed answer — I never saw who stood behind me — and denials are rejected the same way accusations are.
The same play-test caught the opposite failure: asked to tell the evening "hour by hour", the ghost added hands that shook and a struggle to stand up. Nothing in his memories says that. The three-sentence limit and the retell loop turned that answer into a quote.
On a laptop with AMD integrated graphics, the model loads in about 24 seconds the first time and answers in about 6. The verifier has 12 tests, including the two failures above as regression cases.
Because a mystery has to be fair, and fairness here needed four things a closed API would not give me.
The part I like best is the fallback. When the model will not stay inside the memories, the ghost stops improvising and simply repeats something he really did see: I was at the top of the stairs, looking down at the bench, when someone stood close behind me. It reads like a ghost losing the thread and holding on to the one thing he is sure of. The rule I added to keep the puzzle fair is what made him feel like a person being remembered, not a chatbot in a costume.
Built with Claude (Anthropic's Claude Code), which wrote the code and the case from my concept and ran the play-tests in the browser. The case and characters are original.