Skip to content

feat: AI Gamemaster gamemode (voice-first secret-rule games)#7

Merged
f1shyfang merged 28 commits into
mainfrom
feat/gamemaster-mode
Jun 1, 2026
Merged

feat: AI Gamemaster gamemode (voice-first secret-rule games)#7
f1shyfang merged 28 commits into
mainfrom
feat/gamemaster-mode

Conversation

@f1shyfang

Copy link
Copy Markdown
Owner

Summary

  • New standalone /gamemaster gamemode: FreeRooms routes a group to a real empty campus room, then an AI Gamemaster runs 9 classic "secret-rule" deduction games (Magic Moon, Umbrella Land, The Captain, Pineapples, Open & Closed, Triangle, Albert the Frog, Horses, Black Magic).
  • Deterministic rule engine (lib/gamemaster/rules/) decides every verdict/value and the win check — the AI (generateText via AI Gateway) only narrates, so it can never override a verdict. Canned fallback when the model is unavailable.
  • Voice end-to-end via the Vercel AI SDK: experimental_transcribe (/api/gamemaster/listen) for player input and experimental_generateSpeech (/api/gamemaster/speak) for the Gamemaster's voice, with a browser speechSynthesis fallback. Push-to-talk client with a typed fallback.
  • Upstash rate limiting on all three routes (3 weighted buckets; tighter on the paid transcribe/TTS calls).
  • Single shared device, no scoring, no DB changes — gameplay state is client-side only.
  • Spec + implementation plan under docs/superpowers/.

Built with brainstorming → spec → plan → subagent-driven TDD. 137 tests pass; the rule engine is fully unit-tested.

Test Plan

  • npm run test:run — 137 passed (32 files)
  • npx tsc --noEmit clean; eslint clean on all new code
  • Set .env.local: AI_GATEWAY_API_KEY, OPENAI_API_KEY, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
  • Manual QA on /gamemaster (iPhone 14–16 / Firefox responsive): find-room → deck → pick Magic Moon → hold-to-speak "a balloon" (accept) / "the sun" (reject); typed fallback when mic denied
  • Confirm the gpt-4o-mini-tts model + ballad voice against the OpenAI account

🤖 Generated with Claude Code

Michael Feng and others added 28 commits June 1, 2026 11:20
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- umbrella-land: cache lastWord() result; guard empty-string case explicitly
- types: document acceptedItems contract with JSDoc
- captain: broaden isRuleStatement regex to tolerate dropped apostrophes
- magic-moon: remove redundant trailing regex alternative
- black-magic: add test for missing-list guard
- registry: add blank line between final import and GAMES export

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds enforce(), getLimiters(), and identifierFrom() for rate-limiting
the three Gamemaster API routes. Limiters are lazily initialised so
importing the module never calls Redis.fromEnv() without env vars.
Tests inject fake limiters so Redis is never touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `narrate()` which calls the Vercel AI Gateway (openai/gpt-4o-mini)
to turn the engine's deterministic RuleResponse into in-character
Gamemaster patter, with a guaranteed canned fallback on any model error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /api/gamemaster/turn validates game ID, delegates verdict to the
rule engine (judge), then calls narrate for in-character patter.
Rate-limit check runs first; engine — not the model — owns the verdict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /api/gamemaster/listen passes raw audio bytes to Whisper via
experimental_transcribe and returns the transcript. Rate-limit check
runs before any work is done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /api/gamemaster/speak converts text to speech via
experimental_generateSpeech (gpt-4o-mini-tts, ballad voice) and
streams raw audio bytes with the provider's mediaType header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Uint8Array<ArrayBufferLike> is not assignable to BodyInit in the
project's TypeScript config; pass .buffer as ArrayBuffer instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix latent buffer bug: pass Uint8Array view directly to Response instead
  of .buffer (avoids sending extra bytes from a pooled ArrayBuffer)
