# Project export: Super Mafia

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: The classic game of Mafia/Werewolf, with an AI narrator or an AI judge!
- Devpost: https://devpost.com/software/super-mafia-cn468h
- GitHub: https://github.com/Vishvenugopal/SuperMafiaCalHacks
- Demo: https://mafia-but-high.tech/
- Team: 3 GitHub contributor(s) — Vishvenugopal (21 commits), Priyank Bhambu (6 commits), Nakul K (3 commits)

## Devpost submission (written by the team)

### Inspiration

This game is based on the popular social deduction game Mafia or Werewolf, taking the game to a new level. This game solves a key problem with the classic games: The need for a human moderator who has to sit out to narrate.

### What it does

Super Mafia uses AI to host the game, allowing everyone to play. Its key features include: Pass-the-phone gameplay and online multiplayer. AI Host with several unique voices, narrating the game with personalities like serious, funny, or even a rhyming rapper. 2 different game modes, classic and judge, which involve voice interaction between the players and the AI host, as they must convince the AI to choose who to vote out. The classic mode uses Baseten and janitor.ai for the personality and ElevenLabs for the voice. Players have to discuss with each other to find out who the werewolf is In the online mode, each player talks to the Baseten AI or janitor.ai. Their goal is to convice the AI to vote out who they think the werewolf is

### How we built it

TypeScript is the fundamental logic of the game. TailwindCSS for the styling of the game. LiveKit for the fast and efficient streaming of text-to-speech responses. Janitor AI and Baseten for unique personalities. ElevenLabs API to introduce different voices.

### Challenges we ran into

Handling bad user input, as there were many different debugging tests we had to do, especially with player management. Creating a friendly UX, as we had to ensure the privacy of the players and make it clear whose turn it was.

### Accomplishments we're proud of

We are very proud of the overall design of the app, as its simple and fun style made the game pretty engaging and accessible. Lessons learned We learned how to use multiple API's together. We also learned how to create an online multiplayer game.

### What's next

Fixing bugs with the multiplayer mode. Video showing judge mode (main video only shows classic mode): https://www.youtube.com/watch?v=8hDipFwG1qk

## README (from the GitHub repository)

**Made at CalHacks 2025**

**Play SuperMafia at:** https://mafia-but-high.tech/

<img width="2508" height="1349" alt="supermafiamainmenu" src="https://github.com/user-attachments/assets/8997e594-8964-4d58-ad5c-7bd87f609b88" />
<img width="2512" height="1348" alt="SuperMafia screenshot" src="https://github.com/user-attachments/assets/47c28e73-3b51-4d18-bb57-61ed5d2c00c0" />


# SuperMafia - AI-Hosted Werewolf Game

Mobile-first, pass-the-phone Werewolf game with AI host personality and realtime voice.

## Setup

### 1. Install Dependencies
```bash
npm install
```

### 2. Configure API Keys

Copy `.env.local.example` to `.env.local` and fill in your API keys:

#### LiveKit (for realtime voice)
1. Go to https://cloud.livekit.io
2. Create a free account
3. Create a new project
4. Copy your credentials:
   - **WebSocket URL**: `wss://your-project.livekit.cloud`
   - **API Key**: Found in project settings
   - **API Secret**: Found in project settings

Add to `.env.local`:
```env
LIVEKIT_WS_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
```

#### JanitorAI (for host personality)
1. Go to https://janitorai.com
2. Create an account
3. Get your API key from settings
4. Create or find a character for your host
5. Copy the character ID from the URL

Add to `.env.local`:
```env
JANITOR_AI_API_KEY=your_janitor_api_key
JANITOR_AI_CHARACTER_ID=your_character_id
```

**Note**: The app works without these keys using Web Speech API and a mock host. LiveKit and JanitorAI are optional enhancements.

### 3. Run Development Server
```bash
npm run dev
```

Open http://localhost:3000

## Features

- **Pass-the-phone gameplay**: One device for the whole table
- **AI Host**: Narrates game events and answers questions
- **Voice interaction**: Push-to-talk to ask the host questions
- **Roles**: Villager, Werewolf, Seer, Medic
- **Mobile-optimized**: Designed for handheld play
- **Privacy**: Role reveals are private, night actions are hidden

## How to Play

