分享和讨论Javascript相关的话题
头像
ejsoon
一枝独秀
帖子: 6345
注册时间: 2022年 11月 18日 17:36
联系:

Re: 研究提升三菱棋的AI棋力

帖子 ejsoon »

gemini 3.6 fast

代码: 全选

針對您提出的兩項需求,以下是精確的修改位置與替換代碼說明:

---

### 一、修復與改進「回退功能(Undo)」

**問題分析:**
原本的回退機制會在棋盤無棋子時直接禁止(`history.length <= 1`),使得第二局開始且未下子時無法退回第一局的結算畫面。

**修改邏輯:**

1. 允許在「當前對局歷史為 0(或只剩下初始狀態)」時,若存在上一局的紀錄(`gameHistory.length > 0`),則觸發跨局回退。
2. 退回到上一局的終局狀態,並重新跳出上一局的結算全屏視窗。

#### 1. 修改 `undoMove` 函式

請在 HTML 文件中搜尋 `function undoMove()`,將整段函式替換為:

```javascript
      function undoMove() {
        if (isAiThinking) return;

        // 狀況 1:當前對局內有步驟可以回退
        if (history.length > 1) {
          history.pop();
          const prevState = history[history.length - 1];
          board = JSON.parse(JSON.stringify(prevState.board));
          currentPlayer = prevState.player;
          selectedCell = null;
          lastMove = prevState.lastMove;
          moveHistory = [...prevState.moveHistory];
          renderBoard();
          updateStatus();
          updateMoveHistoryUI();
          return;
        }

        // 狀況 2:當前對局已無步驟可退(場上0子),但有上一局的歷史紀錄
        if (gameHistory.length > 0) {
          // 移除上一局紀錄並恢復
          const lastGameRecord = gameHistory.pop();
          
          board = JSON.parse(JSON.stringify(lastGameRecord.finalBoard));
          currentPlayer = lastGameRecord.finalPlayer;
          selectedCell = null;
          lastMove = lastGameRecord.lastMove;
          moveHistory = [...lastGameRecord.moveHistory];
          currentGameIndex = lastGameRecord.gameIndex;
          scores = [...lastGameRecord.scores];

          renderBoard();
          updateStatus();
          updateMoveHistoryUI();

          // 重新彈出全屏勝負結果視窗
          showWinnerModal(lastGameRecord.modalTitle, lastGameRecord.modalDetail);
        }
      }

```

#### 2. 在對局結束時保存回復所需的完整資訊

為了能準確退回到上一局的終局狀態(包含狀態與彈窗內容),請搜尋 `checkGameOver` 函式,找到呼叫 `showWinnerModal` 的地方,在儲存 `gameHistory` 時擴充保存的資料種類:

請搜尋以下類似代碼(在 `checkGameOver` 裡面):

```javascript
        gameHistory.push({
          board: JSON.parse(JSON.stringify(board)),
          moveHistory: [...moveHistory]
        });

```

將其替換為:

```javascript
        let modalTitle = "";
        let modalDetail = "";

        if (scores[0] > scores[1]) {
          modalTitle = "藍方 獲得最終勝利!";
        } else if (scores[1] > scores[0]) {
          modalTitle = "紅方 獲得最終勝利!";
        } else {
          modalTitle = "雙方平手!";
        }
        modalDetail = `比分:藍方 ${scores[0]} - 紅方 ${scores[1]}`;

        // 保存上一局終局的完整狀態以供回退
        gameHistory.push({
          gameIndex: currentGameIndex,
          scores: [...scores],
          finalBoard: JSON.parse(JSON.stringify(board)),
          finalPlayer: currentPlayer,
          lastMove: lastMove,
          moveHistory: [...moveHistory],
          modalTitle: modalTitle,
          modalDetail: modalDetail
        });

        showWinnerModal(modalTitle, modalDetail);

```

---

### 二、記譜格式改進(藍色棋子簡寫規則)

**問題分析:**
針對縱坐標為 `0` 的藍色棋子(通常記譜為三個字符,如 `C0B` 或 `e0d`),當縱坐標為 0 且橫坐標字母不全為 `a` / `A` 時,簡化為兩個橫坐標字母中字母序較小的一個(例如 `C` 與 `B` 取 `B`,`e` 與 `d` 取 `d`)。

