オンライン対戦できるテキサスホールデムのWebアプリ開発（アップデート版）

オンライン対戦可能なテキサスホールデムを作っています。

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

最新版では以下の機能追加やバグ改善をしました。

・Geminiとのオンライン対戦機能の強化
・キッカー勝負になったとき正しく勝敗判定をする。
・その他勝敗判定のバグ改善
・UI（ルーム一覧表示機能）の強化

まだまだ追加したい機能が山盛りですが、今日の改善で結構普通に遊べるようになったかな？と思います。

ご意見ご感想などぜひよろしくお願いいたします。

### ソースコード
```texasholdem.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <title>Texas Hold'em Online</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: 70px;
      margin: 2px;
    }

    #joinedRooms>div {
      border: 1px solid #fff;
      margin: 10px;
      padding: 10px;
    }

    .room-actions {
      margin-top: 10px;
    }

    .room-messages {
      margin-top: 10px;
      color: yellow;
    }

    .community-cards {
      margin-top: 10px;
    }

    .my-turn-room {
      border: 3px solid red !important;
    }

    /* ルーム一覧のテーブルをスタイリング */
    #roomList table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 10px;
      background: #006400;
    }

    #roomList th,
    #roomList td {
      border: 1px solid #fff;
      padding: 8px;
      text-align: center;
    }

    #roomList th {
      background: #228B22;
      cursor: pointer;
    }
  </style>
  <script>
    // --- カード画像のプリロード（既存処理） ---
    const cardImages = {};
    function preloadCards() {
      const suits = ["C", "D", "H", "S"];
      const ranks = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];
      for (let suit of suits) {
        for (let rank of ranks) {
          const key = rank + suit;
          const img = new Image();
          img.src = `playingcard/${key}.svg`;
          cardImages[key] = img;
        }
      }
      const backImg = new Image();
      backImg.src = "playingcard/back.svg";
      cardImages["back"] = backImg;

      const backGrayImg = new Image();
      backGrayImg.src = "playingcard/back-gray.svg";
      cardImages["back-gray"] = backGrayImg;

      console.log("Cards preloaded:", Object.keys(cardImages));
    }
    preloadCards();
    window.cardImages = cardImages;

    // --- カード描画（既存処理） ---
    function getCardImageHtml(card) {
      if (!card) return "";
      let key = "";
      if (card.rank === "??" && card.suit === "??") {
        key = card.folded ? "back-gray" : "back";
      } else {
        let suitLetter = "";
        switch (card.suit) {
          case "♣": suitLetter = "C"; break;
          case "♦": suitLetter = "D"; break;
          case "♥": suitLetter = "H"; break;
          case "♠": suitLetter = "S"; break;
        }
        key = `${card.rank}${suitLetter}`;
      }
      if (window.cardImages && window.cardImages[key]) {
        return `<img src="${window.cardImages[key].src}" alt="${key}" class="card">`;
      } else {
        return `<img src="playingcard/${key}.svg" alt="${key}" class="card">`;
      }
    }

    function renderHand(hand) {
      if (!hand || hand.length === 0) return "No Cards";
      return hand.map(card => getCardImageHtml(card)).join("");
    }
    window.renderHand = renderHand;
  </script>
  <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
</head>

<body>
  <div class="container">
    <h1>テキサスホールデムオンライン</h1>

    <!-- 名前入力とルーム一覧ボタン -->
    <div>
      <input type="text" id="playerName" placeholder="名前">
      <button id="loadRoomListBtn">ルーム一覧を読み込み</button>
      <!-- ここにテーブル形式のルーム一覧を表示する -->
      <div id="roomList">
        <!-- テーブルヘッダ部分 -->
        <table>
          <thead>
            <tr>
              <th onclick="sortRooms('tableName')">テーブル名</th>
              <th onclick="sortRooms('stake')">ステーク</th>
              <th onclick="sortRooms('gameType')">ゲーム</th>
              <th onclick="sortRooms('playerCount')">着席者数</th>
              <th onclick="sortRooms('waitingCount')">空席待ち</th>
              <th>参加</th>
            </tr>
          </thead>
          <tbody id="roomListBody">
            <!-- ルームごとの行が動的に入る -->
          </tbody>
        </table>
      </div>
    </div>

    <hr>
    <div id="messages" style="margin-top:10px; color:yellow;"></div>
    <div id="result" style="margin-top:10px;"></div>

    <!-- 参加中のルーム情報を表示 -->
    <div id="joinedRooms"></div>
    <div id="countdownTimer" style="font-size: 24px; text-align: center; margin-bottom: 10px;"></div>
  </div>

  <script>
    //const socket = io("http://localhost:8080");
    const socket = io("https://socket-io-service-466625290686.us-central1.run.app");
    let myId = null;

    // 受け取ったルームデータを保持し、ソートや再描画に利用する
    let roomsData = [];
    let currentSortColumn = null;
    let currentSortDirection = 1; // 1: 昇順, -1: 降順

    const roomMessages = {};
    const loadRoomListBtn = document.getElementById('loadRoomListBtn');
    const roomListDiv = document.getElementById('roomList');
    const roomListBody = document.getElementById('roomListBody');
    const playerNameInput = document.getElementById('playerName');

    // --- ソート用関数 ---
    function sortRooms(column) {
      if (currentSortColumn === column) {
        // 同じ列なら昇順・降順を反転
        currentSortDirection = -currentSortDirection;
      } else {
        currentSortColumn = column;
        currentSortDirection = 1;
      }

      roomsData.sort((a, b) => {
        let cmp = 0;
        if (column === "tableName") {
          // "テーブル"の後ろの数値部分を抽出して数値として比較
          const numA = parseInt(a.tableName.replace("テーブル", ""), 10);
          const numB = parseInt(b.tableName.replace("テーブル", ""), 10);
          cmp = numA - numB;
        } else {
          // 他の列は数値の場合は数値比較、そうでなければ文字列比較
          if (!isNaN(a[column]) && !isNaN(b[column])) {
            cmp = a[column] - b[column];
          } else {
            cmp = a[column].toString().localeCompare(b[column].toString());
          }
        }
        return cmp * currentSortDirection;
      });

      renderRoomsTable();
    }


    // --- ルーム一覧テーブルを描画 ---
    function renderRoomsTable() {
      // tbodyをクリア
      roomListBody.innerHTML = "";

      roomsData.forEach((r) => {
        // それぞれのカラムを表示
        const tr = document.createElement("tr");

        // テーブル名
        const tdName = document.createElement("td");
        tdName.textContent = r.tableName;
        tr.appendChild(tdName);

        // ステーク (SB/BB)
        const tdStake = document.createElement("td");
        tdStake.textContent = r.stake;
        tr.appendChild(tdStake);

        // ゲーム (VS 対人 or VS Gemini)
        const tdGame = document.createElement("td");
        tdGame.textContent = r.gameType;
        tr.appendChild(tdGame);

        // 着席者数
        const tdPlayerCount = document.createElement("td");
        tdPlayerCount.textContent = r.playerCount;
        tr.appendChild(tdPlayerCount);

        // 空席待ち
        const tdWaiting = document.createElement("td");
        tdWaiting.textContent = r.waitingCount;
        tr.appendChild(tdWaiting);

        // 参加ボタン
        const tdJoin = document.createElement("td");
        const joinBtn = document.createElement("button");
        joinBtn.textContent = "参加";
        joinBtn.onclick = () => joinRoom(r.roomId);
        tdJoin.appendChild(joinBtn);
        tr.appendChild(tdJoin);

        roomListBody.appendChild(tr);
      });
    }

    // --- ルームに参加する処理 ---
    function joinRoom(roomId) {
      const name = playerNameInput.value.trim();
      if (!name) {
        alert("名前を入力してください");
        return;
      }
      socket.emit("joinGame", { playerName: name, roomId });
    }

    // --- ルーム一覧をサーバーに要求 ---
    function loadRoomList() {
      console.log("getRooms emitted");
      socket.emit("getRooms");
    }

    loadRoomListBtn.onclick = loadRoomList;

    // --- ソケット接続時 ---
    socket.on('connect', () => {
      myId = socket.id;
      console.log("Connected with socket id:", myId);
      loadRoomList();
    });

    // --- ルーム一覧を受信したとき ---
    socket.on("roomList", (rooms) => {
      console.log("roomList received:", rooms);
      // roomsData を組み立てる
      // 例: rooms[i] に sb, bb, type, playerCount, waitingCount, roomId などの情報があると仮定
      // テーブル名は「テーブル1, テーブル2, ...」として付ける
      // もしサーバーから "tableName" が来るならそれを使ってもOK

      roomsData = rooms.map((r, index) => {
        // SB/BB の文字列を作成 (例: "10/20")
        const stakeStr = `${r.sb}/${r.bb}`;
        // ゲームタイプ
        const gameTypeStr = r.type === "human" ? "VS 対人" : "VS Gemini";

        return {
          roomId: r.roomId,
          tableName: `テーブル${index + 1}`,  // 任意で付与
          stake: stakeStr,
          gameType: gameTypeStr,
          playerCount: r.playerCount || 0,
          waitingCount: r.waitingCount || 0
        };
      });

      // 受け取った時点で描画（デフォルトはそのままの順番）
      renderRoomsTable();
    });

    // --- ゲーム開始やアクション送信など既存処理 ---
    const joinedRoomsDiv = document.getElementById("joinedRooms");
    function sendActionForRoom(roomId, actionType) {
      if (actionType === "fold") {
        stopCountdown();
      }
      const input = document.getElementById('raiseAmount-' + roomId);
      const raiseAmount = input ? parseInt(input.value, 10) || 0 : 0;
      socket.emit("playerAction", { roomId, action: actionType, raiseAmount });
    }
    function startGameForRoom(roomId) {
      socket.emit("startGame", { roomId });
      let roomMsgDiv = document.getElementById("roomMsg-" + roomId);
      if (roomMsgDiv) { roomMsgDiv.innerHTML = ""; }
    }
    // --- カウントダウン用グローバル変数 ---
    let countdownInterval = null;
    let autoActionTimeout = null;

    // カウントダウン表示用関数
    function startCountdown(seconds) {
      const timerEl = document.getElementById("countdownTimer");
      if (!timerEl) return;
      timerEl.textContent = `残り ${seconds} 秒`;
      // 既存のタイマーがあればクリア
      if (countdownInterval) clearInterval(countdownInterval);
      countdownInterval = setInterval(() => {
        seconds--;
        if (seconds <= 0) {
          clearInterval(countdownInterval);
          timerEl.textContent = "";
        } else {
          timerEl.textContent = `残り ${seconds} 秒`;
        }
      }, 1000);
    }

    // タイマーを停止する関数
    function stopCountdown() {
      const timerEl = document.getElementById("countdownTimer");
      if (countdownInterval) clearInterval(countdownInterval);
      if (autoActionTimeout) clearTimeout(autoActionTimeout);
      countdownInterval = null;
      autoActionTimeout = null;
      if (timerEl) timerEl.textContent = "";
    }

    // --- メッセージやゲーム更新のハンドラ（既存） ---
    socket.on("roomMessage", (data) => {
      if (data.roomId) {
        roomMessages[data.roomId] = data.message;
        let roomMsgDiv = document.getElementById("roomMsg-" + data.roomId);
        if (roomMsgDiv) { roomMsgDiv.innerHTML = `<h3>${data.message}</h3>`; }
      } else {
        document.getElementById("messages").innerHTML = data.message;
      }
    });

    socket.on("gameUpdate", (data) => {
      const roomId = data.roomId;
      let roomDiv = document.getElementById("room-" + roomId);
      if (!roomDiv) {
        roomDiv = document.createElement("div");
        roomDiv.id = "room-" + roomId;
        joinedRoomsDiv.appendChild(roomDiv);
      }
      let roomMsgDiv = document.getElementById("roomMsg-" + roomId);
      if (!roomMsgDiv) {
        roomMsgDiv = document.createElement("div");
        roomMsgDiv.id = "roomMsg-" + roomId;
        roomMsgDiv.className = "room-messages";
        roomDiv.appendChild(roomMsgDiv);
      }
      let roomContentDiv = document.getElementById("roomContent-" + roomId);
      if (!roomContentDiv) {
        roomContentDiv = document.createElement("div");
        roomContentDiv.id = "roomContent-" + roomId;
        roomDiv.insertBefore(roomContentDiv, roomMsgDiv);
      }
      const viewer = data.players.find(p => p.id === myId);
      const maxRaise = viewer ? viewer.chips : 0;

      // チェックボタンの活性／非活性を、数値として viewer.bet と data.currentBet を比較
      const viewerBet = Number(viewer ? viewer.bet : 0);
      const tableBet = Number(data.currentBet);
      const disableCheck = (viewerBet !== tableBet);

      let actionsHtml = '';
      if (data.phase === "WAITING") {
        actionsHtml = `
      <div class="room-actions">
        <button onclick="startGameForRoom(${roomId})">ゲーム開始</button>
      </div>
    `;
      } else {
        actionsHtml = `
      <div class="room-actions">
        <button onclick="sendActionForRoom(${roomId}, 'fold')">フォールド</button>
        <button onclick="sendActionForRoom(${roomId}, 'call')">コール</button>
        <button onclick="sendActionForRoom(${roomId}, 'check')" ${disableCheck ? 'disabled' : ''}>チェック</button>
        <div>
          <label for="raiseAmount-${roomId}">レイズ額: </label>
          <input type="range" id="raiseAmount-${roomId}" min="0" max="${maxRaise}" value="0"
                 oninput="document.getElementById('raiseAmountOutput-${roomId}').value = this.value">
          <output id="raiseAmountOutput-${roomId}">0</output>
          <button onclick="sendActionForRoom(${roomId}, 'raise')">レイズ</button>
        </div>
      </div>
    `;
      }

      roomContentDiv.innerHTML = `
    <div class="room-status">
      <h3>ルーム${roomId}</h3>
      <p>フェーズ: ${data.phase} | ポット: ${data.pot} | 現在のベット: ${data.currentBet}</p>
      <p>現在の手番: ${data.currentPlayerId || ""}</p>
      <div class="community-cards">
        <h3>コミュニティカード:</h3>
        ${renderHand(data.communityCards)}
      </div>
      <div>
        ${data.players.map(p => `
          <div class="player ${p.id === data.currentPlayerId ? 'current-turn' : ''}">
            <strong>${p.positionLabel} ${p.name}</strong> - Chips: ${p.chips}, Bet: ${p.bet}, Fold: ${p.hasFolded}
            <br>Hand: ${renderHand(p.hand)} ${p.rank ? `| ${p.rank}` : ''}
          </div>
        `).join('')}
      </div>
    </div>
    ${actionsHtml}
  `;
      if (data.currentPlayerId === myId) {
        roomDiv.classList.add("my-turn-room");
        roomDiv.scrollIntoView({ behavior: "smooth", block: "start" });
      } else {
        roomDiv.classList.remove("my-turn-room");
      }

      // --- カウントダウン制御 ---
      // ゲーム終了やショウダウン、またはフェーズが WAITING の場合は常にタイマーを停止
      if (data.phase === "WAITING" || data.phase === "SHOWDOWN") {
        stopCountdown();
      }
      // それ以外で、自分の手番ならカウントダウン開始
      else if (data.currentPlayerId === myId && viewer && !viewer.hasFolded) {
        startCountdown(10);
        if (autoActionTimeout) clearTimeout(autoActionTimeout);
        autoActionTimeout = setTimeout(() => {
          const updatedViewer = data.players.find(p => p.id === myId);
          const autoAction = (updatedViewer && updatedViewer.bet === data.currentBet) ? "check" : "fold";
          console.log(`[DEBUG] Auto action timeout reached. Emitting auto action: ${autoAction}`);
          sendActionForRoom(data.roomId, autoAction);
          stopCountdown();
        }, 10000);
      }
      else {
        stopCountdown();
      }
    });


    function sendActionForRoom(roomId, actionType) {
      // ※ここではエラーや正常な手動アクション送信時は gameUpdate で再更新されるので、
      // このタイミングで stopCountdown() は呼ばない（不要な場合は、後から gameUpdate で反映される）
      const input = document.getElementById('raiseAmount-' + roomId);
      const raiseAmount = input ? parseInt(input.value, 10) || 0 : 0;
      socket.emit("playerAction", { roomId, action: actionType, raiseAmount });
    }

    socket.on("gameMessage", (data) => {
      // 「チェック可能な状況でフォールドはできません」というエラーの場合はカウントダウンを止めずにエラー表示のみ
      if (data.message.includes("チェック可能な状況でフォールドはできません")) {
        let roomMsg = document.getElementById("roomMsg-" + data.roomId);
        if (roomMsg) {
          roomMsg.innerHTML = `<h3>${data.message}</h3>`;
        }
        // カウントダウンはそのまま継続
      } else {
        // その他の場合は通常どおりカウントダウンを停止する
        stopCountdown();
        let roomMsg = document.getElementById("roomMsg-" + data.roomId);
        if (roomMsg) {
          roomMsg.innerHTML = `<h3>${data.message}</h3>`;
        }
      }
    });

    socket.on("gameResult", (data) => {
      stopCountdown();
      if (data.roomId) {
        let roomMsgDiv = document.getElementById("roomMsg-" + data.roomId);
        if (roomMsgDiv) {
          roomMsgDiv.innerHTML = `<h3 style="white-space: pre-line;">${data.message}</h3>`;
        }
      } else {
        document.getElementById("messages").innerHTML = data.message;
      }
    });

    socket.on("winNotification", (data) => {
      stopCountdown();
      if (data.roomId && data.winnerId && data.winnerId === myId) {
        let roomMsgDiv = document.getElementById("roomMsg-" + data.roomId);
        if (roomMsgDiv) {
          roomMsgDiv.innerHTML += `<h3 style="color:red;">${data.message}</h3>`;
        }
      }
    });

    socket.on("clearRoomMessage", (data) => {
      if (data.roomId) {
        let roomMsgDiv = document.getElementById("roomMsg-" + data.roomId);
        if (roomMsgDiv) { roomMsgDiv.innerHTML = ""; }
      }
    });
  </script>
</body>

</html>
```
```texasgame.js
// game.js
const { PHASES, SMALL_BLIND, BIG_BLIND } = require("./config");
const { convertToPokerSolverFormat } = require("./utils");
const { Hand } = require("pokersolver");

class TexasHoldemGame {
  constructor() {
    this.players = [];
    this.deck = [];
    this.communityCards = [];
    this.pot = 0;
    this.currentBet = 0;
    this.currentPlayerIndex = 0;
    this.phase = PHASES.WAITING;
    this.dealerButtonIndex = 0;
    this.actionStartIndex = 0;
    this.smallBlindIndex = null;
    this.bigBlindIndex = null;
    this.waitingPlayers = [];
    this.showdownEvaluated = false;
    this.onShowdown = this.evaluateShowdown.bind(this);
    this.sendResultCallback = null;
  }

  incorporateWaitingPlayers() {
    console.log(
      "[DEBUG incorporateWaitingPlayers] waitingPlayers:",
      this.waitingPlayers.map((p) => ({
        id: p.id,
        name: p.name,
        chips: p.chips,
        disconnected: p.disconnected,
      }))
    );
    const validWaiters = this.waitingPlayers.filter(
      (p) => p.chips > 0 && !p.disconnected
    );
    console.log(
      `[DEBUG incorporateWaitingPlayers] Valid waiting players: ${validWaiters.length} / ${this.waitingPlayers.length}`
    );
    this.players.push(...validWaiters);
    this.waitingPlayers = [];
    console.log(
      "[DEBUG incorporateWaitingPlayers] 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 }) {
    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);
    this.waitingPlayers = this.waitingPlayers.filter((p) => p.id !== socketId);
  }

  startRound() {
    this.showdownEvaluated = false;
    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;
    }
    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) {
        this.smallBlindIndex = this.dealerButtonIndex;
        this.bigBlindIndex = (this.dealerButtonIndex + 1) % this.players.length;
        this._postBet(this.players[this.smallBlindIndex], SMALL_BLIND);
        this._postBet(this.players[this.bigBlindIndex], BIG_BLIND);
        this.currentBet = BIG_BLIND;
        this.currentPlayerIndex = this.smallBlindIndex;
        this.actionStartIndex = this.currentPlayerIndex;
      } else {
        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;
        this.currentPlayerIndex =
          (this.dealerButtonIndex + 3) % this.players.length;
        this.actionStartIndex = this.currentPlayerIndex;
        this.smallBlindIndex = sbIndex;
        this.bigBlindIndex = bbIndex;
      }
    }
    console.log(
      `[DEBUG startRound] phase=${this.phase}, dealerButtonIndex=${this.dealerButtonIndex}, smallBlindIndex=${this.smallBlindIndex}, bigBlindIndex=${this.bigBlindIndex}, currentPlayerIndex=${this.currentPlayerIndex}`
    );
  }

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

  // ここから新規追加
  executePlayerAction(player, action) {
    console.log(
      `[DEBUG] executePlayerAction: Executing action "${action}" for player ${player.id}`
    );
    return this.handleAction(player.id, action);
  }

  nextPlayerTurn() {
    console.log("[DEBUG] nextPlayerTurn: Moving to next player");
    return this.nextPlayer();
  }

  // 自動アクションの処理（修正後）
  startPlayerTurn(player) {
    console.log(
      `[DEBUG] startPlayerTurn: Starting turn for player ${player.id} with a 10-second timer.`
    );
    clearTimeout(player.turnTimer);
    player.turnTimer = setTimeout(() => {
      const autoAction = this.currentBet > 0 ? "fold" : "check";
      console.log(
        `[DEBUG] Auto action timer fired for player ${player.id}: ${autoAction}`
      );
      this.executePlayerAction(player, autoAction);
      player.consecutiveTimeouts = (player.consecutiveTimeouts || 0) + 1;
      if (player.consecutiveTimeouts >= 2) {
        console.log(
          `[DEBUG] Player ${player.id} reached consecutive timeout limit. Kicking player.`
        );
        this.kickPlayer(player);
      }
      this.nextPlayerTurn();
    }, 10000);
  }

  // 手動アクションの処理（修正後）
  onPlayerAction(player, action) {
    console.log(
      `[DEBUG] onPlayerAction: Player ${player.id} performed action "${action}" before timeout.`
    );
    clearTimeout(player.turnTimer);
    this.executePlayerAction(player, action);
    player.consecutiveTimeouts = 0;
    this.nextPlayerTurn();
  }

  handleAction(playerId, action, raiseAmount = 0) {
    if (this.phase === PHASES.SHOWDOWN) {
      console.log("[DEBUG] Action received during SHOWDOWN phase, ignoring.");
      return { error: "ショウダウン中は操作できません" };
    }
    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}`
      );
    });

    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: "すでにフォールドまたはオールインしています" };
    }

    switch (action) {
      case "fold":
        // チェック可能な状況（自分のベットが currentBet と等しい場合）は、フォールドが意味を持たない
        if (player.bet === this.currentBet) {
          if (player.id === "GeminiAI") {
            console.log(
              "[DEBUG] -> GeminiAI attempted fold in checkable situation; overriding to check."
            );
            player.hasActed = true;
            break;
          } else {
            console.log(
              "[DEBUG] -> Attempted to fold in a checkable situation; fold is not allowed."
            );
            return { error: "チェック可能な状況でフォールドはできません" };
          }
        }
        // それ以外は通常のフォールド処理
        player.hasFolded = true;
        player.hasActed = true;
        console.log(`[DEBUG] -> Player ${player.id} folded.`);
        break;

      case "check":
        if (player.bet !== this.currentBet && this.currentBet !== 0) {
          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": {
        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] -> Already matched or behind.");
        }
        player.hasActed = true;
        break;
      }
      case "raise": {
        const additional = raiseAmount;
        if (additional <= 0) {
          console.log("[DEBUG] -> Invalid raiseAmount (<=0).");
          console.log("====== handleAction END (error) ======");
          return { error: "レイズ額は正の数を入力してください" };
        }
        // 対戦相手の中で、まだアクション可能な（フォールドしていない）プレイヤーを対象にする
        const opponents = this.players.filter(
          (p) => p.id !== player.id && !p.hasFolded
        );
        // すべての対戦相手がオール・イン状態の場合、レイズは許されない
        if (opponents.length > 0 && opponents.every((p) => p.isAllIn)) {
          console.log(
            "[DEBUG] -> Raise not allowed: 全対戦相手がオール・イン状態です。"
          );
          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}.`);
        }
        console.log(
          `[DEBUG] -> Updating currentBet from ${this.currentBet} to ${player.bet}`
        );
        this.currentBet = player.bet;
        player.hasActed = true;
        this.players.forEach((p, i) => {
          if (!p.hasFolded && !p.isAllIn && i !== playerIndex) {
            p.hasActed = false;
            console.log(`[DEBUG] -> Reset hasActed for idx=${i}, id=${p.id}`);
          }
        });
        this.actionStartIndex = playerIndex;
        break;
      }

      default:
        console.log("[DEBUG] -> Unknown action.");
        console.log("====== handleAction END (error) ======");
        return { error: "不明なアクションです" };
    }

    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}`
      );
    });

    const activePlayers = this.players.filter((p) => !p.hasFolded);
    if (activePlayers.length === 1) {
      console.log("[DEBUG] => Only one active player left => termination.");
      console.log("====== handleAction END (termination) ======");
      return { termination: true };
    }

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

    const roundDone = this.isBettingRoundComplete();
    console.log(`[DEBUG] isBettingRoundComplete() => ${roundDone}`);
    if (roundDone) {
      console.log("[DEBUG] -> Betting round ended => advancePhase().");
      this.advancePhase();
    }
    console.log("====== handleAction END (success) ======");
    return { success: true };
  }

  isBettingRoundComplete() {
    console.log("[DEBUG isBettingRoundComplete] START phase=", this.phase);
    const activePlayers = this.players.filter((p) => !p.hasFolded);
    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, moving to SHOWDOWN."
      );
      while (this.phase !== PHASES.SHOWDOWN) {
        this.advancePhase();
      }
      return true;
    }
    const everyoneActed = activePlayers.every((p) => {
      if (p.isAllIn || p.hasFolded) return true;
      if (this.currentBet === 0) {
        return p.hasActed;
      }
      return p.bet >= this.currentBet && p.hasActed;
    });
    console.log(
      "[DEBUG isBettingRoundComplete] everyoneActed =",
      everyoneActed
    );
    console.log("[DEBUG isBettingRoundComplete] END =>", everyoneActed);
    return everyoneActed;
  }

  nextPlayer() {
    console.log(
      `[DEBUG nextPlayer] START currentPlayerIndex=${this.currentPlayerIndex}, phase=${this.phase}`
    );
    if (this.players.length === 2) {
      // ヘッズアップの場合、次のプレイヤーがオール・インまたは既にアクション済みであれば、ラウンド終了とみなす
      const nextIndex = (this.currentPlayerIndex + 1) % 2;
      const nextPlayer = this.players[nextIndex];
      if (nextPlayer.isAllIn || nextPlayer.hasActed) {
        // ラウンドは完了していると判断する（もしくは強制的に次フェーズへ進む）
        this.currentPlayerIndex = nextIndex;
        console.log(
          `[DEBUG nextPlayer] 2人対戦: next player is all-in or already acted, round complete.`
        );
        return;
      } else {
        this.currentPlayerIndex = nextIndex;
        // 通常は次のプレイヤーのアクション待ち
        this.players[this.currentPlayerIndex].hasActed = false;
        console.log(
          `[DEBUG nextPlayer] 2人対戦 nextPlayerIndex=${this.currentPlayerIndex}`
        );
        return;
      }
    }

    // 2人以外の場合は従来のロジック
    const total = this.players.length;
    let count = 0;
    while (count < total) {
      this.currentPlayerIndex = (this.currentPlayerIndex + 1) % total;
      const curr = this.players[this.currentPlayerIndex];
      if (curr.disconnected && !curr.hasFolded && !curr.isAllIn) {
        console.log(
          `[DEBUG nextPlayer] Skipping disconnected player: ${curr.name}, id=${curr.id}`
        );
        if (curr.bet < this.currentBet) {
          console.log(
            `[DEBUG nextPlayer] Disconnected player ${curr.name} auto-folding.`
          );
          curr.hasFolded = true;
        } else {
          console.log(
            `[DEBUG nextPlayer] Disconnected player ${curr.name} auto-checking.`
          );
          curr.hasActed = true;
        }
        continue;
      }
      if (!curr.hasFolded && !curr.isAllIn && !curr.hasActed) {
        console.log(
          `[DEBUG nextPlayer] Found next player at index=${this.currentPlayerIndex}, name=${curr.name}, id=${curr.id}`
        );
        return;
      }
      count++;
    }
    if (count >= total) {
      console.error("[ERROR] nextPlayer: 無限ループの可能性");
      this.currentPlayerIndex = -1;
    }
    console.log(
      `[DEBUG nextPlayer] END currentPlayerIndex=${this.currentPlayerIndex}`
    );
  }

  advancePhase() {
    console.log(
      `[DEBUG advancePhase] START currentPhase=${this.phase}, currentPlayerIndex=${this.currentPlayerIndex}`
    );
    console.log("[DEBUG advancePhase] Players state before advancing phase:");
    this.players.forEach((p, i) => {
      console.log(
        `[DEBUG advancePhase] Player idx=${i}, id=${p.id}, name=${p.name}, folded=${p.hasFolded}, allIn=${p.isAllIn}, hasActed=${p.hasActed}, bet=${p.bet}, chips=${p.chips}`
      );
    });
    console.log("[DEBUG advancePhase] Community Cards:", this.communityCards);
    console.log(
      "[DEBUG advancePhase] Pot:",
      this.pot,
      "Current Bet:",
      this.currentBet
    );
    console.log(
      "[DEBUG advancePhase] Dealer Button Index:",
      this.dealerButtonIndex
    );
    console.log(
      "[DEBUG advancePhase] Action Start Index:",
      this.actionStartIndex
    );
    console.log(
      "[DEBUG advancePhase] Small Blind Index:",
      this.smallBlindIndex,
      "Big Blind Index:",
      this.bigBlindIndex
    );

    switch (this.phase) {
      case PHASES.PRE_FLOP:
        this.phase = PHASES.FLOP;
        this.dealFlop();
        console.log("[DEBUG advancePhase] -> Moved to FLOP.");
        this.resetBetsForNextRound();
        break;
      case PHASES.FLOP:
        this.phase = PHASES.TURN;
        this.dealTurn();
        console.log("[DEBUG advancePhase] -> Moved to TURN.");
        this.resetBetsForNextRound();
        break;
      case PHASES.TURN:
        this.phase = PHASES.RIVER;
        this.dealRiver();
        console.log("[DEBUG advancePhase] -> Moved to RIVER.");
        this.resetBetsForNextRound();
        break;
      case PHASES.RIVER:
        this.phase = PHASES.SHOWDOWN;
        console.log("[DEBUG advancePhase] -> Moved to SHOWDOWN.");
        // フェーズが SHOWDOWN になったら2秒後に勝敗判定（ショウダウン評価）を実施
        setTimeout(() => {
          if (typeof this.onShowdown === "function") {
            console.log(
              "[DEBUG advancePhase] -> Triggering showdown evaluation."
            );
            this.onShowdown();
          } else {
            console.log(
              "[DEBUG advancePhase] -> onShowdown callback not defined."
            );
          }
        }, 2000);
        break;
      case PHASES.SHOWDOWN:
        console.log("[DEBUG advancePhase] -> Already at SHOWDOWN.");
        break;
      default:
        console.log("[DEBUG advancePhase] -> Unknown phase.");
        break;
    }
    console.log(
      `[DEBUG advancePhase] END currentPhase=${this.phase}, currentPlayerIndex=${this.currentPlayerIndex}`
    );
  }

  resetBetsForNextRound() {
    console.log(
      `[DEBUG resetBetsForNextRound] START phase=${this.phase}, currentPlayerIndex(before)=${this.currentPlayerIndex}`
    );
    this.currentBet = 0;
    for (let p of this.players) {
      if (!p.hasFolded && !p.isAllIn) {
        p.bet = 0;
        p.hasActed = false;
      }
    }
    if (this.phase !== PHASES.PRE_FLOP) {
      if (this.players.length === 2) {
        this.currentPlayerIndex = this.bigBlindIndex;
      } else {
        let eligibleFound = false;
        let nextIndex = (this.dealerButtonIndex + 1) % this.players.length;
        for (let i = 0; i < this.players.length; i++) {
          const idx = (nextIndex + i) % this.players.length;
          const player = this.players[idx];
          if (!player.hasFolded && !player.isAllIn) {
            this.currentPlayerIndex = idx;
            eligibleFound = true;
            break;
          }
        }
        if (!eligibleFound) {
          this.currentPlayerIndex = null;
        }
      }
    }
    this.actionStartIndex = this.currentPlayerIndex;
    console.log(
      `[DEBUG resetBetsForNextRound] END currentPlayerIndex=${this.currentPlayerIndex}, actionStartIndex=${this.actionStartIndex}`
    );
    console.log(
      "[DEBUG resetBetsForNextRound] Next round starting with player:",
      this.players[this.currentPlayerIndex]?.name,
      "at index",
      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);
    const sorted = [...contenders].sort((a, b) => a.totalBet - b.totalBet);
    let sidePots = [];
    let previous = 0;
    let remaining = contenders.length;
    for (let i = 0; i < sorted.length; i++) {
      const current = sorted[i].totalBet;
      const diff = current - previous;
      if (diff > 0) {
        sidePots.push({
          amount: diff * remaining,
          eligiblePlayers: contenders.filter((p) => p.totalBet >= current),
        });
        previous = current;
      }
      remaining--;
    }
    return sidePots;
  }

  evaluateShowdown() {
    console.log("[DEBUG evaluateShowdown] 勝敗判定処理を開始します");
    if (this.showdownEvaluated) {
      console.log("[DEBUG evaluateShowdown] 勝敗判定は既に実行済みです");
      return;
    }
    this.showdownEvaluated = true;
    const results = this.determineSidePotWinners();
    this.showdownResults = results; // 勝敗結果を保存
    console.log("[DEBUG evaluateShowdown] 勝敗判定結果:", results);

    if (results && typeof this.sendResultCallback === "function") {
      this.sendResultCallback(results);
    } else {
      console.error(
        "[ERROR evaluateShowdown] 勝敗判定でエラーが発生しました または送信コールバックが未設定"
      );
    }

    // ショウダウン結果送信後、必ずラウンド終了処理を実行する
    setTimeout(() => {
      if (typeof this.roundEndCallback === "function") {
        console.log("[DEBUG evaluateShowdown] ラウンド終了処理を開始します");
        this.roundEndCallback();
      } else {
        console.error(
          "[ERROR evaluateShowdown] ラウンド終了コールバックが未設定"
        );
      }
    }, 5000); // 5秒後に終了処理（必要に応じて調整）
  }

  determineSidePotWinners() {
    let sidePots = this.computeSidePots();

    // サイドポットが空の場合、全プレイヤーを対象にメインポットを作成する
    if (sidePots.length === 0) {
      console.log(
        "[DEBUG determineSidePotWinners] サイドポットが空なので、メインポットとして全プレイヤーを対象にします"
      );
      sidePots.push({
        amount: this.pot,
        eligiblePlayers: this.players.filter((p) => !p.hasFolded),
      });
    }

    let results = [];
    console.log("Computed sidePots:", sidePots);

    // --- ヘルパー関数 ---

    // カードのランク文字列を数値に変換 (A=14, K=13, Q=12, J=11)
    function rankToNumber(rank) {
      if (rank === "A") return 14;
      if (rank === "K") return 13;
      if (rank === "Q") return 12;
      if (rank === "J") return 11;
      return parseInt(rank, 10);
    }

    // solvedHand.cards からキッカーの数値配列を作成し、降順にソートして比較する
    function compareSolvedHands(a, b) {
      const cardsA = a.cards
        .map((card) => rankToNumber(card.value || card.rank))
        .sort((x, y) => y - x);
      const cardsB = b.cards
        .map((card) => rankToNumber(card.value || card.rank))
        .sort((x, y) => y - x);
      for (let i = 0; i < 5; i++) {
        if (cardsA[i] > cardsB[i]) return 1;
        if (cardsA[i] < cardsB[i]) return -1;
      }
      return 0;
    }

    // 手札比較：まず手札の種類（solvedHand.rank）を比較し、異なるならその差を返す
    // 同じなら、キッカー比較を行う
    function compareHandsCustom(a, b) {
      if (a.rank !== b.rank) {
        return a.rank - b.rank;
      }
      return compareSolvedHands(a, b);
    }

    // --- 各サイドポットごとの勝者判定 ---
    for (const pot of sidePots) {
      console.log("-----");
      console.log("Processing pot:", pot);
      const eligible = pot.eligiblePlayers;
      const playerHands = eligible.map((player) => {
        const solverCards = [...player.hand, ...this.communityCards].map(
          convertToPokerSolverFormat
        );
        const solvedHand = Hand.solve(solverCards);
        if (!solvedHand || !solvedHand.descr) {
          console.error(
            `Hand.solve() の結果が不正です。Player: ${player.name}`,
            solverCards,
            solvedHand
          );
        }
        player.solvedHand = solvedHand;
        console.log(`Player: ${player.name}`);
        console.log("  Cards for solver:", solverCards);
        console.log("  Solved hand:", solvedHand);
        console.log(
          "  Hand rank:",
          solvedHand.rank,
          "description:",
          solvedHand.descr
        );
        return { player, solvedHand };
      });

      // 各プレイヤーの手札を compareHandsCustom で比較し、最強の手を決定
      let bestHand = null;
      for (const ph of playerHands) {
        if (
          bestHand === null ||
          compareHandsCustom(ph.solvedHand, bestHand) > 0
        ) {
          bestHand = ph.solvedHand;
        }
      }

      // 最強手と同じ評価になるプレイヤーを勝者とする
      let winningPlayers = playerHands
        .filter((ph) => compareHandsCustom(ph.solvedHand, bestHand) === 0)
        .map((ph) => ph.player);
      // 同じプレイヤーが重複しないように排除
      winningPlayers = [
        ...new Map(winningPlayers.map((p) => [p.id, p])).values(),
      ];
      console.log("Winning players for current pot:", winningPlayers);

      const winnersInfo = winningPlayers.map((wp) => {
        const ph = playerHands.find((ph) => ph.player.id === wp.id);
        return `${wp.name} (${
          ph && ph.solvedHand && ph.solvedHand.descr
            ? ph.solvedHand.descr
            : "未評価"
        })`;
      });

      results.push({
        potAmount: pot.amount,
        winners: winningPlayers,
        winnersInfo: winnersInfo,
      });
      console.log("Current pot results:", results[results.length - 1]);
    }
    console.log("Final results:", results);
    return results;
  }
}

