A prompt-driven, LLM-generated text RPG with streaming narration and interactive choices. Built with Svelte, TailwindCSS, Node.js, and OpenRouter.
✨ 3 Game Modes
- 🏰 Dungeon Crawl: Explore dark catacombs filled with monsters, traps, and treasure
- ⚔️ Hero's Journey: Epic quest with companions and moral choices
- 🔍 Mystery Night: Solve noir crimes with clues and time pressure
🎮 Core Gameplay
- Real-time streaming narration with cursor animation
- 2-4 interactive choices per turn
- Token budget tracking with visual meter
- Anonymous sessions with 7-day expiry
- Complete state machine preventing UI race conditions
💾 Save/Load System
- Up to 5 save slots per browser
- Export saves as JSON files
- Import saves from other sessions
- Auto-save metadata (mode, turns, tokens)
🧪 Quality Assurance
- Comprehensive unit tests (Vitest)
- State machine validation
- Error recovery with retry capability
- History management prevents context overflow
Frontend:
- Svelte 5 + TypeScript
- TailwindCSS (with typography and forms plugins)
- Vite (build tool)
- Vitest (testing)
Backend:
- Node.js + Express + TypeScript
- OpenRouter API (model-agnostic LLM)
- Pino (structured logging)
- Server-Sent Events (SSE streaming)
- Node.js 18+ and npm
- OpenRouter API key (get one here)
git clone <your-repo-url>
cd Nyx
# Install backend dependencies
cd backend
npm install
# Install frontend dependencies
cd ../frontend
npm installBackend Configuration:
cd backend
cp .env.example .envEdit backend/.env and add your OpenRouter API key:
OPENROUTER_API_KEY=sk-or-v1-your-actual-key-here
PORT=3000
NODE_ENV=development
ALLOWED_ORIGINS=http://localhost:5173
TOKEN_BUDGET_PER_SESSION=20000Frontend Configuration:
cd ../frontend
cp .env.example .envThe default VITE_API_URL=http://localhost:3000 should work for local development.
Terminal 1 - Backend:
cd backend
npm run devBackend will start on http://localhost:3000
Terminal 2 - Frontend:
cd frontend
npm run devFrontend will start on http://localhost:5173
- Open http://localhost:5173 in your browser
- Choose a game mode (Dungeon, Journey, or Mystery)
- Watch the streaming narration appear
- Click choices to advance the story
- Save your progress anytime with the Save button
cd frontend
# Run tests
npm test
# Run tests with UI
npm run test:ui
# Run tests with coverage
npm run test:coveragecd backend
# Run tests
npm test
# Run tests with coverage
npm run test:coverageThe project includes comprehensive tests for:
Frontend:
- ✅ gameStore (state machine, transitions, events)
- ✅ Component rendering and interactions
- ✅ Save/load functionality
Backend:
- ✅ promptService (template generation, JSON parsing)
- ✅ historyManager (sliding window, token estimation)
- ✅ Session management
- ✅ API endpoints
Nyx/
├── backend/
│ ├── src/
│ │ ├── index.ts # Express server
│ │ ├── types/index.ts # TypeScript types
│ │ ├── routes/
│ │ │ ├── session.ts # Session management API
│ │ │ └── play.ts # Game turn API (SSE streaming)
│ │ └── services/
│ │ ├── openRouterClient.ts # LLM client
│ │ ├── sessionManager.ts # Session tracking
│ │ ├── historyManager.ts # Context management
│ │ ├── promptService.ts # Prompt templates
│ │ └── __tests__/ # Unit tests
│ ├── package.json
│ ├── tsconfig.json
│ └── .env.example
│
├── frontend/
│ ├── src/
│ │ ├── App.svelte # Root component
│ │ ├── app.css # Tailwind + custom styles
│ │ ├── stores/
│ │ │ └── gameStore.ts # State machine
│ │ ├── components/
│ │ │ ├── StoryPane.svelte # Narration display
│ │ │ ├── ChoiceList.svelte # Interactive choices
│ │ │ ├── TokenMeter.svelte # Budget tracker
│ │ │ └── SaveSlotModal.svelte # Save/load UI
│ │ ├── pages/
│ │ │ └── Play.svelte # Main game page
│ │ ├── services/
│ │ │ ├── api.ts # Backend API client
│ │ │ └── storage.ts # localStorage manager
│ │ └── test/
│ │ └── setup.ts # Test configuration
│ ├── package.json
│ ├── tailwind.config.js
│ └── .env.example
│
└── Documentation/
├── ai_text_rpg_phase1_mvp_plan.md
├── ai_text_rpg_phase1.5_implementation_details.md
└── ai_text_rpg_phase2_enhancements.md
The game uses a formal state machine with 9 states and validated transitions:
uninitialized → mode_selection → starting → streaming →
awaiting_choice → processing_input → (loop back to streaming)
Error states: error_recoverable, error_fatal
Utility states: paused, game_over
This prevents common bugs like:
- Clicking choices during streaming
- Race conditions between events
- Invalid state transitions
Prevents context window overflow with sliding window strategy:
- Keeps last 8 turns verbatim
- Summarizes older turns using Claude Haiku
- Estimates token usage
- Auto-prunes when approaching limits
Frontend → POST /api/play → Backend → OpenRouter API
↓
SSE Stream ← Parse chunks ← LLM response
↓
Update UI in real-time
Create a new anonymous session.
Response:
{
"sessionId": "abc123",
"token": "xyz789",
"tokenBudget": 20000,
"expiresIn": 604800
}Execute a game turn with SSE streaming.
Request:
{
"sessionId": "abc123",
"mode": "dungeon",
"history": [...],
"player_input": "Go left",
"model": "anthropic/claude-3-haiku",
"temperature": 0.7,
"max_tokens": 600
}Response: SSE stream with chunks and final output
Health check endpoint.
| Variable | Description | Default |
|---|---|---|
OPENROUTER_API_KEY |
OpenRouter API key (required) | - |
PORT |
Server port | 3000 |
NODE_ENV |
Environment | development |
ALLOWED_ORIGINS |
CORS origins (comma-separated) | http://localhost:5173 |
MODEL_DEFAULT |
Default LLM model | anthropic/claude-3-haiku |
TOKEN_BUDGET_PER_SESSION |
Max tokens per session | 20000 |
LOG_LEVEL |
Logging level | info |
Any OpenRouter-compatible model works. Recommended:
anthropic/claude-3-haiku(fast, cheap, good quality)anthropic/claude-3.5-sonnet(best quality, higher cost)openai/gpt-4(alternative, higher cost)meta-llama/llama-3.1-70b-instruct(good balance)
Problem: Frontend can't connect to backend
- Check backend is running on port 3000
- Verify CORS settings in
backend/.env - Check browser console for errors
Problem: Streaming not working
- Ensure you're using a modern browser (Chrome, Firefox, Safari)
- Check network tab for SSE connection
- Verify OpenRouter API key is valid
Problem: Token budget exceeded
- Reduce
max_tokensin game settings - Lower
TOKEN_BUDGET_PER_SESSIONin backend .env - Start a new session (reset game)
Problem: LLM not returning valid JSON
- Try a different model (Claude models work best)
- Check backend logs for parsing errors
- System falls back to text-only mode automatically
# Frontend
cd frontend
npm run check
# Backend
cd backend
npm run build # TypeScript compilation check- Add mode to
GameModetype infrontend/src/stores/gameStore.ts - Add lore to
MODE_LOREinbackend/src/services/promptService.ts - Add initial prompt to
buildInitialPrompt() - Add mode card to
frontend/src/pages/Play.svelte
Edit backend/src/services/promptService.ts:
BASE_SYSTEM_PROMPT: Core LLM instructionsMODE_LORE: Setting and atmosphere for each modeSAFETY_ADDENDUM: Content safety rules
Frontend (Netlify/Vercel):
cd frontend
npm run build
# Deploy dist/ folderBackend (Fly.io/Render):
cd backend
npm run build
# Deploy with Dockerfile or platform CLIdocker-compose up -d(Docker configuration not yet included - see Phase 2 plan)
See ai_text_rpg_phase2_enhancements.md for planned features:
- Enhanced security (JWT sessions, rate limiting)
- LLM-based smart summarization
- Better error recovery and circuit breakers
- Performance optimizations
- Dark/light theme toggle
- Multiplayer support
- And more!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for new functionality
- Ensure all tests pass (
npm test) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - See LICENSE file for details
- Built with OpenRouter for model-agnostic LLM access
- Inspired by classic text adventure games
- Special thanks to Anthropic for Claude models
- 📖 Documentation: See
/Documentationfolder - 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
Happy Adventuring! 🎮✨