A modern, online multiplayer recreation of the Royal Game of Ur — built with TypeScript, Next.js, NestJS, and React Three Fiber. Designed for async play (up to 7 days per turn), device-based persistence, and strong backend game validation.
This project brings the ancient Mesopotamian board game into the modern digital era — featuring:
- Online 2-player asynchronous multiplayer
- Turn-based play with 7-day timeout
- Device-based identity (no login)
- Backend-authoritative move validation
- Beautiful 3D visuals rendered with React Three Fiber
- Fully TypeScript-typed end-to-end
This is a Turborepo monorepo using pnpm workspaces:
game-of-ur/
│
├─ apps/
│ ├─ web/ # Next.js frontend (React + R3F)
│ └─ api/ # NestJS backend (game logic + state)
│
├─ packages/
│ └─ shared/ # Shared TypeScript types, Zod schemas, and core logic
│
├─ specs/ # Phase-by-phase build specs
│ ├─ phase0-setup.md
│ ├─ phase1-users.md
│ ├─ phase2-game-creation.md
│ ├─ phase3-game-rules.md
│ ├─ phase4-homepage.md
│ ├─ phase5-games-view.md
│ └─ phase6-game-board.md
│
├─ turbo.json
├─ docker-compose.yml # Full local stack (web + api + postgres)
├─ pnpm-workspace.yaml
├─ tsconfig.json
├─ .editorconfig
└─ README.md
| Layer | Framework | Key Libraries |
|---|---|---|
| Frontend | Next.js (React, R3F, Drei, Framer Motion) | TanStack Query, Zod |
| Backend | NestJS | class-validator, class-transformer, Zod |
| Shared | TypeScript | Game logic, Zod schemas |
| Monorepo | TurboRepo + pnpm | ESLint, Prettier, Jest, Vitest |
Each user is identified by a deviceSecret, generated server-side when they first visit the site.
- Stored locally (in
localStorage) - Never transferable or manually shareable
- Used to authenticate moves and actions
- Player A creates a game → receives a shareable link.
- Player B opens it → automatically joins as the second player.
- Both take turns rolling dice and making moves.
- If a player fails to move within 7 days, the other player wins automatically.
- Dice rolls, move validation, captures, and rosette rules handled entirely on the server.
- Frontend never calculates legal moves independently.
- Prevents cheating or move manipulation.
- 3D board rendered via React Three Fiber + Drei.
- Animated dice rolls and piece movements.
- Async play with background polling for turn updates (no WebSocket in MVP).
| Tool | Purpose |
|---|---|
| TurboRepo | Pipeline management for build/lint/test/dev |
| pnpm | Package manager |
| TypeScript (strict) | Shared typing and validation |
| Zod | Runtime schema validation |
| ESLint + Prettier | Code consistency |
| Vitest + Jest | Unit and integration testing |
| Husky + Lint-Staged | Git pre-commit hooks |
| EditorConfig | Editor consistency |
-
Install deps:
pnpm install
-
Start via Docker:
docker compose up
-
Visit:
- Frontend → http://localhost:3000
- API → http://localhost:3001
- Database → PostgreSQL on
localhost:5432
| Variable | Used By | Description |
|---|---|---|
DATABASE_URL |
Backend | PostgreSQL connection string |
NEXT_PUBLIC_API_URL |
Frontend | URL to backend (proxied via /api) |
NODE_ENV |
All | Environment (development/production) |
Create an .env in /apps/api:
DATABASE_URL=postgres://ur:urpass@postgres:5432/urdb
NODE_ENV=development
The project is developed via spec-driven phases, each defined under /.spec.
Each spec describes exactly what should be implemented before moving forward.
| Phase | Focus | Status |
|---|---|---|
| Phase 0 | Monorepo scaffolding, linting, testing setup | ✅ Done |
| Phase 1 | Device identity and persistence (deviceSecret model) | ✅ Done |
| Phase 2 | Game creation and joining logic | ✅ Done |
| Phase 3 | Game rules, dice rolls, and move logic | ✅ Done |
| Phase 4 | Homepage redesign with 3D clay tablet aesthetic | ✅ Done |
| Phase 5 | Games view wireframe and active games dashboard | ✅ Done |
| Phase 6 | Game board interface and 2D board rendering | ✅ Done |
| Phase 7 | 3D board rendering and enhanced visuals | ⏳ Next |
| Phase 8 | Async multiplayer and turn timeouts | 🔜 |
| Phase 9 | Polish, UX, and final improvements | 🔜 |
- Authoritative backend — all moves validated server-side
- Stateless frontend — UI as reflection of game state
- Strong typing across every layer
- LLM-spec driven — each development phase fully documented
- Incremental development — MVP first, extensibility later
Frontend calls the backend through a local proxy:
// next.config.js
async rewrites() {
return [
{ source: '/api/:path*', destination: 'http://localhost:3001/:path*' }
]
}✅ Simplifies local dev (no CORS) ✅ Unified domain in production
- Real-time WebSocket updates
- Cloud storage (PostgreSQL or Redis)
- Replayable match history
- Spectator mode
- AI opponent mode
- Mobile-friendly touch input