- Tighten text guard in speak/route.ts to match /turn (typeof check)
- Assert engine/persona not reached after 429 in turn, listen, speak tests
- Add 502 tests for listen (transcription throws) and speak (TTS throws)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re-record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te-limit key

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 1, 2026 02:34
@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
unsw-quest Ready Ready Preview, Comment Jun 1, 2026 2:34am

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new /gamemaster mode to Quest: a voice-first “secret rule” party-game experience powered by a deterministic rule engine, with AI used only for narration plus STT/TTS routes and Upstash rate limiting.

Changes:

  • Introduces a fully unit-tested deterministic rules engine for 9 secret-rule games (lib/gamemaster/rules/*) with a registry + judge orchestrator.
  • Adds AI narration (generateText via AI Gateway) plus voice in/out API routes using Vercel AI SDK (experimental_transcribe, experimental_generateSpeech) and a client push-to-talk UI with typed and speechSynthesis fallbacks.
  • Adds Upstash Redis sliding-window rate limiting for /turn, /listen, and /speak, plus env scaffolding and a home-nav entry point.

Reviewed changes

Copilot reviewed 42 out of 43 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
package.json Adds AI SDK + Upstash dependencies for gamemaster mode.
package-lock.json Locks new AI SDK and Upstash dependency graph.
.env.example Documents required env vars for AI Gateway/OpenAI + Upstash rate limiting.
app/page.tsx Adds a nav link to the new /gamemaster surface.
app/gamemaster/page.tsx New gamemaster entry page.
app/gamemaster/gamemaster-shell.tsx Client orchestrator for room routing, game deck, and voice-first turn loop.
app/gamemaster/useSpeechInput.ts Push-to-talk recording + /listen transcription client hook with typed fallback.
app/gamemaster/voice-client.ts /speak TTS client with browser speechSynthesis fallback.
app/api/gamemaster/turn/route.ts Server route that runs deterministic judge + narration, rate-limited.
app/api/gamemaster/turn/route.test.ts Tests for /turn route wiring and rate-limit behavior.
app/api/gamemaster/listen/route.ts Server route for STT transcription, rate-limited.
app/api/gamemaster/listen/route.test.ts Tests for /listen route and error/limit handling.
app/api/gamemaster/speak/route.ts Server route for TTS audio generation, rate-limited.
app/api/gamemaster/speak/route.test.ts Tests for /speak route and error/limit handling.
lib/gamemaster/rate-limit.ts Upstash Redis rate limiting helper (lazily initialized).
lib/gamemaster/rate-limit.test.ts Unit tests for rate limiting helper behavior.
lib/gamemaster/gamemaster.ts AI persona narration layer with deterministic fallback lines.
lib/gamemaster/gamemaster.test.ts Tests for narration fallback behavior and AI call wiring.
lib/gamemaster/rules/types.ts Rule engine types + shared word helpers (pure/deterministic).
lib/gamemaster/rules/types.test.ts Unit tests for word helper utilities.
lib/gamemaster/rules/magic-moon.ts Magic Moon rule implementation.
lib/gamemaster/rules/magic-moon.test.ts Tests for Magic Moon rule + win-statement recognition.
lib/gamemaster/rules/umbrella-land.ts Umbrella Land rule implementation.
lib/gamemaster/rules/umbrella-land.test.ts Tests for Umbrella Land rule + rule-statement recognition.
lib/gamemaster/rules/captain.ts The Captain rule implementation.
lib/gamemaster/rules/captain.test.ts Tests for The Captain rule + rule-statement recognition.
lib/gamemaster/rules/pineapples.ts Pineapples rule implementation.
lib/gamemaster/rules/pineapples.test.ts Tests for Pineapples rule + rule-statement recognition.
lib/gamemaster/rules/open-and-closed.ts Open/Closed rule implementation (vowel-start heuristic).
lib/gamemaster/rules/open-and-closed.test.ts Tests for Open/Closed rule + rule-statement recognition.
lib/gamemaster/rules/triangle.ts Triangle rule implementation (throwaway word first).
lib/gamemaster/rules/triangle.test.ts Tests for Triangle rule + rule-statement recognition.
lib/gamemaster/rules/albert-the-frog.ts Albert the Frog rule implementation (ascending length).
lib/gamemaster/rules/albert-the-frog.test.ts Tests for Albert the Frog context-dependent behavior.
lib/gamemaster/rules/horses.ts Horses rule implementation (letters-in-question value).
lib/gamemaster/rules/horses.test.ts Tests for Horses value response + rule-statement recognition.
lib/gamemaster/rules/black-magic.ts Black Magic rule implementation (item after black).
lib/gamemaster/rules/black-magic.test.ts Tests for Black Magic list-dependent value response.
lib/gamemaster/rules/registry.ts Central registry of all nine games.
lib/gamemaster/rules/judge.ts Thin judge orchestrator delegating to selected game.
lib/gamemaster/rules/judge.test.ts Tests for registry completeness and judge behavior.
docs/superpowers/specs/2026-06-01-gamemaster-mode-design.md Design spec describing architecture, UX, and constraints.
docs/superpowers/plans/2026-06-01-gamemaster-mode.md Implementation plan outlining tasks and testing strategy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +27 to +29
if (!(body.gameId in GAMES)) {
return Response.json({ error: "unknown_game" }, { status: 400 });
}
Comment on lines +10 to +11
const audio = new Uint8Array(await req.arrayBuffer());
const { text } = await transcribe({ model: openai.transcription("whisper-1"), audio });
Comment on lines +12 to +14
if (typeof body.text !== "string" || !body.text) {
return Response.json({ error: "missing_text" }, { status: 400 });
}
Comment on lines +45 to +48
const stop = useCallback(() => {
recorderRef.current?.stop();
recorderRef.current = null;
}, []);
Comment on lines +73 to +88
const data = (await res.json()) as { text: string; accepted: boolean; response: TurnResponse };
setTurns((t) => [...t, { who: "gamemaster", text: data.text }]);
if (res.status === 429) {
await speak(data.text);
return;
}
if (data.response?.kind === "verdict" && data.response.fits) {
// Mirror the engine's lastWord(): lowercase, alphabetic-only tokens,
// so Albert the Frog's length comparison matches what the engine sees.
const item = utterance.toLowerCase().split(/[^a-z]+/).filter(Boolean).pop() ?? "";
setAccepted((a) => [...a, item]);
setWrongStreak(0);
} else if (data.response?.kind === "verdict") {
setWrongStreak((w) => w + 1);
}
await speak(data.text);
Comment on lines +10 to +11
const audio = new Uint8Array(await req.arrayBuffer());
const { text } = await transcribe({ model: openai.transcription("whisper-1"), audio });
Comment on lines +12 to +14
if (typeof body.text !== "string" || !body.text) {
return Response.json({ error: "missing_text" }, { status: 400 });
}
Comment on lines +45 to +48
const stop = useCallback(() => {
recorderRef.current?.stop();
recorderRef.current = null;
}, []);
Comment on lines +73 to +88
const data = (await res.json()) as { text: string; accepted: boolean; response: TurnResponse };
setTurns((t) => [...t, { who: "gamemaster", text: data.text }]);
if (res.status === 429) {
await speak(data.text);
return;
}
if (data.response?.kind === "verdict" && data.response.fits) {
// Mirror the engine's lastWord(): lowercase, alphabetic-only tokens,
// so Albert the Frog's length comparison matches what the engine sees.
const item = utterance.toLowerCase().split(/[^a-z]+/).filter(Boolean).pop() ?? "";
setAccepted((a) => [...a, item]);
setWrongStreak(0);
} else if (data.response?.kind === "verdict") {
setWrongStreak((w) => w + 1);
}
await speak(data.text);
Comment thread package.json
Comment on lines 13 to 20
"dependencies": {
"@ai-sdk/openai": "^3.0.67",
"@neondatabase/serverless": "^1.1.0",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.38.0",
"@vercel/blob": "^2.4.0",
"ai": "^6.0.193",
"drizzle-orm": "^0.45.2",
@f1shyfang f1shyfang merged commit 8688bc6 into main Jun 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants