---
# System prepended metadata

title: AI Reinforce Lab 開發日誌
tags: [ReinforceLab]

---

# AI Reinforce Lab 開發日誌

**日期：** 2025-02-10 
**作者：** 趙士豪 Colombo



## 1. 專案概述

AI Reinforce Lab 是一個以網頁為平台的強化學習 (Reinforcement Learning, RL) 實驗室，主要目標在於結合互動式遊戲、視覺化數據展示以及積木式程式設計 (Blockly) 來進行 RL 訓練與策略優化。平台分為左右兩個主要部分：

- **左側子遊戲區：** 以 iframe 嵌入不同的遊戲頁面，提供實際互動環境。
- **右側父平台：** 多 Agent、多 Tabs 的控制與數據展示介面，包含 Blockly 編輯區、數據圖表、日誌記錄等功能。



## 2. 使用技術與函式庫

- **前端技術：**
  - **HTML5、CSS3、JavaScript**：整體頁面結構、樣式與互動邏輯。
  - **Flexbox**：頁面左右區塊佈局，確保響應式設計與全屏/半屏切換。

- **視覺化與數據展示：**
  - **Plotly.js**：用於繪製累積 Reward 圖表與 Q-Table 熱點圖，動態更新訓練數據。
  - **Chart.js**：雖已引入但目前主要使用 Plotly，未來可依需求擴展。

- **積木式程式設計：**
  - **Blockly**：提供兩個獨立的 Blockly 工作區 (分別對應不同 Agent)，讓使用者能以積木方式編寫與調整 RL 策略。

- **強化學習核心：**
  - **Q-Learning**：利用 Q-Table 存儲狀態－動作評分，實作 ε-greedy 策略選擇動作與 Q 值更新。
  
- **跨頁面通訊：**
  - **Window.postMessage API**：實現父頁面與 iframe 子頁面之間的雙向通訊（如詢問遊戲資訊、傳送動作、接收 reward 與 state）。



## 3. 系統架構與頁面佈局

### 左側子遊戲區
- **功能：**
  - 透過 iframe 嵌入子遊戲頁面，可動態載入不同遊戲 URL。
  - 提供全屏/半屏切換功能，方便使用者專注於遊戲操作或 RL 訓練。
  - 控制列包含載入網址、快速選擇遊戲、暫停/繼續控制。

### 右側父平台
- **功能：**
  - **多 Tabs 切換：** 包括 Tutorial、Blockly 編輯區、Charts 圖表展示、Logs 日誌紀錄等區塊。
  - **Blockly 區：** 兩個獨立的工作區 (如 Agent 1 與 Agent 2)，支援預載積木與積木拖曳編輯。
  - **Charts 區：**
    - 累積 Reward 圖表：以 Plotly 顯示 RL 訓練中的累計獎勵變化。
    - Q-Table Heatmap：利用 Plotly 依據狀態離散化結果，呈現各狀態下 Q 值的分佈與最佳動作 (採用 HSV → RGB 漸層色)。
  - **Logs 區：** 實時記錄 ARS (Action, Reward, State) 訊息，方便檢查訓練過程與數據細節。



## 4. 已完成的功能

1. **頁面佈局與全屏切換**
   - 利用 Flexbox 佈局將頁面分為左右兩區。
   - 實作子遊戲區全屏/半屏切換，同時隱藏或顯示右側父平台。

2. **遊戲載入與控制**
   - 支援使用者手動輸入或透過快速載入按鈕切換不同遊戲 URL。
   - 提供暫停/繼續功能，通過 `postMessage` 傳送指令給子遊戲頁面。

3. **Blockly 整合**
   - 實作兩個獨立 Blockly 工作區，分別對應不同 Agent。
   - 預載預設積木，並在 Tabs 切換時重新渲染以確保視覺正確。

4. **Q-Learning 強化學習核心**
   - 建立 Q-Table 結構、實作 Q 值更新與 ε-greedy 動作選擇。
   - 根據子遊戲回傳的 reward 與 state 進行 Q 表動態更新。

5. **數據視覺化**
   - 使用 Plotly 畫出累積 Reward 圖表，每秒更新並展示 RL 訓練效果。
   - 利用 Plotly 生成 Q-Table Heatmap，依據狀態離散化後的 Q 值與最佳動作以顏色區分。

6. **跨頁面通訊**
   - 透過 `window.postMessage` 機制，實現父頁面與嵌入子遊戲頁面之間的狀態、獎勵與動作指令交換。

7. **Q-Table 匯出/匯入**
   - 支援將 Q-Table 以 JSON 格式匯出保存，並能夠從檔案匯入恢復訓練數據。



## 5. 未來發展方向與改進

- **多 Agent 擴充：**
  - 除了目前的 Agent 1 與 Agent 2，未來可進一步支援更多 Agent，並研究 Agent 間的協作或競爭模式。

- **進階視覺化數據分析：**
  - 增加更多數據圖表，如 Q 值分布、訓練收斂趨勢等，提供更豐富的數據參考。

- **後端資料儲存與監控：**
  - 考慮與後端 API 整合，實現長期訓練資料的儲存、分析與遠端監控功能。

- **UI/UX 優化：**
  - 持續改進使用者介面，提升操作體驗並加入更多操作提示與互動日誌。

- **整合其他強化學習演算法：**
  - 未來可考慮加入其他 RL 演算法 (例如 Deep Q-Networks, Policy Gradient 等)，豐富平台功能與實驗範圍。



## 6. 結語

目前的開發成果已完成從遊戲載入、Blockly 編輯、Q-Learning 訓練到數據視覺化等核心功能，為後續擴展與優化奠定了堅實基礎。未來將持續改進系統，並探索更多強化學習與互動式視覺化的應用可能性。



*此開發日誌將隨著專案進展持續更新，歡迎各位提供建議與回饋！*

## 7.原始碼

``` html=0
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <title>AI Reinforce Lab</title>

  <!-- Chart.js -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <script src="https://cdn.plot.ly/plotly-2.20.0.min.js"></script>

  <!-- Blockly -->
  <script src="https://unpkg.com/blockly/blockly.min.js"></script>

  <style>
    /************************************************************
     * 整體頁面佈局：body使用flex
     ************************************************************/
    body {
      margin: 0;
      padding: 0;
      font-family: Consolas, sans-serif;
      height: 100vh; /* 若需要整頁滿版，可用 100vh；若需要可捲動，則移除。 */
      display: flex;
    }

    /************************************************************
     * 左側：子遊戲 (iframe) 區域
     ************************************************************/
    #leftPanel {
      width: 40%;               /* 預設 30% (或根據需求調成40%) */
      flex-shrink: 0;          /* 避免視窗縮小時被壓縮 */
      display: flex;
      flex-direction: column;
      border-right: 1px solid #ddd;
      transition: width 0.3s ease; /* 全屏/半屏轉換動畫 */
    }

    /* 若按下「全屏」時，切換 class，置於頂層、寬高100%，並鎖定位置 */
    #leftPanel.fullscreen {
      width: 100vw;  /* 全螢幕寬度 */
      height: 100vh; /* 全螢幕高度 */
      position: fixed;
      top: 0;
      left: 0;
      z-index: 999;  
    }

    /* 左側上方控制列 (固定高度 35px，垂直置中) */
    #controls {
      display: flex;
      align-items: center;
      height: 35px;
      background-color: #cfe;
      border-bottom: 1px solid #ddd;
      padding: 0 8px;
    }
    #controls button,
    #controls input {
      margin-right: 6px;
      padding: 4px 6px;
      font-size: 13px;
      line-height: 1;
    }
    #controls button:last-child,
    #controls input:last-child {
      margin-right: 0;
    }
    /* 讓輸入框可撐開剩餘空間 */
    #gameUrlInput {
      flex-grow: 1;
    }

    /* 左側 iframe (子遊戲) */
    iframe {
      flex-grow: 1;   /* 撐滿剩餘空間 */
      width: 100%;
      border: none;
    }

    /************************************************************
     * 右側：父平台 (多Agent / 多 Tabs)
     ************************************************************/
    #rightPanel {
      width: 70%;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
    }

    /* Tabs 上方的按鈕列 (固定高度 35px)，與底部線對齊 */
    .sub-tab-buttons {
      font-family: Consolas, sans-serif;
      display: flex;
      align-items: flex-end; /* 讓按鈕與容器底緣貼齊 */
      height: 36px;
      background: #cef;
      border-bottom: 1px solid #ddd;
      padding: 0 8px;
    }

    /* 「Agent 1:」等標籤 */
    .sub-tab-buttons .agent-label {
      display: inline-flex;
      align-items: center;
      margin-right: 8px;
      font-weight: bold;
      white-space: nowrap;
    }

    /* Tabs 按鈕 */
    .sub-tab-buttons button {
      font-family: Consolas, sans-serif;
      margin-bottom: -1px; /* 為了讓按鈕底邊與容器對齊 */
      margin-right: 6px;
      min-height: 28px;
      padding: 4px 8px;
      font-size: 13px;
      line-height: 1;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: #e9e9e9;
      border: 1px solid #ccc;
      border-radius: 4px 4px 0 0;
      cursor: pointer;
      white-space: nowrap;
    }
    .sub-tab-buttons button:last-child {
      margin-right: 0;
    }
    .sub-tab-buttons button:hover {
      background: #ddd;
    }
    .sub-tab-buttons button.active {
      background: #fff;
      border-bottom: 1px solid #fff; /* 讓 active 的按鈕看起來像「浮起」 */
      font-weight: bold;
      color: #0078d7;
    }
    
    /************************************************************
     * 下面是每個Tab裡的細部內容：Blockly / Charts / Logs 等
    ************************************************************/

    /* 6 個對應的 Tab 內容區塊 */
    .sub-tab-content {
      display: none;
      flex-grow: 1;
      padding: 0px;
      background: #fff;
      overflow: auto;
    }
    .sub-tab-content.active {
      display: block;
    }


    /* 按鈕美化 */
    button {
      margin: 5px 0;
      padding: 6px 12px;
      cursor: pointer;
    }
    button:hover {
      background-color: #eee;
    }

    #p1-blocklyDiv, #p2-blocklyDiv {
      width: 100%;
      height: 100%;
    }
    .blocklyPath {
      stroke: white;
    }
    /* ARS 紀錄區 */
    #p1-log {
      box-sizing: border-box;
      padding: 10px;
      overflow-y: auto;
      font-size: 14px;
      /* 讓換行符 \n 生效 */
      white-space: pre-wrap; 
    }



  </style>
</head>

<body>
  <!-- 左側：子遊戲區 (可全屏/半屏切換) -->
  <div id="leftPanel">
    <!-- 上方控制列 (如：載入網址、暫停、全屏等) -->
    <div id="controls">
      <button id="toggleFullscreen">全屏</button>
      <input id="gameUrlInput" type="text" value="https://colombo0718.neocities.org/graphicalGYM/games/dinasourCheck"
             placeholder="輸入遊戲網址" />
      <button id="loadGame">載入</button>
      <button id="togglePause">暫停</button>
    </div>

    <!-- 下方 iframe：用於嵌入子遊戲 -->
    <iframe id="game-iframe" src="https://colombo0718.neocities.org/graphicalGYM/games/dinasourCheck"></iframe>
  </div>

  <!-- 右側：父平台 (多 Agent、多 Tabs) -->
  <div id="rightPanel">
    <!-- Tabs 按鈕列 -->
    
    <div class="sub-tab-buttons">
      <button class="sub-tab-link active" data-subtab="tutorial">Tutorial</button>
      <!-- Agent1 -->
      <span class="agent-label">Agent 1:</span>
      <button class="sub-tab-link" data-subtab="p1-blocks">Blocks</button>
      <button id='me' class="sub-tab-link" data-subtab="p1-charts">Charts</button>
      <button class="sub-tab-link" data-subtab="p1-logs">Logs</button>

      <!-- Agent2 -->
      <span class="agent-label" style="margin-left: 16px;">Agent 2:</span>
      <button class="sub-tab-link" data-subtab="p2-blocks">Blocks</button>
      <button class="sub-tab-link" data-subtab="p2-charts">Charts</button>
      <button class="sub-tab-link" data-subtab="p2-logs">Logs</button>
      
      <!-- Agent3 -->
      <!--<span class="agent-label" style="margin-left: 16px;">Agent 3:</span>-->
      <!--<button class="sub-tab-link" data-subtab="p3-blocks">Blocks</button>-->
      <!--<button class="sub-tab-link" data-subtab="p3-charts">Charts</button>-->
      <!--<button class="sub-tab-link" data-subtab="p3-logs">Logs</button>-->
      
      <!-- Agent4 -->
      <!--<span class="agent-label" style="margin-left: 16px;">Agent 4:</span>-->
      <!--<button class="sub-tab-link" data-subtab="p4-blocks">Blocks</button>-->
      <!--<button class="sub-tab-link" data-subtab="p4-charts">Charts</button>-->
      <!--<button class="sub-tab-link" data-subtab="p4-logs">Logs</button>-->
      

    </div>

    <!-- Tabs 對應的內容區 (共6塊) -->
    <div id="tutorial" class="sub-tab-content active"  style="width:100%; height: 100%">
      AI Reinforce Lab: Explore. Train. Compete.
      <p>選擇下列遊戲快速載入：</p>
      <div>
        <button class="quick-load-button" data-url="https://colombo0718.neocities.org/graphicalGYM/games/MAB">Multi-Armed Bandit</button>
        <button class="quick-load-button" data-url="https://colombo0718.neocities.org/graphicalGYM/games/dinasourCheck">Dinosaur Jump</button>
        <button class="quick-load-button" data-url="https://colombo0718.neocities.org/graphicalGYM/games/beatInfo">Taiko Beat</button>
        <button class="quick-load-button" data-url="https://colombo0718.neocities.org/graphicalGYM/games/easyShoot">Easy Shoot</button>
        <button class="quick-load-button" data-url="https://colombo0718.neocities.org/graphicalGYM/games/Maze2D">Maze2D</button>
      </div>
    </div>
    
    
    
    
    
    <!-- 1. p1-blocks -->
    <div id="p1-blocks" class="sub-tab-content"  style="width:100%; height: 100%">
      <!--<div id="blockly-container">-->
        <div id="p1-blocklyDiv"></div>
      <!--</div>-->
    </div>
    <!-- 2. p1-charts -->
    <div id="p1-charts" class="sub-tab-content"  style="width:100%; height: 100%">
      <!-- 第一區：Cumulative Reward Chart -->
      <div id="p1-rewards-chart" style="width: 600px; height: 300px;"></div>
    
      <!-- 第二區：Q-Table Heatmap -->
      <div id="p1-qValues-chart" style="width: 600px; height: 400px;"></div>
    
      <!-- 第三區：按鈕 -->
      <div style="width: 100%; ">
        <button onclick="exportQtable()" style="margin: 5px ;">Export Q-table</button>
        <input type="file" style="margin: 5px ;" id="importQtableInput" accept=".json,.txt" 
       onchange="importQtable(event)" />
      </div>
    </div>
    <!-- 3. p1-logs -->
    <div id="p1-logs" class="sub-tab-content" style="width:100%; height: 100%">
    
      <div id="p1-log" style="width: 100%; height: 100%;"></div>
    </div>
    
    
    
    

    <!-- 4. p2-blocks -->
    <div id="p2-blocks" class="sub-tab-content" style="width:100%; height: 100%">
      <div id="p2-blocklyDiv"></div>
    </div>
    <!-- 5. p2-charts -->
    <div id="p2-charts" class="sub-tab-content"  style="width:100%; height: 100%">
      <div style="width:100%; height: 33%">
        <canvas id="p2-rewards-chart"></canvas>
        
      </div>
      
      <div style="width:100%; height: 33%">
        <canvas id="p2-qValues-chart"></canvas>
      </div>
    </div>
    <!-- 6. p2-logs -->
    <div id="p2-logs" class="sub-tab-content"  style="width:100%; height: 100%">
      <div id="p2-log" style="width: 100%; height: 100%;"></div>
    </div>
    
    
  </div>

  <!-- (可選) jQuery UI Dialog 用於顯示 Q 值圖表的彈窗 (若要用彈窗) -->


  <!-- ================================ -->
  <!-- JS 區段：包括 全屏切換、多 Tabs 控制、Q-Learning程式邏輯、Blockly初始設定 等 -->
  <!-- ================================ -->
  <script>
    /***************************************************
     * [1] 全屏 / 半屏 切換功能
     ***************************************************/
    const leftPanel = document.getElementById("leftPanel");
    const rightPanel = document.getElementById("rightPanel"); // 取得右側面板
    const fullscreenButton = document.getElementById("toggleFullscreen");

    fullscreenButton.addEventListener("click", () => {
      if (leftPanel.classList.contains("fullscreen")) {
        // 若目前全屏，則恢復
        leftPanel.classList.remove("fullscreen");
        rightPanel.style.display = "flex";
        fullscreenButton.textContent = "全屏";
      } else {
        // 進入全屏
        leftPanel.classList.add("fullscreen");
        rightPanel.style.display = "none"; // 隱藏右側面板
        fullscreenButton.textContent = "半屏";
      }
    });

    /***************************************************
     * [2] 左側：載入/暫停/繼續 等控制
     ***************************************************/
    const loadGameBtn = document.getElementById("loadGame");
    const gameUrlInput = document.getElementById("gameUrlInput");
    const gameIframe = document.getElementById("game-iframe");
    const togglePauseBtn = document.getElementById("togglePause");

    loadGameBtn.addEventListener("click", () => {
      const url = gameUrlInput.value.trim();
      if (url) {
        gameIframe.src = url;
      }
      isPaused = false;
      togglePauseBtn.textContent = "暫停";
      // gameIframe.addEventListener("load", () => {
      //   // const iframe = document.getElementById("game-iframe");
      //   gameIframe.contentWindow.postMessage({ type: "questInfo" }, "*");
      // }, { once: true }); // 使用 { once: true } 確保事件僅執行一次
    });

    let isPaused = false;
    togglePauseBtn.addEventListener("click", () => {
      if (!isPaused) {
        // 這裡可做暫停遊戲 or 停止訓練的動作
        gameIframe.contentWindow.postMessage({ type: "pause" }, "*");
        togglePauseBtn.textContent = "繼續";

      } else {
        // 這裡可做恢復遊戲 or 繼續訓練的動作
        gameIframe.contentWindow.postMessage({ type: "pause" }, "*");
        gameIframe.contentWindow.postMessage({ type: "action",action:0}, "*");
        togglePauseBtn.textContent = "暫停";
      }
      isPaused = !isPaused;
    });

    /***************************************************
     * [3] 多 Agent 多 Tabs 切換
     ***************************************************/
    const subTabLinks = document.querySelectorAll(".sub-tab-link");
    const subTabContents = document.querySelectorAll(".sub-tab-content");

    subTabLinks.forEach(link => {
      link.addEventListener("click", () => {
        // 先移除全部 active
        subTabLinks.forEach(l => l.classList.remove("active"));
        subTabContents.forEach(c => c.classList.remove("active"));

        // 啟用當前被點擊的
        link.classList.add("active");
        const target = link.getAttribute("data-subtab");
        document.getElementById(target).classList.add("active");
        
            // 檢查是否需要重新渲染 Blockly
            if (target === "p1-blocks" && workspace1) {
              Blockly.svgResize(workspace1);
            }
            if (target === "p2-blocks" && workspace2) {
              Blockly.svgResize(workspace2);
            }
      });
    });
    
    
    // 快速載入遊戲按鈕
    document.querySelectorAll('.quick-load-button').forEach(button => {
      button.addEventListener('click', () => {
        const url = button.getAttribute('data-url');
        if (url) {
          gameUrlInput.value= url
          gameIframe.src = url; // 設定 iframe 的 src
        }
        isPaused = false;
        togglePauseBtn.textContent = "暫停";
      });
    });



    /***************************************************
     * [4] Blockly 初始化
     ***************************************************/
    var workspace1 = Blockly.inject('p1-blocklyDiv', {
      toolbox: 
        `<xml>
          <block type="controls_repeat_ext"></block>
          <block type="math_number"></block>
          <block type="math_arithmetic"></block>
          <block type="logic_compare"></block>
          <block type="variables_get"></block>
          <block type="variables_set"></block>
          <block type="controls_if"></block>
          <block type="text_print"></block>
         </xml>`,
      trashcan: true,
      theme: Blockly.Themes.Zelos,
      renderer: 'thrasos',
    });

    // 預載一些積木
    const initialBlocks = 
      `<xml>
        <block type="controls_repeat_ext" x="20" y="20">
          <value name="TIMES">
            <shadow type="math_number">
              <field name="NUM">5</field>
            </shadow>
          </value>
          <statement name="DO">
            <block type="text_print">
              <value name="TEXT">
                <shadow type="text">
                  <field name="TEXT">Q-Learning</field>
                </shadow>
              </value>
            </block>
          </statement>
        </block>
      </xml>`;
    loadInitialBlocks(initialBlocks, workspace1);

    function loadInitialBlocks(xmlText, workspace) {
      const parser = new DOMParser();
      const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
      Blockly.Xml.domToWorkspace(xmlDoc.documentElement, workspace);
    }
    
    
    var workspace2 = Blockly.inject('p2-blocklyDiv', {
      toolbox: 
        `<xml>
          <block type="controls_repeat_ext"></block>
          <block type="math_number"></block>
          <block type="math_arithmetic"></block>
          <block type="logic_compare"></block>
          <block type="variables_get"></block>
         </xml>`,
      trashcan: true,
      theme: Blockly.Themes.Zelos,
      renderer: 'thrasos',
    });
    
    
        // // 預載一些積木
    const initialBlocks2 = 
      `<xml>
        <block type="controls_repeat_ext" x="20" y="20">
          <value name="TIMES">
            <shadow type="math_number">
              <field name="NUM">5</field>
            </shadow>
          </value>
          <statement name="DO">
            <block type="text_print">
              <value name="TEXT">
                <shadow type="text">
                  <field name="TEXT">Q-Learning</field>
                </shadow>
              </value>
            </block>
          </statement>
        </block>
      </xml>`;
    loadInitialBlocks(initialBlocks, workspace2);

    /***************************************************
     * [5] Q-Learning 相關設定 (範例)
     ***************************************************/

    let QTable = {};
    let Alpha   = .01;  // 學習率
    let Gamma   = 0.95; // 折扣因子
    let Epsilon = .2;  // 探索率
    let Psi     = 1;    // 樂觀值   
    
    let delayTime =1


    // Q 值更新
    function qTableUpdate(prevS, prevA, r, nextS,nextA) {
      // 初始化當前狀態和下一狀態的 Q 值空間
      if (!QTable[prevS]) QTable[prevS] = Array(action_size).fill(0);
      if (!QTable[nextS]) QTable[nextS] = Array(action_size).fill(0);

      
      // 計算下一狀態的最大值和最小值
      let maxQNext = 0; // 初始化为默认值
      let minQNext = 0;
      if (QTable[nextS] && QTable[nextS].length > 0) {
        maxQNext = Math.max(...QTable[nextS]);
        minQNext = Math.min(...QTable[nextS]);
      }
      
      let actualQNext = QTable[nextS][nextA] || 0; // 實際選擇的動作 Q 值
      
      
      // 根據 λ 的值計算目標 Q 值
      let targetQ = (1 - Math.abs(Psi)) * actualQNext +
                    Math.max(0, Psi) * maxQNext +
                    Math.max(0, -Psi) * minQNext;
                    
                    
      if (isNaN(targetQ)) {
        console.error("Invalid Q update detected.", { QTable,nextS ,actualQNext , maxQNext , minQNext});
      }
      // 計算 Q 值更新
      let oldQ = QTable[prevS][prevA] || 0;
      let newQ = oldQ + Alpha * (r + Gamma * targetQ - oldQ);
      
      
      // 檢查數據有效性，防止 NaN
      
      if (isNaN(oldQ)) {
        console.error(QTable[prevS][prevA]);
      }
      if (isNaN(newQ)) {
        console.error("Invalid Q update detected.", {oldQ , Alpha , r , Gamma , targetQ});
      }
    
      // 更新 Q 表
      QTable[prevS][prevA] = newQ;
    }
    
    // ε-greedy 選擇動作
    function eGreedyChooseAction(stateKey) {
      if (Math.random() < Epsilon ||  !QTable[stateKey]) {
        return Math.floor(Math.random() * action_size);
      } else {
        if (!QTable[stateKey]){
          console.warn(`QTable[${stateKey}] 未初始化，將自動初始化。`);
          QTable[stateKey] = Array(action_size).fill(0);
        }
        let qVals = QTable[stateKey];
        let maxQ  = Math.max(...qVals);
        let chosenAction = qVals.indexOf(maxQ);
        if (chosenAction === -1) {
          console.error("無法找到最大 Q 值的索引，qVals:", qVals, "maxQ:", maxQ);
        }
        return chosenAction;
      }
    }

         

    /***************************************************
     * [6] 父/子頁面 postMessage 通訊：
     *     接收子頁面 (遊戲)傳回的 (reward, state)，並回傳動作
     ***************************************************/
    let stateRange = []; 
    let numBins = [];

    // 計算當下狀態對應到Q表的鍵值
    function getStateKey(stateValues) {
      let indices = []; // 用於儲存每個維度的離散化索引
      // console.log(stateValues)
      stateValues.forEach((value,dim) => {
         // 設定預設範圍
        let min = 0, max = 100;
        // // 取得該狀態有效範圍
        // if (stateRange[index]) {
        //   min = stateRange[index].min;
        //   max = stateRange[index].max;
        // }
        // // 計算桶的大小
        // let binSize = (max - min) / numBins[index]; 
        // // 限制值在範圍內
        // let clipped = Math.max(min, Math.min(max, value)); 
        // // 計算索引
        // let idx = Math.floor((clipped - min) / binSize);
        // // 確保索引在合法範圍內
        // idx = Math.min(Math.max(idx, 0), numBins[index] - 1); 
        const bucketIdx = getBucketIndex(value,dim);
        // 將索引加入陣列
        indices.push(bucketIdx); 
      });
      // 索引轉成字串標籤
      // console.log(indices.join("_"))
      return indices.join("_");
    }
    
    function getBucketIndex(value,dim){
        // 取得該狀態有效範圍
        if (stateRange[dim]) {
          min = stateRange[dim].min;
          max = stateRange[dim].max;
        }
        // 計算桶的大小
        let binSize = (max - min) / numBins[dim]; 
        // 限制值在範圍內
        let clipped = Math.max(min, Math.min(max, value)); 
        // 計算索引
        let idx = Math.floor((clipped - min) / binSize);
        // 確保索引在合法範圍內
        idx = Math.min(Math.max(idx, 0), numBins[dim] - 1); 
        
        return idx;
    }
    
    
    let stateInfo
    let actionInfo
    let state_size  
    let action_size   
    
    // 每當遊戲頁面載入完成，就詢問遊戲資訊
    gameIframe.addEventListener("load", () => {
      gameIframe.contentWindow.postMessage({ type: "questInfo" }, "*");
    });     
     
    window.addEventListener("message", (event) => {
      const message = event.data;
      
      if (message.type === "gameInfo") {
        stateInfo=message.stateInfo
        actionInfo=message.actionInfo
                
        state_size=stateInfo.length
        action_size=actionInfo[0].level
        
        // 價值型算法專屬
        QTable = {};     // 清空Q表
        stateRange = []; // 清空狀態範圍
        numBins = [];    // 清空每軸桶數
        message.stateInfo.forEach((info) => {
          stateRange.push({ min: info.min, max: info.max });
          if(info.bin){
            numBins.push(info.bin);
          }else{
            numBins.push(20); // 預設每個狀態分成 20 桶，可根據需求調整
          }
        });
        
        // 發送第一個動作
        gameIframe.contentWindow.postMessage({ type: "action", action: 0 }, "*");
      }
    });   
      
    let currentState 
    let prevAction,prevStateKey
    let nextAction,nextStateKey
    let reward  
    let cumulativeReward = 0; // 用於畫獎勵圖表 
    
    window.addEventListener("message", (event) => {
      const message = event.data;  
      // 從子頁面拿到報酬與狀態
      if (message.type === "reward_state") {
        // 更新瞬間報酬
        reward = message.reward;
        // 更新繪圖用的累計報酬
        cumulativeReward += message.reward;
        // console.log(reward,cumulativeReward)
        // 取得當下狀態對應到Q表的鍵值
        currentState=message.state
        nextStateKey = getStateKey(message.state);
        // console.log(message.state,nextStateKey)
        // 選擇下一次動作
        nextAction = eGreedyChooseAction(nextStateKey);
        // 更新Q表
        if (reward !== null  && 
        // prevAction必須是正整數
        Number.isInteger(prevAction) && prevAction >= 0) {
          qTableUpdate(prevStateKey, prevAction, reward, nextStateKey,nextAction);
        }
        // 繼承狀態與動作資訊
        prevStateKey = nextStateKey;
        prevAction   = nextAction;
        // console.log(nextAction)


        // 回傳動作給子頁面
        if(isPaused){return}
        setTimeout(() => {
          gameIframe.contentWindow.postMessage({
            type: "action",
            action: prevAction
          }, "*");
        // 故意設置的延遲時間
        }, delayTime);
      }
    });
    
    
    /***************************************************
     * [7] ARS (Action, Reward, State) 日誌紀錄
     ***************************************************/


    let lastTime = null;
    
    function updateARSLog(agentId,action, reward, state) {
      const now = new Date();
      let dt = lastTime ? now - lastTime : 0;
      lastTime = now;
      let sIndex = getStateKey(state);
    
      // Create the log entry
      const logEntry = `dt=${dt}ms, A=${action}, R=${reward}, state=${state}, sIndex=${sIndex}`;
    
      // Write the log for the specified agent
      writeLog(agentId, logEntry);
    }
    
    function writeLog(agentId, logEntry) {
      // Get the log element for the specified agent
      const logElement = document.getElementById(`p${agentId}-log`);
      if (!logElement) {
        console.error(`Log element for agent ${agentId} not found.`);
        return;
      }
    
      // Append the log entry
      logElement.textContent += (logElement.textContent ? "\n" : "") + logEntry;
    
      // Automatically scroll to the bottom
      logElement.scrollTop = logElement.scrollHeight;
    }


    
    
/***************************************************
 * [8] Rewards Chart  
 ***************************************************/
    let rewardsChart;
    let rewardData = [];
    let rewardLabels = [];

    // 初始化 Rewards 图表
    function initRewardsChart() {
      const layout = {
        title: 'Cumulative Reward Over Time',
        xaxis: { title: 'Time (s)' },
        yaxis: { title: 'Cumulative Reward' },
      };

      Plotly.newPlot('p1-rewards-chart', [{
        x: rewardLabels,
        y: rewardData,
        mode: 'lines',
        fill: 'tozeroy',
        line: { color: 'rgba(54, 162, 235, 1)' },
      }], layout);
    }
    let startTime=Date.now()
    // 更新 Rewards 图表的函数
    function updateRewardsChart() {
      
      const currentTime = ((Date.now() - startTime) / 1000).toFixed(1); // 计算经过时间
      rewardLabels.push(currentTime);
      rewardData.push(cumulativeReward);
      // console.log(cumulativeReward)

      // 保证 rewardLabels 和 rewardData 的数量始终一致
      if (rewardData.length > 60) {
        rewardLabels.shift(); // 移除最旧的时间点
        rewardData.shift();   // 移除最旧的数据
      }

      // 更新图表
      Plotly.update('p1-rewards-chart', {
        x: [rewardLabels],
        y: [rewardData],
      });

      // 重置累积 Reward
      cumulativeReward = 0;
    }

    // 初始化图表
    initRewardsChart();

    // 模拟每秒更新一次图表
    // let cumulativeReward = 0; // 初始化累计 Reward
    setInterval(() => {
      // cumulativeReward += Math.random() * 10 - 5; // 模拟累积 Reward 的变化
      updateRewardsChart();
    }, 1000);
    
    
    
    
/***************************************************
 * [9] QTable Chart  
 ***************************************************/
/****************************************************************
 * 1) HSV→RGB: 將 (h, s, v) 轉換成 rgb(r,g,b)
 ****************************************************************/
function hsvToRgb(h, s, v) {
  // h: [0, 360], s: [0,1], v: [0,1]
  const c = v * s;
  const x = c * (1 - Math.abs((h / 60) % 2 - 1));
  const m = v - c;
  let r, g, b;
  if (h < 60) {
    r = c; g = x; b = 0;
  } else if (h < 120) {
    r = x; g = c; b = 0;
  } else if (h < 180) {
    r = 0; g = c; b = x;
  } else if (h < 240) {
    r = 0; g = x; b = c;
  } else if (h < 300) {
    r = x; g = 0; b = c;
  } else {
    r = c; g = 0; b = x;
  }
  r = Math.round((r + m) * 255);
  g = Math.round((g + m) * 255);
  b = Math.round((b + m) * 255);
  return `rgb(${r},${g},${b})`;
}

/****************************************************************
 * 2) 給定 (gap, bestAction) → 傳回對應漸層色
 *    - action=0 => hue=0° (紅) 
 *    - action=1 => hue=180° (青)
 *    - gap=0 => 白色, gap≥gapMax => 純色
 ****************************************************************/
 // 預設 gap 最大值，用於控制漸層飽和度
const gapMax = 10;
 
/**
 * 通用著色函式：
 * - gap: (最佳 Q - 第二佳 Q)
 * - bestAction: 最佳動作編號 (0 ~ action_size-1)
 * - gapMax: gap 超過此值時，飽和度視為 1
 * - actionSize: 總動作數
 *
 * 規則：
 * 1) gap=0 => 白色
 * 2) bestAction=0 => 以黑色表示 (可由 ratio=gap/gapMax 做漸層)
 * 3) 其餘動作 => 在色環上平分
 *    例如 action=1 => hue=0°, action=2 => hue=120°, action=3 => hue=240° (若 actionSize=4)
 *    gap越大 => 飽和度越高 => 顏色更純
 */

function colorByGapAndBest(gap, bestAction, gapMax) {
  // 先計算 0..1 的 ratio
  let ratio = Math.max(0, Math.min(gap / gapMax, 1));

  // (1) gap=0: 表示動作價值無差，直接白色
  if (ratio === 0) {
    return 'rgb(255,255,255)';
  }

  // (2) 若動作 0 = 「不動作」，用黑色 (可隨 gap 變化在白~黑之間)
  if (bestAction === 0) {
    // 白(255,255,255) → 黑(0,0,0) 做線性插值
    // ratio=1 => 全黑；ratio=0 => 全白(上面已經擋掉)
    let c = Math.round(255 * (1 - ratio));
    return `rgb(${c},${c},${c})`;
  }

  // (3) 其餘動作均勻分佈於 360° 的色相環
  // 假設 action=1,2,...,actionSize-1；總共有 (actionSize-1) 種顏色
  // hueStep = 360 / (actionSize - 1)
  let hueStep = 360 / (action_size - 1);
  // hue  = (bestAction-1) * hueStep
  let hue = (bestAction - 1) * hueStep;

  // 飽和度 = ratio；value = 1 => gap越大，顏色越飽和
  let s = ratio;
  let v = 1;
  return hsvToRgb(hue, s, v);
}

/****************************************************************
 * 3) 產生 Plotly Scatter 的資料
 *    - 只針對前兩個維度 i, j => i in [0..numBins[0]-1], j in [0..numBins[1]-1]
 *    - 每個網格對應 QTable["i_j"] = [q0, q1], 若不存在 => 白色
 ****************************************************************/
function generatePlotlyData2D() {
  const xvals = [];
  const yvals = [];
  const colors = [];
  const texts  = [];

  // 假設只用前兩個維度 x, y
  const binsX = numBins[0];
  const binsY = numBins[1];

    
  for (let i = 0; i < binsX; i++) {
    for (let j = 0; j < binsY; j++) {
      
      // console.log(currentState[2],getBucketIndex(currentState[2],2))
      let i2=getBucketIndex(currentState[2],2)
      
      let stateKey = `${i}_${j}`; // 與 getStateKey([i,j]) 產生的key一致
      // if(!isNaN(i2)){stateKey = `${i}_${j}_${i2}`}
      if(!isNaN(i2)){stateKey = `0_${j}_${i}`}
      // console.log(stateKey)

      let qArr = QTable[stateKey]; // e.g. [q0, q1]
      if (!qArr || qArr.length < 2) {
        // 若查無資料或資料不足，顯示白色
        xvals.push(i);
        yvals.push(j);
        colors.push('rgb(255,255,255)');
        texts.push(`(i=${i}, j=${j})<br>No Data`);
      } else {
        // 有 Q 值 => 找出最佳與第二佳
        let sortedQ = qArr.slice().sort((a, b) => b - a); // 由大到小排序
        let bestAction = qArr.indexOf(sortedQ[0]); // 最佳動作
        let secondBestQ = sortedQ[1] !== undefined ? sortedQ[1] : sortedQ[0]; // 確保至少有兩個 Q 值比較
        let gap = sortedQ[0] //- secondBestQ; // 計算 Q 值差距
        let col = colorByGapAndBest(gap, bestAction, gapMax, qArr.length); // 計算顏色

        xvals.push(i);
        yvals.push(j);
        colors.push(col);
      texts.push(`(i=${i}, j=${j})<br>Q=${qArr.map(q => q.toFixed(2)).join(', ')}<br>
                  gap=${gap.toFixed(2)}, best=${bestAction}`);
      }
    }
  }

  // 回傳 Plotly Scatter 數據
  return {
    x: xvals,
    y: yvals,
    text: texts,
    mode: 'markers',
    marker: {
      symbol: 'square', // 方形點
      size: 25,         // 可視需求調整
      color: colors,
      line: { width: 0 } // 若不要網格外框就設0
    },
    hoverinfo: 'text'
  };
}


    /************************************************
     * 5. 用 Plotly 畫圖
     ************************************************/
    let trace = generatePlotlyData2D();
    let layout = {
      title: 'Q-table Heatmap (Two Actions)',
      xaxis: {
        title: 'State Dimension X',
      },
      yaxis: {
        title: 'State Dimension Y',
        scaleratio: .5,
        // autorange: 'reversed', // 讓 Y 軸上下顛倒
      },
    };

    Plotly.newPlot('p1-qValues-chart', [trace], layout);
    
    /************************************************
     * 6. 每隔 2 秒隨機更新 Qtable => 更新圖表
     ************************************************/
    setInterval(() => {
      // 重新生成資料，更新 Plotly
      let newTrace = generatePlotlyData2D();
      Plotly.react('p1-qValues-chart', [newTrace], layout);
    }, 1000);
    
    
    
/***************************************************
 * [10] QTable Export / Import 
 ***************************************************/
 
 function exportQtable() {
  // 將 QTable 序列化為 JSON 字串
  const dataStr = JSON.stringify(QTable, null, 2); 
  // 建立一個 Blob 物件，型態為 text/plain 或 application/json
  const blob = new Blob([dataStr], { type: "application/json" });
  // 建立臨時連結並點擊，觸發下載
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = "Qtable.json"; // 檔名可自訂
  link.click();
  URL.revokeObjectURL(url);
}

function importQtable(evt) {
  const file = evt.target.files[0];
  if (!file) return;
  
  const reader = new FileReader();
  reader.onload = function(e) {
    try {
      const fileContent = e.target.result;
      // 解析 JSON
      const obj = JSON.parse(fileContent);
      
      // 如果檔案裡只有 QTable 物件
      QTable = obj;
      
      console.log("QTable imported successfully:", QTable);
      // alert("QTable 已成功載入，後續訓練可沿用此表格。");

    } catch (error) {
      console.error("Error parsing QTable file", error);
      // alert("無法解析此 QTable 檔案");
    }
  };
  reader.readAsText(file, "utf-8");
}
    
  </script>
</body>
</html>
```