"Can you survive being a customer?"
At every hackathon, teams build the same thing: chatbots, AI agents, customer support copilots. All of them try to fix customer experience. None of them force you to feel what broken customer experience is actually like.
So I built the opposite. A game.
The Idea
Customer Escape Room is a mobile-first, voice-powered game where players must escape increasingly absurd customer service nightmares. Think Portal meets The Stanley Parable meets your worst call center experience.
Six levels. Six NPCs. Six unique mechanics. All connected by one question: can you survive being a customer?
The Tech Stack
| Technology | Purpose |
|---|---|
| Next.js 16 (App Router) | Full-stack framework |
| TypeScript | Type safety |
| Tailwind CSS v4 | Styling |
| Framer Motion | Animations |
| shadcn/ui | Component library |
| MediaRecorder API | Voice recording |
| OpenAI Whisper | Speech-to-text |
| React Context + useReducer | State management |
Architecture
┌──────────────────────────────────────────┐
│ Page Layer │
│ / (Game Hub) /game/judge (Demo) │
├──────────────────────────────────────────┤
│ Game Engine │
│ ┌────────────────────────────────────┐ │
│ │ Level 1 │ Level 2 │ Level 3 │ │
│ │ Level 4 │ Level 5 │ Level 6 │ │
│ └────────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ Shared Components │
│ VoiceInput NPCDialogue ScoreDisplay │
│ Achievements ShareCard LevelComplete │
├──────────────────────────────────────────┤
│ State Layer │
│ GameProvider → useReducer → Context │
├──────────────────────────────────────────┤
│ API Layer │
│ /api/ai → OpenAI / Featherless │
│ /api/ai → Whisper Transcription │
└──────────────────────────────────────────┘
The 6 Levels
Level 1: The Refund Maze
The player is trapped in a corporate maze. The Refund Robot NPC obsesses over "Policy 42(a)(3)(b)(ii)" — a fictional policy that keeps getting more nested the deeper you go. The player must ask for Form R-1, fill it out, then demand supervisor escalation to escape.
Mechanic: Bureaucratic layer navigation. Each stage requires specific keywords to advance.
Level 2: IVR Hell
A timed level. The player is stuck in an infinite phone menu system that loops back on itself. Press 1 for Billing. Press 2 for Billing Questions. Press 3 for Billing Questions About Billing. The only escape is saying "operator" or "representative" — the hidden human path.
Mechanic: Countdown timer + menu depth tracking. The hint system activates after 3+ failed attempts.
Level 3: Transfer Dungeon
The Transfer Goblin enthusiastically transfers the player to random departments, forgetting everything each time. The player must track their own story consistency — repeating yourself gets penalized. Escape by asking for "Escalations" after 5+ transfers.
Mechanic: Transfer counter + repetition detection (word-level matching against previous responses).
Level 4: Subscription Prison
Manager Dragon uses every retention tactic in the book. "Are you sure?" asked 4 times. Discount offers. Guilt trips. The player must stay firm through multiple "sure?" checkpoints without accepting any offers.
Mechanic: Persuasion gauntlet. Saying "no" or accepting offers resets progress. Only firm persistence wins.
Level 5: Chatbot Labyrinth
The Chatbot Wizard answers every question wrong with extreme confidence. The player must discover 3 hidden keywords ("human", "agent", "representative") to prove they're not a bot. Every regular input is met with creatively unhelpful responses.
Mechanic: Keyword collection. Misunderstanding counter unlocks progressive hints.
Level 6: Karen Boss Fight
The final boss. Karen Queen interrupts, contradicts herself, escalates unreasonably, and demands everything for free. The player must use empathy words ("understand", "sorry", "help") to de-escalate without triggering more rage.
Mechanic: De-escalation score. Empathy words reduce anger; demanding or fighting back triggers interruptions.
Level Design Pattern
Every level follows the same architecture:
type Stage = "intro" | "stage1" | "stage2" | "escaped"
// Stage data with deterministic responses
const STAGE_DATA: Record<Stage, StageConfig> = {
intro: {
npc: "...NPC dialogue...",
advanceOn: ["keyword1", "keyword2"],
advanceTo: "stage1",
hint: "Hint for confused players"
},
// ...
}
function handleTranscript(text: string) {
// 1. Detect confused input ("i don't know", "help")
if (isConfused(text)) return showHint()
// 2. Check for advancement keywords
if (matchesKeyword(text, currentStage.advanceOn)) {
return advanceStage(currentStage.advanceTo)
}
// 3. Redirect to objective
return showRedirectMessage()
}
This pattern ensures:
- No random responses — every NPC line is stage-appropriate
- Confused players get help — "i don't know" always shows a hint
- Clear progression — keywords gate advancement
- Graceful failure — off-script input redirects to the objective
Voice System
The voice system uses the native MediaRecorder API:
// src/lib/voice.ts
export function useVoice() {
const startRecording = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const mediaRecorder = new MediaRecorder(stream)
// ... record audio chunks
}
const stopRecording = async () => {
// ... create blob, send to /api/ai with X-Transcribe
const text = await transcribeAudio(blob)
return text
}
}
The VoiceInput component wraps this with a toggle:
<VoiceInput onTranscript={handleTranscript} />
// Shows mic button + "⌨️ Type instead" toggle
Falls back gracefully:
- No microphone → shows text input only
- No API key → simulates transcription with context-aware phrases
- Error → shows text input as fallback
AI Abstraction Layer
The API route handles both dialogue generation and transcription through a unified endpoint:
// /api/ai handles:
// 1. Chat completions (dialogue)
// 2. Audio transcriptions (Whisper)
const provider = process.env.AI_PROVIDER || "openai"
const baseUrl = provider === "featherless"
? "https://api.featherless.ai/v1"
: "https://api.openai.com/v1"
Provider switching via env vars:
AI_PROVIDER=openai # Uses gpt-4o-mini
AI_PROVIDER=featherless # Uses Llama 3.1 8B
State Management
Game state uses React Context with useReducer:
type Action =
| { type: "SET_PHASE"; phase: GamePhase }
| { type: "SET_LEVEL"; levelId: LevelId }
| { type: "ADD_SCORE"; levelId: LevelId; category: ScoreCategory; amount: number }
| { type: "ADD_ACHIEVEMENT"; achievementId: string }
| { type: "RESET_GAME" }
// Scores are calculated cumulatively
// Achievements are checked and dispatched by level logic
// Game phase controls which screen renders
Scoring & Ranks
6 Categories × 6 Levels × 100 points = 3,600 max
Categories:
patience - Staying calm under absurdity
problemSolving - Finding the escape path
persistence - Not giving up
empathy - Showing understanding (critical for Level 6)
escapeSpeed - Completing efficiently
negotiation - Arguing effectively
Ranks:
🥉 Casual Complainer (0+ pts)
🥈 Support Veteran (300+ pts)
🥇 Customer Champion (600+ pts)
👑 Escape Legend (900+ pts)
Judge Demo Mode
One of the most important features: /game/judge drops judges directly into Level 2 (IVR Hell) with zero friction.
// /game/judge/page.tsx
<GameEngine initialLevel={2} />
No signup. No onboarding. Ten seconds from scanning a QR code to playing.
What I'd Add Next
- Multiplayer — Socket.io is installed. 4-8 players in a shared escape room with voice cooperation.
- Dynamic AI scenarios — Use the AI provider to generate unique company names and NPC dialogue per playthrough.
- Supabase persistence — Leaderboards, saved scores, daily challenges.
- Sound design — Hold music, menu beeps, NPC voice lines.
- More levels — "The Data Deletion Request," "The Warranty Voidening," "The Return Policy Paradox."
- Share cards — Generate shareable SVG images with scores and rank.
Try It
npm install
npm run dev
# → http://localhost:3000
# → http://localhost:3000/game/judge (judge demo)
Screenshots
Code & more: https://www.dailybuild.xyz/project/153-escape-velocity




Top comments (0)