Files
Poker/README.md
T
agomizi1cmandClaude Opus 5.5 02c8e76237 Close CPU tables as soon as every human player leaves
- When the last human presses leave on a CPU table (even mid-hand), the table
  is deleted right away instead of lingering with CPUs only; friends / cash-only
  tables keep the existing 30-minute idle cleanup
- Spectators still watching get a "テーブルが終了しました" notice, and the lobby
  updates immediately
- Don't show the "接続が切れました" banner when the page disconnects on purpose
  (closed or missing table)
- Test for the close condition; README notes the behavior

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:03:58 +09:00

191 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hold'em Night
友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。
「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。
## ルール・仕様
テーブル作成時に種類を選ぶ(既定はキャッシュオンリー)。
| 種類 | 内容 |
| --- | --- |
| キャッシュオンリー | カードは実物で配り、サイトはチップの代わりに使う(ベット・ポット・スタックの管理)。ショーダウンの勝者はホストが選ぶ |
| 友人戦 | 実物のカードがなくても遊べる。サイトがカードを配り、勝敗とポットの分配まで自動で判定する |
| CPU戦 | 友人戦に CPU プレイヤー(1〜8人)が加わる。空いている席には友達も参加できる |
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
- 画面にディーラーボタン(D)・SB・BB と、いまのストリート(PREFLOP / FLOP / TURN / RIVER)が出るので、それに合わせて実物のカードを配る。
- ブラインドは自動で支払われ、ハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
- 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
- ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。
- **ショーダウン(キャッシュオンリー)**: カードを見せ合い、ホストが勝った人を画面で選んで確定する(ポットの分配はテーブルのホストだけが行える)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。
- **カードを配るテーブル(友人戦・CPU戦)**: 各自のスマホに自分の手札だけが表示され、場札はストリートごとに画面に配られる。ショーダウンでは残った人の手札を公開して役を判定し、ポット(サイドポット含む)を自動で分配する。引き分けは山分け(端数はボタンの左から)。全員オールインになったら手札を公開し、残りの場札を順にめくる。山札は暗号論的乱数でシャッフルし、サーバの外に出さない(他人の手札は公開されるまで送らない)。
- **CPU**: 自前実装(`server/ai.js`、外部ライブラリなし)。自分の手札と場札から、残っている相手に勝つ確率をモンテカルロ法で見積もり、ポットオッズ・レイズの回数・相手の人数から行動を決める。CPU ごとに攻撃性・ブラフ頻度がランダムに違う。相手の手札は見ない。1手ごとに 0.7〜1.8 秒考える。チップがなくなると次のハンドで自動でリバイする。人間が1人も参加していないと次のハンドは始まらない。CPU はホストにならない。人間が全員「退席」するとテーブルはすぐに削除される(ブラウザを閉じただけの場合は、ほかのテーブルと同じく誰も接続していない状態が30分続いたら削除)。
- 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。
- 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。
- チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
- ハンド中に退席するとフォールド扱い(出したチップはポットに残る)。
- ホストの一時停止中は、アクション・勝者の確定・オールイン後の場札・ブラインドタイマー・次のハンドの開始がすべて止まる。
## ローカルで動かす
```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ハンドの進行(ブラインド・ベッティング・サイドポット・勝者への分配、友人戦・CPU戦での配札とショーダウン) |
| `server/cards.js` | 山札のシャッフルと役の判定(カードを配るテーブル) |
| `server/ai.js` | CPU の思考(高速な役判定・勝率の見積もり・行動の決定) |
| `test/` | チップ処理・役の判定・CPU のテスト(`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
```