1. Add 5+ players with names and photos
2. Configure roles (optional)
3. Choose theme and host personality
4. Start game
5. Pass phone for role reveals (in order players were added)
6. **Night Phase**: Pass to each special role in order
   - Medic protects someone (if they're killed, protection saves them)
   - Werewolves choose a target
   - Seer peeks at someone's role
7. **Day Phase**: Discuss and vote to eliminate suspects
8. Win when all werewolves are eliminated (villagers) or werewolves equal villagers (werewolves)

### Role Abilities
- **Medic**: Protect one player each night (werewolves can still target them, but protection saves them)
- **Werewolf**: Choose one player to eliminate each night
- **Seer**: Peek at one player's role each night
- **Villager**: No special ability, but crucial for voting

## Tech Stack

- **Next.js 14** + React 18
- **TypeScript** (strict mode)
- **TailwindCSS** for styling
- **Zustand** for state management
- **LiveKit** for realtime voice (optional)
- **JanitorAI** for AI host personality (optional)
- **Web Speech API** for fallback STT/TTS

## Architecture

- **Client-side first**: Game state managed in browser
- **Offline-capable**: Works without internet (using Web Speech API)
- **Progressive enhancement**: LiveKit and JanitorAI add features when configured
- **Type-safe**: Full TypeScript coverage with strict mode

## API Integration

### LiveKit
- Automatically used when `LIVEKIT_WS_URL` is configured
- Falls back to Web Speech API if unavailable
- Provides lower latency and better quality voice

### JanitorAI
- Automatically used when `JANITOR_AI_API_KEY` is configured
- Falls back to mock host if unavailable
- Provides personality-driven responses

Both integrations work together seamlessly - LiveKit handles voice, JanitorAI handles personality.

## License

MIT


## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 241 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (51 of 51)

```
.env.local.example
.gitattributes
.gitignore
.vs/slnx.sqlite
.vs/SuperMafiaCalHacks/FileContentIndex/580579b1-3465-4eba-80d7-829d58410e46.vsidx
.vs/SuperMafiaCalHacks/v17/.wsuo
.vs/SuperMafiaCalHacks/v17/DocumentLayout.json
.vs/VSWorkspaceState.json
agent/judge_agent.py
agent/multi_agent.py
agent/requirements.txt
agent/simple_judge.py
API_KEYS_GUIDE.md
app/api/agent-webhook/route.ts
app/api/debug-env/route.ts
app/api/debug-janitor/route.ts
app/api/host/route.ts
app/api/livekit-token/route.ts
app/api/room/route.ts
app/api/simple-agent/route.ts
app/api/test-janitor/route.ts
app/api/tts-elevenlabs/route.ts
app/globals.css
app/judge-mode/page.tsx
app/layout.tsx
app/page.tsx
app/werewolf/layout.tsx
app/werewolf/page.tsx
baseten-error.txt
baseten-success.txt
debug.log
ENV_TEMPLATE.txt
janitorai-raw.txt
janitorai-test-raw.txt
lib/env-validation.ts
lib/livekit-room.ts
lib/livekit.ts
lib/types.ts
lib/util.ts
lib/voice.ts
lib/voices.ts
next-env.d.ts
next.config.mjs
package.json
postcss.config.js
README.md
render.yaml
start_agent.bat
store/game.ts
tailwind.config.ts
tsconfig.json
```

### Dependencies

- agent/requirements.txt: livekit@==0.17.0, python-dotenv@==1.0.0, requests@==2.31.0
- package.json: @types/node@20.14.12, @types/react@18.3.5, @types/react-dom@18.3.0, autoprefixer@10.4.20, livekit-client@^2.5.0, livekit-server-sdk@^2.6.0, next@^14.2.33, postcss@8.4.47, react@18.2.0, react-dom@18.2.0, tailwindcss@3.4.10, typescript@5.6.2, zustand@4.5.2

### Recent commits (newest first)

- Removed useless markdown files
- Updated ReadMe with Screenshots
- ai works without elevenlabs
- temp fix fpor build
- werewolf mode maybe fixed
- judge mode fixed
- Reapply "Cursor changes"
- Reapply "still not working but testing on render"
- Reapply "debug logs"
- Reapply "new logo"
- Reapply "Merge branch 'main' of https://github.com/Vishvenugopal/SuperMafiaCalHacks"
- Reapply "Trying to fix the app"
- Revert "Trying to fix the app"
- Revert "Merge branch 'main' of https://github.com/Vishvenugopal/SuperMafiaCalHacks"
- Revert "new logo"
- Revert "debug logs"
- Revert "still not working but testing on render"
- Revert "Cursor changes"
- Cursor changes
- still not working but testing on render

## Key source files (fetched from GitHub, selected and truncated for size)

### API_KEYS_GUIDE.md

```markdown
# API Keys Setup Guide

## What You Need

Your `.env.local` file needs these keys for full functionality:

### 1. LiveKit (Realtime Voice)
**What it does**: Provides low-latency, high-quality voice communication for the game.

**How to get it**:
1. Visit https://cloud.livekit.io
2. Sign up for a free account
3. Click "Create Project"
4. Name your project (e.g., "SuperMafia")
5. Once created, go to "Settings" → "Keys"
6. Copy these values:
   - **WebSocket URL**: `wss://your-project-name.livekit.cloud`
   - **API Key**: Starts with `API...`
   - **API Secret**: Long string shown once

**Add to .env.local**:
```env
LIVEKIT_WS_URL=wss://your-project-name.livekit.cloud
LIVEKIT_API_KEY=APIxxxxxxxxxxxxxxxxx
LIVEKIT_API_SECRET=your_secret_here
```

### 2. Baseten (AI Narrator/Host - Recommended)
**What it does**: Powers a game-aware AI narrator that knows the current game state and provides immersive, context-aware commentary.

**How to get it**:
1. Visit https://baseten.co
2. Sign up for an account
3. Get your API key from account settings
4. Choose a model from their library (e.g., `zai-org/GLM-4.6`, `meta-llama/Llama-3.1-8B-Instruct`, etc.)

**Add to .env.local**:
```env
BASETEN_API_KEY=your_api_key_here
BASETEN_MODEL_ID=zai-org/GLM-4.6
```

**Note**: Use the full model name (e.g., `zai-org/GLM-4.6`) not a deployment ID. The app uses Baseten's OpenAI-compatible inference endpoint.

### 3. JanitorAI (AI Host Personality - Alternative)
**What it does**: Powers the AI host with personality and context-aware responses.

**How to get it**:
1. Visit https://janitorai.com
2. Create an account
3. Go to your profile → Settings → API
4. Generate an API key
5. Browse or create a character for your game host
6. Open the character page and copy the ID from the URL
   - Example URL: `https://janitorai.com/characters/abc123def456`
   - Character ID: `abc123def456`

**Add to .env.local**:
```env
JANITOR_AI_API_KEY=your_api_key_here
JANITOR_AI_CHARACTER_ID=abc123def456
```

## How They Work Together

- **LiveKit** handles voice input (STT - Speech to Text)
- **Baseten** provides game-aware AI narrator (knows current phase, players, events)
- **ElevenLabs** provides high-quality voice output (TTS)
- When you ask the host a question:
  1. Your voice is captured and converted to text (LiveKit or Web Speech API)
  2. Question + current game state is sent to Baseten → gets context-aware response
  3. Response is synthesized using ElevenLabs → spoken aloud with natural voice

## AI Priority Order

The app tries AI services in this order:
1. **Baseten** (best - game-aware, knows what's happening)
2. **JanitorAI** (good - personality-driven but less game-aware)
3. **Mock Host** (fallback - basic scripted responses)

## Fallback Behavior

**Without LiveKit**: Uses browser's Web Speech API (works but lower quality)
**Without JanitorAI**: Uses mock host with basic rule responses

The app works without any API keys, but the experience is much better with them!

## Testing Your S
[truncated — 1087 more characters]
```

### package.json

```
{
  "name": "super-mafia",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "autoprefixer": "10.4.20",
    "livekit-client": "^2.5.0",
    "livekit-server-sdk": "^2.6.0",
    "next": "^14.2.33",
    "postcss": "8.4.47",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "3.4.10",
    "zustand": "4.5.2"
  },
  "devDependencies": {
    "@types/node": "20.14.12",
    "@types/react": "18.3.5",
    "@types/react-dom": "18.3.0",
    "typescript": "5.6.2"
  }
}

```

### agent/requirements.txt

```
livekit==0.17.0
python-dotenv==1.0.0
requests==2.31.0

```

### app/layout.tsx

```typescript
import './globals.css'
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: process.env.NEXT_PUBLIC_APP_NAME || 'SuperMafia',
  description: 'AI-hosted Werewolf',
  icons: {
    icon: '/SuperMafiaLogo.png',
    shortcut: '/SuperMafiaLogo.png',
    apple: '/SuperMafiaLogo.png',
  },
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <link href="https://fonts.googleapis.com/css2?family=Boldonse:wght@400;700&display=swap" rel="stylesheet" />
        <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
      </head>
      <body className="min-h-dvh" suppressHydrationWarning>
        <div className="relative z-[1] h-dvh overflow-y-auto px-4 py-6">{children}</div>
      </body>
    </html>
  )
}

