Files
野口煌陽andClaude Opus 5.5 f64c502086 Wait for the host to press "next hand" on every table
Previously only the review CPU table paused after each hand; cash-only,
friends and CPU tables moved on automatically after a short delay. Now
every table keeps the result on screen until the host presses 次のハンドへ,
and the next hand is dealt right away. If the host has left, the new host
presses it; with no human seated the table still advances on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:56:37 +09:00

24 KiB
Raw Permalink Blame History

Hold'em Night

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

「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。Node.js 24 以上が必要(@pokertools/engine の要件)。

ルール・仕様

テーブル作成時に種類を選ぶ(既定はキャッシュオンリー)。

種類 内容
キャッシュオンリー カードは実物で配り、サイトはチップの代わりに使う(ベット・ポット・スタックの管理)。ショーダウンの勝者はホストが選ぶ
友人戦 実物のカードがなくても遊べる。サイトがカードを配り、勝敗とポットの分配まで自動で判定する
CPU戦 友人戦に CPU プレイヤー(1〜8人)が加わる。空いている席には友達も参加できる
レビュー付きCPU戦 管理者だけが作れる。作成者と CPU だけで遊び(友達は入れない・ロビーにも出ない)、ハンドごとにレビューが出る(下の「管理者」を参照)
  • ポーカーのルール(ブラインド・手番・最低レイズ額・再レイズ権・返金・サイドポット・ショーダウンの判定と分配)と役の判定は、公開ライブラリ @pokertools/engine / @pokertools/evaluator(MIT、バージョン固定)に任せている。

  • ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すとハンドが始まる。ハンドが終わると結果を表示したまま止まり、ホストが「次のハンドへ」を押すと次のハンドが配られる(ホストが退席したら次のホストが押す。人間が誰も着席していなければ待たずに進む)。

  • 画面にディーラーボタン(D)・SB・BB と、いまのストリート(PREFLOP / FLOP / TURN / RIVER)が出るので、それに合わせて実物のカードを配る。

  • ブラインドは自動で支払われ、ハンド開始時のレベルで固定。BB は SB より大きくする。ヘッズアップはボタンが SB。

  • 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。

  • ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。

  • ショーダウン(キャッシュオンリー): カードを見せ合い、ホストが勝った人を画面で選んで確定する(ポットの分配はテーブルのホストだけが行える)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。

  • カードを配るテーブル(友人戦・CPU戦): 各自のスマホに自分の手札だけが表示され、場札はストリートごとに画面に配られる。ショーダウンでは残った人の手札を公開して役を判定し、ポット(サイドポット含む)を自動で分配する。引き分けは山分け(端数はボタンの左から)。全員オールインになったら手札を公開し、残りの場札を順にめくる。山札はエンジンが暗号論的乱数(Node.js の crypto)でシャッフルし、サーバの外に出さない(他人の手札は公開されるまで送らない)。

  • CPU: 性格ごとに別の公開ライブラリで考える。テーブルの CPU にはなるべく違う種類が割り当てられ、さらに同じ種類でも攻撃性・ブラフ頻度などがランダムに違う。どの CPU がどのライブラリを使っているかは画面にもブラウザに送るデータにも出さない。どの CPU も相手の手札は見ない(自分の手札と場札、公開されているベット状況だけを使う)。

    種類 ライブラリ 打ち方
    バランス型 @pokertools/evaluator モンテカルロ法で残っている相手に勝つ確率を見積もり、ポットオッズ・レイズの回数・相手の人数から決める
    計算型 poker-calculations(ネイティブ実装) ライブラリの decideAction(勝率・ポットオッズ・コールの期待値)。積極的にレイズする。12BB 以下はオールインの損益分岐勝率でプッシュ/フォールド
    理論型(タイト) poker-utils プリフロップは手の強さの順位で上位だけ参加。フロップ以降は相手のレンジ(ベットされるほど強い手に絞る)に対する勝率で判断し、ブラフは alpha・bluffEV で割に合うときだけ
    直感型(ルーズ) pokersolver 勝率は計算せず、いまできている役とドローだけを見る。参加する手が広く、ペアがあればなかなか降りない

    計算型のネイティブアドオンが読み込めない環境では、残りの3種類だけを使う。思考がエラーになったときはバランス型で決め直し、最後に必ず受け付けられるアクションに直す(server/ai.js)。1手ごとに 0.7〜1.8 秒考える。チップがなくなると次のハンドで自動でリバイする。人間が1人も参加していないと次のハンドは始まらない。CPU はホストにならない。人間が全員「退席」するとテーブルはすぐに削除される(ブラウザを閉じただけの場合は、ほかのテーブルと同じく誰も接続していない状態が30分続いたら削除)。

  • 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。

  • 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。

  • チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。

  • テーブル上部のトロフィーのボタンで、着席している全員のランキングを開ける。「スタック」(持っているチップ。ハンド中は、まだ降りていない人がそのハンドで出した額も含める)と「収支」(持っているチップ − バイインの合計。リバイを含む)を切り替えられる。同じ値は同じ順位。

  • ハンド中に退席するとフォールド扱い(出したチップはポットに残る。ただし誰もコールしていない分は返る)。オールインしている人の手は退席しても最後まで生きている。

  • チェックできるのに降りるとサイドポットの対象者がいなくなる場合、エンジンが受け付けないので「チェックしてください」と出る(退席のときは自動でチェックになる)。

  • ホストの一時停止中は、アクション・勝者の確定・オールイン後の場札・ブラインドタイマー・次のハンドの開始がすべて止まる。