module.exports = TexasHoldemGame;

```
```ai.js
// ai.js
const { GoogleGenerativeAI } = require("@google/generative-ai");
const { parseGeminiResponse } = require("./utils");

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
const model = genAI.getGenerativeModel({
  model: "gemini-1.5-flash",
  generationConfig: { responseMimeType: "application/json" },
});

async function geminiAIMove(room, broadcastGameState, handleTermination) {
  const aiPlayer = room.players.find((p) => p.id === "GeminiAI");
  if (!aiPlayer) {
    console.log("[DEBUG] geminiAIMove: GeminiAI プレイヤーが見つかりません");
    return;
  }
  if (
    room.currentPlayerIndex !==
    room.players.findIndex((p) => p.id === "GeminiAI")
  ) {
    console.log(
      "[DEBUG] geminiAIMove: GeminiAI のターンではありません. currentPlayerIndex=",
      room.currentPlayerIndex
    );
    return;
  }

  const humanPlayers = room.players
    .filter((p) => p.id !== "GeminiAI")
    .map((p) => ({ name: p.name, chips: p.chips, bet: p.bet }));

  const prompt = `
あなたはポーカーのプロです。以下の状況から、GeminiAIとして最善のアクションを選んでください。
GeminiAIの手札: ${JSON.stringify(aiPlayer.hand)}
コミュニティカード: ${JSON.stringify(room.communityCards)}
ポット: ${room.pot}
現在のベット: ${room.currentBet}
相手プレイヤーの状況: ${JSON.stringify(humanPlayers)}
ゲームフェーズ: ${room.phase}
アクションは "fold", "check", "call", "raise" のいずれかで、レイズの場合は "raiseAmount" も指定してください。
出力は必ず以下の形式の有効な JSON としてください。どのようなロジックでそうしたか説明も加えてください。:
{ "action": "<fold|check|call|raise>", "raiseAmount": <number> ,"解説": "◯◯のため◯◯しました。"}
  `.trim();

  console.log(
    "[DEBUG] geminiAIMove: Gemini API 呼び出し前のプロンプト:",
    prompt
  );

  // API 呼び出しと5秒タイムアウトを競合させる
  const responsePromise = model.generateContent(prompt);
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(
      () =>
        reject(new Error("Timeout: Gemini did not respond within 5 seconds")),
      5000
    )
  );

  let result;
  try {
    result = await Promise.race([responsePromise, timeoutPromise]);
  } catch (err) {
    console.error(
      "[DEBUG] geminiAIMove: Gemini API 呼び出しタイムアウトまたはエラー:",
      err
    );
    // タイムアウトなどで失敗した場合、5秒後に再度Geminiのターンを起動する
    setTimeout(
      () => geminiAIMove(room, broadcastGameState, handleTermination),
      5000
    );
    return;
  }

  const responseText = result.response.text();
  console.log("[DEBUG] geminiAIMove: Gemini API 返答テキスト:", responseText);
  const actionObj = parseGeminiResponse(responseText);
  console.log(
    "[DEBUG] geminiAIMove: 解析されたアクションオブジェクト:",
    actionObj
  );
  const actionResult = await room.handleAction(
    "GeminiAI",
    actionObj.action,
    actionObj.raiseAmount || 0
  );
  if (actionResult.error) {
    console.error("GeminiAI action error:", actionResult.error);
  }
  if (actionResult.termination) {
    handleTermination(room);
  } else {
    broadcastGameState();
    const currentPlayer = room.players[room.currentPlayerIndex];
    if (
      currentPlayer &&
      currentPlayer.id === "GeminiAI" &&
      !currentPlayer.hasActed
    ) {
      setTimeout(
        () => geminiAIMove(room, broadcastGameState, handleTermination),
        2000
      );
    }
  }
}