```

### app/page.tsx

```typescript
"use client"
import Link from 'next/link'

// Game title styling constant
const GAME_TITLE_STYLE = {
  fontFamily: 'Boldonse, sans-serif',
  background: 'linear-gradient(135deg, #ff0000 0%, #cc0000 100%)',
  WebkitBackgroundClip: 'text',
  WebkitTextFillColor: 'transparent',
  backgroundClip: 'text',
  paddingTop: '0.1em',
  paddingBottom: '0.1em',
  lineHeight: '1.2'
} as const

// Card title styling
const CARD_TITLE_STYLE = {
  fontFamily: 'Boldonse, sans-serif',
  background: 'linear-gradient(135deg, #ff0000 0%, #cc0000 100%)',
  WebkitBackgroundClip: 'text',
  WebkitTextFillColor: 'transparent',
  backgroundClip: 'text',
  paddingTop: '0.1em',
  paddingBottom: '0.1em',
  lineHeight: '1.2',
  display: 'inline-block'
} as const

interface GameModeCardProps {
  title: string
  description: string
  icon: string
  href?: string
  locked?: boolean
}

function GameModeCard({ title, description, icon, href, locked = false }: GameModeCardProps) {
  const cardContent = (
    <div className={`relative glass-strong rounded-3xl p-8 transition-all duration-500 h-full flex flex-col items-center justify-center animate-fadeIn ${
      locked 
        ? 'opacity-50 cursor-not-allowed' 
        : 'hover:scale-105 cursor-pointer animate-float'
    }`}>
      {locked && (
        <div className="absolute top-4 right-4 text-3xl opacity-70">🔒</div>
      )}
      <div className="text-7xl mb-6 text-center animate-float">{icon}</div>
      <h2 className="text-4xl font-bold text-center mb-4" style={CARD_TITLE_STYLE}>
        {title}
      </h2>
      <p className="text-center text-gray-300 text-base leading-relaxed">
        {description}
      </p>
      {locked && (
        <div className="mt-6 text-center text-yellow-400 text-sm font-semibold tracking-wider">
          COMING SOON
        </div>
      )}
    </div>
  )

  if (locked || !href) {
    return cardContent
  }

  return (
    <Link href={{ pathname: href }} className="block h-full">

      {cardContent}
    </Link>
  )
}

