ChatGPTでオンライン対戦可能なテキサスホールデムを作った

現在開発中のコードクエストというゲーム内のカジノ機能に、オンライ対戦できるテキサスホールデムポーカーをChatGPTと格闘しながら実装しました！

https://codequest-e825e.web.app/texasholdem.html

一応動くのができましたが、まだ完璧ではありません。
エラーや実際のポーカーと違う挙動があればぜひご報告おねがいします。

以下ソースコード(htmlとnode.jsです。)

```texasholdem.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <title>Texas Hold'em Demo</title>
  <style>
    body {
      background: #006400;
      color: #fff;
      font-family: sans-serif;
      padding: 20px;
    }

    .container {
      max-width: 800px;
      margin: auto;
      background: #228B22;
      padding: 20px;
      border-radius: 10px;
    }

    input,
    button {
      margin: 5px;
      padding: 5px 10px;
      border: none;
      border-radius: 4px;
    }

    button {
      cursor: pointer;
    }

    .player {
      margin-bottom: 5px;
      padding: 5px;
    }

    .current-turn {
      background-color: yellow;
      color: black;
      font-weight: bold;
    }

    .card {
      width: 100px;
      margin: 2px;
    }
  </style>
  <script>
    function getCardImageHtml(card) {
      if (card.rank === "??" && card.suit === "??") {
        return `<img src="playingcard/back.svg" alt="back" class="card">`;
      } else {
        let suitLetter = "";
        switch (card.suit) {
          case "♣": suitLetter = "C"; break;
          case "♦": suitLetter = "D"; break;
          case "♥": suitLetter = "H"; break;
          case "♠": suitLetter = "S"; break;
        }
        return `<img src="playingcard/${card.rank}${suitLetter}.svg" 
                    alt="${card.rank}${suitLetter}" class="card">`;
      }
    }
    function renderHand(hand) {
      return hand.map(card => getCardImageHtml(card)).join("");
    }
  </script>
</head>

<body>
  <div class="container">
    <h1>Texas Hold'em Online</h1>
    <div>
      <input type="text" id="playerName" placeholder="名前">
      <input type="number" id="roomId" placeholder="テーブル番号 (1～10)" min="1" max="10">
      <button id="joinBtn">参加</button>
      <button id="startBtn">ゲーム開始</button>
    </div>
    <hr>
    <div>
      <h3>フェーズ: <span id="phase"></span></h3>
      <h3>ポット: <span id="pot"></span></h3>
      <h3>テーブル最高ベット: <span id="currentBet"></span></h3>
      <h3>現在アクション中: <span id="currentPlayerId"></span></h3>
    </div>
    <div id="communityCards"></div>
    <div id="players"></div>
    <div>
      <h3>アクション</h3>
      <button id="foldBtn" onclick="sendAction('fold')">フォールド</button>
      <button id="callBtn" onclick="sendAction('call')">コール</button>
      <button id="checkBtn" onclick="sendAction('check')" style="display:none;">チェック</button>
      <input type="number" id="raiseAmount" placeholder="追加レイズ額 (例:10)" style="width:80px;">
      <button id="raiseBtn" onclick="sendAction('raise')">レイズ</button>
    </div>
    <div id="messages" style="margin-top:10px; color:yellow;"></div>
    <div id="result" style="margin-top:10px;"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io();
    let myId = null;
    socket.on('connect', () => {
      myId = socket.id;
    });

    const joinBtn = document.getElementById('joinBtn');
    const startBtn = document.getElementById('startBtn');
    const foldBtn = document.getElementById('foldBtn');
    const callBtn = document.getElementById('callBtn');
    const checkBtn = document.getElementById('checkBtn');
    const raiseBtn = document.getElementById('raiseBtn');

    const playerNameInput = document.getElementById('playerName');
    const roomIdInput = document.getElementById('roomId');

    const phaseSpan = document.getElementById('phase');
    const potSpan = document.getElementById('pot');
    const currentBetSpan = document.getElementById('currentBet');
    const currentPlayerSpan = document.getElementById('currentPlayerId');
    const communityDiv = document.getElementById('communityCards');
    const playersDiv = document.getElementById('players');
    const messagesDiv = document.getElementById('messages');
    const resultDiv = document.getElementById('result');

    joinBtn.onclick = () => {
      const name = playerNameInput.value.trim();
      const room = roomIdInput.value.trim();
      if (name && room) {
        socket.emit("joinGame", { playerName: name, roomId: room });
      }
    };

    startBtn.onclick = () => {
      socket.emit("startGame");
    };

    function sendAction(actionType) {
      const raiseAmount = parseInt(document.getElementById('raiseAmount').value, 10) || 0;
      socket.emit("playerAction", { action: actionType, raiseAmount });
    }

    socket.on("gameUpdate", (data) => {
      messagesDiv.innerHTML = "";
      // ※ ショウダウンやWAITINGに変わっても、ここではresultをクリアしない

      phaseSpan.textContent = data.phase;
      potSpan.textContent = data.pot;
      currentBetSpan.textContent = data.currentBet;
      currentPlayerSpan.textContent = data.currentPlayerId || "";

      // フェーズがWAITINGならアクション不可
      if (data.phase === "WAITING") {
        foldBtn.disabled = true;
        callBtn.disabled = true;
        checkBtn.disabled = true;
        raiseBtn.disabled = true;
      } else {
        const isMyTurn = (data.currentPlayerId === myId);
        foldBtn.disabled = !isMyTurn;
        raiseBtn.disabled = !isMyTurn;
        // call / check切り替え
        const me = data.players.find(p => p.id === myId);
        if (me) {
          if (me.bet === data.currentBet) {
            checkBtn.style.display = "inline-block";
            callBtn.style.display = "none";
            checkBtn.disabled = !isMyTurn;
          } else {
            checkBtn.style.display = "none";
            callBtn.style.display = "inline-block";
            callBtn.disabled = !isMyTurn;
          }
        }
      }

      // コミュニティカード
      communityDiv.innerHTML = `<h3>コミュニティカード:</h3> ${renderHand(data.communityCards)}`;

      // プレイヤー一覧
      playersDiv.innerHTML = `<h3>プレイヤー:</h3>`;
      data.players.forEach(p => {
        const isCurrent = (p.id === data.currentPlayerId);
        // p.rank には "Pair, Tens" などが格納されている（自分 or SHOWDOWN時）
        // ただし、相手はショウダウン前には "N/A" or 空文字のはず
        playersDiv.innerHTML += `
      <div class="player ${isCurrent ? 'current-turn' : ''}">
        <strong>Name:</strong> ${p.positionLabel} ${p.name} |
        <strong>Chips:</strong> ${p.chips} |
        <strong>Bet:</strong> ${p.bet} |
        <strong>Fold:</strong> ${p.hasFolded} |
        <strong>Hand:</strong> ${renderHand(p.hand)}
       <strong>Rank:</strong> ${p.rank || ""}
      </div>
    `;
      });
    });

    socket.on("gameMessage", (data) => {
      messagesDiv.innerHTML = data.message;
    });

    socket.on("gameResult", (data) => {
      // 結果を表示(複数行になる場合もあるので改行に注意)
      resultDiv.innerHTML = `<h3 style="white-space:pre-line;">${data.message}</h3>`;
    });

    socket.on("winNotification", (data) => {
      // 勝者には "Win" 表示を上書き or 追加
      // 必要があれば、既存の表示に追記しても良い
      resultDiv.innerHTML += `<h3>${data.message}</h3>`;
    });
  </script>
</body>

</html>
```