#### 修改 `getNotation` 相關記譜轉換函式

請在 HTML 文件中搜尋負責轉換記譜的函式(例如 `getNotation` 或處理棋格座標轉記譜文字的邏輯區塊)。

尋找類似下面的記譜生成代碼並加以替換/修改:

```javascript
      function formatCellNotation(cell) {
        // 假設原本邏輯取得 col1, row, col2 (例如 'C', 0, 'B')
        let c1 = cell.col1;
        let r = cell.row;
        let c2 = cell.col2;

        // 新增判斷邏輯:藍色棋子 (縱坐標 row === 0)
        if (r === 0) {
          // 檢查是否橫坐標都不為 'a' 或 'A'
          const isAllA = (c1.toLowerCase() === 'a' && c2.toLowerCase() === 'a');
          if (!isAllA) {
            // 取字母序較小者(例如 'C' 與 'B' 比較,取 'B')
            const minCol = c1.localeCompare(c2) <= 0 ? c1 : c2;
            return minCol;
          }
        }

        // 其他情況維持原有的記譜格式輸出
        return `${c1}${r}${c2}`;
      }

```

*註:如果您的程式碼中是在落子紀錄 `moveHistory` 生成時直接拼湊字串,請將拼接邏輯改為呼叫上述邏輯,或將對應的字串處理改為:*

```javascript
// 在生成藍棋記譜字串的位置:
if (row === 0 && col1.toLowerCase() !== 'a' && col2.toLowerCase() !== 'a') {
  notation = col1.localeCompare(col2) <= 0 ? col1 : col2;
} else {
  notation = `${col1}${row}${col2}`;
}

```
gemini 3.1 pro

代码: 全选

這裡為你提供修復這兩個問題的具體代碼修改。

### 一、 修復「第二局回退」邏輯錯誤

原代碼在第二局開始階段處理 `historyIndex` 的邏輯有缺陷,導致回退時未能正確先回到第二局的空棋盤狀態,而是直接跳回了第一局。我們需要重構 `undoMove` 函數。

請找到代碼中的 `function undoMove()`,將整個函數替換為以下代碼:

```javascript
      function undoMove() {
        if (historyIndex >= 0) {
          forceHumanPlayer(); // 中斷 AI 並切換為人類
          document.getElementById('game-over-screen').style.display = 'none'; // 必定收起結算視窗

          let roundStart = currentRound === 2 ? 36 : 0;

          // 處理已退回到該局最開頭的情況
          if (historyIndex < roundStart) {
            // 如果是第二局的空棋盤,再次點擊回退則回到第一局終局狀態
            if (currentRound === 2 && historyIndex === 35) {
              currentRound = 1;
              redrawFromHistory();
              gameState = 'ended';
              showRoundEnd();
            }
            return;
          }

          let move = moveHistory[historyIndex];

          let scores = gameLogic.undo();
          p1CurrentScore = scores.p1Score;
          p2CurrentScore = scores.p2Score;
          piecesLeft[move.player][move.pid]++;
          historyIndex--;

          let dropG = document.getElementById('etanidrop');
          let tiles = dropG.querySelectorAll('.tiledropped');
          if (tiles.length > 0) {
            dropG.removeChild(tiles[tiles.length - 1]);
          }

          isFirstMove = historyIndex < roundStart;
          currentPlayer = move.player;
          gameState = 'playing'; // 強制恢復為遊玩狀態
          updateUI();
        }
      }

```

---

### 二、 修復並改進記譜格式(藍色棋子 `y=0` 簡寫)

這部分需要修改兩處:一是導出時生成正確的簡寫字串,二是導入時能夠正確將單字母反向解析成完整的座標。

#### 1. 修改導出格式 (`formatExportNotation`)

請找到 `formatExportNotation` 函數中處理**藍色棋子**的分支 `if (y1 === y2) { ... }`,將這部分的 `else` 替換為支援 `y1 === 0` 簡寫的邏輯:

