A fullstack monorepo implementation of the card game War with real-time multiplayer, AI opponents, and Redis-backed game state.
- Real-time multiplayer via GraphQL subscriptions over WebSockets
- Single-player vs AI with automatic turn execution
- Step-by-step war resolution — dramatic face-down / face-up card reveals
- Battle logs persisted in Redis for every game
- Opponent deck size visible at all times
- Auto-forfeit on disconnect or page leave (with 5-second refresh grace period)
- Game expiry — old games auto-cleaned after 30 minutes of inactivity
- Matchmaking — auto-join open lobbies or create new games
- Single active game enforcement — prevents duplicate lobbies per player
| Layer | Technology |
|---|---|
| Monorepo | npm workspaces |
| Backend | Fastify 5 + Mercurius 16 + TypeScript |
| Real-time | graphql-ws over WebSockets |
| Data Store | Redis 7 (hashes, lists, pub/sub) |
| Frontend | Nuxt 3.21.2 (SSR) + Vue 3 Composition API |
| State | Pinia |
| Styling | Tailwind CSS |
| Testing | Vitest |
| Containers | Docker + Docker Compose |
| Deployment | Fly.io |
- Node.js 22+
- Docker & Docker Compose
- npm 10+
npm installStarts Redis, backend, and frontend:
docker-compose up --build- Frontend: http://localhost:3000
- Backend: http://localhost:3001
- GraphQL Playground: http://localhost:3001/graphql
# Terminal 1 — Start Redis
docker-compose up -d redis
# Terminal 2 — Backend
npm run dev --workspace=apps/backend
# Terminal 3 — Frontend
npm run dev --workspace=apps/frontend# One-command deploy (backend + frontend)
./deploy.shRequires a Fly.io account. The backend runs Redis + Fastify in a single machine. See fly.toml and fly.frontend.toml for configuration.
- GameService — deterministic game logic (draw, compare, war recursion, win detection, war tie on insufficient cards)
- AIService — watches game state via Redis pub/sub; auto-plays AI turns after 0.8–2.0s human-like delay
- RedisStore — ioredis wrapper (single-node; marked with
// SCALE:for cluster upgrade) - GameRepository — Redis hashes with JSON fields + 30-min TTL
- BattleLogRepository — Redis lists capped at 100 entries
- RedisPubSub — Redis pub/sub powering GraphQL subscriptions; supports multiple concurrent subscribers
- Forfeit logic —
leaveGamemutation ends the game if inPLAYINGstatus
- Pages:
/lobby(matchmaking),/game/[id](play) - Stores:
userStore,gameStore,socketStore - Composables:
useGraphQL(mutations/queries via$fetch) - Real-time: Single subscription
gameUpdated(gameId)pushes full Game state; UI derives all events from it
- Standard 52-card deck (values 2–14, Ace = 14)
- Shuffled and split 26/26
- Each Play Turn reveals top cards simultaneously
- Higher card wins the pile (added to winner's score pile)
- Tie → WAR: each player places 1 face-down + 1 face-up card; compare again recursively
- Insufficient cards during war → war ties — cards returned to their owners, game continues
- Game ends when a player has 0 cards and can't play, or forfeits
# Utils
npm run test --workspace=packages/utils
# Backend
npm run test --workspace=apps/backend
# Frontend
npm run test --workspace=apps/frontendREDIS_URL— Redis connection string (default:redis://localhost:6379)PORT— HTTP port (default:3001)NODE_ENV—developmentorproduction
NUXT_PUBLIC_API_URL— GraphQL HTTP endpoint (default:http://localhost:3001/graphql)NUXT_PUBLIC_WS_URL— GraphQL WS endpoint (default:ws://localhost:3001/graphql)
Marked throughout the codebase with // SCALE: comments:
- RedisStore → Redis Cluster or AWS ElastiCache
- BattleLogRepository → append-only event store (Kafka/EventStoreDB)
- graphql-ws → sticky sessions or managed WebSocket gateway
- GameService → snapshot game state to PostgreSQL every N turns for recovery
war/
├── apps/
│ ├── backend/ # Fastify + Mercurius + Redis
│ └── frontend/ # Nuxt 3 + Pinia + Tailwind
├── packages/
│ ├── types/ # Shared TypeScript types
│ └── utils/ # Deck logic, shuffle, helpers
├── docker-compose.yml # Redis + backend + frontend
├── fly.toml # Backend Fly.io config
├── fly.frontend.toml # Frontend Fly.io config
├── deploy.sh # One-command deployment script
├── PLAN.md # Detailed implementation plan
└── AGENTS.md # Agent quick-start guide
MIT