export default function Home() {
  return (
    <main className="min-h-screen flex flex-col items-center justify-center p-6">
      <div className="max-w-5xl w-full space-y-16 animate-fadeIn">
        {/* Title */}
        <div className="text-center space-y-4">
          {/* Use big logo; requires SuperMafiaLogo.png in /public */}
          <div className="mx-auto animate-float" style={{ maxWidth: '100%', display: 'inline-block' }}>
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src="/SuperMafiaLogo.png"
              alt="SuperMafia"
              style={{ width: 'min(90vw,1200px)', height: 'auto' }}
              onError={(e) => {
                // Fallback to text if logo not found
                const parent = (e.currentTarget.parentElement)
                if (parent) {
                  parent.innerHTML = '<div style="font-family: Boldonse, sans-serif; font-size: 72px;">SuperMafia</div>'
                }
              }}
            />
          </div>
          <div className="warm-divider w-48 mx-auto"></div>
        </div>

        {/* Subtitle */}
        <p className="text-center text-2xl text-gray-300 font-light tracking-wide">
          Choose Your Game Mode
        </p>

        {/* Game Mode Selection */}
        <div className="grid grid-cols-1 md:grid-cols-2 gap-10 px-4">
          <GameModeCard
            title="Classic Mode"
            description="Classic social deduction game with werewolves, villagers, and special roles. [Single-Device]"
            icon="🐺"
            href="/werewolf"
          />
          
          <GameModeCard
            title="Judge Mode"
            description="Basically Werewolf but an AI Judge decides who to vote out (and players have to persuade it). [Multi-Device]"
            icon="⚖️"
            href="/judge-mode"
          />
        </div>
      </div>
    </main>
  )
}