```javascript
          if (y1 === y2) {
            // 藍色棋子
            let c1 = xToCharFormat(x1),
              c2 = xToCharFormat(x2);
            if (x1 === 0 && x2 === 1) {
              // 坐標為 a 和 A
              if (y1 > 0) return 'A' + y1;
              else return 'a' + Math.abs(y1);
            } else {
              let chars = [c1, c2].sort((a, b) => {
                let A = a.toLowerCase(),
                  B = b.toLowerCase();
                if (A === B) return a < b ? -1 : 1;
                return A < B ? -1 : 1;
              });
              if (y1 > 0) {
                return chars[0] + y1 + chars[1]; // 字母順序
              } else if (y1 < 0) {
                return chars[1] + Math.abs(y1) + chars[0]; // 倒序
              } else {
                return chars[0]; // y1 === 0 時簡寫為字母序最小的一個
              }
            }
          }

```

#### 2. 修改導入解析 (`parseImportNotation`)

為了讓導出的單一字母(例如 `B`, `d`)能夠被順利導入,請找到 `parseImportNotation` 函數,在拋出錯誤(`throw new Error('格式錯誤')`)的前面加上對應的正則表達式解析條件:

```javascript
      function parseImportNotation(token) {
        if (token === '0') return { t1_idx: 0, t1_N: 0, t2_idx: 1, t2_N: 0 };
        let match;
        if ((match = token.match(/^([Aa])(\d+)$/))) {
          let y = parseInt(match[2]);
          if (match[1] === 'A') return { t1_idx: 0, t1_N: y, t2_idx: 1, t2_N: y };
          else return { t1_idx: 0, t1_N: -y, t2_idx: 1, t2_N: -y };
        } else if ((match = token.match(/^([A-Za-z])(\d+)([A-Za-z])$/))) {
          let c1 = match[1],
            c2 = match[3];
          let yAbs = parseInt(match[2]);
          let chars = [c1, c2].sort((a, b) => {
            let A = a.toLowerCase(),
              B = b.toLowerCase();
            if (A === B) return a < b ? -1 : 1;
            return A < B ? -1 : 1;
          });
          let y = c1 === chars[0] ? yAbs : -yAbs;
          return { t1_idx: charToXFormat(c1), t1_N: y, t2_idx: charToXFormat(c2), t2_N: y };
        } else if ((match = token.match(/^(\d+)([A-Za-z])(\d+)$/))) {
          let num1 = parseInt(match[1]),
            num2 = parseInt(match[3]);
          let x = charToXFormat(match[2]);
          let y1, y2;
          if (num1 < num2) {
            y1 = num1;
            y2 = num2;
          } else {
            y1 = -num1;
            y2 = -num2;
          }
          return { t1_idx: x, t1_N: y1, t2_idx: x, t2_N: y2 };
        } else if ((match = token.match(/^([A-Za-z])$/))) {
          // 解析單字母簡寫(y=0 的藍色棋子)
          let x1 = charToXFormat(match[1]);
          // 透過遊戲網格奇偶性推導相鄰的另一半座標
          let isRight = (0 === ((x1 - 1) & 1));
          let x2 = isRight ? x1 - 1 : x1 + 1;
          return { t1_idx: x1, t1_N: 0, t2_idx: x2, t2_N: 0 };
        }
        throw new Error('格式錯誤');
      }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6345
注册时间: 2022年 11月 18日 17:36
联系:

Re: 研究提升三菱棋的AI棋力

帖子 ejsoon »

trirhombus2887.html

搞完?
附件
trirhombus2887.html.7z
(34.67 KiB) 已下载 4 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6345
注册时间: 2022年 11月 18日 17:36
联系:

Re: 研究提升三菱棋的AI棋力

帖子 ejsoon »

有兩處techfont的url,在js中寫到一個變量中,以減少文檔體積。

查找程式中是否還有其它地方可以精簡大小,注意不要改變程式功能。


gemini 3.1 pro

代码: 全选

這是一個很好的優化方向!將巨大的 Base64 字體字串提取到單一 JavaScript 變數中,不僅能大幅縮減 HTML 檔案體積,還能讓程式碼更易於維護。

除了字體之外,程式碼中還有幾個地方可以透過**合併重複的 CSS 規則**、**共用相似的 JS 函式**,以及**移除 HTML5 中非必要的 SVG 屬性**來進一步精簡大小,且完全不會影響原有的遊戲功能。

以下是具體的優化步驟與替換程式碼:

### 1. 提取 Techfont 到全域 JS 變數

**第一步:移除 CSS 中的字體宣告**
將 `<head>` 區塊內 `<style>` 標籤中原本又長又佔空間的 `@font-face` 整個刪除:

```css
/* 將這整段刪除 */
@font-face {
  font-family: 'Techfont';
  src: url('data:font/woff2;charset=utf-8;base64,d09GMgAB... (略) ...') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

```

**第二步:在 JS 開頭宣告變數並動態載入**
在 `<script>` 標籤的最上方,建立字體變數並利用 JS 將其注入到網頁中:

```javascript
<script>
  // 1. 宣告 Base64 字體變數 (只存一次)
  const TECHFONT_B64 = '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';
  
  // 動態注入主網頁字體
  const fontStyle = document.createElement('style');
  fontStyle.innerHTML = `@font-face { font-family: 'Techfont'; src: url('data:font/woff2;charset=utf-8;base64,${TECHFONT_B64}') format('woff2'); font-weight: normal; font-style: normal; font-display: swap; }`;
  document.head.appendChild(fontStyle);

  let gameLogic = null;
  // ...後續原有程式碼

```

**第三步:修改 SVG 導出的邏輯**
在 JS 的 `updateDlPreview()` 函式中,原本有一大串字體代碼,現在可以直接引用該變數:

```javascript
// 修改前:
styleEl.innerHTML = `
  @font-face {
    font-family: 'Techfont';
    src: url('data:font/woff2;charset=utf-8;base64,d09GMgAB... (略)') format('woff2');
...

// 修改後:
styleEl.innerHTML = `
  @font-face {
    font-family: 'Techfont';
    src: url('data:font/woff2;charset=utf-8;base64,${TECHFONT_B64}') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
  }
`;

```

---

### 2. 合併高度相似的調整按鈕 JS 函式

`adjustAiParam` 和 `adjustGlobalParam` 功能幾乎一樣,可以合併成一個泛用的 `adjustParam` 來縮減體積:

**替換 JS 函式:**

```javascript
// 刪除原有的 adjustAiParam 和 adjustGlobalParam,替換為這一個:
function adjustParam(inputId, delta, min, max, toFixed, isGlobal) {
  let input = document.getElementById(inputId);
  let val = parseFloat(input.value) || min;
  val = Math.max(min, Math.min(max, val + delta)); // 確保不超出邊界
  input.value = toFixed > 0 ? val.toFixed(toFixed) : Math.round(val);
  
  if (isGlobal) updateGlobalParams();
  else markCustom();
}

```

**對應更新 HTML 呼叫:**
將 AI 設定面板(`#ai-modal` 內)所有的按鈕 `onclick` 更新為新的函式。例如:

```html
<button class="num-btn" onclick="adjustAiParam('ai-time', -1, 1, 60)">-</button>
<button class="num-btn" onclick="adjustGlobalParam('mcts-c', -0.1, 0.1, 10, 1)">-</button>

<button class="num-btn" onclick="adjustParam('ai-time', -1, 1, 60, 0, false)">-</button>
<button class="num-btn" onclick="adjustParam('ai-time', 1, 1, 60, 0, false)">+</button>

<button class="num-btn" onclick="adjustParam('mcts-c', -0.1, 0.1, 10, 1, true)">-</button>
<button class="num-btn" onclick="adjustParam('mcts-c', 0.1, 0.1, 10, 1, true)">+</button>

<button class="num-btn" onclick="adjustParam('heuristic-n', -1, 0, 36, 0, true)">-</button>

```

---

### 3. 合併重複的 CSS 規則

`#floating-tools` 和 `#floating-tools-clone` 的樣式重複度極高,可以將共通點提取出來:

```css
/* 修改前的兩大段 CSS */

/* 修改後:提取共通樣式 */
#floating-tools, #floating-tools-clone {
  position: absolute;
  left: 50%;
  z-index: 100;
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
  width: 100%;
}

/* 只保留差異部分 */
#floating-tools {
  bottom: 20px;
  transform: translateX(-50%);
}

#floating-tools-clone {
  top: 20px;
  transform: translateX(-50%) rotate(180deg);
  display: none; /* 由 JS 控制顯示 */
}

/* 同理,手機版 (Mobile Layout) 的 Media Query 內也可以合併 */
@media (max-width: 768px) {
  #floating-tools, #floating-tools-clone {
    gap: 5px;
    width: 100%;
  }
  #floating-tools { bottom: 12px; }
  #floating-tools-clone { top: 12px; }
  /* ... */
}

```

---

### 4. 移除行內 SVG 的冗餘屬性

在 HTML5 中,直接寫在網頁內的 `<svg>` 標籤會自動套用 SVG 命名空間,不需要再寫 `xmlns` 或 `version` 屬性(這些主要是為了解析獨立的 `.svg` 檔案時準備的)。

你可以將 HTML 中寫死的 SVG 屬性精簡:

```html
<svg id="etani" version="1.1" xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet">

<svg id="etani" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet">

```

*註:在 `downloadBoardSVG` 函式中動態建立要提供下載的 SVG 原始碼時,還是必須保留 `xmlns`,這部分你的原始程式碼已經處理得很正確。*
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6345
注册时间: 2022年 11月 18日 17:36
联系:

Re: 研究提升三菱棋的AI棋力

帖子 ejsoon »

trirhombus2888.html
精簡了一下

trirhombus2889.html
把between改成了betwixt
附件
trirhombus2889.html.7z
(34.74 KiB) 已下载 3 次
trirhombus2888.html.7z
(34.73 KiB) 已下载 4 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6345
注册时间: 2022年 11月 18日 17:36
联系:

Re: 研究提升三菱棋的AI棋力

帖子 ejsoon »

當打開AI設置窗口並點擊「搜尋」時,在給出搜尋結果的同時,增加一個下載按鈕,所下載的文本資料是用來反饋給你,讓你看下這個MCTS算法應該如何改進的。

不要影響到現有功能,下載的內容不要跟「搜尋結果」完全一樣,而是適合反饋給你的內容及格式。

請明確給出所要修改的地方和所要替換的代碼。

gemini 3.1 pro + thinking

代码: 全选

為了解決這個問題並協助提供 MCTS / Minimax 算法改進的上下文,我們需要在 AI 面板中加入一個隱藏的「下載報告」按鈕。當搜尋結束後,我們會將當前盤面的狀態、演算法參數設定,以及每個分支的探索次數與期望值整理成一份適合供 AI 閱讀的 `.txt` 報告。

請在你的 `trirhombus2889.html` 檔案中,找到以下兩個區塊並進行替換:

### 1. 修改 HTML 結構 (新增下載按鈕)

**找到以下代碼:**

```html
        <button
          id="ai-search-btn"
          class="btn-primary"
          onclick="runAISearch()"
          style="width: 100%; margin: 10px 0; padding: 10px; font-size: 1rem"
        >
          🔍 <span id="text-run-search">執行盤面搜尋分析</span>
        </button>
        <div id="ai-search-status">準備就緒</div>

        <div
          id="ai-results"

```

**替換為:**

```html
        <button
          id="ai-search-btn"
          class="btn-primary"
          onclick="runAISearch()"
          style="width: 100%; margin: 10px 0; padding: 10px; font-size: 1rem"
        >
          🔍 <span id="text-run-search">執行盤面搜尋分析</span>
        </button>
        <div id="ai-search-status">準備就緒</div>

        <button
          id="ai-dl-report-btn"
          class="btn-secondary"
          onclick="downloadAiDiagnosticReport()"
          style="width: 100%; margin: 10px 0 0 0; padding: 10px; font-size: 1rem; display: none;"
        >
          📥 <span>下載分析報告 (供 AI 診斷改進)</span>
        </button>

        <div
          id="ai-results"

```

---

### 2. 修改 JS 邏輯 (產生與下載報告)

**找到 `runAISearch()` 函式及其上方的位置:**

```javascript
      // === AI 分析搜尋引擎與繪製樹狀圖 ===
      function buildMinimaxNodeDOM(resultObj) {
        let div = document.createElement('div');
        div.className = 'ai-tree-node';
        div.style.marginLeft = '0';
        
        // ... (中間省略) ...

      async function runAISearch() {
        if (gameState !== 'playing') {
          alert(getText('gameNotStarted'));
          return;
        }

        // ... (中間省略) ...
        // (整個 runAISearch 函式到最後一個 '}' 為止)

```

**將整個 `runAISearch()` 函式替換為以下包含報告生成邏輯的版本(並在上方加上輔助函式):**

```javascript
      // === AI 分析搜尋引擎與繪製樹狀圖 ===
      let latestAiDiagnosticReport = "";

      function downloadAiDiagnosticReport() {
        if (!latestAiDiagnosticReport) return;
        const blob = new Blob([latestAiDiagnosticReport], { type: 'text/plain;charset=utf-8' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        let pad = n => n.toString().padStart(2, '0');
        let d = new Date();
        a.download = `ai_diagnostic_${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}.txt`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }

      function getMoveNotationForReport(moveInt) {
        let pid = (moveInt >> 24) & 0xff;
        let p1Idx = (moveInt >> 12) & 0xfff;
        let p2Idx = moveInt & 0xfff;
        let c1 = gameLogic._idxToCoord(p1Idx);
        let c2 = gameLogic._idxToCoord(p2Idx);
        let pieceName = pid === 0 ? '藍' : pid === 1 ? '紅' : '橙';
        return `${pieceName} [(${c1.x},${c1.y}),(${c2.x},${c2.y})]`;
      }

      function buildMinimaxNodeDOM(resultObj) {
        let div = document.createElement('div');
        div.className = 'ai-tree-node';
        div.style.marginLeft = '0';

        let pid = (resultObj.move >> 24) & 0xff;
        let p1Idx = (resultObj.move >> 12) & 0xfff;
        let p2Idx = resultObj.move & 0xfff;
        let c1 = gameLogic._idxToCoord(p1Idx);
        let c2 = gameLogic._idxToCoord(p2Idx);

        let pieceName = pid === 0 ? '🔵藍' : pid === 1 ? '🔴紅' : '🟠橙';
        let notation = `[(${c1.x},${c1.y}),(${c2.x},${c2.y})]`;

        let header = document.createElement('div');
        header.className = 'ai-tree-header';

        let scoreDiff = resultObj.score;
        let scoreStr = scoreDiff > 0 ? `P1領先 ${scoreDiff}` : scoreDiff < 0 ? `P2領先 ${-scoreDiff}` : `平局 0`;

        header.innerHTML = `
          <span style="width: 150px; display: inline-block;">${pieceName} ${notation}</span>
          <span style="color: #333; font-weight: bold; width: 140px; display: inline-block;">絕對分差: ${scoreStr}</span>
        `;
        div.appendChild(header);
        return div;
      }

      async function runAISearch() {
        if (gameState !== 'playing') {
          alert(getText('gameNotStarted'));
          return;
        }

        abortAiSearch = false;
        const resDiv = document.getElementById('ai-results');
        const statusDiv = document.getElementById('ai-search-status');
        const btn = document.getElementById('ai-search-btn');
        const dlBtn = document.getElementById('ai-dl-report-btn');

        resDiv.innerHTML = '';
        btn.disabled = true;
        btn.style.opacity = '0.5';
        dlBtn.style.display = 'none';

        let timeLimit = parseFloat(document.getElementById('ai-time').value) * 1000;
        let rootState = gameLogic.clone();
        let startTime = Date.now();

        let totalPiecesLeft = piecesLeft[1].reduce((a, b) => a + b) + piecesLeft[2].reduce((a, b) => a + b);

        // --- 準備供 AI 診斷的報告表頭 ---
        let report = "=== 三菱棋 (Trirhombus) AI 盤面診斷報告 ===\n";
        report += `產生時間: ${new Date().toLocaleString()}\n`;
        report += `當前輪次: 第 ${currentRound} 局, Player ${currentPlayer} 行動\n`;
        report += `剩餘棋子 - P1(藍/紅/橙): ${piecesLeft[1].join('/')} | P2: ${piecesLeft[2].join('/')}\n`;
        report += `當前比分 - P1: ${p1CurrentScore} | P2: ${p2CurrentScore}\n`;
        report += `當前盤面已下子數: ${36 - totalPiecesLeft}\n\n`;
        report += `【AI 參數設定】\n`;
        report += `思考時間限制: ${timeLimit} ms\n`;
        report += `MCTS 探索常數 C: ${mctsExplorationConstant}\n`;
        report += `預判步數 (heuristicN): ${mctsHeuristicN}\n`;
        report += `預判檢查數 (heuristicChecks): ${mctsHeuristicChecks}\n`;
        report += `必殺切換閾值 (pureMinimaxN): ${pureMinimaxN}\n\n`;

        // 如果符合殘局條件,使用 Minimax 分析
        if (totalPiecesLeft <= pureMinimaxN) {
          statusDiv.innerHTML = `Minimax 搜尋中... 經過時間: 0.0s / ${(timeLimit / 1000).toFixed(0)}s`;

          let res = await runIterativeDeepening(
            rootState,
            currentPlayer,
            timeLimit,
            async (timeSpent, nodes, depth) => {
              statusDiv.innerHTML = `Minimax 搜尋中... 經過時間: ${(timeSpent / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s | 深度: ${depth} | 節點: ${nodes}`;
              await new Promise(r => setTimeout(r, 0));
            }
          );

          let totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
          statusDiv.innerHTML = `✅ Minimax 搜尋完成!總耗時: ${totalTime}s / 總節點: ${res.nodes}`;
          btn.disabled = false;
          btn.style.opacity = '1';

          report += `【搜尋模式: Iterative Deepening Minimax】\n`;
          report += `總耗時: ${totalTime} s\n`;
          report += `探索節點數: ${res.nodes}\n\n`;
          report += `【最佳應對與評估分數】(正數代表 P1 領先,負數代表 P2 領先):\n`;

          if (res.results.length === 0) {
            resDiv.innerHTML = '<div style="text-align:center; padding: 20px; color: #888;">無合法著法</div>';
            report += "無合法著法\n";
          } else {
            for (let i = 0; i < res.results.length; i++) {
              let resultObj = res.results[i];
              resDiv.appendChild(buildMinimaxNodeDOM(resultObj));
              report += `${i + 1}. ${getMoveNotationForReport(resultObj.move)} => 絕對分差: ${resultObj.score}\n`;
            }
          }

          latestAiDiagnosticReport = report;
          dlBtn.style.display = 'block';
          return;
        }

        // --- MCTS 分析邏輯 ---
        let root = new MCTSNode(null, null, rootState, currentPlayer, piecesLeft);
        let lastUiTime = startTime;
        let iterations = 0;
        statusDiv.innerHTML = `搜尋中... 經過時間: 0.0s / ${(timeLimit / 1000).toFixed(0)}s`;
        let workPieces = { 1: new Int8Array(3), 2: new Int8Array(3) };

        while (Date.now() - startTime < timeLimit) {
          if (abortAiSearch) break;

          let ran = runMCTSBatch(root, rootState, workPieces, 30);
          iterations += ran;
          if (ran === 0 || root.isFullyExplored) break;

          let now = Date.now();
          if (now - lastUiTime > 100) {
            statusDiv.innerHTML = `搜尋中... 經過時間: ${((now - startTime) / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s | 模擬次數: ${iterations}`;
            await new Promise(r => setTimeout(r, 0));
            lastUiTime = now;
          }
        }

        let totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
        statusDiv.innerHTML = `✅ 搜尋完成!總耗時: ${totalTime}s / 總次數: ${iterations}`;
        btn.disabled = false;
        btn.style.opacity = '1';

        root.children.sort((a, b) => b.visits - a.visits);

        report += `【搜尋模式: MCTS (蒙地卡羅樹搜尋)】\n`;
        report += `總耗時: ${totalTime} s\n`;
        report += `總模擬次數 (Rollouts): ${iterations}\n\n`;
        report += `【根節點行動分析 (依訪問次數排序)】\n`;

        if (root.children.length === 0) {
          resDiv.innerHTML = '<div style="text-align:center; padding: 20px; color: #888;">無合法著法</div>';
          report += "無合法著法\n";
        } else {
          for (let i = 0; i < root.children.length; i++) {
            let child = root.children[i];
            resDiv.appendChild(buildTreeNodeDOM(child, true, currentPlayer));

            let expected = child.visits > 0 ? child.wins / child.visits : 0;
            let winRate = (((expected + 1) / 2) * 100).toFixed(2);
            
            report += `${i + 1}. 行動: ${getMoveNotationForReport(child.move)}\n`;
            report += `   - 訪問次數: ${child.visits}\n`;
            report += `   - 勝率預估: ${winRate}%\n`;
            report += `   - 原始期望值 (未正規化): ${expected.toFixed(4)}\n`;
            if (child.isTerminal) {
              report += `   - [已達終局] 絕對分差: ${child.terminalScore}\n`;
            }
          }
        }
        
        latestAiDiagnosticReport = report;
        dlBtn.style.display = 'block';
      }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: Bing [Bot] 和 66 访客