ローカルで動かす

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 に生成・保存

統計

ロビーの「統計」(/stats)で、自分の成績を見られる。ハンドが終わるたびに、参加した人間のプレイヤーごと(ブラウザのプレイヤーIDごと。CPU は数えない)に自動で記録する。

  • まとめ: プレイスタイル(タイト/ルーズ × アグレッシブ/パッシブのレーダーと、TAG・LAG・ロック・フィッシュのどれか)、VPIP・PFR・AFq・プレイ時間。VPIP 25% 以上をルーズ、PFR が VPIP の半分以上をアグレッシブとして判定する。レーダーを押すと4つのスタイルの説明を横に送って見られる。
  • すべての統計: 合計・キャッシュ(キャッシュオンリー)・友人戦・CPU戦(レビュー付きCPU戦を含む)ごとに、VPIP・PFR・ATS(全員が降りて CO・BTN・SB に回ってきたときのレイズ率)・3BET・WTSD・W$SD・WWSF・ストリートごとのフォールド率・アグレッション頻度(全部の行動のうちベット・レイズの割合)・ハンドの勝率・収支(BB と 100ハンドあたり)。各数字の「?」で段階(ニット〜フィッシュなど)と説明を開き、「友達に共有」で文章を共有・コピーできる。
  • 離席・退席で自動で行った行動は本人の判断として数えない。VPIP・PFR の分母は、プリフロップで判断したハンド(全員が降りて BB に回ってきたハンドなどは除く)。
  • 保存先は data/stats/(git 管理外)。ファイル名はプレイヤーIDの SHA-256 で、中身は回数の合計だけ(ハンドの内容は残さない)。プレイヤーIDは URL に載せないよう POST の本文で送る。

管理者(ハンドレンジ学習・レビュー付きCPU戦)

ADMIN_KEY を入力して入室すると管理者になり、ロビーに「ハンドレンジ学習」(/range:レンジ表と色・分類クイズ。レンジ表には CO のオープンを BTN で受けたときの対応も枠で示す。3bet は水色、コールは黄)と「CPU戦のレビュー」(/review)が表示され、テーブル作成で「レビュー付きCPU戦」を選べる。ロビーのテーブルの行を左へスワイプすると「削除」が出て(キーボードでは Tab で削除ボタンに移ると開く)、どのテーブルでも削除できる(着席者・観戦者には「管理者がテーブルを削除しました」と表示される)。管理者は通常のプレイもそのままできる。

レビュー付きCPU戦

