- Hand engine (server/hand.js): crypto-shuffled deck kept server-side, blinds (heads-up button = SB), min-raise rules incl. incomplete all-in, uncalled bet refunds, side pots, split pots, all-in runout - Hand evaluator (server/cards.js) and node:test suite (npm test) - Table flow: hands auto-continue after host START, 30s action clock (timeout = check/fold + sit out), sit out / sit in, rebuy, mid-hand leave folds and keeps the seat reserved until the hand ends, pause freezes all clocks - Only each player's own hole cards are sent until a showdown/runout reveal; all actions validated server-side - Play UI: board, hole cards, bets, action tags, turn timer, raise panel with presets, showdown results with winning cards highlighted, compact 7+ layout - Remove the host crown and the temporary "next phase" control Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
8.4 KiB
Markdown
168 lines
8.4 KiB
Markdown
# Hold'em Night
|
||
|
||
友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。
|
||
|
||
「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。
|
||
|
||
## ルール・仕様
|
||
|
||
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
|
||
- ブラインドはハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
|
||
- 最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
|
||
- サイドポット・山分け(端数はボタンの左から)・誰もコールしなかった分の返金に対応。
|
||
- 全員オールインになったら手札を公開して残りのカードを順にめくる。ショーダウンでは残った全員の手札を公開。
|
||
- 持ち時間は1手30秒(`server/config.js` の `ACTION_TIME_MS`)。過ぎるとチェック(できなければフォールド)して「離席中」になり、「復帰する」を押すまで配られない。
|
||
- チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
|
||
- ハンド中に退席するとフォールド扱い(出したチップはポットに残る)。
|
||
- ホストの一時停止中は、持ち時間・ブラインドタイマー・次のハンドの開始がすべて止まる。
|
||
|
||
## ローカルで動かす
|
||
|
||
```bash
|
||
cp .env.example .env # ACCESS_KEY を設定
|
||
npm install
|
||
npm start
|
||
```
|
||
|
||
http://localhost:4671 を開く。
|
||
|
||
## 設定(.env)
|
||
|
||
秘密情報はソースコードに書かず、プロジェクト直下の `.env`(git 管理外)か環境変数で渡す。`ACCESS_KEY` が無いとサーバは起動しない。
|
||
|
||
| 変数 | 既定値 | 内容 |
|
||
| --- | --- | --- |
|
||
| `ACCESS_KEY` | なし(必須) | 6桁の参加キー |
|
||
| `PORT` | `4671` | 待ち受けポート |
|
||
| `HOST` | `127.0.0.1` | 待ち受けアドレス。既定では外部から直接つながらない(nginx 経由で公開する) |
|
||
| `PUBLIC_ORIGIN` | なし | 本番の公開URL `https://poker.kanawo.net`。設定するとこのドメイン以外(IP直打ちや別ドメイン)へのアクセスと、他オリジンからの操作を拒否する。ローカル開発では設定しない |
|
||
| `SESSION_SECRET` | 自動生成 | Cookie 署名用。未指定なら `data/session-secret` に生成・保存 |
|
||
|
||
## セキュリティ
|
||
|
||
- **参加キー**: 正しいキーを入れたブラウザに 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はブラウザ内だけに保存し、他のプレイヤーには送らない。
|
||
- **カード**: 山札は暗号論的乱数(`crypto.randomInt`)でシャッフルしてサーバ内だけに保持。各プレイヤーには自分の手札だけを送り、他人の手札は公開時(オールイン・ショーダウン)まで送らない。アクションはサーバ側で手番・額をすべて検証する。
|
||
|
||
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` | 各画面 |
|
||
|
||
- テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が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
|
||
```
|