分享和讨论Javascript相关的话题
头像
ejsoon
一枝独秀一枝独秀
帖子: 6626
注册时间: 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
一枝独秀一枝独秀
帖子: 6626
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

trirhombus2887.html

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

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

帖子 ejsoon »

trirhombus2888.html
精簡了一下

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

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

帖子 ejsoon »

在手機上,三菱棋「專家」級別被打敗:

代码: 全选

0 2a1 2b1 2A1 4b3 a1b b5a 2c1 2B1 2C1 2a3 4B3 4a3 2A3 a6 4c3 2b3 3B4 0C1 1D0 1E0 3D2 E4D b4c 3E2 A5B c3d b 1E2 A4 4A3 b7a B5A 9b8 1D2 11b10 0 1A2 2a1 1a2 2A1 B3A a3b 0B1 1b0 c2b d1c 0d1 B2C a4 0C1 b3a 6a5 4a5 5B4 4b5 5b4 5c4 5d4 b7a 1b2 0c1 A3B 2d3 d3c c6b 6A5 2c3 4C3 4A5 9a8 2B1
图片

图片

第二局:

图片

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

在线用户

正浏览此版面之用户: 没有注册用户 和 43 访客