```

### app/werewolf/layout.tsx

```typescript
export default function WerewolfLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="mx-auto max-w-md h-dvh overflow-y-auto p-4">
      {children}
    </div>
  )
}

```

### app/api/debug-janitor/route.ts

```typescript
import { NextResponse } from 'next/server'

export async function GET() {
  const apiKey = process.env.JANITOR_AI_API_KEY
  
  return NextResponse.json({
    hasApiKey: !!apiKey,
    apiKeyLength: apiKey?.length || 0,
    apiKeyPrefix: apiKey ? apiKey.substring(0, 10) + '...' : 'not set',
    endpoint: 'https://janitorai.com/hackathon/chat/completions'
  })
}

```

### app/api/debug-env/route.ts

```typescript
import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({
    baseten_api_key: process.env.BASETEN_API_KEY ? 'SET' : 'NOT SET',
    baseten_model_id: process.env.BASETEN_MODEL_ID || 'NOT SET',
    janitor_api_key: process.env.JANITOR_AI_API_KEY ? 'SET' : 'NOT SET',
    elevenlabs_api_key: process.env.ELEVENLABS_API_KEY ? 'SET' : 'NOT SET',
  })
}

```

### app/api/livekit-token/route.ts

```typescript
import { NextResponse } from 'next/server'
import { getEnv } from '@/lib/env-validation'

export async function POST(request: Request) {
  try {
    const apiKey = getEnv('LIVEKIT_API_KEY')
    const apiSecret = getEnv('LIVEKIT_API_SECRET')
    
    if (!apiKey || !apiSecret) {
      return NextResponse.json({ 
        error: 'LiveKit not configured - please set LIVEKIT_API_KEY and LIVEKIT_API_SECRET environment variables' 
      }, { status: 500 })
    }

    // Parse request body for room name and identity
    const body = await request.json().catch(() => ({}))
    const roomName = body.roomName || 'werewolf-game'
    const playerName = body.playerName || `Player${Math.random().toString(36).slice(2, 6).toUpperCase()}`
    const identity = `${playerName}_${Date.now()}`
    
    // Dynamically import server SDK
    const { AccessToken } = await import('livekit-server-sdk')
    
    const token = new AccessToken(apiKey, apiSecret, {
      identity,
      name: playerName,
      ttl: '2h',
    })
    
    token.addGrant({
      room: roomName,
      roomJoin: true,
      canPublish: true,
      canSubscribe: true,
      canPublishData: true,
    })

    return NextResponse.json({ 
      token: await token.toJwt(),
      identity,
      roomName 
    })
  } catch (error) {
    console.error('Token generation error:', error)
    return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })
  }
}

```

### app/api/agent-webhook/route.ts

```typescript
import { NextResponse } from 'next/server'

// This endpoint simulates an agent for demo purposes
// In production, you'd use a real agent, but for CalHacks this is simpler!

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { roomCode, action, playerIdentity } = body
    
    console.log(`[Agent ${roomCode}] ${action} from ${playerIdentity}`)
    
    if (action === 'start_turn') {
      // Player started talking
      return NextResponse.json({ status: 'listening' })
    }
    
    if (action === 'end_turn') {
      // Player finished talking - generate response
      // Call your existing AI endpoint
      const aiResponse = await fetch(`${process.env.NEXT_PUBLIC_URL || 'http://localhost:3000'}/api/host`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          question: `Player ${playerIdentity} has made their case.`,
          gameContext: {
            phase: { kind: 'Discussion' },
            round: 1,
            alivePlayers: [],
          },
          provider: 'baseten'
        })
      })
      
      const data = await aiResponse.json()
      const response = data.answer || "I hear you. Continue."
      
      return NextResponse.json({ 
        status: 'responded',
        message: response 
      })
    }
    
    return NextResponse.json({ status: 'ok' })
  } catch (error) {
    console.error('Agent webhook error:', error)
    return NextResponse.json({ error: 'Agent error' }, { status: 500 })
  }
}

```

[21 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]