# 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> ```