Two teams of LLMs play Codenames against each other. Each team fields an AI Spymaster that gives clues and AI Operatives that discuss and guess, running as Mastra agents and workflows on top of models served through OpenRouter.
The Blue and Red teams can be backed by different models, letting you pit one model against another and watch a full game play out — clues, operative discussion, guesses, and win/loss conditions included.
A game is a Mastra workflow that runs until one team wins:
gameWorkflow
└─ setupStep create a fresh 25-card board (9/8 split, 1 assassin)
└─ dowhile(game.winner == null)
└─ gamePhaseRouterWorkflow branch on the current phase
├─ Spymaster phase → spymasterAgent gives a { clue, number }
└─ Operative phase → operativeAgent discusses + guesses
src/codenames/— a self-contained, framework-agnostic game engine.Game(ingame.ts) is an immutable state machine:Game.create()deals a board,takeAction()returns a newGamefor each clue, guess, or end-turn, and it enforces all the rules (turn continuation, the "plus one" bonus guess, assassin = instant loss, win detection).wordlist.tsholds the 400-word pool;description.tsis the shared rules explainer fed to both agents.src/mastra/agents/— thespymasterAgentandoperativeAgent. Each has a detailed system prompt, a per-team model selected via request context, and a helper that formats the current game state into a role-appropriate view (the spymaster sees every card's identity; operatives see only revealed cards).src/mastra/workflows/— the game loop described above. The operative phase exposesDiscuss,Guess, andEnd turn voluntarilytools so operatives can deliberate before committing; the spymaster phase uses structured output to return a validated clue.src/mastra/config/— model selection (models.ts) and LibSQL storage (storage.ts).
- Node.js
>=22.13.0 - An OpenRouter API key
npm installCreate a .env file with:
# API key for OpenRouter (used for all team models)
OPENROUTER_API_KEY=sk-or-v1-xxxThe two teams are configured in src/mastra/config/models.ts:
export const teamBlueModel = openRouter("google/gemini-3-flash-preview");
export const teamRedModel = openRouter("openai/gpt-5-mini");Swap in any model slug OpenRouter supports to change the matchup.
Start the Mastra dev server, which exposes a UI/playground for running the
Game workflow and inspecting each agent's traces:
npm run devThen open the workflow in the Mastra playground and trigger a run (the Game
workflow takes no input).
| Script | Description |
|---|---|
npm run dev |
Start the Mastra dev server / playground |
npm run build |
Build with Mastra |
npm start |
Start the built app |
npm test |
Run the Vitest suite (game-engine tests in src/codenames/game.test.ts) |
npm run lint / npm run lint-fix |
Lint with ESLint |
npm run prettier-check / npm run prettier-write |
Check / apply Prettier formatting |
- Mastra — agent and workflow framework
- OpenRouter via
@openrouter/ai-sdk-provider— model access - Zod — schema validation and structured agent output
- LibSQL — Mastra storage
- Vitest — testing
- TypeScript with
~/*path aliases tosrc/