A full-stack, real-time implementation of the classic Hearts card game with single-player AI, online multiplayer, achievements, and a polished, responsive UI.
Live at hearts.shmem.dev
- Single-player vs. AI -- four difficulty levels from random play to Monte Carlo simulation
- Online multiplayer -- create a lobby, share a code, and play with friends in real time
- Lobby system -- host migration, AI backfill for empty seats, spectator mode
- Achievements -- 20+ tiered and secret achievements tracked across games
- User accounts -- registration with email verification, password reset, persistent stats
- Sound & music -- multiple sound variants per action, independent volume controls
- Animations -- card dealing, trick-taking, shoot-the-moon celebrations, and confetti
- Responsive design -- mobile-optimized game table with touch-friendly card selection
- Theming & preferences -- card deck styles, difficulty defaults, layout options
| Technology | Purpose |
|---|---|
| React 18 + TypeScript | UI framework |
| Vite | Build tool and dev server |
| Tailwind CSS 4 + CSS Modules | Styling |
| React Router 7 | Client-side routing |
| Socket.IO Client | Real-time WebSocket communication |
| Framer Motion | Card and UI animations |
| Howler.js | Sound effects and music |
| Radix UI | Accessible tooltip primitives |
| Font Awesome Pro | Icons |
| party-js | Confetti effects |
| Technology | Purpose |
|---|---|
| Flask | REST API framework |
| Flask-SocketIO + Eventlet | WebSocket server (3 namespaces: game, lobby, multiplayer) |
| PostgreSQL 16 | Persistent storage |
| SQLAlchemy + Flask-Migrate | ORM and schema migrations |
| PyJWT | JWT authentication |
| Argon2 | Password hashing |
| Flask-Limiter | Rate limiting |
| Flask-CORS | Cross-origin support |
| Technology | Purpose |
|---|---|
| Docker + Docker Compose | Containerized dev and prod environments |
| Nginx | Reverse proxy and static file serving (production) |
| GitHub Actions | CI/CD -- builds and pushes images to GHCR on version tags |
| Watchtower | Auto-deploys new images in production |
| Husky + lint-staged | Pre-commit formatting and linting |
ββββββββββββββββ WebSocket / REST ββββββββββββββββ
β β ββββββββββββββββββββββββββββΊ β β
β React SPA β (Socket.IO) β Flask API β
β (Vite) β β (Eventlet) β
β β β β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β
β Nginx (prod) / Vite proxy (dev) β SQLAlchemy
β serves static + proxies /api, /socket.io β
β βΌ
β ββββββββββββββββ
β β PostgreSQL β
ββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββ
The frontend is a single-page app that communicates with the backend over REST (game CRUD, auth, stats) and WebSockets (real-time gameplay). In production, Nginx serves the built SPA and reverse-proxies API and Socket.IO traffic to the Flask container.
The game includes four AI difficulty tiers:
| Difficulty | Strategy |
|---|---|
| Easy | Random legal play |
| Medium | Rule-based heuristics (void creation, point avoidance, safe leads) |
| Hard / Harder / Hardest | Determinized Monte Carlo -- samples possible opponent hands consistent with observed play, simulates remaining tricks, and picks the move with the lowest expected score. Higher tiers increase the simulation budget. |
The hard AI tracks opponent voids inferred from trick play and never peeks at hidden cards, ensuring fair play while still providing a strong challenge.
Multiplayer uses a lobby-based flow:
- Create a lobby and receive a 6-character room code
- Share the code with friends, who join via the code
- Fill remaining seats with AI at any difficulty
- Play in real time with per-player state views (you only see your own hand)
Additional multiplayer features:
- 120-second reconnect window before auto-concede
- Host migration if the creator disconnects
- Spectator support
- AI takeover on concede
- Docker & Docker Compose v2
- Node.js 20+ (for local frontend dev)
- Python 3.9+ (for local backend dev, managed via uv)
The root .env file is read by Docker Compose and configures Postgres, the Flask API, and mail settings. It is never committed (listed in .gitignore). The frontend uses a relative /api path for all API calls, so no frontend env vars are needed -- in development the Vite dev server proxies /api and /socket.io to the API container; in production Nginx does the same.
| Variable | Used by | When | Required |
|---|---|---|---|
POSTGRES_USER |
db, api | Runtime | Yes |
POSTGRES_PASSWORD |
db, api | Runtime | Yes |
POSTGRES_DB |
db, api | Runtime | Yes |
JWT_SECRET |
api | Runtime | Yes |
CORS_ORIGINS |
api | Runtime | Yes (prod) |
FRONTEND_URL |
api | Runtime | Yes (prod) |
MAIL_SERVER |
api | Runtime | For email |
MAIL_PORT |
api | Runtime | For email |
MAIL_USE_TLS |
api | Runtime | For email |
MAIL_USERNAME |
api | Runtime | For email |
MAIL_PASSWORD |
api | Runtime | For email |
MAIL_DEFAULT_SENDER |
api | Runtime | For email |
FONTAWESOME_PACKAGE_TOKEN |
web | Build-time | Yes |
-
Clone the repo and copy the example environment file (defaults work for local dev):
git clone https://github.com/shmemcat/hearts.git cd hearts cp .env.example .env -
Start the full stack with hot reload:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
- Frontend: http://localhost:3001
- API: http://localhost:5001
- Postgres: localhost:5432
-
To rebuild after dependency changes:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
-
Stop everything:
docker compose down
Migrations run automatically when the API container starts. To create a new migration after changing models:
docker compose exec api flask db migrate -m "describe the change"Then commit the new file in migrations/versions/ and restart the API container to apply it.
To manually run pending migrations:
docker compose exec api flask db upgradecd web
npm test # watch mode
npm run test:run # single run
npm run test:coverageUses Vitest with Testing Library and jsdom.
cd api
uv run pytestUses pytest with fixtures for the Flask app and database.
- Frontend: ESLint + Prettier, enforced via pre-commit hook
- Backend: Black formatter, enforced via pre-commit hook
- Git hooks: Husky + lint-staged runs formatters and linters on staged files
This is a personal project and is not currently licensed for redistribution.