```server.js
const express = require("express");
const http = require("http");
const socketIo = require("socket.io");
const { Hand } = require("pokersolver"); // 手役判定ライブラリ

// 定数
const SMALL_BLIND = 10;
const BIG_BLIND = 20;
const MAX_TABLES = 10;
const MAX_PLAYERS_PER_TABLE = 6;
const PHASES = {
  WAITING: "WAITING",
  PRE_FLOP: "PRE_FLOP",
  FLOP: "FLOP",
  TURN: "TURN",
  RIVER: "RIVER",
  SHOWDOWN: "SHOWDOWN",
};

//
// TexasHoldemGame クラス（オールイン・サイドポット対応版）
//
class TexasHoldemGame {
  constructor() {
    this.players = []; // { id, name, chips, bet, totalBet, hasFolded, isAllIn, hand: [], hasActed: false }
    this.deck = [];
    this.communityCards = [];
    this.pot = 0;
    this.currentBet = 0;
    this.currentPlayerIndex = 0;
    this.phase = PHASES.WAITING;
    this.dealerButtonIndex = 0;
    this.actionStartIndex = 0;
    // ヘッズアップ用にSBとBBのインデックスを記録
    this.smallBlindIndex = null;
    this.bigBlindIndex = null;
    this.waitingPlayers = [];
  }

  // [追加] ゲーム終了時に呼び出して、待機中プレイヤーを本参加に編入する
  incorporateWaitingPlayers() {
    console.log(
      "[DEBUG incorporateWaitingPlayers] start. waitingPlayers:",
      this.waitingPlayers.map((p) => ({
        id: p.id,
        name: p.name,
        chips: p.chips,
        disconnected: p.disconnected,
      }))
    );

    // 0チップ or disconnected な人は合流させない
    const validWaiters = this.waitingPlayers.filter(
      (p) => p.chips > 0 && !p.disconnected
    );
    console.log(
      `[DEBUG incorporateWaitingPlayers] from ${this.waitingPlayers.length} to ${validWaiters.length} valid players.`
    );

    // 合流
    this.players.push(...validWaiters);
    // waitingPlayers クリア
    this.waitingPlayers = [];

    console.log(
      "[DEBUG incorporateWaitingPlayers] end. players:",
      this.players.map((p) => ({
        id: p.id,
        name: p.name,
        chips: p.chips,
        disconnected: p.disconnected,
      }))
    );
  }

  // デッキ初期化・シャッフル
  initializeDeck() {
    const suits = ["♠", "♥", "♦", "♣"];
    const ranks = [
      "2",
      "3",
      "4",
      "5",
      "6",
      "7",
      "8",
      "9",
      "10",
      "J",
      "Q",
      "K",
      "A",
    ];
    this.deck = [];
    for (let suit of suits) {
      for (let rank of ranks) {
        this.deck.push({ suit, rank });
      }
    }
    for (let i = this.deck.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [this.deck[i], this.deck[j]] = [this.deck[j], this.deck[i]];
    }
  }

  // プレイヤー追加
  addPlayer({ id, name, chips = 1000 }) {
    // もし現在フェーズが WAITING (次ゲーム待ち) ならすぐ参加可能
    if (this.phase === PHASES.WAITING) {
      this.players.push({
        id,
        name,
        chips,
        bet: 0,
        totalBet: 0,
        hasFolded: false,
        isAllIn: false,
        hand: [],
        hasActed: false,
        disconnected: false,
      });
    } else {
      // それ以外のフェーズ(ゲーム進行中)なら次のゲームまで待機
      this.waitingPlayers.push({
        id,
        name,
        chips,
        bet: 0,
        totalBet: 0,
        hasFolded: false,
        isAllIn: false,
        hand: [],
        hasActed: false,
      });
    }
  }
  removePlayer(socketId) {
    this.players = this.players.filter((p) => p.id !== socketId);
  }

  startRound() {
    // すでに動いているテーブルでもディーラーボタンを進める(複数人時)など
    if (this.phase !== PHASES.WAITING && this.players.length > 1) {
      this.dealerButtonIndex =
        (this.dealerButtonIndex + 1) % this.players.length;
    }
    this.phase = PHASES.PRE_FLOP;
    this.initializeDeck();
    this.communityCards = [];
    this.pot = 0;
    this.currentBet = 0;

    for (let p of this.players) {
      p.bet = 0;
      p.totalBet = 0;
      p.hasFolded = false;
      p.isAllIn = false;
      p.hand = [];
      p.hasActed = false;
    }
    // 2枚配る
    for (let p of this.players) {
      p.hand.push(this.deck.pop(), this.deck.pop());
    }

    if (this.players.length >= 2) {
      if (this.players.length === 2) {
        // ヘッズアップ
        // dealerButtonIndex が SB(ボタン)を担当
        this.smallBlindIndex = this.dealerButtonIndex;
        this.bigBlindIndex = (this.dealerButtonIndex + 1) % this.players.length;

        // SBとBBのブラインドを支払う
        this._postBet(this.players[this.smallBlindIndex], SMALL_BLIND);
        this._postBet(this.players[this.bigBlindIndex], BIG_BLIND);
        this.currentBet = BIG_BLIND;

        // プリフロップはSBが先にアクション
        this.currentPlayerIndex = this.smallBlindIndex;
        this.actionStartIndex = this.smallBlindIndex;
      } else {
        // 3人以上
        const sbIndex = (this.dealerButtonIndex + 1) % this.players.length;
        const bbIndex = (this.dealerButtonIndex + 2) % this.players.length;
        this._postBet(this.players[sbIndex], SMALL_BLIND);
        this._postBet(this.players[bbIndex], BIG_BLIND);
        this.currentBet = BIG_BLIND;

        // 3人以上はBBの次(ディーラーから見て3つ先)が最初にアクション
        this.currentPlayerIndex =
          (this.dealerButtonIndex + 3) % this.players.length;
        this.actionStartIndex = this.currentPlayerIndex;

        // 必要に応じて smallBlindIndex, bigBlindIndex を記録
        this.smallBlindIndex = sbIndex;
        this.bigBlindIndex = bbIndex;
      }
    }
  }

  // プレイヤーが金額をベットする（オールイン含む）
  _postBet(player, amount) {
    let toPost = amount;
    if (amount > player.chips) {
      toPost = player.chips;
      player.isAllIn = true;
      player.hasActed = true;
    }
    player.chips -= toPost;
    player.bet += toPost;
    player.totalBet += toPost;
    this.pot += toPost;
  }

  handleAction(playerId, action, raiseAmount = 0) {
    console.log("====== handleAction START ======");
    console.log(
      `[DEBUG] Called by playerId=${playerId}, action=${action}, raiseAmount=${raiseAmount}`
    );
    console.log(
      `[DEBUG] currentPlayerIndex=${this.currentPlayerIndex}, currentBet=${this.currentBet}`
    );

    // プレイヤー一覧をデバッグ出力
    this.players.forEach((p, i) => {
      console.log(
        `[DEBUG] Player idx=${i}, id=${p.id}, name=${p.name}, ` +
          `folded=${p.hasFolded}, allIn=${p.isAllIn}, hasActed=${p.hasActed}, ` +
          `bet=${p.bet}, chips=${p.chips}`
      );
    });

    // ---------- [1] ターンチェック ----------
    const playerIndex = this.players.findIndex((p) => p.id === playerId);
    if (playerIndex !== this.currentPlayerIndex) {
      console.log("[DEBUG] -> Not the correct turn for this player.");
      console.log("====== handleAction END (error) ======");
      return { error: "現在のアクションはあなたの番ではありません" };
    }

    const player = this.players[playerIndex];
    if (player.hasFolded || player.isAllIn) {
      console.log("[DEBUG] -> Player is already folded or all-in.");
      console.log("====== handleAction END (error) ======");
      return { error: "すでにフォールドまたはオールインしています" };
    }

    // ---------- [2] アクション別処理 ----------
    switch (action) {
      case "fold": {
        if (player.bet === this.currentBet) {
          console.log(
            "[DEBUG] -> Attempted to fold when check is possible. Disallowed by current logic."
          );
          console.log("====== handleAction END (error) ======");
          return { error: "チェック可能な状況でフォールドはできません" };
        }
        player.hasFolded = true;
        player.hasActed = true;
        console.log(`[DEBUG] -> Player ${player.id} folded.`);
        break;
      }
      case "check": {
        // currentBetとの差があればチェック不可
        if (player.bet !== this.currentBet) {
          console.log("[DEBUG] -> Attempted to check, but bet mismatch.");
          console.log("====== handleAction END (error) ======");
          return {
            error: "チェックは現在のベット額と一致している場合のみ可能です",
          };
        }
        player.hasActed = true;
        console.log(`[DEBUG] -> Player ${player.id} checked.`);
        break;
      }
      case "call": {
        // callに必要な額
        const diff = this.currentBet - player.bet;
        console.log(
          `[DEBUG] -> call diff=${diff}, playerChips=${player.chips}`
        );

        if (diff > 0) {
          if (diff >= player.chips) {
            // 追いつくのに自分のチップ全部必要(=オールイン)
            console.log("[DEBUG] -> Player goes all-in (call).");
            this._postBet(player, player.chips);
            player.isAllIn = true;
          } else {
            this._postBet(player, diff);
            console.log(`[DEBUG] -> Player calls. Bet +${diff}.`);
          }
        } else {
          console.log(
            "[DEBUG] -> diff <= 0 => Already matched or behind. No additional bet needed."
          );
        }
        player.hasActed = true;
        break;
      }
      case "raise": {
        // ここでは raiseAmount を「currentBet にいくら上乗せするか」と解釈
        const additional = raiseAmount;
        if (additional <= 0) {
          console.log("[DEBUG] -> Invalid raiseAmount (<=0).");
          console.log("====== handleAction END (error) ======");
          return { error: "レイズ額は正の数を入力してください" };
        }

        const finalBet = this.currentBet + additional;
        console.log(
          `[DEBUG] -> finalBet=${finalBet}, currentBet(before)=${this.currentBet}, player.bet=${player.bet}`
        );

        if (finalBet <= this.currentBet) {
          console.log("[DEBUG] -> finalBet <= currentBet => invalid raise.");
          console.log("====== handleAction END (error) ======");
          return {
            error: "レイズ額は現在のベットより大きくなければなりません",
          };
        }

        const diff = finalBet - player.bet;
        if (diff <= 0) {
          console.log("[DEBUG] -> diff <= 0 => already bet enough?");
          console.log("====== handleAction END (error) ======");
          return { error: "既にその額以上をベットしています。" };
        }

        if (diff >= player.chips) {
          console.log("[DEBUG] -> Raise exceeds player's chips => all-in.");
          this._postBet(player, player.chips);
          player.isAllIn = true;
        } else {
          this._postBet(player, diff);
          console.log(`[DEBUG] -> Player raised. Bet +${diff}.`);
        }

        // テーブルのcurrentBetを更新
        console.log(
          `[DEBUG] -> Updating currentBet from ${this.currentBet} to ${player.bet}`
        );
        this.currentBet = player.bet;
        player.hasActed = true;

        // レイズしたので他アクティブプレイヤーのhasActedをfalseに戻す
        this.players.forEach((p, i) => {
          if (!p.hasFolded && !p.isAllIn && i !== playerIndex) {
            p.hasActed = false;
            console.log(
              `[DEBUG] -> Reset hasActed=false for idx=${i}, id=${p.id}`
            );
          }
        });

        // レイズしたプレイヤーをactionStartIndexに
        this.actionStartIndex = playerIndex;
        break;
      }
      default: {
        console.log("[DEBUG] -> Unknown action.");
        console.log("====== handleAction END (error) ======");
        return { error: "不明なアクションです" };
      }
    }

    // ---------- [3] アクション処理後の状態をデバッグ出力 ----------
    console.log("[DEBUG] After action processing:");
    console.log(
      `[DEBUG] currentBet=${this.currentBet}, currentPlayerIndex=${this.currentPlayerIndex}`
    );
    this.players.forEach((p, i) => {
      console.log(
        `[DEBUG] idx=${i}, id=${p.id}, name=${p.name}, ` +
          `folded=${p.hasFolded}, allIn=${p.isAllIn}, hasActed=${p.hasActed}, ` +
          `bet=${p.bet}, chips=${p.chips}`
      );
    });

    // ---------- [4] フォールドによる1人残りチェック ----------
    const activePlayers = this.players.filter((p) => !p.hasFolded);
    if (activePlayers.length === 1) {
      console.log(
        "[DEBUG] => Only one active player left => immediate termination (fold)."
      );
      console.log("====== handleAction END (termination) ======");
      return { termination: true };
    }

    // ---------- [5] 次のプレイヤーへ ----------
    console.log("[DEBUG] -> Calling nextPlayer()...");
    const oldIndex = this.currentPlayerIndex;
    this.nextPlayer(); // 内部で isAllIn や hasFolded のプレイヤーをスキップする実装が必要

    console.log(`[DEBUG] -> nextPlayer() done. 
    oldIndex=${oldIndex}, newIndex=${this.currentPlayerIndex}`);

    // ---------- [6] ベッティングラウンド終了判定 ----------
    //   - isBettingRoundComplete() は内部で「全員オールイン or 全員hasActed」をチェック
    //   - ここでtrueならthis.advancePhase() → フェーズ移行
    const roundDone = this.isBettingRoundComplete();
    console.log(`[DEBUG] isBettingRoundComplete() => ${roundDone}`);

    if (roundDone || this.currentPlayerIndex === this.actionStartIndex) {
      // 「1周したら終了」判定を残す場合は this.currentPlayerIndex === this.actionStartIndex も考慮
      console.log("[DEBUG] -> Betting round ended => advancePhase().");
      this.advancePhase();
    }

    console.log("====== handleAction END (success) ======");
    return { success: true };
  }

  isBettingRoundComplete() {
    // フォールドしていないプレイヤー
    const activePlayers = this.players.filter((p) => !p.hasFolded);

    // デバッグログ：呼び出しの都度、状況を確認
    console.log(
      "[DEBUG isBettingRoundComplete] activePlayers.length=",
      activePlayers.length
    );

    // 1人以下なら既に終わり(フォールド総取りなど)
    if (activePlayers.length <= 1) {
      console.log(
        "[DEBUG isBettingRoundComplete] => true (only one player remains)"
      );
      return true;
    }

    // 全員オールインなら残りのカードをすべて配ってショウダウンへ
    const allAllIn = activePlayers.every((p) => p.isAllIn);
    console.log("[DEBUG isBettingRoundComplete] allAllIn =", allAllIn);

    if (allAllIn) {
      console.log(
        "[DEBUG isBettingRoundComplete] => All players all-in, deal remaining cards and go SHOWDOWN."
      );

      // フロップ→ターン→リバーを自動的にdealしてSHOWDOWNへ
      while (this.phase !== PHASES.SHOWDOWN) {
        this.advancePhase();
      }
      // ここでショウダウン処理(サイドポット計算など)が走る想定
      return true;
    }

    // ここに来た時点で「複数のアクティブプレイヤーがいて、全員オールインではない」状態
    // => 全員の hasActed が終わったか(またはフォールドorオールイン)をチェック
    const everyoneActed = activePlayers.every((p) => p.hasActed || p.isAllIn);

    console.log("[DEBUG isBettingRoundComplete] =>", everyoneActed);
    return everyoneActed;
  }

  // 次のプレイヤー選出
  nextPlayer() {
    const total = this.players.length;
    let count = 0;
    while (count < total) {
      this.currentPlayerIndex = (this.currentPlayerIndex + 1) % total;
      const curr = this.players[this.currentPlayerIndex];

      // --- [1] もしこのプレイヤーがまだアクション必要な状態 && 切断済みなら自動アクション ---
      if (
        curr.disconnected &&
        !curr.hasFolded &&
        !curr.isAllIn &&
        !curr.hasActed
      ) {
        // チップが足りていれば(=すでにcurrentBetに達していれば)チェック扱い、
        // そうでなければフォールドにする
        if (curr.bet < this.currentBet) {
          // コール or レイズする必要があるが、切断中なのでフォールド
          curr.hasFolded = true;
          curr.hasActed = true;
        } else {
          // すでに currentBet に追いついてる => チェック扱い
          curr.hasActed = true;
        }
      }

      // --- [2] このプレイヤーが「まだ行動していない、フォールドしていない、オールインでもない」なら手番終了 ---
      if (!curr.hasFolded && !curr.isAllIn && !curr.hasActed) {
        return;
      }
      count++;
    }
    // 全員がアクション完了 or フォールド状態なら、ベットラウンド終了
  }

  // フェーズ進行
  advancePhase() {
    console.log(`[DEBUG] advancePhase: currentPhase=${this.phase}`);

    switch (this.phase) {
      case PHASES.PRE_FLOP:
        this.phase = PHASES.FLOP;
        this.dealFlop();
        console.log("[DEBUG] -> Moved to FLOP. Dealt flop cards.");
        this.resetBetsForNextRound(); // ★フロップに移るタイミングでリセット
        break;

      case PHASES.FLOP:
        this.phase = PHASES.TURN;
        this.dealTurn();
        console.log("[DEBUG] -> Moved to TURN. Dealt turn card.");
        this.resetBetsForNextRound(); // ★ターンに移るタイミングでリセット
        break;

      case PHASES.TURN:
        this.phase = PHASES.RIVER;
        this.dealRiver();
        console.log("[DEBUG] -> Moved to RIVER. Dealt river card.");
        this.resetBetsForNextRound(); // ★リバーに移るタイミングでリセット
        break;

      case PHASES.RIVER:
        this.phase = PHASES.SHOWDOWN;
        console.log("[DEBUG] -> Moved to SHOWDOWN.");
        // ここではSHOWDOWNに入るので、resetBetsForNextRound()は不要
        break;

      case PHASES.SHOWDOWN:
        console.log("[DEBUG] -> Already at SHOWDOWN. No further advance.");
        break;

      default:
        console.log("[DEBUG] -> Unknown phase. Doing nothing.");
        break;
    }

    console.log(`[DEBUG] advancePhase end: currentPhase=${this.phase}`);
  }

  resetBetsForNextRound() {
    this.currentBet = 0;
    for (let p of this.players) {
      if (!p.hasFolded && !p.isAllIn) {
        p.bet = 0;
        p.hasActed = false;
      }
    }

    // 今までは「前のベッティングラウンド終了時の currentPlayerIndex」を開始位置にしていたが
    // フロップ以降は「ボタンの左隣」(ヘッズアップはBB)が先にアクションするよう修正

    if (this.phase !== PHASES.PRE_FLOP) {
      // フロップ,ターン,リバーの場合
      if (this.players.length === 2) {
        // ヘッズアップはBBが先
        this.currentPlayerIndex = this.bigBlindIndex;
      } else {
        // 3人以上はボタンの左隣= (dealerButtonIndex + 1) からスタート
        let nextIndex = (this.dealerButtonIndex + 1) % this.players.length;

        // SBがフォールドしていたり、オールインならさらに左隣を探す
        while (
          this.players[nextIndex].hasFolded ||
          this.players[nextIndex].isAllIn
        ) {
          nextIndex = (nextIndex + 1) % this.players.length;
        }
        this.currentPlayerIndex = nextIndex;
      }
    }
    // プリフロップ時(phase===PRE_FLOP)は既存のロジックどおりでOK
    this.actionStartIndex = this.currentPlayerIndex;
  }

  dealFlop() {
    console.log("[DEBUG] dealFlop: dealing three cards...");
    this.deck.pop(); // バーン
    this.communityCards.push(this.deck.pop(), this.deck.pop(), this.deck.pop());
  }
  dealTurn() {
    console.log("[DEBUG] dealTurn: dealing one card...");
    this.deck.pop(); // バーン
    this.communityCards.push(this.deck.pop());
  }
  dealRiver() {
    console.log("[DEBUG] dealRiver: dealing one card...");
    this.deck.pop(); // バーン
    this.communityCards.push(this.deck.pop());
  }

  // -------- ① サイドポットを算出する --------
  computeSidePots() {
    // フォールドしていないプレイヤーを抽出
    const contenders = this.players.filter((p) => !p.hasFolded);

    // totalBet が小さい順にソート
    const sorted = [...contenders].sort((a, b) => a.totalBet - b.totalBet);

    let sidePots = [];
    let previous = 0; // 直前の totalBet
    let remaining = contenders.length; // 残りのプレイヤー数

    for (let i = 0; i < sorted.length; i++) {
      const current = sorted[i].totalBet;
      const diff = current - previous;
      if (diff > 0) {
        // diff * remaining が、このポットの総チップ量になる
        sidePots.push({
          amount: diff * remaining,
          // このポットに参加できるのは totalBet >= current の人
          eligiblePlayers: contenders.filter((p) => p.totalBet >= current),
        });
        previous = current;
      }
      remaining--;
    }
    return sidePots;
  }

  // pokersolverからHandをimportしている想定
  // const { Hand } = require('pokersolver');

  determineSidePotWinners() {
    const sidePots = this.computeSidePots();
    let results = [];

    for (const pot of sidePots) {
      const eligible = pot.eligiblePlayers;
      const playerHands = eligible.map((player) => {
        const solverCards = [...player.hand, ...this.communityCards].map(
          convertToPokerSolverFormat
        );
        const solvedHand = Hand.solve(solverCards);
        return { player, solvedHand };
      });

      const winnersHands = Hand.winners(playerHands.map((ph) => ph.solvedHand));
      // winnersHandsは複数の要素が含まれる場合があります（完全に同じ強さでタイのとき）

      // winnersHands に対応するプレイヤーを取得
      const winningPlayers = winnersHands
        .map((wHand) => {
          return (
            playerHands.find((ph) => ph.solvedHand === wHand)?.player || null
          );
        })
        .filter(Boolean);

      // ---- サイドポットを勝者で分配 ----
      const share = Math.floor(pot.amount / winningPlayers.length);
      let leftover = pot.amount % winningPlayers.length;

      winningPlayers.forEach((w) => {
        w.chips += share;
      });

      // leftover配分時に、"ボタン+1" から順にプレイヤーを回す例
      if (leftover > 0) {
        // 現在のプレイヤーリストを「ボタンからスタートして左回り」の並びに並べ替え
        const reorder = [];
        for (let i = 0; i < this.players.length; i++) {
          reorder.push(
            this.players[(this.dealerButtonIndex + 1 + i) % this.players.length]
          );
        }
        // reorderの順序に従って、winners だけ取り出す
        const sortedWinners = reorder.filter((p) => winningPlayers.includes(p));

        // これで button+1 の座席から順に leftover を1チップずつ加算
        let i = 0;
        while (leftover > 0 && sortedWinners.length > 0) {
          sortedWinners[i].chips += 1;
          leftover--;
          i = (i + 1) % sortedWinners.length;
        }
      }

      // ---- 勝者の手役（descr）を表示したい場合 ----
      // 同じポットを勝った複数のプレイヤーがいる場合、それぞれ手役をまとめる
      const winnersInfo = winningPlayers.map((wp) => {
        // 自分がどの solvedHand だったか探す
        const ph = playerHands.find((ph) => ph.player === wp);
        // ph.solvedHand.name => 例 "Full House"
        // ph.solvedHand.descr => 例 "Full House, Aces full of Kings"
        return `${wp.name} (${ph.solvedHand.descr})`;
      });

      results.push({
        potAmount: pot.amount,
        winners: winningPlayers,
        winnersInfo, // 追加: 勝者たちの役情報も保持
      });
    }
    return results;
  }
}

// pokersolver用のカードフォーマット変換
function convertToPokerSolverFormat(card) {
  const rankMap = {
    2: "2",
    3: "3",
    4: "4",
    5: "5",
    6: "6",
    7: "7",
    8: "8",
    9: "9",
    10: "T",
    J: "J",
    Q: "Q",
    K: "K",
    A: "A",
  };
  const suitMap = { "♠": "s", "♥": "h", "♦": "d", "♣": "c" };
  return rankMap[card.rank] + suitMap[card.suit];
}

//
// ルーム（テーブル）管理
//
const rooms = {};
for (let i = 1; i <= MAX_TABLES; i++) {
  rooms[i] = new TexasHoldemGame();
}
const playerRoomMap = {};

//
// サーバーセットアップ
//
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
app.use(express.static("public"));

// ゲーム状態を各ソケットごとに送信
async function broadcastGameState(roomId) {
  const sockets = await io.in(String(roomId)).fetchSockets();
  sockets.forEach((socket) => {
    socket.emit("gameUpdate", getPublicGameState(roomId, socket.id));
  });
}

function getPublicGameState(roomId, viewerId) {
  const room = rooms[roomId];
  return {
    phase: room.phase,
    pot: room.pot,
    currentBet: room.currentBet,
    currentPlayerId: room.players[room.currentPlayerIndex]?.id || null,
    communityCards: room.communityCards,

    players: room.players.map((p, idx) => {
      // SHOWDOWNかつフォールドしていなければ全員のカード公開
      // それ以外のフェーズでは、自分のカードだけ公開
      const showCards =
        p.id === viewerId || (room.phase === PHASES.SHOWDOWN && !p.hasFolded);

      // 手札は公開条件を満たしていればそのまま表示、そうでなければ裏表示にする
      const displayedHand = showCards
        ? p.hand
        : p.hand.map(() => ({ rank: "??", suit: "??" }));

      // --- ここで現在の役を計算するロジックを追加 ---
      // (1) デフォルトでは空文字（または null）
      let currentRank = "";

      // (2) まだフォールドしていない & フロップ以降 & 自分だけが見られる場合
      //     という条件を満たせば、pokersolverで手役を計算
      if (
        !p.hasFolded &&
        showCards &&
        room.phase !== PHASES.PRE_FLOP &&
        room.communityCards.length >= 3
      ) {
        // 現在公開されているコミュニティカードを合算
        const combined = [...p.hand, ...room.communityCards];
        const solved = Hand.solve(combined.map(convertToPokerSolverFormat));
        // 例: solved.name => 'Pair', solved.descr => 'Pair, Tens'
        currentRank = solved.descr;
      }

      return {
        id: p.id,
        name: p.name,
        chips: p.chips,
        bet: p.bet,
        hasFolded: p.hasFolded,
        positionLabel: getPositionLabel(room, idx),
        hand: displayedHand,

        // フロップ以降で見られる場合にのみ役を返す
        // (ショウダウンなら全員の役が見られる)
        rank: currentRank,
      };
    }),
  };
}

// ソケットイベント
io.on("connection", (socket) => {
  console.log("New connection:", socket.id);

  socket.on("joinGame", (data) => {
    let roomId = parseInt(data.roomId, 10);
    if (isNaN(roomId) || roomId < 1 || roomId > MAX_TABLES) {
      roomId = 1;
    }
    const room = rooms[roomId];
    if (room.players.length >= MAX_PLAYERS_PER_TABLE) {
      socket.emit("gameMessage", {
        message: `テーブル${roomId}は満員です。別のテーブルを選んでください。`,
      });
      return;
    }
    room.addPlayer({ id: socket.id, name: data.playerName });
    socket.join(String(roomId));
    playerRoomMap[socket.id] = roomId;
    broadcastGameState(roomId);
  });

  socket.on("startGame", () => {
    const roomId = playerRoomMap[socket.id];
    if (!roomId) {
      socket.emit("gameMessage", { message: "テーブルに参加していません" });
      return;
    }
    const room = rooms[roomId];

    console.log(`[DEBUG startGame] StartGame called by ${socket.id}.`);
    console.log(
      "[DEBUG startGame] Current players:",
      room.players.map((p) => ({ id: p.id, name: p.name, chips: p.chips }))
    );

    if (room.players.length < 2) {
      io.to(String(roomId)).emit("gameMessage", {
        message: "プレイヤーが2人以上必要です",
      });
      return;
    }

    // ここで room.startRound() など
    room.startRound();
    broadcastGameState(roomId);
  });

  socket.on("playerAction", ({ action, raiseAmount }) => {
    const roomId = playerRoomMap[socket.id];
    if (!roomId) {
      socket.emit("gameMessage", { message: "テーブルに参加していません" });
      return;
    }
    const room = rooms[roomId];
    const result = room.handleAction(socket.id, action, raiseAmount);

    if (result.error) {
      socket.emit("gameMessage", { message: result.error });
      return;
    }

    // 誰か以外全員フォールドの終了処理
    if (result.termination) {
      const activePlayers = room.players.filter((p) => !p.hasFolded);
      const winner = activePlayers[0];
      winner.chips += room.pot;
      const resultMessage = `全員がフォールドしたため、${winner.name}の勝利です。`;
      io.to(String(roomId)).emit("gameResult", {
        message: resultMessage,
        pot: room.pot,
      });
      io.to(winner.id).emit("winNotification", { message: "Win" });
      broadcastGameState(roomId);

      // 5秒待ってから endRoundCleanup() → さらに2秒後に startRound()
      setTimeout(() => {
        endRoundCleanup(roomId);
        broadcastGameState(roomId);

        setTimeout(() => {
          const room = rooms[roomId];
          if (room && room.players.filter((p) => p.chips > 0).length >= 2) {
            room.startRound();
          } else {
            room.phase = PHASES.WAITING;
          }
          broadcastGameState(roomId);
        }, 2000);
      }, 5000);

      return;
    }

    // 通常の更新
    broadcastGameState(roomId);

    // ショウダウンに突入したらサイドポット計算
    if (room.phase === PHASES.SHOWDOWN) {
      const sidePotResults = room.determineSidePotWinners();
      let resultMessage = "";
      const winnersSet = new Set();
      sidePotResults.forEach((pot, idx) => {
        // winnersInfo を直接使って「○○(Four of a Kind)」のように表示
        const winnersText = pot.winnersInfo.join(", ");
        resultMessage += `サイドポット${idx + 1} (${
          pot.potAmount
        }): ${winnersText}\n`;

        const share = Math.floor(pot.potAmount / pot.winners.length);
        pot.winners.forEach((w) => {
          w.chips += share;
          winnersSet.add(w);
        });
      });

      io.to(String(roomId)).emit("gameResult", {
        message: resultMessage,
        pot: room.pot,
      });
      // 勝者にWin通知
      winnersSet.forEach((w) => {
        io.to(w.id).emit("winNotification", { message: "Win" });
      });

      // ショウダウン画面を十分表示するため、数秒待ってから次へ
      // (ここでは8秒)
      setTimeout(() => {
        room.phase = PHASES.WAITING;
        room.incorporateWaitingPlayers();
        room.players = room.players.filter((p) => !p.disconnected);
        endRoundCleanup(roomId);
        broadcastGameState(roomId);
        setTimeout(() => {
          if (room.players.filter((p) => p.chips > 0).length >= 2) {
            room.startRound();
          } else {
            room.phase = PHASES.WAITING;
          }
          broadcastGameState(roomId);
        }, 2000); // WAITINGにしてからさらに2秒後に次スタート(計10秒)
      }, 8000);
    }
  });

  socket.on("disconnect", () => {
    const roomId = playerRoomMap[socket.id];
    if (roomId) {
      const room = rooms[roomId];

      // プレイヤーを即座に remove せず、disconnected=true にする
      const target = room.players.find((p) => p.id === socket.id);
      if (target) {
        target.disconnected = true;
      }

      // ただし waitingPlayers にいる場合は取り除いてOK(まだハンドに参加していないので)
      room.waitingPlayers = room.waitingPlayers.filter(
        (p) => p.id !== socket.id
      );

      // [注意] ここでは room.players からは削除しない
      // room.players = room.players.filter((p) => p.id !== socket.id);

      delete playerRoomMap[socket.id];

      broadcastGameState(roomId);
    }
    console.log("Disconnect:", socket.id);
  });
});

// 6max用のポジション名配列
const SIX_MAX_POSITIONS = ["BTN", "SB", "BB", "UTG", "HJ", "CO"];

function getPositionLabel(game, seatIndex) {
  const n = game.players.length;
  // ディーラーボタンの位置
  const button = game.dealerButtonIndex;
  // offsetを計算 (seatIndexがボタンから何番目にいるか)
  const offset = (seatIndex - button + n) % n;

  // ヘッズアップ(2人)なら特別処理
  if (n === 2) {
    // offset=0 => BTN/SB, offset=1 => BB
    return offset === 0 ? "SB(BTN)" : "BB";
  }
  // 3～6人の場合: offsetに従ってSIX_MAX_POSITIONSを割り当て
  if (offset < SIX_MAX_POSITIONS.length) {
    return SIX_MAX_POSITIONS[offset];
  }
  // 念のため
  return "";
}

// ラウンド終了処理の例
function endRoundCleanup(roomId) {
  console.log(`[DEBUG endRoundCleanup] Called for roomId=${roomId}`);
  const room = rooms[roomId];
  if (!room) return;

  // (1) 待機者合流
  if (typeof room.incorporateWaitingPlayers === "function") {
    console.log(
      "[DEBUG endRoundCleanup] -> Calling incorporateWaitingPlayers()"
    );
    room.incorporateWaitingPlayers();
  }

  // (2) 0チップ or disconnected をplayers配列から除外
  const beforeCount = room.players.length;
  room.players = room.players.filter((p) => p.chips > 0 && !p.disconnected);
  const removedPlayers = beforeCount - room.players.length;
  console.log(
    `[DEBUG endRoundCleanup] -> Removed ${removedPlayers} players from room.players.`
  );

  // waitingPlayers も同様に除外 (不要なら省略可)
  const beforeWait = room.waitingPlayers.length;
  room.waitingPlayers = room.waitingPlayers.filter(
    (p) => p.chips > 0 && !p.disconnected
  );
  console.log(
    `[DEBUG endRoundCleanup] -> Removed ${
      beforeWait - room.waitingPlayers.length
    } from waitingPlayers.`
  );

  // ログ出し
  console.log(
    "[DEBUG endRoundCleanup] final players:",
    room.players.map((p) => ({
      id: p.id,
      name: p.name,
      chips: p.chips,
    }))
  );
  console.log(
    "[DEBUG endRoundCleanup] final waitingPlayers:",
    room.waitingPlayers.map((p) => ({
      id: p.id,
      name: p.name,
      chips: p.chips,
    }))
  );

  // 2人未満ならWAITINGにしておく
  if (room.players.length < 2) {
    console.log("[DEBUG endRoundCleanup] -> Not enough players => WAITING");
    room.phase = PHASES.WAITING;
  }

  // (4) 最後に broadcast
  console.log("[DEBUG endRoundCleanup] -> broadcastGameState(roomId)");
  broadcastGameState(roomId);
  console.log("[DEBUG endRoundCleanup] Done.");
}

server.listen(process.env.PORT || 8080, () => {
  console.log(`Server running on port ${process.env.PORT || 8080}`);
});

```