Each CPU now thinks with one of four engines, each backed by a different published library (pinned versions): - montecarlo (@pokertools/evaluator): the existing balanced Monte Carlo equity player, moved to server/bots/montecarlo.js. - calculator (poker-calculations): the library's decideAction over the table state; push/fold below 12BB via its jam breakeven equity. Skipped automatically if the native addon cannot load. - range (poker-utils): tight player; preflop by hand-strength percentile, postflop equity vs an estimated opponent range, bluffs only when alpha/bluffEV say they pay. - madehand (pokersolver): loose, sticky player that only looks at the made hand and draws. server/ai.js assigns a mix of engines per table (makeStyles), falls back to the Monte Carlo engine on errors, and legalizes every decision. The engine and its parameters stay server-side; players cannot tell which library a CPU uses. Tests cover every engine and the no-leak rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
202 lines
15 KiB
Markdown
202 lines
15 KiB
Markdown
# Hold'em Night
|
||
|
||
友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。
|
||
|
||
「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。Node.js 24 以上が必要(@pokertools/engine の要件)。
|
||
|
||
## ルール・仕様
|
||
|
||
テーブル作成時に種類を選ぶ(既定はキャッシュオンリー)。
|
||
|
||
| 種類 | 内容 |
|
||
| --- | --- |
|
||
| キャッシュオンリー | カードは実物で配り、サイトはチップの代わりに使う(ベット・ポット・スタックの管理)。ショーダウンの勝者はホストが選ぶ |
|
||
| 友人戦 | 実物のカードがなくても遊べる。サイトがカードを配り、勝敗とポットの分配まで自動で判定する |
|
||
| CPU戦 | 友人戦に CPU プレイヤー(1〜8人)が加わる。空いている席には友達も参加できる |
|
||
|
||
- ポーカーのルール(ブラインド・手番・最低レイズ額・再レイズ権・返金・サイドポット・ショーダウンの判定と分配)と役の判定は、公開ライブラリ [@pokertools/engine](https://www.npmjs.com/package/@pokertools/engine) / [@pokertools/evaluator](https://www.npmjs.com/package/@pokertools/evaluator)(MIT、バージョン固定)に任せている。
|
||
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
|
||
- 画面にディーラーボタン(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](https://www.npmjs.com/package/@pokertools/evaluator) | モンテカルロ法で残っている相手に勝つ確率を見積もり、ポットオッズ・レイズの回数・相手の人数から決める |
|
||
| 計算型 | [poker-calculations](https://www.npmjs.com/package/poker-calculations)(ネイティブ実装) | ライブラリの `decideAction`(勝率・ポットオッズ・コールの期待値)。積極的にレイズする。12BB 以下はオールインの損益分岐勝率でプッシュ/フォールド |
|
||
| 理論型(タイト) | [poker-utils](https://www.npmjs.com/package/poker-utils) | プリフロップは手の強さの順位で上位だけ参加。フロップ以降は相手のレンジ(ベットされるほど強い手に絞る)に対する勝率で判断し、ブラフは alpha・bluffEV で割に合うときだけ |
|
||
| 直感型(ルーズ) | [pokersolver](https://www.npmjs.com/package/pokersolver) | 勝率は計算せず、いまできている役とドローだけを見る。参加する手が広く、ペアがあればなかなか降りない |
|
||
|
||
計算型のネイティブアドオンが読み込めない環境では、残りの3種類だけを使う。思考がエラーになったときはバランス型で決め直し、最後に必ず受け付けられるアクションに直す(`server/ai.js`)。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ハンドの進行。ルールは @pokertools/engine に任せ、テーブルとの橋渡し(席番号・スタック・退席・離席)、オールイン後に場札を順にめくる表示、キャッシュオンリーでホストが選んだ勝者への分配を行う |
|
||
| `server/cards.js` | 役の名前と最強の5枚(@pokertools/evaluator) |
|
||
| `server/ai.js` | CPU の思考の割り当て(種類と性格)と呼び出し |
|
||
| `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` | 各画面 |
|
||
| `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
|
||
```
|