- New "カードを配る" switch on the create-table screen (default off); the lobby shows "カードあり" for tables that use it - Card mode (server/cards.js restored): crypto-shuffled deck kept server-side, hole cards sent only to their owner until showdown/all-in reveal, board dealt per street with burns, all-in runout on a timer (frozen while paused) - Showdown is evaluated and every pot (incl. side pots, split pots) is paid out automatically; host winner selection is only used when card mode is off - Play UI: board, hole cards, own cards in the action bar, hand names, winning cards highlighted; layout tuned for 7+ seats and short screens - Playing-card styles use .pcard so they don't clash with the existing .card panel - Tests for hand evaluation, card privacy, runout, side pots, ties and random play Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
11 KiB
Markdown
184 lines
11 KiB
Markdown
# Hold'em Night
|
||
|
||
友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。
|
||
|
||
「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。
|
||
|
||
## ルール・仕様
|
||
|
||
基本はカードを実物で配り、このサイトはチップの代わりに使う(ベット・ポット・スタックの管理)。
|
||
実物のカードがないときは、テーブル作成時に「カードを配る」をオンにすると、サイトがカードを配って勝敗まで自動で判定する(既定はオフ)。
|
||
|
||
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
|
||
- 画面にディーラーボタン(D)・SB・BB と、いまのストリート(PREFLOP / FLOP / TURN / RIVER)が出るので、それに合わせて実物のカードを配る。
|
||
- ブラインドは自動で支払われ、ハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
|
||
- 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
|
||
- ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。
|
||
- **ショーダウン(カード機能オフ)**: カードを見せ合い、ホストが勝った人を画面で選んで確定する(ポットの分配はテーブルのホストだけが行える)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。
|
||
- **カード機能オン**: 各自のスマホに自分の手札だけが表示され、場札はストリートごとに画面に配られる。ショーダウンでは残った人の手札を公開して役を判定し、ポット(サイドポット含む)を自動で分配する。引き分けは山分け(端数はボタンの左から)。全員オールインになったら手札を公開し、残りの場札を順にめくる。山札は暗号論的乱数でシャッフルし、サーバの外に出さない(他人の手札は公開されるまで送らない)。
|
||
- 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。
|
||
- 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。
|
||
- チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
|
||
- ハンド中に退席するとフォールド扱い(出したチップはポットに残る)。
|
||
- ホストの一時停止中は、アクション・勝者の確定・オールイン後の場札・ブラインドタイマー・次のハンドの開始がすべて止まる。
|
||
|
||
## ローカルで動かす
|
||
|
||
```bash
|
||
cp .env.example .env # ACCESS_KEY を設定
|
||
npm install
|
||
npm start
|
||
```
|
||
|
||
http://localhost:4671 を開く。
|
||
|
||
## 設定(.env)
|
||
|
||
秘密情報はソースコードに書かず、プロジェクト直下の `.env`(git 管理外)か環境変数で渡す。`ACCESS_KEY` が無いとサーバは起動しない。
|
||
|
||
| 変数 | 既定値 | 内容 |
|
||
| --- | --- | --- |
|
||
| `ACCESS_KEY` | なし(必須) | 6桁の参加キー |
|
||
| `ADMIN_KEY` | なし(任意) | 6桁の管理者キー(`ACCESS_KEY` と別の値)。入力すると管理者として入室する |
|
||
| `PORT` | `4671` | 待ち受けポート |
|
||
| `HOST` | `127.0.0.1` | 待ち受けアドレス。既定では外部から直接つながらない(nginx 経由で公開する) |
|
||
| `PUBLIC_ORIGIN` | なし | 本番の公開URL `https://poker.kanawo.net`。設定するとこのドメイン以外(IP直打ちや別ドメイン)へのアクセスと、他オリジンからの操作を拒否する。ローカル開発では設定しない |
|
||
| `SESSION_SECRET` | 自動生成 | Cookie 署名用。未指定なら `data/session-secret` に生成・保存 |
|
||
|
||
## 管理者(ハンドレンジ学習)
|
||
|
||
`ADMIN_KEY` を入力して入室すると管理者になり、ロビーに「♠ ハンドレンジ学習」が表示される(`/range`:レンジ表と色・分類クイズ)。管理者は通常のプレイもそのままできる。
|
||
|
||
- 初めて開くときは鍵入力画面で `ADMIN_KEY` を入れる。すでに参加キーで入室済みのブラウザは `https://poker.kanawo.net/key?admin` を開いて管理者キーを入れる。
|
||
- 管理者以外には `/range` とその画像・スクリプトは 404 を返す(ページがあること自体を見せない)。ファイルは `public/` の外の `admin/` に置いている。
|
||
- 管理者キーを変えると、管理者の Cookie だけが無効になる。
|
||
|
||
## セキュリティ
|
||
|
||
- **参加キー**: 正しいキーを入れたブラウザに HttpOnly / SameSite=Lax(HTTPS 時は Secure)の Cookie を保存。アクセスのたびに有効期限(400日)が延長されるので、以降は入力不要。
|
||
- **総当たり対策**: 同じIP(IPv6 は /64 単位)から30分に5回間違えるとロック。サーバ全体で1時間に30回間違えると新規の鍵入力を一時停止(入室済みの人は影響なし)。失敗はログに出る(`journalctl -u holdem`)。
|
||
- **入室状態の取り消し**: キーを変えるか、`data/session-secret` を削除して再起動すると、全員の Cookie が無効になる。
|
||
- **CSRF / Cross-Site WebSocket**: 状態を変える API と WebSocket 接続は Origin が自サイトのときだけ受け付ける。Socket.IO は WebSocket のみ。
|
||
- **XSS**: 厳格な CSP(インラインスクリプト・インラインスタイル禁止)、表示時のエスケープ、名前から制御文字・不可視文字を除去。
|
||
- **その他**: クリックジャッキング防止(`frame-ancestors 'none'`)、`nosniff`、`no-referrer`、HTML/API は `no-store`、検索エンジン除外、エラー時に内部情報を返さない、リクエストサイズ・イベント連打・テーブル数の上限。
|
||
- 他人の席を操作できないよう、席の持ち主を示すプレイヤーIDはブラウザ内だけに保存し、他のプレイヤーには送らない。
|
||
- **チップ操作**: アクションはサーバ側で手番・額をすべて検証する。ショーダウンの勝者を確定できるのは残った人とホストだけで、ポットの対象外の人は選べない。
|
||
|
||
6桁のキーはそれ自体が強い秘密ではないので、上記の回数制限が守りの要になっている。キーは友達にだけ直接伝えること。
|
||
|
||
## 構成
|
||
|
||
| パス | 内容 |
|
||
| --- | --- |
|
||
| `server/index.js` | HTTP ルーティング・セキュリティヘッダ・REST API・Socket.IO |
|
||
| `server/auth.js` | 参加キー認証(Cookie)・回数制限・Origin チェック |
|
||
| `server/tables.js` | テーブル状態(席・ボタン・ブラインド・タイマー・ハンドの開始/進行) |
|
||
| `server/hand.js` | 1ハンドの進行(ブラインド・ベッティング・サイドポット・勝者への分配、カード機能オン時の配札とショーダウン) |
|
||
| `server/cards.js` | 山札のシャッフルと役の判定(カード機能オン時) |
|
||
| `test/` | チップ処理・役の判定のテスト(`npm test`) |
|
||
| `server/config.js` | `.env` の読み込みと各種上限値 |
|
||
| `public/assets/blinds.js` | ブラインド計算(サーバ・ブラウザ共用) |
|
||
| `public/*.html`, `public/assets/*.js` | 各画面 |
|
||
| `admin/` | 管理者専用ページ(ハンドレンジ学習・カード画像) |
|
||
|
||
- テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が30分続くと自動削除。
|
||
- 名前とプレイヤーID はブラウザの localStorage に保存。リロードしても同じ席に戻れる。
|
||
- 作成者がホスト(START・一時停止・ショーダウン後のポットの分配ができる。ボタンはテーブル名の右に出る)。ホストが退席すると次の着席者に移る。
|
||
|
||
## VPS へのデプロイ例(Ubuntu + nginx + systemd + HTTPS)
|
||
|
||
公開URLは https://poker.kanawo.net 。HTTPS なしで公開するとキーと Cookie が平文で流れるので、必ず nginx + Let's Encrypt 経由で公開する。
|
||
|
||
事前に DNS で `poker.kanawo.net` の A レコード(IPv6 があれば AAAA も)を VPS の IP に向けておく。
|
||
|
||
```bash
|
||
# 専用ユーザーで動かす
|
||
sudo useradd --system --home /opt/holdem --shell /usr/sbin/nologin holdem
|
||
sudo git clone <このリポジトリ> /opt/holdem
|
||
cd /opt/holdem
|
||
sudo npm ci --omit=dev
|
||
|
||
# 秘密情報(本人のみ読める)
|
||
sudo cp .env.example .env
|
||
sudo nano .env # ACCESS_KEY=197107, PUBLIC_ORIGIN=https://poker.kanawo.net を設定
|
||
sudo mkdir -p data
|
||
sudo chown -R holdem:holdem .env data
|
||
sudo chmod 600 .env && sudo chmod 700 data
|
||
```
|
||
|
||
`/etc/systemd/system/holdem.service`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Hold'em Night
|
||
After=network.target
|
||
|
||
[Service]
|
||
User=holdem
|
||
WorkingDirectory=/opt/holdem
|
||
ExecStart=/usr/bin/node server/index.js
|
||
Environment=NODE_ENV=production
|
||
Restart=always
|
||
|
||
# サンドボックス
|
||
NoNewPrivileges=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
PrivateTmp=true
|
||
PrivateDevices=true
|
||
ReadWritePaths=/opt/holdem/data
|
||
CapabilityBoundingSet=
|
||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
nginx(`/etc/nginx/sites-available/holdem`、WebSocket を通す):
|
||
|
||
```nginx
|
||
# poker.kanawo.net 以外のホスト名(IP直打ち等)は接続ごと切る
|
||
server {
|
||
listen 80 default_server;
|
||
listen [::]:80 default_server;
|
||
server_name _;
|
||
return 444;
|
||
}
|
||
|
||
server {
|
||
server_name poker.kanawo.net;
|
||
client_max_body_size 8k;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:4671;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $http_host;
|
||
proxy_set_header X-Forwarded-For $remote_addr;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 1h;
|
||
}
|
||
}
|
||
```
|
||
|
||
```bash
|
||
sudo ln -s /etc/nginx/sites-available/holdem /etc/nginx/sites-enabled/
|
||
sudo certbot --nginx -d poker.kanawo.net # HTTPS 化(HTTP→HTTPS リダイレクトも設定される)
|
||
sudo systemctl daemon-reload && sudo systemctl enable --now holdem
|
||
|
||
# ファイアウォール: 4671 は開けない(nginx だけが 127.0.0.1 経由で使う)
|
||
sudo ufw allow OpenSSH && sudo ufw allow 'Nginx Full' && sudo ufw enable
|
||
```
|
||
|
||
## 更新(デプロイ済みのサーバに反映)
|
||
|
||
テーブルはメモリ上にしかないので、再起動すると進行中のテーブルは消える。誰も遊んでいないときに行う。
|
||
|
||
```bash
|
||
cd /opt/holdem
|
||
sudo git pull
|
||
sudo npm ci --omit=dev
|
||
sudo systemctl restart holdem
|
||
```
|