A mobile-friendly web application for playing ancient and classic board games online with multiplayer support. Play with friends without registration - just create a session and share the code!
- 🎲 Six Games: Royal Game of Ur, Senet, Nine Men's Morris, Wolves & Ravens, Rock-Paper-Scissors, and Stellar Siege
- 👥 Multiplayer: Real-time gameplay using Socket.io
- 📱 Mobile-Friendly: Responsive design with touch controls
- 🚀 No Registration: Guest play with display names
- 🔄 Real-time Updates: Instant game state synchronization
- 🏆 Tournaments: Bo1/Bo3/Bo5/Bo7 and round-robin formats
- 🐳 Containerized: Docker and Kubernetes ready
- 💬 In-Game Chat: Real-time chat during sessions
- 🔒 Production Ready: Health checks, TLS, and resource limits
- 🤖 AI Opponents: Play against computer-controlled bots with optional LLM commentary via Ollama
- Frontend: React 18 + Vite + TypeScript + Tailwind CSS
- Backend: Node.js + Express + TypeScript + Socket.io
- Database: MongoDB
- Deployment: Docker + Kubernetes + Traefik
/
├── frontend/ # React application
├── backend/ # Express server + game engines
├── shared/ # Shared TypeScript types
├── k8s/ # Kubernetes manifests
├── Dockerfile # Multi-stage container build
└── docker-compose.yml # Local development setup
- Node.js 18+
- MongoDB (or use Docker Compose)
- npm
- Install dependencies
npm install- Setup environment variables
Create backend/.env from the example:
cd backend
cp .env.example .envEdit .env with your MongoDB connection string and optional basic auth credentials:
MONGODB_URI=mongodb://localhost:27017/ancient-games
AUTH_USERNAME=admin
AUTH_PASSWORD=your-secure-passwordEnvironment Variables:
MONGODB_URI- MongoDB connection stringAUTH_USERNAME- Username for basic auth on feedback endpoints (optional)AUTH_PASSWORD- Password for basic auth on feedback endpoints (optional)
Note: The feedback endpoints (GET /api/feedback, DELETE /api/feedback/clear, DELETE /api/feedback/:id) are protected with HTTP basic auth. If AUTH_USERNAME and AUTH_PASSWORD are not set, these endpoints will return 403 Forbidden for all requests.
AI bots use an expectiminimax engine and work without any external services. Optionally, bots can generate in-character commentary after notable moves using a locally-running Ollama instance.
To enable LLM commentary:
-
Install Ollama — download from https://ollama.com and follow the installer for your OS.
-
Pull a model — the default model is
llama3.2:1b(fast, ~800 MB):ollama pull llama3.2:1b
Any other model supported by Ollama can be used (e.g.
llama3.2:3b,mistral). The model is selected per-session when creating a game with a bot opponent. -
Start Ollama — it runs as a local HTTP server on port 11434:
ollama serve
Ollama starts automatically on most platforms after installation. Verify it is running:
curl http://localhost:11434/api/tags
-
Enable commentary in the UI — when creating a session with an AI opponent, enable the "AI Commentary" option and optionally specify a model name. If Ollama is not running or the model is unavailable, commentary is silently skipped and the game continues normally.
Notes:
- Set the
OLLAMA_URLenvironment variable to point the backend at a remote Ollama instance (default:http://localhost:11434). - Commentary generation has a 3-second timeout. Slow models will time out silently.
- Ollama is never required — AI opponents work fully without it.
Using Docker Compose (shared Ollama instance):
Ollama is included as a service in docker-compose.yml. An ollama-init sidecar pulls the default model (llama3.2:1b) on first run. The app container is automatically pointed at it via OLLAMA_URL=http://ollama:11434.
To use a different model, set OLLAMA_MODEL before starting:
OLLAMA_MODEL=llama3.2:3b docker-compose up --buildModel weights are stored in the ollama_data Docker volume and survive restarts.
Using Kubernetes (shared Ollama instance):
kubectl apply -f k8s/ollama.yaml
kubectl apply -f k8s/deployment.yamlk8s/ollama.yaml creates:
- A
Deploymentrunningollama/ollama:latestwith an init container that pulls the model on first start - A
ClusterIPService atollama-service:11434 - A
PersistentVolumeClaim(5 Gi) for model weights - A
ConfigMapto set the model name (default:llama3.2:1b)
To change the model, edit the ollama-config ConfigMap and restart the Ollama pod:
kubectl patch configmap ollama-config -p '{"data":{"model":"llama3.2:3b"}}'
kubectl rollout restart deployment/ollamaThe app deployment is pre-configured to use http://ollama-service:11434 via the OLLAMA_URL env var.
- Start MongoDB (if not using external service)
docker run -d -p 27017:27017 --name mongodb mongo:7- Start the development servers
In separate terminals:
# Terminal 1: Start backend
npm run dev:backend
# Terminal 2: Start frontend
npm run dev:frontend- Access the application
Open http://localhost:5173 in your browser.
The easiest way to run the full stack locally:
# Build and start all services
docker-compose up --build
# Access the application
open http://localhost:3000To stop:
docker-compose downdocker build -t ancient-games:latest .docker run -d \
-p 3000:3000 \
-e MONGODB_URI=mongodb://your-mongodb:27017/ancient-games \
-e NODE_ENV=production \
ancient-games:latest- Kubernetes cluster (1.19+)
- kubectl configured
- Traefik ingress controller
- cert-manager (for TLS certificates)
- Build and push Docker image
docker build -t your-registry/ancient-games:latest .
docker push your-registry/ancient-games:latest- Update image in deployment
Edit k8s/deployment.yaml and update the image field:
image: your-registry/ancient-games:latest- Update domain in ingress
Edit k8s/ingress.yaml and replace games.yourdomain.com with your domain.
- Deploy MongoDB (optional)
For development/testing, you can deploy MongoDB in-cluster:
kubectl apply -f k8s/mongodb.yamlFor production, use MongoDB Atlas or a managed service, and update the secret in k8s/deployment.yaml.
- Deploy the application
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/ingress.yaml- Verify deployment
# Check pods
kubectl get pods
# Check service
kubectl get svc
# Check ingress
kubectl get ingress
# View logs
kubectl logs -l app=ancient-games- Scale deployment
kubectl scale deployment ancient-games --replicas=3The ingress is configured to use cert-manager with Let's Encrypt. Ensure you have cert-manager installed:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yamlCreate a ClusterIssuer for Let's Encrypt:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefikApply it:
kubectl apply -f cluster-issuer.yaml- Players: 2
- Pieces: 7 per player
- Dice: 4 binary dice (0-4 result)
- Board: 20 squares with special rosette squares
- Objective: First to get all pieces off the board wins
- Special: Landing on a rosette gives an extra turn and safe space (except middle rosette which allows captures)
- Players: 2
- Pieces: 5 per player starting on alternating squares
- Dice: 4 stick dice (0-5 result)
- Board: 30 squares in S-shaped path
- Objective: First to get all pieces off the board wins
- Special Squares:
- House of Rebirth (14): Captured pieces restart here
- House of Beauty (25): Need exact roll to leave
- House of Water (26): Returns piece to House of Rebirth
- Extra Turns: Rolling 1, 4, or 5 gives another turn
The platform uses an abstract GameEngine interface that makes adding new games easy:
interface GameEngine {
gameType: string;
playerCount: number;
initializeBoard(): BoardState;
rollDice(): number;
validateMove(board, move, player): boolean;
applyMove(board, move): BoardState;
checkWinCondition(board): number | null;
getValidMoves(board, player, diceRoll): Move[];
}To add a new game:
- Create
backend/src/games/[gamename]/[GameName]Game.tsimplementingGameEngine - Register in
GameRegistry.ts - Create frontend component in
frontend/src/components/games/[gamename]/ - Add route in
GameRoom.tsx
Socket.io events:
Client → Server:
session:join- Join game sessionsession:leave- Leave sessionsession:ready- Toggle ready statusgame:start- Start game (host only)game:roll-dice- Roll dicegame:move- Make a movegame:skip-turn- Skip turn
Server → Client:
session:updated- Session state changedgame:started- Game startedgame:dice-rolled- Dice rolledgame:move-made- Move madegame:turn-changed- Turn changedgame:ended- Game finishedgame:error- Error occurred
The application exposes a health endpoint:
curl http://localhost:3000/healthResponse:
{
"status": "ok",
"timestamp": "2024-01-01T00:00:00.000Z"
}In Kubernetes:
# View logs
kubectl logs -f deployment/ancient-games
# View logs from all replicas
kubectl logs -l app=ancient-games --all-containers=true -fThis is a monorepo using npm workspaces:
frontend- React applicationbackend- Express servershared- Shared TypeScript types
# Build all packages
npm run build
# Build specific package
npm run build:frontend
npm run build:backendThe shared package contains TypeScript interfaces used by both frontend and backend, ensuring type safety across the entire application.
If you see MongooseServerSelectionError:
- Check MongoDB is running:
docker ps - Verify connection string in
.env - Check network connectivity
- Ensure CORS is configured correctly
- Check firewall settings
- Verify WebSocket support in proxy/ingress
Clear node_modules and reinstall:
rm -rf node_modules frontend/node_modules backend/node_modules shared/node_modules
npm installMIT
Contributions welcome! Please open an issue or PR.
- User accounts and game history
- Game replays
- Additional games (Mancala, etc.)
- Sound effects and animations