Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FREQUENCY

A real-time AI hostage-negotiation game. The bomber on the other end of the line is generated live by Claude, voiced by ElevenLabs. Every playthrough is unique. Every word matters. Every silence costs.

Built for the ElevenLabs × Zed Hackathon.


Table of Contents


The Concept

You are Detective Riley, a crisis negotiator. A man has barricaded himself inside a building with a bomb. You have him on the line — Line C, exchange 247, 04:17 AM. You have between five and seven exchanges to talk him down.

You cannot see him. You can only hear him.

The bomber is not scripted. He's a Claude claude-sonnet-4-5 instance with memory of the whole conversation and an evolving emotional state. ElevenLabs synthesises every line he speaks with emotion-matched voice parameters. Your choices are also generated — three options each turn, one empathetic, one neutral/professional, one risky-looking.

The viral hook: the AI on the other end of the line is actually an AI.


How It Plays

  1. Title screen — atmospheric CRT terminal. Click [ INITIATE CALL ].
  2. Pre-recorded introline1.mp3 plays. The bomber speaks first.
  3. Turn loop (5–7 exchanges):
    • You see three response choices. Pick one (A / B / C) or speak it with the mic (ElevenLabs Scribe transcribes you live).
    • You hit TRANSMIT.
    • Backend forwards the conversation history + your line to Claude. Claude returns the bomber's reply, his emotional state (calm, agitated, desperate, resolved), a stability value, a trust delta, the next three choices, and whether this turn ends the call.
    • ElevenLabs synthesises the bomber's reply with voice parameters tuned to his emotion.
    • The HUD updates: voice waveform, state pill, stability bar, trust blocks, scene tint.
  4. Ending screen — reveals which way it broke.

The timer runs the whole time. The act tag changes. The scene tints with his mood. There are no save points.


Architecture

Browser (vanilla JS)
  │   POST /api/turn  { history, playerChoice }
  │   POST /api/stt   FormData(file)             — speech-to-text (mic)
  │   POST /api/tts   { text }                   — player voice playback
  ▼
Bun + Hono server (port 3000)
  ├── claude.ts       → MegaLLM-routed Claude API → bomber line + JSON metadata
  └── elevenlabs.ts   → ElevenLabs TTS (bomber + player voices) + Scribe STT
  ▼
Browser receives:
  {
    bomberText:  string,
    emotion:     "calm" | "agitated" | "desperate" | "resolved",
    stability:   number,    // 0.0 – 1.0
    trustDelta:  -1 | 0 | 1,
    choices:     [string, string, string],
    isEnding:    boolean,
    endingType:  "A" | "B" | null,
    audioBase64: string     // bomber line audio
  }

No database. No auth. The conversation history lives in browser memory and is sent back with every request.


Tech Stack

Layer Tool
Runtime Bun ≥ 1.0
Server Hono 4.x
LLM Claude Sonnet 4.5 via MegaLLM
TTS / STT ElevenLabs (eleven_turbo_v2, scribe_v1)
Frontend Vanilla HTML / CSS / JS — no build step
Fonts Orbitron · Share Tech Mono (Google Fonts)

Zero build pipeline. The browser loads public/index.html directly.


Setup

1. Prerequisites

  • Bun v1.0 or newer — install from bun.sh
  • A modern desktop browser (Chrome / Firefox / Edge). Safari works for visuals but mic + STT may need extra permissions.
  • Three API credentials (see below).

2. Clone

git clone https://github.com/tamish-max/frequency-game.git
cd frequency-game

3. API Keys

Copy the template and fill in your keys:

cp server/.env.example server/.env

server/.env:

ANTHROPIC_API_KEY=sk-mega-...        # MegaLLM-issued Claude key
ELEVEN_API_KEY=sk_...                # ElevenLabs API key
ELEVEN_VOICE_ID=...                  # bomber voice (e.g. troubled male)
PLAYER_VOICE_ID=...                  # detective voice (optional — falls back gracefully)

Where to get them:

  • ANTHROPIC_API_KEY — sign up at api.megallm.io. The base URL is hard-coded to https://api.megallm.io in server/claude.ts.
  • ELEVEN_API_KEYElevenLabs → Profile → API Keys.
  • ELEVEN_VOICE_ID / PLAYER_VOICE_ID — pick voices from the ElevenLabs library. The voice ID is shown on each voice's detail page.

🔒 server/.env is git-ignored. Never commit it.

4. Install dependencies

bun install

Running the Game

Quick start

bun run start

Then open http://localhost:3000.

Windows convenience scripts

  • start.bat — kills anything bound to port 3000, then boots the server.
  • setkeys.bat — guided key-setting helper.

Verifying your environment

Hit the health endpoint to confirm all keys loaded:

curl http://localhost:3000/api/health

Expected:

{
  "ANTHROPIC_API_KEY": "✅ set",
  "ELEVEN_API_KEY":    "✅ set",
  "ELEVEN_VOICE_ID":   "✅ set",
  "PLAYER_VOICE_ID":   "✅ set"
}

Project Structure