module.exports = { geminiAIMove };

```
```config.js
// config.js
require("dotenv").config();

module.exports = {
  SMALL_BLIND: 10,
  BIG_BLIND: 20,
  MAX_TABLES: 10,
  MAX_PLAYERS_PER_TABLE: 6,
  PHASES: {
    WAITING: "WAITING",
    PRE_FLOP: "PRE_FLOP",
    FLOP: "FLOP",
    TURN: "TURN",
    RIVER: "RIVER",
    SHOWDOWN: "SHOWDOWN",
  },
  GEMINI_ROOM_ID: 11, // MAX_TABLES + 1
  PORT: process.env.PORT || 8080,
};

```

```socketHandlers.js
// socketHandlers.js
console.log("socketHandlers.js loaded");
const {
  MAX_TABLES,
  GEMINI_ROOM_ID,
  MAX_PLAYERS_PER_TABLE,
  PHASES,
  SMALL_BLIND,
  BIG_BLIND,
} = require("./config");

const TexasHoldemGame = require("./texasgame");
const { getPublicGameState } = require("./utils");
const { geminiAIMove } = require("./ai");

const rooms = {};
const geminiRoomIds = [GEMINI_ROOM_ID, GEMINI_ROOM_ID + 1, GEMINI_ROOM_ID + 2];
const playerRoomMap = {};