ハンドが終わるたびにレビューが開き、「次のハンドへ」を押すまで次のハンドは始まらない。

  • ハンドレンジ: 配られた手が登録してあるハンドレンジ(Poker Skill の 6-max オープンレンジ。admin/range-data.js。レンジ学習ページと共用)の何色か、そのとおりに行動できたかを判定する。

    • 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)の色以上ならレイズ、それより弱ければフォールド。6-max の席に当てはめて、5人以上は UTG の赤(紺も可)、4人は HJ の黄、3人は CO の緑、2人は BTN の白、1人は SB の青。SB のレンジは BTN より狭いので、青は白の一つ上の段階として扱う。
    • 前にコール・レイズした人がいる: その人のレンジを、その人の後ろの人数に対応する色と推定し、自分の手がその一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド。紺はどんな相手にもコールまではできる。
    • 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ。
    • BTN で CO のオープン(ほかに参加者なし)に直面: レンジ表の枠のとおり。水色枠は 3bet、黄枠はコール、枠なしはフォールド。
    • BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。緑以上でコール(CO のオープンには青、BTN・SB には白以上。相手の色の一つ下まで)、相手の三つ上ならレイズ。
    • 2人目以降に参加した人のレンジも同じ決まりでコール・レイズできる一番弱い色と推定する(ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。
    • チェックできるときのフォールド・コールの推奨はチェックになる。判定は ◎(100点)・△(消極的 50点/攻めすぎ 40点)・✕(参加しすぎ・降りすぎ 0点)。
  • フロップ以降の点数: 相手のレンジ(上の色の推定から、ベット・レイズ・コールのたびに、いまの場札で強い手に絞る)に対する勝率をモンテカルロ法で求め、選択肢(フォールド・チェック・コール・標準サイズと実際のサイズのベット/レイズ)ごとの期待値を見積もる。一番良い選択肢との差(失った期待値)がポットの3%までは満点で、それを超えると1%ごとに2.5点減る(◎90〜 ○70〜 △40〜 ✕)。ソルバー(GTO)ではなく目安。

  • ラウンドごとの点数表: ハンド × PF/Flop/Turn/River の点数と平均。行を押すとリプレイ(1手ずつ再生し、自分の判断の場面で判定・勝率・選択肢ごとの期待値を表示)。

  • その他: コールに必要な勝率と推定勝率の比較、オールイン時の勝率と期待値(運の影響)、成績(VPIP・PFR・3bet・Cベット・WTSD・W$SD・AF)、色ごと・ポジションごとの参加率とレンジどおりの割合、気をつけたいクセ、見直したいハンド(失った期待値の大きい順に5つ)、実際の収支と運を除いた収支の推移グラフ。

  • AIに質問する用にコピー: テーブルのレビューとリプレイ画面のボタンで、そのハンドの状況(ゲームの条件・席とスタック・自分の手札・ストリートごとの場札とポット・全員のアクション・ショーダウンで公開された手札・結果・サイトの判定・質問文)を、ChatGPT や Claude などに貼り付けてそのまま質問できる文章にしてコピーする。判断の時点で見えていた情報だけを書き、公開されなかった相手の手札は含めない(server/review/prompt.js)。自動でコピーできないブラウザでは文章を表示するので手でコピーする。

  • レビューは data/reviews/(git 管理外)に対局ごとに保存し、テーブルを閉じた後も /review で見返せる(新しい50対局まで。1対局1000ハンドまで)。

  • テーブルのレビューの「対局全体のレビュー」は同じタブで開き、左上の「テーブル」でそのテーブルに戻れる(リプレイを開いても引き継ぐ。URL の t=テーブルID)。

  • 初めて開くときは鍵入力画面で 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ハンドの進行。ルールは @pokertools/engine に任せ、テーブルとの橋渡し(席番号・スタック・退席・離席)、オールイン後に場札を順にめくる表示、キャッシュオンリーでホストが選んだ勝者への分配を行う
server/cards.js 役の名前と最強の5枚(@pokertools/evaluator)
server/ai.js CPU の思考の割り当て(種類と性格)と呼び出し
server/review/*.js レビュー付きCPU戦のレビュー(preflop ハンドレンジの判定 / equity 勝率 / analyze 1ハンドの点数 / summary 対局のまとめ / prompt AI に質問する文章 / store 保存)
server/stats/*.js プレイヤーごとの統計(hand 1ハンドから数える / store 保存 / summary 割合にまとめる)
server/bots/*.js CPU の思考の種類ごとの実装(montecarlo / calculator / range / madehand)と共通の道具(common.js)
test/ チップ処理・役の判定・CPU・レビュー・統計のテスト(npm test)
server/config.js .env の読み込みと各種上限値
public/assets/blinds.js ブラインド計算(サーバ・ブラウザ共用)
public/*.html, public/assets/*.js 各画面
public/assets/style.css, public/assets/fluid.js 見た目の土台(light / dark の色・半透明の素材・字の大きさ)と、動きの共通部品(ばね・下から出るシート・滑るセグメント・指の速度と勢いの先読み)
admin/ 管理者専用ページ(ハンドレンジ学習・CPU戦のレビュー・登録してあるハンドレンジ・カード画像)
  • テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が30分続くと自動削除。
  • 名前とプレイヤーID はブラウザの localStorage に保存。リロードしても同じ席に戻れる。
  • 作成者がホスト(START・一時停止・ショーダウン後のポットの分配ができる。ボタンはテーブル名の右に出る)。ホストが退席すると次の着席者に移る。
  • ホストはテーブル名の右の「席順を変更」(↑↓のボタン、3人以上のとき)で、着席者の並びを変えられる。自分を一番上に固定し、その下に左隣から時計回りの順で並び、行の右端のつまみをドラッグ(キーボードでは ↑↓)して並べ替える。小さなテーブルの図で仕上がりを確かめられ、「シャッフル」でランダムにもできる。ハンドの途中(結果の表示中を含む)に決めた席順は次のハンドの始めに反映し(ボタンに印が付く)、決めた後に座った人は並びの最後に入る。ディーラーボタンは同じ人に付いたまま、次のハンドで新しい並びの次の人へ回る。

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