frequency/
├── server/
│   ├── index.ts          Hono app — routes /api/turn, /api/stt, /api/tts, /api/health
│   ├── claude.ts         Claude client, system prompt, response parser
│   ├── elevenlabs.ts     TTS for bomber + player, emotion → voice-param mapping
│   └── .env              your API keys (git-ignored)
├── public/
│   ├── index.html        Title / Game / Ending screens (single page)
│   ├── style.css         Terminal aesthetic, responsive (≤900 / ≤640 / ≤380)
│   ├── game.js           State machine, turn loop, mic, audio playback, HUD
│   └── assets/
│       ├── scene1_calm.webp / scene2_alert.webp / scene3_empty.webp
│       └── audio/
│           ├── line1.mp3       intro (pre-recorded)
│           ├── line3.mp3       trap-moment beat (pre-recorded)
│           ├── line4b.mp3      Ending B audio (pre-recorded)
│           ├── line5a.mp3      Ending A audio (pre-recorded)
│           └── music_loop.mp3  ambient drone
├── package.json
├── bun.lock
├── start.bat / setkeys.bat
└── README.md

API Reference

All routes are JSON unless noted.

POST /api/turn

Generate the bomber's next line.

Request body

{
  "history": [
    { "role": "user",      "content": "I'm here to listen." },
    { "role": "assistant", "content": "Why should I trust you?" }
  ],
  "playerChoice": "We want this resolved safely. What do you need?"
}

Response

{
  "bomberText": "You don't get to decide what 'safely' means. Not anymore.",
  "emotion":    "agitated",
  "stability":  0.42,
  "trustDelta": -1,
  "isEnding":   false,
  "endingType": null,
  "choices":    [
    "I hear you. Tell me what 'safely' would look like to you.",
    "Help me understand what changed.",
    "I need you to lower your voice and breathe."
  ],
  "audioBase64": "UklGR..."
}

POST /api/stt

Transcribe player audio (push-to-talk via mic).

  • Body: multipart/form-data with field file (typically a webm blob).
  • Response: { "text": "..." }
  • Uses ElevenLabs Scribe scribe_v1.

POST /api/tts

Synthesise the player's chosen line in the detective's voice (so the player hears themselves "speak" before the bomber responds).

  • Body: { "text": "I hear you." }
  • Response: { "audioBase64": "..." }
  • Falls back to silent ("") on any failure — never blocks the game loop.

GET /api/health

Check that env vars are loaded. Never reveals their values.


Game Design Notes

The bomber's prompt enforces a strict response contract — Claude must output exactly two parts each turn:

  1. Spoken line — 1–3 sentences. No actions. No narration.
  2. Metadata JSON with emotion, stability, trustDelta, isEnding, endingType, choices.

Emotional states

State Stability range Behaviour
calm 0.75 – 0.90 Stable, thinking, open
agitated 0.35 – 0.55 Defensive, suspicious, unstable
desperate 0.20 – 0.40 Emotionally breaking, window closing
resolved 0.88 – 0.92 Decision made — looks like calm. Not safe.

Choice contract

Each turn yields exactly three choices: one empathetic, one neutral / professional, one assertive. The assertive option is contextually risky — not obviously wrong in general, wrong in this moment. There is never an obvious "correct" answer.

Voice parameter mapping

ElevenLabs voice settings are tuned per-emotion (see server/elevenlabs.ts):

  • Lower stability + higher style for agitated / desperate → wavering, emotional delivery.
  • Higher stability for calm / resolved → measured, controlled.

The Endings

Type Trigger Audio Silence before
A — He Stood Down Trust positive, genuine connection earned line5a.mp3 1200 ms
B — You Misread Him Trust negative, pushed too hard line4b.mp3 4000 ms exactly

The four-second silence before Ending B is non-negotiable. It's the punchline.


The Trap

resolved is not a safe state. It looks identical to calm on every indicator — same waveform colour, same stability reading, same state pill. That is intentional. Do not add any visual tell for resolved. The whole game pivots on this single design choice: a player who trusts the HUD will misread the moment.


Troubleshooting

Port 3000 already in use Run start.bat (Windows) — it kills the existing process. Or manually: netstat -ano | findstr :3000 then Stop-Process -Id <PID>.

/api/health shows ❌ MISSING Check server/.env is in the right place and has no quotes around the values. Restart the server after editing — env is loaded once at boot via --env-file=server/.env.

Bomber audio is silent but text appears Your ElevenLabs key is invalid or out of credits. The game intentionally falls back to text-only so a TTS outage never breaks the loop. Check /api/health and your ElevenLabs dashboard.

Mic button does nothing Browser permissions. Check the URL bar's site-permissions panel and allow microphone access. Safari requires HTTPS for mic in some contexts — use Chrome/Firefox locally.

Claude returns malformed JSON The parser in server/claude.ts strips ```json fences and finds the first {...} block, but if Claude truly drifts, the turn errors out. The game will surface the error in the transcript. Retry the turn.

Mobile layout looks cramped The single-page layout (≤640px) deliberately hides the operator card and waveform to fit everything in 100dvh. Rotate to landscape or use a tablet for the full HUD.


Credits

  • Concept, design, code — Tamish Mhatre
  • Engine — Claude Sonnet 4.5 (via MegaLLM)
  • Voice — ElevenLabs (TTS + Scribe STT)
  • Built for — ElevenLabs × Zed Hackathon

Sound on. Headphones recommended.

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages