DEV Community

Cover image for Scary Truc: a stranger calls you from inside a maze, and your voice is all they have
Manuel Alejandro Hermoso
Manuel Alejandro Hermoso

Posted on Edited on AI-assisted

Scary Truc: a stranger calls you from inside a maze, and your voice is all they have

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

It's past midnight and your phone is ringing. Unknown number.

You answer, and someone is whispering on the line. They woke up somewhere they shouldn't be. They don't know how they got there. The people who came with them are gone, and you're the only one who picked up.

You can't see where they are. You have their voice, a battery that is running out, and a notebook where you draw the maze from what they tell you. The pencil fades. Something else is down there with them. It has no eyes, and it finds them by how they sound. So you talk them through it, left, right, hide, don't breathe, and you keep them calm, because fear is loud.

Scary Truc is that phone call. Gemma plays the person on the line, and it also plays the thing hunting them, which sometimes gets on the phone and speaks to you:

They still bleed for the mistake you called a plan.

Nobody wrote that line. Gemma did, during a call.

There are no buttons to choose from. You talk to them the way you'd talk to a terrified stranger, and Gemma reads how you said it as well as where you said to go. A kind word steadies them. Shouting makes them stumble, refuse, or run down a corridor you never chose. Ask them for a joke and they'll wonder who you really are. Go quiet and they'll beg you to say something, and then they'll choose for themselves, badly.

And they don't know you. Yours is the only number in their phone, and they never saved it. Every so often they stop and ask who you are, or whether you're the one who put them there, and they won't take another step until you answer.

There are six calls, each a hand-made maze after a horror film: The Cave after The Descent, The Woods after The Blair Witch Project, The Cube after Cube, The Tape after Ring, The Hedge after The Shining, and The Ship after Alien. At midnight a bell tolls, the walls move, and what's in your notebook becomes a memory. When the line goes dead, the phone tells you how many steps they were from the way out.

One of my calls in The Cave, in Spanish, ended like this:

I went through the door. I can see the sky. I'm out. Thank you. Who are you?

Demo

Answer the phone. It runs in the browser, and it's best on a phone, with headphones on and the lights off. Tap Talk and speak out loud. It works in English and Spanish.

Play it with Gemma here plays with Gemma, no setup needed (the word is pumpkin). It works until the credit I put on it runs out. Without it, the game still runs, but the caller only knows a few words. To get the real thing, add your own OpenRouter key or your own Ollama under Model on the lock screen.

A call in The Hedge: the phone unlocks, the player taps Talk and speaks, the caller stops to ask if there is a person on the phone, midnight turns the screen dark, the Hollow speaks on the line, and the caller is taken mid-word

That's one real call with Gemma as the caller and the Hollow, cut down to about a minute. Everything they say is what Gemma said. The player's lines in this recording are scripted, so that the filming could be repeated.

Code

GitHub logo deeptelar / scary-truc

A horror game played by phone: talk a terrified stranger out of a maze while something hunts by sound. Gemma 4 plays the caller, in your browser. Kotlin Multiplatform, built with Telar.

Scary Truc

Don't trust the voice on the phone.

— scratched into a wall of The Tape

It's past midnight and your phone is ringing. Unknown number.

You answer, and someone is whispering on the line, too fast, too close. They woke up somewhere they shouldn't be. They don't know how they got there. The others who came with them are gone and yours is the only number in that phone.

You can't see where they are. You have their voice, a battery that is running out, and a notebook where you draw the maze from what they tell you. The pencil fades.

Something else is down there with them. It has no eyes. It finds you by how you sound. So you talk them through it, left, right, hide, don't breathe, and you keep them calm, because fear is loud.

Answer the phone. Headphones on, lights off…

About 6,500 lines of Kotlin, plus 1,800 lines of tests. It's one Compose Multiplatform codebase that builds the web page (Wasm) and a desktop game. The first commit is from October 27.

How I Built It

My first version wasn't scary. It was a top-down maze where you threw treats and tricks to guide four little agents past certain death. It worked, and when I played it, it was boring, predictable and not scary at all. I added real-time pressure, a hunter, a siren at midnight, radio static. It got better and it still wasn't frightening, because I was looking at it from above, safe.

So I threw it away. If the fear comes from not seeing, take the screen away. On day two the game became a phone call with one scared person on the line. The first time I played that version on my phone, just chatting with them was already scary, which all the sirens had never managed.

(Truc started as trick, from trick-or-treat. In Catalan, fer un truc also means to give someone a call. I kept the name.)

The model: Gemma 4, playing three parts. The hosted page uses Gemma 4 31B on OpenRouter, a few seconds a turn. The desktop build uses gemma4:12b in a local Ollama. Gemma does three jobs in a call:

  • The caller's ears: what you told them to do, and how it made them feel.
  • The caller's voice: saying what happened as a frightened person would.
  • The Hollow: the whispers on the line, made from what the caller fears most.

The workflow: a graph that waits for you. A call is a Telar graph. Telar is an open-source Kotlin library for AI workflows that I maintain. It existed before this challenge, and Scary Truc uses it from Maven Central like any other library.

StateGraph<Call> {
    val listen = node(LISTEN, work = { call -> ears.listen(call) }) { call, heard -> call.copy(heard = heard) }
    val walk = node(WALK) { call -> call.act(mazeOf, haunt) }
    val whisper = node(WHISPER, work = { call -> if (call.whispers(mazeOf(call))) whisperer.whisper(call) else null }) { call, said ->
        if (said == null) call else call.whisper(said)
    }
    val speak = node(SPEAK, work = { call -> voice.speak(call) }) { call, said -> call.spoke(said) }
    START then speak
    conditionalEdge(speak, targets = setOf(listen, NodeRef.END)) { call -> if (call.over) NodeRef.END else listen }
    listen then walk then whisper then speak
}.compile()
Enter fullscreen mode Exit fullscreen mode

The run pauses before listen on every turn, until you say something. The important node is the one with no model in it: walk is plain code. The caller obeys, refuses or bolts. The maze answers. The Hollow hunts. Only then does Gemma get a list of facts and say them out loud.

That's the rule the whole game stands on: Gemma never decides what is true. It decides how it sounds. So the caller can't invent a door that isn't there, and you can't talk them through a wall.

The ears answer with a typed value, not text to parse. This class is the whole contract between what you say and the game:

@Serializable
data class Understood(
    @Description("What the stranger tells you to do now: ahead, left, right, back, stay, hide, mark, take, use, or none")
    val order: String,
    @Description("How the stranger's words make you feel, from -2 (harsh, rushed, scary) to 2 (calm, kind, reassuring)")
    val calm: Int,
    @Description("When the stranger says to do something after that, the next thing ...")
    val then: String = "none",
    @Description("Whether the stranger's words have nothing to do with getting you out alive: a joke, trivia ...")
    val offTopic: Boolean = false,
    // ...
)

val understood = model.ask<Understood>(prompt(call), system = system(call))
Enter fullscreen mode Exit fullscreen mode

"Go right, you're doing great" gives right, calm 2. A bare "left" needs no model at all, so it's instant when something is behind them.

