Files
Poker/README.md
T
agomizi1cmandClaude Opus 5.5 6d33440048 Only the table host splits the pot; host buttons beside the table name
- Showdown winners can now be chosen only by the table host (others see
  "ホストが勝者を選んでいます"); if the host leaves, the next host does it
- START / pause moved into the header to the right of the table name
  (pause is an icon button), freeing the row they used to occupy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:15:19 +09:00

10 KiB
Raw Blame History

Hold'em Night

友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。

「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。

ルール・仕様

カードは実物で配り、このサイトはチップの代わりに使う(ベット・ポット・スタックの管理)。

  • ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
  • 画面にディーラーボタン(D)・SB・BB と、いまのストリート(PREFLOP / FLOP / TURN / RIVER)が出るので、それに合わせて実物のカードを配る。
  • ブラインドは自動で支払われ、ハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
  • 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
  • ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。
  • ショーダウン: カードを見せ合い、ホストが勝った人を画面で選んで確定する(ポットの分配はテーブルのホストだけが行える)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。
  • 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。
  • 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。
  • チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
  • ハンド中に退席するとフォールド扱い(出したチップはポットに残る)。
  • ホストの一時停止中は、アクション・勝者の確定・ブラインドタイマー・次のハンドの開始がすべて止まる。

ローカルで動かす

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ハンドのチップの動き(ブラインド・ベッティング・サイドポット・勝者への分配)
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 に向けておく。

# 専用ユーザーで動かす
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:

[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 を通す):

# 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;
    }
}
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

更新(デプロイ済みのサーバに反映)

テーブルはメモリ上にしかないので、再起動すると進行中のテーブルは消える。誰も遊んでいないときに行う。

cd /opt/holdem
sudo git pull
sudo npm ci --omit=dev
sudo systemctl restart holdem