function registerSocketHandlers(io) {
  const ioRef = io;

  // --- ルーム初期化 ---
  // 通常テーブルのルームを生成
  for (let i = 1; i <= MAX_TABLES; i++) {
    rooms[i] = new TexasHoldemGame();
    rooms[i].onShowdown = () => {
      console.log(`[DEBUG] onShowdown triggered for room ${i}`);
      rooms[i].evaluateShowdown();
    };
    rooms[i].sendResultCallback = (results) => {
      const resultMessage = results
        .map((pot) => pot.winnersInfo.join(", "))
        .join("\n");
      ioRef.to(String(i)).emit("gameResult", {
        roomId: i,
        message: resultMessage,
        pot: rooms[i].pot,
      });
    };
    // roundEndCallback は handleTermination が定義された後に設定
    rooms[i].roundEndCallback = () => {
      handleTermination(i);
    };
  }
  // Gemini 対戦用ルームの生成
  geminiRoomIds.forEach((id) => {
    rooms[id] = new TexasHoldemGame();
    rooms[id].addPlayer({
      id: "GeminiAI",
      name: "Google GeminiAI",
      chips: 1000,
    });
    rooms[id].onShowdown = () => {
      console.log(`[DEBUG] onShowdown triggered for Gemini room ${id}`);
      rooms[id].evaluateShowdown();
    };
    rooms[id].sendResultCallback = (results) => {
      const resultMessage = results
        .map((pot) => pot.winnersInfo.join(", "))
        .join("\n");
      ioRef.to(String(id)).emit("gameResult", {
        roomId: id,
        message: resultMessage,
        pot: rooms[id].pot,
      });
    };
    rooms[id].roundEndCallback = () => {
      handleTermination(id);
    };
  });
  // --- ルーム初期化 終了 ---

  async function broadcastGameState(roomId) {
    const sockets = await ioRef.in(String(roomId)).fetchSockets();
    sockets.forEach((socket) => {
      socket.emit(
        "gameUpdate",
        getPublicGameState(roomId, socket.id, rooms[roomId])
      );
    });
  }

  function handleTermination(roomId) {
    const room = rooms[roomId];
    // ショウダウンフェーズの場合は、evaluateShowdown で得た結果を使って終了処理を行う
    if (room.phase === PHASES.SHOWDOWN) {
      if (room.showdownResults && room.showdownResults.length > 0) {
        // ここでは、最初のポットの勝者を対象にポットを均等分割する例
        const winners = room.showdownResults[0].winners;
        if (winners.length > 0) {
          const share = room.pot / winners.length;
          winners.forEach((w) => {
            w.chips += share;
          });
          const winnerNames = winners.map((w) => w.name).join(", ");
          const resultMessage = `ショウダウンの結果、${winnerNames} の勝利です。`;
          ioRef
            .to(String(roomId))
            .emit("roomMessage", { roomId, message: resultMessage });
          winners.forEach((w) => {
            ioRef.to(w.id).emit("winNotification", {
              roomId,
              message: "Win",
              winnerId: w.id,
            });
          });
          broadcastGameState(roomId);
        }
      } else {
        console.error(
          "[DEBUG handleTermination] ショウダウン結果が得られていません"
        );
      }
    } else {
      // 通常の終了処理：フォールド等で1人になった場合
      const activePlayers = room.players.filter((p) => !p.hasFolded);
      if (activePlayers.length !== 1) return;
      const winner = activePlayers[0];
      winner.chips += room.pot;
      const resultMessage = `全員がフォールドしたため、${winner.name}の勝利です。`;
      ioRef
        .to(String(roomId))
        .emit("roomMessage", { roomId, message: resultMessage });
      ioRef.to(String(roomId)).emit("winNotification", {
        roomId,
        message: "Win",
        winnerId: winner.id,
      });
      broadcastGameState(roomId);
    }

    // 共通のラウンド終了後の後処理
    setTimeout(() => {
      console.log("[DEBUG] Calling endRoundCleanup for room", roomId);
      endRoundCleanup(roomId);
      broadcastGameState(roomId);
      setTimeout(() => {
        if (geminiRoomIds.includes(roomId)) {
          if (room.players.filter((p) => p.id !== "GeminiAI").length >= 1) {
            console.log("[DEBUG] Starting new round for Gemini room", roomId);
            room.startRound();
          } else {
            console.log("[DEBUG] Not enough human players in Gemini room.");
            room.phase = PHASES.WAITING;
          }
        } else if (room.players.filter((p) => p.chips > 0).length >= 2) {
          console.log("[DEBUG] Starting new round for room", roomId);
          room.startRound();
        } else {
          console.log("[DEBUG] Not enough players to start a new round.");
          room.phase = PHASES.WAITING;
        }
        broadcastGameState(roomId);
        ioRef.to(String(roomId)).emit("clearRoomMessage", { roomId });
        const newRoom = rooms[roomId];
        if (
          newRoom.players[newRoom.currentPlayerIndex] &&
          newRoom.players[newRoom.currentPlayerIndex].id === "GeminiAI"
        ) {
          setTimeout(() => {
            geminiAIMove(
              newRoom,
              () => broadcastGameState(roomId),
              () => handleTermination(roomId)
            );
          }, 2000);
        }
      }, 2000);
    }, 5000);
  }

  function endRoundCleanup(roomId) {
    console.log(`[DEBUG endRoundCleanup] Called for roomId=${roomId}`);
    const room = rooms[roomId];
    if (!room) return;
    room.incorporateWaitingPlayers();
    const beforeCount = room.players.length;
    room.players = room.players.filter(
      (p) => p.id === "GeminiAI" || (p.chips > 0 && !p.disconnected)
    );
    room.waitingPlayers = room.waitingPlayers.filter(
      (p) => p.id === "GeminiAI" || (p.chips > 0 && !p.disconnected)
    );
    const gemini = room.players.find((p) => p.id === "GeminiAI");
    if (gemini && gemini.chips <= 0) {
      gemini.chips = 1000;
    }
    console.log(
      `[DEBUG endRoundCleanup] -> Removed ${
        beforeCount - room.players.length
      } players from room.players.`
    );
    if (room.players.length < 2) {
      console.log("[DEBUG endRoundCleanup] -> Not enough players => WAITING");
      room.phase = PHASES.WAITING;
    }
    broadcastGameState(roomId);
  }

  ioRef.on("connection", (socket) => {
    console.log("New connection:", socket.id);

    socket.on("getRooms", () => {
      const roomList = [];
      // 通常テーブルの場合
      for (let i = 1; i <= MAX_TABLES; i++) {
        const game = rooms[i];
        const playerCount = game.players.length; // 着席しているプレイヤー数
        const waitingCount = game.waitingPlayers.length; // 空席待ちの人数
        const status = game.phase === PHASES.WAITING ? "待機中" : "ゲーム中";
        roomList.push({
          roomId: i,
          tableName: `テーブル${i}`, // テーブル名を付与
          sb: SMALL_BLIND, // ステーク：SB
          bb: BIG_BLIND, // ステーク：BB
          type: "human", // ゲーム種別（通常対戦の場合）
          playerCount, // 着席者数
          waitingCount, // 空席待ち人数
          status, // 状態
        });
      }
      // Gemini 対戦用ルームの場合
      geminiRoomIds.forEach((id) => {
        const geminiGame = rooms[id];
        const playerCount = geminiGame.players.length;
        const waitingCount = geminiGame.waitingPlayers.length;
        const status =
          geminiGame.phase === PHASES.WAITING ? "待機中" : "ゲーム中";
        roomList.push({
          roomId: id,
          tableName: `テーブル${id}`, // Gemini ルームもテーブル名として付与
          sb: SMALL_BLIND,
          bb: BIG_BLIND,
          type: "gemini", // ゲーム種別を Gemini として設定
          playerCount,
          waitingCount,
          status: status + " (GeminiAI)",
        });
      });
      socket.emit("roomList", roomList);
    });

    socket.on("joinGame", (data) => {
      let roomId = parseInt(data.roomId, 10);
      if (
        isNaN(roomId) ||
        roomId < 1 ||
        (roomId > MAX_TABLES && !geminiRoomIds.includes(roomId))
      ) {
        roomId = 1;
      }
      const room = rooms[roomId];
      if (geminiRoomIds.includes(roomId)) {
        const humanCount = room.players.filter(
          (p) => p.id !== "GeminiAI"
        ).length;
        if (humanCount >= 1) {
          socket.emit("gameMessage", {
            message:
              "GeminiAIルームは既に対戦中です。別のルームをお選びください。",
          });
          return;
        }
      }
      const exists =
        room.players.some((p) => p.id === socket.id) ||
        room.waitingPlayers.some((p) => p.id === socket.id);
      if (exists) {
        socket.emit("gameMessage", {
          message: "既にこのルームに参加しています。",
        });
        return;
      }
      if (geminiRoomIds.includes(roomId) && room.players.length >= 2) {
        socket.emit("gameMessage", {
          message: "GeminiAIルームは満室です。別のルームをお選びください。",
        });
        return;
      } else 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));
      if (playerRoomMap[socket.id]) {
        if (!playerRoomMap[socket.id].includes(roomId)) {
          playerRoomMap[socket.id].push(roomId);
        }
      } else {
        playerRoomMap[socket.id] = [roomId];
      }
      broadcastGameState(roomId);
    });

    socket.on("startGame", (data) => {
      let roomId = data?.roomId;
      if (!roomId) {
        const arr = playerRoomMap[socket.id];
        if (arr && arr.length > 0) {
          roomId = arr[0];
        } else {
          socket.emit("gameMessage", { message: "テーブルに参加していません" });
          return;
        }
      }
      const room = rooms[roomId];
      if (!room) {
        socket.emit("gameMessage", { message: "存在しないテーブルです" });
        return;
      }
      if (geminiRoomIds.includes(roomId)) {
        const humanCount = room.players.filter(
          (p) => p.id !== "GeminiAI"
        ).length;
        if (humanCount < 1) {
          ioRef
            .to(String(roomId))
            .emit("gameMessage", { message: "対戦相手が必要です" });
          return;
        }
      } else if (room.players.length < 2) {
        ioRef
          .to(String(roomId))
          .emit("gameMessage", { message: "プレイヤーが2人以上必要です" });
        return;
      }
      ioRef.to(String(roomId)).emit("clearRoomMessage", { roomId });
      room.startRound();
      broadcastGameState(roomId);
      if (geminiRoomIds.includes(roomId)) {
        const currentPlayer = room.players[room.currentPlayerIndex];
        if (currentPlayer && currentPlayer.id === "GeminiAI") {
          console.log("[DEBUG] GeminiAIが先頭。geminiAIMoveを起動します。");
          setTimeout(() => {
            geminiAIMove(
              room,
              () => broadcastGameState(roomId),
              () => handleTermination(roomId)
            );
          }, 2000);
        }
      }
    });

    socket.on("playerAction", async ({ roomId, action, raiseAmount }) => {
      const room = rooms[roomId];
      if (!room) {
        console.error(
          `[ERROR] playerAction: ルーム ${roomId} が見つかりません`
        );
        return;
      }
      if (room.phase === PHASES.SHOWDOWN) {
        socket.emit("gameMessage", {
          roomId,
          message: "ショウダウン中は操作できません",
        });
        return;
      }
      const result = room.handleAction(socket.id, action, raiseAmount);
      if (result.error) {
        socket.emit("gameMessage", { roomId, message: result.error });
        return;
      }
      if (result.termination) {
        handleTermination(roomId);
        return;
      }
      broadcastGameState(roomId);

      const currentPlayer = room.players[room.currentPlayerIndex];
      if (
        currentPlayer &&
        currentPlayer.id === "GeminiAI" &&
        !currentPlayer.hasActed
      ) {
        setTimeout(() => {
          geminiAIMove(
            room,
            () => broadcastGameState(roomId),
            () => handleTermination(roomId)
          );
        }, 2000);
      }
    });

    socket.on("disconnect", () => {
      const roomIds = playerRoomMap[socket.id] || [];
      roomIds.forEach((roomId) => {
        const room = rooms[roomId];
        if (room) {
          const target = room.players.find((p) => p.id === socket.id);
          if (target) {
            target.disconnected = true;
            console.log(
              `[DEBUG] Player ${socket.id} marked as disconnected in room ${roomId}.`
            );
          }
          room.waitingPlayers = room.waitingPlayers.filter(
            (p) => p.id !== socket.id
          );
          if (
            room.players[room.currentPlayerIndex] &&
            room.players[room.currentPlayerIndex].id === socket.id
          ) {
            console.log(
              `[DEBUG] Current player ${socket.id} is disconnected in room ${roomId}. Auto-folding.`
            );
            if (target) {
              target.hasFolded = true;
              target.hasActed = true;
            }
            room.nextPlayer();
          }
          const activePlayers = room.players.filter((p) => !p.hasFolded);
          if (activePlayers.length === 1) {
            console.log(
              `[DEBUG] Only one active player left in room ${roomId} after disconnect.`
            );
            const winner = activePlayers[0];
            winner.chips += room.pot;
            const resultMessage = `全員がフォールドしたため、${winner.name}の勝利です。`;
            ioRef
              .to(String(roomId))
              .emit("gameResult", { message: resultMessage, pot: room.pot });
            ioRef.to(winner.id).emit("winNotification", { message: "Win" });
            broadcastGameState(roomId);
            setTimeout(() => {
              endRoundCleanup(roomId);
            }, 5000);
          }
          broadcastGameState(roomId);
        }
      });
      delete playerRoomMap[socket.id];
      console.log("Disconnect:", socket.id);
    });
  });
}

module.exports = { registerSocketHandlers };

```