The ghost in the machine. The game keeps a log of each call on the phone, with what I said, what the caller understood, and how the call stood after each turn. I played, exported the log, and read what really happened. These six moments changed the game.

  • The caller lied to me. In The Woods I said "go right and pick up a stone if you can". The caller answered "I have the stone." There was no stone. Later, with the Hollow two steps away, I told them to throw it. "I don't have any stones. It found me!" Dead. My graph had only run the "go right" part, so no fact said that nothing was picked up, and Gemma filled the silence with what I wanted to hear. A rule in the prompt forbade exactly that, and it wasn't enough. Now both parts of an order run, and what fails becomes a fact. When a model makes something up, first look for the fact you didn't give it.
  • Buttons killed the conversation. I had added tap options for one-handed play. Reading my own logs, my first four calls were nearly all taps and all ended badly, with the caller reciting menus at me. The call where I got someone out was mostly typed. It was also my third try at that maze, so the buttons weren't the only reason, but the point of the game is that it feels like a conversation. I removed them, and taught the caller to answer me first and report second.
  • "Don't worry" was a cheat code. My winning call took panic to zero by saying no te preocupes on every turn. Nobody in that situation is at zero. Now the same comfort wears thin while all is quiet, and panic has a floor. My first attempt at this made The Ship impossible: my test guide, which plays perfectly, went from 12 of 12 to 0 of 12. It's 9 of 12 now, and 12 of 12 on the others.
  • Gemma was gone for a day and I didn't notice. A regular expression that works on the JVM throws in the browser. Gemma failed silently, the game fell back to its script, and every test was green. Now model calls and faults go in the log, and I check model behavior in a real headless Chrome with a real key.
  • The best input was my own voice. My last call was entirely by microphone, and it worked so well that I made the mic the main key: it's a phone call, so you should speak. It also gives the game its strangest moments. The speech recognition once heard me say a name I never said, and the caller, in the dark, with the Hollow awake, answered: "I'm not Silvia. My name is Sam."
  • A tester found what I couldn't see. A real player liked it and told me three things. It got too easy, because there was plenty of time to think on every turn. The caller shouldn't explain that a breathing door is bad: say there's a door that breathes, and if someone dies going in, the player will understand. And trust, the main mechanic, was never tested. The log agreed: trust only ever rose, 60 to 87 in one call, and a 33-second pause cost nothing. So thinking now costs battery and nerve, and past a point the Hollow keeps walking. The notes stopped teaching the traps and started talking about the voice on the phone. And the caller now stops and asks who you are. Gemma judges your answer: "I don't know, I'm scared too" convinces them, "just go left" is no answer, and if you keep dodging, they hang up on you. It also remembers what you said. In the call in the recording above, the player said "I'm staying with you", and the Hollow answered:

Staying with you... and the weight of all they lost.

Why Does Open Innovation Matter?

Horror needs a character who stays in character. I can control that here because every part of it is mine to change: the prompts, the examples Gemma learns its voice from, the model itself.

You can swap the model. The caller speaks the OpenAI API, so any server works: Gemma 4 31B on OpenRouter, gemma4:12b on my own 8 GB GPU, or a friend's proxy. Changing it is a field on the lock screen. I tuned the prompts by measuring what Gemma does, and I can keep doing that because the weights aren't going to change under me without warning.

It doesn't depend on any one service. If a provider disappears, the game keeps working on the next one, or on your own computer. With no model at all it still plays, with a simpler caller. The maze, the Hollow and the rules are code, so the game itself survives its model.

It can run offline. The desktop build talks to a local Ollama, so a whole call can stay on your machine. I checked instead of assuming: I traced every connection during full calls in English and Spanish on gemma4:12b. With Ollama's cloud features switched off (OLLAMA_NO_CLOUD=1), neither the game nor Ollama connected to anything outside my PC. On my 8 GB GPU, the first line takes up to a minute while the model loads, then it's 4 to 8 seconds a turn.

What you say stays yours, with one honest exception. The API key stays in your browser and goes from there straight to the server you chose. The play log stays on the device until you export it, and never holds a key. The exception is the microphone: speech recognition is the browser's own, and most browsers run it on their maker's servers, so on Chrome your voice goes to Google. The desktop build has no microphone.

I didn't run a closed model next to Gemma, so I can't say one would write a worse caller. What I can say is that an open model let me build this the way I wanted: a game anyone can run on their own machine, that outlives any API, with a character I can take apart and fix when it lies to me.

My Agent Session

I built this with Claude Code. I had the ideas, made the design decisions, picked every sound, and tested it on my phone. Claude Code wrote most of the code, ran the measurements against Gemma, and drafted this post with me.

The loop was simple, and it's the part I'd recommend: play a call, export the log, and hand it over with one line about what felt wrong. Then measure the fix against the real model before believing it.

Prize Categories

  • Best Use of Gemma: Gemma 4 understands the player and voices both the caller and the Hollow, in English and Spanish, through a typed interface, on OpenRouter or locally in Ollama. The maze keeps it honest.

Top comments (0)