Files
hearts/README.md
T
goyban dc051cbe45 Port Shelem layout/engine, add history, fix card flashing and count desync
Layout & engine borrowed from Shelem: waiting-room menu with seat swap and
force-start, game menu with bar-to-bottom, escalating turn reminder, three
hand modes defaulting to fan on touch, card-count badges instead of card
backs, leave buttons inside phase overlays, version footer with tap-to-update.

Seats now run clockwise from the viewer (left = +1, across = +2, right = +3)
so play flows anti-clockwise on screen as the server advances it; the pass
exchange was flipped to match, otherwise "pass left" delivered to the right.

Game history: finished games are saved with a per-hand breakdown, readable
from the profile and shown on the end-of-hand and end-of-game screens.

Card flashing: the service worker precaches all 52 faces, each face is decoded
once into a template image, and the renderers reconcile against existing DOM
instead of rebuilding it, so card nodes and their images survive a re-render.

Card-count integrity: a seat can no longer put two cards into one trick, and
the server snapshots each trick start and rewinds to the last sound state if
the four hands ever drift apart.

Leaderboard: ranked by win rate with a 5-game minimum; below-threshold players
are listed unranked. Moon shots are now recorded instead of always zero, and
stat reads no longer create and persist a row per user on every request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:13:46 +00:00

135 lines
4.4 KiB
Markdown

# 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://<host>:4000`
- HTTPS: `https://<host>: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.