# Hearts A multiplayer Hearts card game with real-time gameplay via WebSockets. Shares accounts with [Hokm](../hokm) — log in with the same credentials on both games. ## Requirements - Docker & Docker Compose - [Hokm](../hokm) installed at `/root/hokm` (provides card assets and shared user accounts) ## Install ```bash git clone https://git.goyban.com/goyban/hearts.git cd hearts cp .env.example .env # edit .env — see below ``` Then start: ```bash docker compose up -d --build ``` Or use the helper script (stops, rebuilds, and tails logs): ```bash ./rerun.sh ``` The game is available at: - HTTP: `http://:4000` - HTTPS: `https://:4443` (self-signed cert, needed for PWA install) ## .env reference ```dotenv # Secret used to sign JWT session tokens. Change this to a random string. JWT_SECRET=hearts-secret-change-me # Username that gets admin privileges (access to /admin panel). # Leave empty to disable admin. ADMIN_USERNAME= # HTTP and HTTPS ports (must match docker-compose.yml port mapping if changed). PORT=4000 HTTPS_PORT=4443 # Path to Hokm's users.json so Hokm accounts work here too. # Docker path (default, matches the volume mount in docker-compose.yml): SHARED_USERS_FILE=/hokm-data/users.json # If running directly with `node server.js` instead of Docker, use: # SHARED_USERS_FILE=/root/hokm/data/users.json # Optional: Resend (resend.com) for email verification codes on signup. # Leave blank to skip email verification entirely. RESEND_API_KEY= RESEND_FROM=noreply@example.com # Optional: Cloudflare Turnstile CAPTCHA on the signup form. # Get keys at dash.cloudflare.com → Turnstile. # Leave blank to disable CAPTCHA. TURNSTILE_SITE_KEY= TURNSTILE_SECRET= ``` ### Minimum required changes | Variable | What to set | |---|---| | `JWT_SECRET` | Any long random string (e.g. `openssl rand -hex 32`) | | `ADMIN_USERNAME` | Your username, to unlock the admin panel | Everything else is optional. ## Updating ```bash git pull ./rerun.sh ``` ## Data Game data is stored in `./data/`, bind-mounted into the container, and persists across restarts and rebuilds: | File | Contents | |---|---| | `users.json` | Local accounts (Hokm/Shelem accounts come from `SHARED_USERS_FILE`) | | `stats.json` | Per-player totals behind the profile and leaderboard | | `config.json` | Runtime config (e.g. whether signups are open) | | `history.json` | Finished games with their round-by-round breakdown | `history.json` keeps the most recent 2000 games. Games with a bot at the table are not recorded, so neither history nor the leaderboard counts them. ## Game history Every finished all-human game is saved with the score each player picked up in each hand. Players reach it from **Profile → 📜 Game History**; tapping a game shows the final standings and a hand-by-hand table with a ruled-off totals row. The same breakdown appears on the end-of-hand and end-of-game screens, so the running scores are visible from the first hand onwards rather than only at the end. ## Leaderboard Ranked by **win rate** — the percentage of finished games in which the player had the lowest score. A game that ends in a tie for lowest counts as a win for each tied player. Average score per game breaks ties in the ranking, and there *lower* is better, since Hearts scores are penalty points. A player needs **5 finished games** before they take a rank, so one lucky game can't top the board. Players below that are still listed underneath the ranking, unranked and dimmed, showing how many more games they need. Change `MIN_RANKED_GAMES` in `server.js` to adjust; the client reads the value back from `/api/config`, so the label follows automatically. Games with a bot at the table are not counted at all. ## Card-count integrity Hearts games could previously end early if one seat somehow got two cards into a single trick: that player ran out while everyone else still held cards. The server now rejects a second card from a seat that has already played in the current trick, refuses cards that aren't in the player's hand, and puts nobody on turn while a trick is resolving. As a backstop, it snapshots the table at the start of every trick (only when all four hands are level) and re-checks the counts after each trick and at the end of each hand. If they have drifted apart it rewinds to that last sound snapshot, tells every client (they show a brief notice), and play resumes from the start of that trick.