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

研究提升三菱棋的AI棋力

帖子 ejsoon »

我贏了「專家」:

代码: 全选

[(0,0),(1,0)] [(0,-2),(0,-1)] [(0,2),(0,1)] [(1,-2),(1,-1)] [(0,-3),(-1,-3)] [(2,-1),(2,0)] [(1,2),(1,1)] [(-1,3),(0,3)] [(-1,1),(-1,0)] [(-2,0),(-2,1)] [(-1,-1),(-1,-2)] [(-2,-2),(-2,-1)] [(3,0),(3,-1)] [(1,3),(2,3)] [(3,-2),(2,-2)] [(-2,2),(-1,2)] [(2,4),(2,5)] [(0,4),(1,4)] [(0,6),(0,5)] [(2,-4),(2,-3)] [(2,-5),(1,-5)] [(-1,-5),(-1,-4)] [(1,6),(1,5)] [(-2,-4),(-2,-3)] [(-1,5),(-1,4)] [(3,1),(4,1)] [(4,-1),(4,-2)] [(4,0),(5,0)] [(6,-1),(5,-1)] [(3,-4),(3,-3)] [(4,-5),(3,-5)] [(2,1),(2,2)] [(-2,4),(-2,3)] [(4,-7),(4,-6)] [(4,-4),(4,-3)] [(-3,2),(-3,3)]
專家只要在最後兩步棋走對,他就能贏。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進MCTS搜尋,加入minimx算法

代码: 全选

改進MCTS搜尋,加入minimx算法:
一,在MCTS搜尋時,用「近似minimax的算法」評估當前這一步的最終分差

「近似minimax的算法」為,首先獲取所有可能造成分數變化的下法(不管給哪一方加分),以及所有能阻止對方影響分數的下法(假定輪到對方,對方的某一個下法能影響分數,其所佔的兩個三角形,我方有一步棋正好佔了其中一個三角形),之後以最終分差來作為當前下法的期望值。

當剩餘棋子數量等於或少於n1時,將在MCTS搜尋中加入這個算法。

n1在不同等級為:簡單3,困難6,專家9,自訂12。將n1換成合適的英文命名,並加入AI設置窗口中。

二,在局末加入minimax算法,將只用分差來排序。

當剩餘棋子數量等於或少於n2時,將不再使用MCTS搜尋,而是改用minimax,並用分差來排序搜尋結果。

n2在不同等級為:簡單2,困難3,專家4,自訂5。將n2換成合適的英文命名,並加入AI設置窗口中。

需要明確給出所要修改的地方和所要替換的代碼。
gemini 3.1 pro

代码: 全选

這是一個非常精確且能有效提升 MCTS 在局末與得分點判斷能力的改進方案!透過加入近似 Minimax 的啟發式搜尋,以及完全交由純 Minimax 收尾,可以大幅解決 MCTS 在最後幾手容易發生隨機失誤的問題。

以下是針對你提出的兩個需求,所需修改的對應代碼片段與替換說明。

### 一、修改 AI 配置與全域變數

首先,我們需要將 `n1`(`heuristicMinimaxN`)與 `n2`(`pureMinimaxN`)加入到多語言字典、預設配置與全域變數中。

**1. 在 `I18N` 字典中加入新翻譯(約在 `const I18N = { ... }` 內):**

```javascript
// 在 zh 語言包內加入:
heuristicMinimaxN: '啟發式 Minimax 閾值 (n1)',
pureMinimaxN: '純 Minimax 閾值 (局末 n2)',

// 在 en 語言包內加入:
heuristicMinimaxN: 'Heuristic Minimax Threshold (n1)',
pureMinimaxN: 'Pure Minimax Threshold (Endgame n2)',

```

**2. 替換 `aiConfigs` 與新增全域變數(約行號 660 附近):**

```javascript
      const aiConfigs = {
        simple: {
          time: 12000, c: 2.4, heuristicN: 4, heuristicChecks: 6,
          heuristicMinimaxN: 3, pureMinimaxN: 2,
          icon: ' <rect x="5" y="7" width="14" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <line x1="12" y1="2" x2="12" y2="6" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="12" r="1.2" fill="currentColor" /> <circle cx="15" cy="12" r="1.2" fill="currentColor" /> '
        },
        hard: {
          time: 24000, c: 3.6, heuristicN: 8, heuristicChecks: 12,
          heuristicMinimaxN: 6, pureMinimaxN: 3,
          icon: ' <rect x="2" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="20" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="5" y="6" width="14" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="11" r="1.2" fill="currentColor" /> <circle cx="15" cy="11" r="1.2" fill="currentColor" /> <rect x="9" y="14" width="6" height="2" fill="currentColor" /> '
        },
        expert: {
          time: 36000, c: 4.8, heuristicN: 12, heuristicChecks: 24,
          heuristicMinimaxN: 9, pureMinimaxN: 4,
          icon: '<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z" fill="currentColor"/>'
        },
        custom: {
          time: 48000, c: 6.0, heuristicN: 16, heuristicChecks: 48,
          heuristicMinimaxN: 12, pureMinimaxN: 5,
          icon: '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/>'
        },
        human: {
          icon: '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>'
        }
      };
      
      // 全域變數補充
      let mctsExplorationConstant = 3.6;
      let mctsHeuristicN = 8;
      let mctsHeuristicChecks = 12;
      let mctsHeuristicMinimaxN = 6;
      let pureMinimaxN = 3;

```

---

### 二、更新 AI 設置面板 UI 與事件

確保玩家可以在「自訂」模式調整新加入的 $n_1$ 與 $n_2$ 參數。

**1. 在 HTML 內 `#ai-modal` 區域中加入 UI(放在 `heuristic-checks` 的 setting-row 下方):**

```html
        <div class="setting-row">
          <label id="label-heuristic-minimax-n">啟發式 Minimax 閾值 (n1)</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('heuristic-minimax-n', -1, 0, 36, 0)">-</button>
            <input type="number" id="heuristic-minimax-n" class="num-input" value="6" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('heuristic-minimax-n', 1, 0, 36, 0)">+</button>
          </div>
        </div>

        <div class="setting-row">
          <label id="label-pure-minimax-n">純 Minimax 閾值 (局末 n2)</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('pure-minimax-n', -1, 0, 36, 0)">-</button>
            <input type="number" id="pure-minimax-n" class="num-input" value="3" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('pure-minimax-n', 1, 0, 36, 0)">+</button>
          </div>
        </div>

```

**2. 綁定多語言與自動更新數值(替換 `toggleLanguage` 與 `updateGlobalParams` 等相關片段):**

```javascript
// 在 toggleLanguage() 中加入:
if (document.getElementById('label-heuristic-minimax-n')) document.getElementById('label-heuristic-minimax-n').innerText = getText('heuristicMinimaxN');
if (document.getElementById('label-pure-minimax-n')) document.getElementById('label-pure-minimax-n').innerText = getText('pureMinimaxN');

// 在 selectAiTab() 內加入數值更新:
document.getElementById('heuristic-minimax-n').value = aiConfigs[preset].heuristicMinimaxN;
document.getElementById('pure-minimax-n').value = aiConfigs[preset].pureMinimaxN;

// 在 markCustom() 的判斷式與賦值加入:
let hMn = parseInt(document.getElementById('heuristic-minimax-n').value, 10);
let pMn = parseInt(document.getElementById('pure-minimax-n').value, 10);
// ... 判斷條件中加入 aiConfigs[k].heuristicMinimaxN === hMn && aiConfigs[k].pureMinimaxN === pMn ...
aiConfigs.custom.heuristicMinimaxN = hMn;
aiConfigs.custom.pureMinimaxN = pMn;

// 替換 updateGlobalParams():
function updateGlobalParams() {
  mctsExplorationConstant = parseFloat(document.getElementById('mcts-c').value);
  mctsHeuristicN = parseInt(document.getElementById('heuristic-n').value, 10);
  mctsHeuristicChecks = parseInt(document.getElementById('heuristic-checks').value, 10);
  mctsHeuristicMinimaxN = parseInt(document.getElementById('heuristic-minimax-n').value, 10);
  pureMinimaxN = parseInt(document.getElementById('pure-minimax-n').value, 10);
  markCustom();
}

```

---

### 三、實作 Minimax 演算法核心

將以下這兩個全新的搜尋算法函數,放在 `runMCTSBatch` 的上方。

```javascript
      // 近似 Minimax (用於 MCTS Rollout 評估)
      function selectiveMinimax(state, player, workPieces, alpha, beta) {
        let totalPieces = workPieces[1][0] + workPieces[1][1] + workPieces[1][2] + workPieces[2][0] + workPieces[2][1] + workPieces[2][2];
        if (totalPieces === 0) {
          let s = state.calculateScores();
          return s.p1Score - s.p2Score;
        }

        let validMoves = state.getValidMoves(player, workPieces);
        if (validMoves.length === 0) {
          let s = state.calculateScores();
          return s.p1Score - s.p2Score;
        }

        let candidateMoves = [];
        let baseScoreObj = state.calculateScores();
        let p1Base = baseScoreObj.p1Score;
        let p2Base = baseScoreObj.p2Score;

        let oppPlayer = player === 1 ? 2 : 1;
        let oppValidMoves = state.getValidMoves(oppPlayer, workPieces);
        let oppScoringPieces = new Set();

        // 預先找出對手會得分的三角形坐標
        for (let om of oppValidMoves) {
          let simPid = (om >> 24) & 0xff;
          let r = state.tryPlacePiece(simPid + 1, state._idxToCoord((om >> 12) & 0xfff), state._idxToCoord(om & 0xfff), false);
          if (r.success) {
            let newS = state.calculateScores();
            state.undo();
            if (newS.p1Score !== p1Base || newS.p2Score !== p2Base) {
              oppScoringPieces.add((om >> 12) & 0xfff);
              oppScoringPieces.add(om & 0xfff);
            }
          }
        }

        // 篩選會改變分數或阻擋對手的棋子
        for (let m of validMoves) {
          let simPid = (m >> 24) & 0xff;
          let idx1 = (m >> 12) & 0xfff;
          let idx2 = m & 0xfff;
          let r = state.tryPlacePiece(simPid + 1, state._idxToCoord(idx1), state._idxToCoord(idx2), false);
          if (r.success) {
            let newScoreObj = state.calculateScores();
            state.undo();
            if (newScoreObj.p1Score !== p1Base || newScoreObj.p2Score !== p2Base) {
              candidateMoves.push(m);
            } else if (oppScoringPieces.has(idx1) || oppScoringPieces.has(idx2)) {
              candidateMoves.push(m);
            }
          }
        }

        // 若無直接衝突點,代表分數暫時穩定,直接返回當前分差作為期望值
        if (candidateMoves.length === 0) {
          return p1Base - p2Base;
        }

        if (player === 1) { // 追求最大化
          let maxEval = -Infinity;
          for (let m of candidateMoves) {
            let simPid = (m >> 24) & 0xff;
            state.tryPlacePiece(simPid + 1, state._idxToCoord((m >> 12) & 0xfff), state._idxToCoord(m & 0xfff), false);
            workPieces[player][simPid]--;
            let ev = selectiveMinimax(state, oppPlayer, workPieces, alpha, beta);
            workPieces[player][simPid]++;
            state.undo();
            maxEval = Math.max(maxEval, ev);
            alpha = Math.max(alpha, ev);
            if (beta <= alpha) break;
          }
          return maxEval;
        } else { // 追求最小化 (P2 視角)
          let minEval = Infinity;
          for (let m of candidateMoves) {
            let simPid = (m >> 24) & 0xff;
            state.tryPlacePiece(simPid + 1, state._idxToCoord((m >> 12) & 0xfff), state._idxToCoord(m & 0xfff), false);
            workPieces[player][simPid]--;
            let ev = selectiveMinimax(state, oppPlayer, workPieces, alpha, beta);
            workPieces[player][simPid]++;
            state.undo();
            minEval = Math.min(minEval, ev);
            beta = Math.min(beta, ev);
            if (beta <= alpha) break;
          }
          return minEval;
        }
      }

      // 純 Minimax (用於極限局末徹底搜尋,無剪枝)
      function fullMinimax(state, player, workPieces, alpha, beta) {
        let totalPieces = workPieces[1][0] + workPieces[1][1] + workPieces[1][2] + workPieces[2][0] + workPieces[2][1] + workPieces[2][2];
        if (totalPieces === 0) {
          let s = state.calculateScores();
          return { score: s.p1Score - s.p2Score, move: null };
        }

        let validMoves = state.getValidMoves(player, workPieces);
        if (validMoves.length === 0) {
          let s = state.calculateScores();
          return { score: s.p1Score - s.p2Score, move: null };
        }

        let bestMove = null;
        if (player === 1) {
          let maxEval = -Infinity;
          for (let m of validMoves) {
            let simPid = (m >> 24) & 0xff;
            state.tryPlacePiece(simPid + 1, state._idxToCoord((m >> 12) & 0xfff), state._idxToCoord(m & 0xfff), false);
            workPieces[player][simPid]--;
            let res = fullMinimax(state, 2, workPieces, alpha, beta);
            workPieces[player][simPid]++;
            state.undo();
            if (res.score > maxEval) { maxEval = res.score; bestMove = m; }
            alpha = Math.max(alpha, maxEval);
            if (beta <= alpha) break;
          }
          return { score: maxEval, move: bestMove };
        } else {
          let minEval = Infinity;
          for (let m of validMoves) {
            let simPid = (m >> 24) & 0xff;
            state.tryPlacePiece(simPid + 1, state._idxToCoord((m >> 12) & 0xfff), state._idxToCoord(m & 0xfff), false);
            workPieces[player][simPid]--;
            let res = fullMinimax(state, 1, workPieces, alpha, beta);
            workPieces[player][simPid]++;
            state.undo();
            if (res.score < minEval) { minEval = res.score; bestMove = m; }
            beta = Math.min(beta, minEval);
            if (beta <= alpha) break;
          }
          return { score: minEval, move: bestMove };
        }
      }

```

---

### 四、將演算法整合進入遊戲流程

**1. 替換 `runMCTSBatch` 內的 Rollout(Simulation)邏輯:**
找到 `// 3. Simulation (Rollout) - 保證一直探索至局終` 段落,將內部邏輯替換:

```javascript
          // 3. Simulation (Rollout)
          let advantage;
          if (node.isTerminal) {
            advantage = node.terminalScore;
          } else {
            let remainingTotal = workPieces[1][0] + workPieces[1][1] + workPieces[1][2] + workPieces[2][0] + workPieces[2][1] + workPieces[2][2];
            
            if (remainingTotal <= mctsHeuristicMinimaxN) {
              // 觸發 n1 條件:使用近似 Minimax 直接評估
              advantage = selectiveMinimax(simState, simPlayer, workPieces, -Infinity, Infinity);
            } else {
              // 原本的隨機 Rollout 邏輯 (包含 mctsHeuristicN 介入)
              while (true) {
                let moves = simState.getValidMoves(simPlayer, workPieces);
                if (moves.length === 0) break;
                // ... 保留舊版從 let movesPlayed = 36 - (...) 一直到 simPlayer 切換結束的邏輯 ...
                
                // --- 為了保持範例整潔,這段就是你原本程式碼第 1210-1287 行的 Rollout 迴圈 ---
              }
              let scores = simState.calculateScores();
              advantage = scores.p1Score - scores.p2Score;
            }
          }

```

**2. 在 `triggerAITurn` 與 `runAISearch` 加入 $n_2$ 攔截條件:**
在 `triggerAITurn()` 中,準備執行 `let rootState = gameLogic.clone();` 之前,攔截檢查:

```javascript
        let remainingPieces = piecesLeft[1][0] + piecesLeft[1][1] + piecesLeft[1][2] + piecesLeft[2][0] + piecesLeft[2][1] + piecesLeft[2][2];
        if (remainingPieces <= pureMinimaxN) {
          let workPieces = { 1: new Int8Array(piecesLeft[1]), 2: new Int8Array(piecesLeft[2]) };
          box.innerText = `局末必勝推演中...`;
          await new Promise(r => setTimeout(r, 50));
          
          let res = fullMinimax(gameLogic.clone(), currentPlayer, workPieces, -Infinity, Infinity);
          if (res.move !== null) {
            let simPid = (res.move >> 24) & 0xff;
            let p1Coord = gameLogic._idxToCoord((res.move >> 12) & 0xfff);
            currentSelectedPiece = simPid;
            isFirstMove = false;
            let halfWidth = 31.1769;
            let isRight = gameLogic.isRight(p1Coord.x, p1Coord.y);
            let cx = isRight ? (p1Coord.x - 1) * halfWidth + 10.392 : (p1Coord.x - 1) * halfWidth + 20.784;
            let tri = { idx: p1Coord.x, N: p1Coord.y, isRight: isRight, cx: cx, cy: p1Coord.y * 18 };
            attemptDrop(tri);
          } else {
            box.innerText = `無合法著法`;
            setTimeout(() => (box.style.opacity = '0'), 2000);
          }
          isAiThinking = false;
          return;
        }
        
        // 如果還沒到 n2,則繼續往下執行正常的 MCTS 初始化與迴圈

```

在 `runAISearch()` 中做類似修改,把窮舉出來的最佳解答直接畫成樹狀節點。
上次由 ejsoon 在 2026年 7月 20日 06:39,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

修復MCTS的計算權重問題
當在以下局面時:

代码: 全选

[(0,0),(1,0)] [(0,-2),(0,-1)] [(0,1),(0,2)] [(1,-2),(1,-1)] [(0,-3),(-1,-3)] [(-1,-1),(-1,0)] [(-2,-1),(-2,0)] [(-1,3),(0,3)] [(1,2),(1,1)] [(-1,1),(-1,2)] [(-2,1),(-2,2)] [(-3,3),(-2,3)] [(2,-3),(1,-3)] [(-3,1),(-3,2)] [(2,1),(2,0)] [(-2,4),(-1,4)] [(-1,5),(-1,6)] [(-3,-1),(-3,0)] [(-3,5),(-3,4)] [(2,2),(3,2)] [(1,-4),(0,-4)] [(3,0),(3,1)] [(1,3),(1,4)] [(1,5),(2,5)] [(0,4),(0,5)] [(2,3),(2,4)] [(2,-1),(2,-2)] [(0,6),(1,6)] [(-2,5),(-2,6)] [(3,-2),(3,-1)] [(-2,-2),(-1,-2)] [(0,-6),(0,-5)] [(-3,-2),(-4,-2)] [(1,-6),(1,-5)] [(-4,0),(-4,-1)] [(2,-5),(2,-4)] [(0,0),(1,0)] [(1,1),(1,2)] [(1,-2),(1,-1)] [(0,1),(0,2)] [(0,-2),(0,-1)] [(2,-2),(2,-1)] [(2,0),(3,0)] [(1,-3),(2,-3)] [(3,-2),(3,-1)] [(3,1),(3,2)] [(2,-5),(2,-4)] [(2,1),(2,2)] [(1,3),(2,3)] [(0,-4),(1,-4)] [(0,-6),(0,-5)] [(-1,-2),(-1,-1)] [(1,-6),(1,-5)] [(-1,-3),(0,-3)] [(-1,-5),(-1,-4)] [(-1,0),(-1,1)] [(3,-4),(3,-3)] [(3,-5),(4,-5)] [(4,-4),(5,-4)] [(4,-3),(4,-2)] [(5,-3),(6,-3)] [(6,-2),(6,-1)] [(-2,2),(-1,2)] [(-2,1),(-3,1)]
不管怎麼設置,對方AI一定會下[(-2,-1),(-2,0)],而這時我方如果下[(5,-1),(5,-2)]會立即得3分,但是我方完全可以先下[(-3,0),(-4,0)]使對方不得分,在後面的回合中再下[(5,-1),(5,-2)]得到這3分。所以對方AI下的[(-2,-1),(-2,0)]這步棋是錯誤的。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進MCTS搜尋,加入minimx算法:
一,在局末加入minimax算法,將只用分差來排序。

當剩餘棋子數量等於或少於pureMinimaxN時,將不再使用MCTS搜尋,而是改用minimax,並用分差來排序搜尋結果。

pureMinimaxN在不同等級為:簡單2,困難3,專家4,自訂5,將pureMinimaxN加入AI設置窗口中。

注意當時間結束時,如果minimax沒有運算結束,停止運算,給出提示,之後緊接著採用「簡單」等級的MCTS進行搜尋。

二,在MCTS搜尋時,增加新的排序算法

之前在runMCTSBatch中,if (36 - movesPlayed <= mctsHeuristicN) {...}是一個啟發式搜尋,它會尋找「自己得分」和「防止對方得分」的下法,。這個啟發式搜尋用於從開局開始的任意階段。

現在改為,當到達mctsHeuristicN的階段時(也就是當前已經到了第N手棋,36-N等於或小於mctsHeuristicN,但在pureMinimaxN之前),用以下新的方法,替代之前的啟發式搜尋:把所有「自己得分」和「防止對方得分」的下法作暴力破解(如果某個分支不存在「使自己得分」或「防止對方得分」的下法,則這個分支的探索就結束),把絕對分差作為這些下法的排序,然後再繼續MCTS搜尋。

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

代码: 全选

改進MCTS搜尋,加入minimx算法:
一,在局末加入minimax算法,將只用分差來排序。

當剩餘棋子數量等於或少於pureMinimaxN時,將不再使用MCTS搜尋,而是改用minimax,並用分差來排序搜尋結果。

pureMinimaxN在不同等級為:簡單2,困難3,專家4,自訂5,將pureMinimaxN加入AI設置窗口中。

注意當時間結束時,如果minimax沒有運算結束,停止運算,給出提示,之後緊接著採用「簡單」等級的MCTS進行搜尋。

二,在MCTS搜尋時,增加新的排序算法

之前在runMCTSBatch中,if (36 - movesPlayed <= mctsHeuristicN) {...}是一個啟發式搜尋,它會尋找「自己得分」和「防止對方得分」的下法,。這個啟發式搜尋用於從開局開始的任意階段。

現在改為,當到達mctsHeuristicN的階段時(也就是當前已經到了第N手棋,36-N等於或小於mctsHeuristicN,但在pureMinimaxN之前),用以下新的方法,替代之前的啟發式搜尋:把所有「自己得分」和「防止對方得分」的下法作暴力破解(如果某個分支不存在「使自己得分」或「防止對方得分」的下法,則這個分支的探索就結束),把絕對分差作為這些下法的排序,然後再繼續MCTS搜尋。

需要明確給出所要修改的地方和所要替換的代碼。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

gemini 3.1 pro

代码: 全选

這是一個非常具體且進階的演算法修改。為了達成您的需求,我們需要修改 MCTS 參數設定、在模擬(Rollout)階段替換啟發式搜尋邏輯,並在 AI 執行回合的入口處加入 Minimax 與超時降級機制。

以下是具體需要修改與替換的程式碼區塊:

### 第一部分:設定檔與全域變數更新

找到 `aiConfigs` 與全域變數宣告區域,加入 `pureMinimaxN` 參數。

**修改前的程式碼 (大約 480 行):**

```javascript
      const aiConfigs = {
        simple: {
          time: 12000,
          c: 2.4,
          heuristicN: 4,
          heuristicChecks: 6,
          // ...省略圖示
        },
        // ... (其他設定)
      };
      let isAiThinking = false;
      let abortAiSearch = false;
      let mctsExplorationConstant = 3.6;
      let mctsHeuristicN = 8;
      let mctsHeuristicChecks = 12;

```

**替換為:**

```javascript
      const aiConfigs = {
        simple: {
          time: 12000,
          c: 2.4,
          heuristicN: 4,
          heuristicChecks: 6,
          pureMinimaxN: 2,
          icon: ' <rect x="5" y="7" width="14" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <line x1="12" y1="2" x2="12" y2="6" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="12" r="1.2" fill="currentColor" /> <circle cx="15" cy="12" r="1.2" fill="currentColor" /> '
        },
        hard: {
          time: 24000,
          c: 3.6,
          heuristicN: 8,
          heuristicChecks: 12,
          pureMinimaxN: 3,
          icon: ' <rect x="2" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="20" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="5" y="6" width="14" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="11" r="1.2" fill="currentColor" /> <circle cx="15" cy="11" r="1.2" fill="currentColor" /> <rect x="9" y="14" width="6" height="2" fill="currentColor" /> '
        },
        expert: {
          time: 36000,
          c: 4.8,
          heuristicN: 12,
          heuristicChecks: 24,
          pureMinimaxN: 4,
          icon: '<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z" fill="currentColor"/>'
        },
        custom: {
          time: 48000,
          c: 6.0,
          heuristicN: 16,
          heuristicChecks: 48,
          pureMinimaxN: 5,
          icon: '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/>'
        },
        human: {
          icon: '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>'
        }
      };
      let isAiThinking = false;
      let abortAiSearch = false;
      let mctsExplorationConstant = 3.6;
      let mctsHeuristicN = 8;
      let mctsHeuristicChecks = 12;
      let mctsPureMinimaxN = 3; // 新增全域變數

```

---

### 第二部分:UI 面板新增 Minimax 步數設定

找到 `<div id="ai-modal"` 裡面的設定行區塊,在 `heuristic-checks` 之後加入 `pureMinimaxN` 的輸入列。

**在 `<div class="setting-row">...<label id="label-heuristic-checks">...</label>...</div>` 後方加入:**

```html
        <div class="setting-row">
          <label id="label-minimax-n">局末 Minimax 啟動步數</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('minimax-n', -1, 0, 10, 0)">-</button>
            <input
              type="number"
              id="minimax-n"
              class="num-input"
              value="3"
              step="1"
              onchange="updateGlobalParams()"
            />
            <button class="num-btn" onclick="adjustGlobalParam('minimax-n', 1, 0, 10, 0)">+</button>
          </div>
        </div>

```

---

### 第三部分:同步 UI 更新與自訂切換邏輯

修改 `selectAiTab`、`markCustom` 與 `updateGlobalParams` 函式,以包含新的變數。

**修改前的 `selectAiTab` 內容:**

```javascript
          document.getElementById('heuristic-n').value = aiConfigs[preset].heuristicN;
          document.getElementById('heuristic-checks').value = aiConfigs[preset].heuristicChecks;

```

**替換為:**

```javascript
          document.getElementById('heuristic-n').value = aiConfigs[preset].heuristicN;
          document.getElementById('heuristic-checks').value = aiConfigs[preset].heuristicChecks;
          document.getElementById('minimax-n').value = aiConfigs[preset].pureMinimaxN;

```

**修改 `markCustom` 中的判斷式:**

```javascript
        let n = parseInt(document.getElementById('heuristic-n').value, 10);
        let checks = parseInt(document.getElementById('heuristic-checks').value, 10);
        let mm = parseInt(document.getElementById('minimax-n').value, 10); // 新增

        let matchedPreset = 'custom';
        for (let k of ['simple', 'hard', 'expert']) {
          if (
            aiConfigs[k].time === t &&
            Math.abs(aiConfigs[k].c - c) < 0.01 &&
            aiConfigs[k].heuristicN === n &&
            aiConfigs[k].heuristicChecks === checks &&
            aiConfigs[k].pureMinimaxN === mm // 新增條件
          ) {
            matchedPreset = k;
            break;
          }
        }
        // ... 中略 ...
        aiConfigs.custom.heuristicChecks = checks;
        aiConfigs.custom.pureMinimaxN = mm; // 新增

```

**修改 `updateGlobalParams`:**

```javascript
      function updateGlobalParams() {
        mctsExplorationConstant = parseFloat(document.getElementById('mcts-c').value);
        mctsHeuristicN = parseInt(document.getElementById('heuristic-n').value, 10);
        mctsHeuristicChecks = parseInt(document.getElementById('heuristic-checks').value, 10);
        mctsPureMinimaxN = parseInt(document.getElementById('minimax-n').value, 10); // 新增
        markCustom();
      }

```

---

### 第四部分:替換 MCTS 模擬階段的啟發式搜尋

找到 `runMCTSBatch` 函式內部,第 3 步 Simulation (Rollout) 中的啟發式介入區塊。

**修改前的程式碼:**

```javascript
              // 啟發式探索:在局末 n 手內介入
              if (36 - movesPlayed <= mctsHeuristicN) {
                  // ... 原本的隨機洗牌與打分邏輯 ...
                  // (一直到)
                  m = moves[(Math.random() * checkCount) | 0];
                  }
                }
              } else {

```

**替換為全新的暴力破解與排序邏輯:**

```javascript
              // 啟發式探索:在局末 n 手內介入
              if (36 - movesPlayed <= mctsHeuristicN) {
                let baseScores = simState.calculateScores();
                let myBase = simPlayer === 1 ? baseScores.p1Score : baseScores.p2Score;
                let oppPlayer = simPlayer === 1 ? 2 : 1;
                let oppBase = oppPlayer === 1 ? baseScores.p1Score : baseScores.p2Score;

                let candidateMoves = [];

                // 條件一:暴力窮舉所有使自己得分的著法
                for (let i = 0; i < moves.length; i++) {
                  let move = moves[i];
                  let simPid = (move >> 24) & 0xff;
                  let res = simState.tryPlacePiece(simPid + 1, simState._idxToCoord((move >> 12) & 0xfff), simState._idxToCoord(move & 0xfff), false);
                  if (res.success) {
                    let newScore = simPlayer === 1 ? res.scores.p1Score : res.scores.p2Score;
                    let diff = newScore - myBase;
                    simState.undo();
                    if (diff > 0) {
                      candidateMoves.push({ move: move, diff: diff, type: 'score' });
                    }
                  }
                }

                // 條件二:若無法得分,暴力窮舉所有防守對方的著法
                if (candidateMoves.length === 0) {
                  let oppMoves = simState.getValidMoves(oppPlayer, workPieces);
                  let threatMoves = [];
                  for (let i = 0; i < oppMoves.length; i++) {
                    let omove = oppMoves[i];
                    let simPid = (omove >> 24) & 0xff;
                    let res = simState.tryPlacePiece(simPid + 1, simState._idxToCoord((omove >> 12) & 0xfff), simState._idxToCoord(omove & 0xfff), false);
                    if (res.success) {
                      let newScore = oppPlayer === 1 ? res.scores.p1Score : res.scores.p2Score;
                      if (newScore - oppBase > 0) threatMoves.push(omove);
                      simState.undo();
                    }
                  }

                  if (threatMoves.length > 0) {
                    for (let omove of threatMoves) {
                      let p1Idx = (omove >> 12) & 0xfff;
                      let p2Idx = omove & 0xfff;
                      for (let myMove of moves) {
                        let mp1 = (myMove >> 12) & 0xfff;
                        let mp2 = myMove & 0xfff;
                        // 檢查是否佔用了威脅步的格子(阻擋)
                        if (mp1 === p1Idx || mp1 === p2Idx || mp2 === p1Idx || mp2 === p2Idx) {
                          let bRes = simState.tryPlacePiece(((myMove >> 24) & 0xff) + 1, simState._idxToCoord(mp1), simState._idxToCoord(mp2), false);
                          if (bRes.success) {
                            let bDiff = (simPlayer === 1 ? bRes.scores.p1Score : bRes.scores.p2Score) - myBase;
                            simState.undo();
                            candidateMoves.push({ move: myMove, diff: bDiff, type: 'block' });
                          }
                        }
                      }
                    }
                  }
                }

                // 若該分支不存在得分或防守,提前結束探索此分支 (End rollout early)
                if (candidateMoves.length === 0) {
                  break; 
                } else {
                  // 依據絕對分差排序 (由大到小)
                  candidateMoves.sort((a, b) => b.diff - a.diff);
                  m = candidateMoves[0].move; // 取最佳著法繼續模擬
                }
              } else {

```

---

### 第五部分:新增 Minimax 演算法

在 `runMCTSBatch` 函式的下方(或 `triggerAITurn` 之前),加入帶有超時控制的 Minimax 函式:

```javascript
      // 帶有時間限制的 Minimax 演算法
      function runEndgameMinimax(state, player, workPieces, startTime, timeLimit) {
        let isTimeout = false;

        function minimax(currState, currPlayer, alpha, beta) {
          if (Date.now() - startTime >= timeLimit) {
            isTimeout = true;
            return 0;
          }

          let moves = currState.getValidMoves(currPlayer, workPieces);
          if (moves.length === 0) {
            let scores = currState.calculateScores();
            // 以「當前評估玩家」的分差作為回傳值
            return currPlayer === player ? (scores.p1Score - scores.p2Score) * (player === 1 ? 1 : -1) 
                                         : (scores.p2Score - scores.p1Score) * (player === 1 ? 1 : -1);
          }

          let bestVal = -Infinity;
          for (let move of moves) {
            let pid = (move >> 24) & 0xff;
            currState.tryPlacePiece(pid + 1, currState._idxToCoord((move >> 12) & 0xfff), currState._idxToCoord(move & 0xfff), false);
            workPieces[currPlayer][pid]--;
            
            let val = -minimax(currState, currPlayer === 1 ? 2 : 1, -beta, -alpha);
            
            workPieces[currPlayer][pid]++;
            currState.undo();

            if (isTimeout) return 0;
            bestVal = Math.max(bestVal, val);
            alpha = Math.max(alpha, val);
            if (alpha >= beta) break;
          }
          return bestVal;
        }

        let bestMove = null;
        let bestVal = -Infinity;
        let rootMoves = state.getValidMoves(player, workPieces);

        for (let move of rootMoves) {
          if (Date.now() - startTime >= timeLimit) {
            isTimeout = true;
            break;
          }
          let pid = (move >> 24) & 0xff;
          state.tryPlacePiece(pid + 1, state._idxToCoord((move >> 12) & 0xfff), state._idxToCoord(move & 0xfff), false);
          workPieces[player][pid]--;

          let val = -minimax(state, player === 1 ? 2 : 1, -Infinity, Infinity);
          
          workPieces[player][pid]++;
          state.undo();

          if (isTimeout) break;
          if (val > bestVal) {
            bestVal = val;
            bestMove = move;
          }
        }
        return { bestMove, isTimeout, bestVal };
      }

```

---

### 第六部分:在回合入口調用 Minimax 或 MCTS

找到 `triggerAITurn()`。在迴圈開始 MCTS 搜尋之前,加入 Minimax 的判定與降級機制。

**修改前的程式碼:**

```javascript
        let rootState = gameLogic.clone();
        let root = new MCTSNode(null, null, rootState, currentPlayer, piecesLeft);

        let startTime = Date.now();
        let lastUiTime = startTime;
        let iterations = 0;

        // 在迴圈外預先建立工作區陣列,避免大量產生垃圾物件引發 GC 卡頓
        let workPieces = { 1: new Int8Array(3), 2: new Int8Array(3) };

        while (Date.now() - startTime < timeLimit) {

```

**替換為:**

```javascript
        let rootState = gameLogic.clone();
        let root = new MCTSNode(null, null, rootState, currentPlayer, piecesLeft);

        let startTime = Date.now();
        let lastUiTime = startTime;
        let iterations = 0;

        let workPieces = { 1: new Int8Array(3), 2: new Int8Array(3) };
        workPieces[1][0] = piecesLeft[1][0]; workPieces[1][1] = piecesLeft[1][1]; workPieces[1][2] = piecesLeft[1][2];
        workPieces[2][0] = piecesLeft[2][0]; workPieces[2][1] = piecesLeft[2][1]; workPieces[2][2] = piecesLeft[2][2];

        let movesPlayed = 36 - (workPieces[1][0] + workPieces[1][1] + workPieces[1][2] + workPieces[2][0] + workPieces[2][1] + workPieces[2][2]);
        
        let targetMove = null;
        let useMinimax = (36 - movesPlayed) <= config.pureMinimaxN;

        if (useMinimax) {
          box.innerText = `AI Minimax 深度運算中...`;
          await new Promise(r => setTimeout(r, 50));
          let mmResult = runEndgameMinimax(rootState, currentPlayer, workPieces, startTime, timeLimit);
          
          if (mmResult.isTimeout) {
            box.innerText = `Minimax 運算超時,降級為 MCTS (簡單)...`;
            await new Promise(r => setTimeout(r, 50));
            // 降級套用簡單設定
            mctsExplorationConstant = aiConfigs['simple'].c;
            mctsHeuristicN = aiConfigs['simple'].heuristicN;
            useMinimax = false; 
            // 重置時間繼續 MCTS
            startTime = Date.now();
          } else {
            targetMove = mmResult.bestMove;
            let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
            box.innerText = `Minimax 完成: ${finalTime}s\n終局估值: ${mmResult.bestVal}`;
          }
        }

        if (!useMinimax) {
          while (Date.now() - startTime < timeLimit) {
            if (abortAiSearch) {
              isAiThinking = false;
              box.style.opacity = '0';
              return; 
            }

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

            let now = Date.now();
            if (now - lastUiTime > 100) {
              box.innerText = `AI 思考中...\n時間: ${((now - startTime) / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s\n次數: ${iterations}`;
              await new Promise(r => setTimeout(r, 0));
              lastUiTime = now;
            }
          }

          let bestChild = null, mostVisits = -1;
          for (let c of root.children) {
            if (c.visits > mostVisits) {
              mostVisits = c.visits;
              bestChild = c;
            }
          }
          if (bestChild) {
            targetMove = bestChild.move;
            let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
            let winRate = (((bestChild.wins / bestChild.visits + 1) / 2) * 100).toFixed(1);
            box.innerText = `思考完成: ${finalTime}s\n當前勝率: ${winRate}%`;
          }
        }

        if (targetMove !== null) {
          let pid = (targetMove >> 24) & 0xff;
          let p1Idx = (targetMove >> 12) & 0xfff;
          let p1Coord = gameLogic._idxToCoord(p1Idx);

          currentSelectedPiece = pid;
          isFirstMove = false;

          let halfWidth = 31.1769;
          let isRight = gameLogic.isRight(p1Coord.x, p1Coord.y);
          let cx = isRight ? (p1Coord.x - 1) * halfWidth + 10.392 : (p1Coord.x - 1) * halfWidth + 20.784;

          let tri = { idx: p1Coord.x, N: p1Coord.y, isRight: isRight, cx: cx, cy: p1Coord.y * 18 };
          attemptDrop(tri);
        } else {
          box.innerText = `無合法著法`;
          setTimeout(() => (box.style.opacity = '0'), 2000);
        }
        isAiThinking = false;

```

這樣修改後,遊戲會在殘局階段(剩餘步數達到設定值時)自動切換至絕對嚴謹的 Minimax 樹尋找必勝路線,並將原有的 MCTS Rollout 從局部洗牌升級為完全根據分差推導的窮舉啟發,大幅提升 AI 在防守與收官時的強度。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進MCTS搜尋,加入minimx算法:
一,在局末加入minimax算法,將只用分差來排序。

當剩餘棋子數量等於或少於pureMinimaxN時,將不再使用MCTS搜尋,而是改用minimax,並用分差來排序搜尋結果。

pureMinimaxN在不同等級為:簡單2,困難3,專家4,自訂5,將pureMinimaxN加入AI設置窗口中。

注意當時間結束時,如果minimax沒有運算結束,停止運算,給出提示,之後緊接著採用「簡單」等級的MCTS進行搜尋。

二,在MCTS搜尋時,把「預判檢查數」改為「保底補充數」

把目前的程式碼中的mctsHeuristicChecks(預判檢查數),改為「保底補充數(假設有5步)」:掃描全盤所有合法步(假設有 20 步),找出所有「自己得分」和「阻止對方」的步(假設只找到 2 步),發現 2 步小於設定的 預判檢查數 (5),從剩下的 18 步普通棋裡,隨機抓 3 步來補滿到 5 步。

三,在局末(pureMinimaxN前)加入最終得分驗證

當所剩棋子數量小於「智能預判步數 (局末介入)」時,將用36%的時間來作最終得分驗證(等同於一個小範圍的minimax)。

把當前MCTS的搜尋結果及其往後的所有分支中的前4名,作絕對分差的比較,取最高分。

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

gemini 3.1 pro

代码: 全选

This is an excellent approach to upgrading your AI! Combining Monte Carlo Tree Search (MCTS) for mid-game macro-strategy with Minimax (Alpha-Beta pruning) for exact endgame calculation is a classic and highly effective technique in abstract board game programming.

Here are the precise modifications you need to apply to `trirhombus287.html` to fulfill all three of your requirements.

### 一、 修改設定檔、全域變數與多語言字典

**1. 找到 `aiConfigs`,在每個難度中加入 `pureMinimaxN`,並宣告全域變數:**

```javascript
      // 替換原有的 aiConfigs 宣告
      const aiConfigs = {
        simple: {
          time: 12000, c: 2.4, heuristicN: 4, heuristicChecks: 6, pureMinimaxN: 2,
          icon: ' <rect x="5" y="7" width="14" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <line x1="12" y1="2" x2="12" y2="6" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="12" r="1.2" fill="currentColor" /> <circle cx="15" cy="12" r="1.2" fill="currentColor" /> '
        },
        hard: {
          time: 24000, c: 3.6, heuristicN: 8, heuristicChecks: 12, pureMinimaxN: 3,
          icon: ' <rect x="2" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="20" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="5" y="6" width="14" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="11" r="1.2" fill="currentColor" /> <circle cx="15" cy="11" r="1.2" fill="currentColor" /> <rect x="9" y="14" width="6" height="2" fill="currentColor" /> '
        },
        expert: {
          time: 36000, c: 4.8, heuristicN: 12, heuristicChecks: 24, pureMinimaxN: 4,
          icon: '<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z" fill="currentColor"/>'
        },
        custom: {
          time: 48000, c: 6.0, heuristicN: 16, heuristicChecks: 48, pureMinimaxN: 5,
          icon: '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/>'
        },
        human: {
          icon: '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>'
        }
      };
      
      // 在 mctsHeuristicChecks 下方新增全域變數
      let mctsExplorationConstant = 3.6;
      let mctsHeuristicN = 8;
      let mctsHeuristicChecks = 12;
      let mctsPureMinimaxN = 3; // 新增這行

```

**2. 找到 `I18N` 字典,修改與新增文字:**

將原本的 `heuristicChecks: '預判檢查數 (局部洗牌)'` 替換,並加入 `pureMinimaxN` 的翻譯。

```javascript
          // 在 zh 區塊中:
          heuristicChecks: '保底補充數',
          pureMinimaxN: '絕對預判步數 (Minimax)',
          minimaxTimeout: 'Minimax 運算超時,降級為簡單 MCTS...',

          // 在 en 區塊中:
          heuristicChecks: 'Guaranteed Fill Count',
          pureMinimaxN: 'Pure Minimax Steps',
          minimaxTimeout: 'Minimax timeout, falling back to Simple MCTS...',

```

### 二、 更新 AI 設定的 HTML 與 UI 綁定邏輯

**1. 在 HTML 中的 `#ai-modal` 內,找到 `<label id="label-heuristic-checks">` 的區塊,修改它並在下方加上 `pureMinimaxN` 的控制項:**

```html
        <div class="setting-row">
          <label id="label-heuristic-checks">保底補充數</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('heuristic-checks', -1, 1, 100, 0)">-</button>
            <input type="number" id="heuristic-checks" class="num-input" value="12" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('heuristic-checks', 1, 1, 100, 0)">+</button>
          </div>
        </div>

        <div class="setting-row">
          <label id="label-pure-minimax-n">絕對預判步數 (Minimax)</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('pure-minimax-n', -1, 1, 10, 0)">-</button>
            <input type="number" id="pure-minimax-n" class="num-input" value="3" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('pure-minimax-n', 1, 1, 10, 0)">+</button>
          </div>
        </div>

```

**2. 更新 UI 綁定的三個函數 (`selectAiTab`, `markCustom`, `updateGlobalParams`):**

```javascript
      function selectAiTab(preset) {
        document.querySelectorAll('.ai-tab').forEach(tab => {
          if (tab.dataset.preset === preset) tab.classList.add('active');
          else tab.classList.remove('active');
        });
        if (aiConfigs[preset]) {
          document.getElementById('ai-time').value = aiConfigs[preset].time / 1000;
          document.getElementById('mcts-c').value = aiConfigs[preset].c.toFixed(1);
          document.getElementById('heuristic-n').value = aiConfigs[preset].heuristicN;
          document.getElementById('heuristic-checks').value = aiConfigs[preset].heuristicChecks;
          document.getElementById('pure-minimax-n').value = aiConfigs[preset].pureMinimaxN; // 新增
          updateGlobalParams();
        }
      }

      function markCustom() {
        let t = parseFloat(document.getElementById('ai-time').value) * 1000;
        let c = parseFloat(document.getElementById('mcts-c').value);
        let n = parseInt(document.getElementById('heuristic-n').value, 10);
        let checks = parseInt(document.getElementById('heuristic-checks').value, 10);
        let pmn = parseInt(document.getElementById('pure-minimax-n').value, 10); // 新增

        let matchedPreset = 'custom';
        for (let k of ['simple', 'hard', 'expert']) {
          if (
            aiConfigs[k].time === t && Math.abs(aiConfigs[k].c - c) < 0.01 &&
            aiConfigs[k].heuristicN === n && aiConfigs[k].heuristicChecks === checks &&
            aiConfigs[k].pureMinimaxN === pmn // 新增判斷
          ) {
            matchedPreset = k;
            break;
          }
        }
        document.querySelectorAll('.ai-tab').forEach(tab => {
          if (tab.dataset.preset === matchedPreset) tab.classList.add('active');
          else tab.classList.remove('active');
        });
        aiConfigs.custom.time = t;
        aiConfigs.custom.c = c;
        aiConfigs.custom.heuristicN = n;
        aiConfigs.custom.heuristicChecks = checks;
        aiConfigs.custom.pureMinimaxN = pmn; // 新增
      }

      function updateGlobalParams() {
        mctsExplorationConstant = parseFloat(document.getElementById('mcts-c').value);
        mctsHeuristicN = parseInt(document.getElementById('heuristic-n').value, 10);
        mctsHeuristicChecks = parseInt(document.getElementById('heuristic-checks').value, 10);
        mctsPureMinimaxN = parseInt(document.getElementById('pure-minimax-n').value, 10); // 新增
        markCustom();
      }

```

*(記得在 `toggleLanguage` 中加入 `document.getElementById('label-pure-minimax-n').innerText = getText('pureMinimaxN');` 以支援雙語切換)*

### 三、 修改 MCTS 模擬階段:預判保底補充機制

找到 `runMCTSBatch` 中 `// 啟發式探索:在局末 n 手內介入` 區塊,**完全替換**其內的邏輯:

```javascript
              // 啟發式探索:在局末 n 手內介入
              if (36 - movesPlayed <= mctsHeuristicN) {
                let baseScores = simState.calculateScores();
                let myBase = simPlayer === 1 ? baseScores.p1Score : baseScores.p2Score;
                let oppPlayer = simPlayer === 1 ? 2 : 1;
                let oppBase = oppPlayer === 1 ? baseScores.p1Score : baseScores.p2Score;

                let myScoringMoves = [];
                let blockingMoves = [];
                
                // 掃描全盤合法步
                for (let j = 0; j < moves.length; j++) {
                  let move = moves[j];
                  let simPid = (move >> 24) & 0xff;
                  let p1Idx = (move >> 12) & 0xfff;
                  let p2Idx = move & 0xfff;

                  // 測試自己落子
                  let res = simState.tryPlacePiece(simPid + 1, simState._idxToCoord(p1Idx), simState._idxToCoord(p2Idx), false);
                  if (res.success) {
                    let newScore = simPlayer === 1 ? res.scores.p1Score : res.scores.p2Score;
                    simState.undo();
                    if (newScore > myBase) myScoringMoves.push(move);
                  }
                  
                  // 測試對手落子 (阻擋)
                  let oppRes = simState.tryPlacePiece(simPid + 1, simState._idxToCoord(p1Idx), simState._idxToCoord(p2Idx), false);
                  if (oppRes.success) {
                    let oppNewScore = oppPlayer === 1 ? oppRes.scores.p1Score : oppRes.scores.p2Score;
                    simState.undo();
                    if (oppNewScore > oppBase) blockingMoves.push(move);
                  }
                }

                // 決定優先使用的池子
                let pool = myScoringMoves.length > 0 ? myScoringMoves : blockingMoves;

                // 保底補充機制 (隨機補滿至 mctsHeuristicChecks)
                if (pool.length < mctsHeuristicChecks && moves.length > pool.length) {
                  let needed = mctsHeuristicChecks - pool.length;
                  // 過濾掉已經在 pool 中的步
                  let remainingMoves = moves.filter(x => !pool.includes(x));
                  
                  // 洗牌剩餘的步來隨機抽取
                  for (let i = remainingMoves.length - 1; i > 0; i--) {
                    let r = (Math.random() * (i + 1)) | 0;
                    [remainingMoves[i], remainingMoves[r]] = [remainingMoves[r], remainingMoves[i]];
                  }
                  
                  let fillCount = Math.min(needed, remainingMoves.length);
                  for(let i=0; i<fillCount; i++) {
                    pool.push(remainingMoves[i]);
                  }
                }

                if (pool.length > 0) {
                  m = pool[(Math.random() * pool.length) | 0];
                } else {
                  m = moves[Math.floor(Math.random() * moves.length)];
                }
              } else {
                // 一般隨機落子
                m = moves[Math.floor(Math.random() * moves.length)];
              }

```

### 四、 實作 Minimax 引擎與核心決策流程

**1. 在程式碼底部(`class HexGridGame` 外)新增 Minimax 演算法核心:**

```javascript
      function runMinimax(state, player, alpha, beta, workPieces, endTime) {
        if (Date.now() > endTime) return { timeout: true };
        
        let validMoves = state.getValidMoves(player, workPieces);
        if (validMoves.length === 0) {
          let scores = state.calculateScores();
          return { score: scores.p1Score - scores.p2Score, move: null };
        }

        let bestMove = null;
        let isMaximizing = (player === 1);
        let bestScore = isMaximizing ? -Infinity : Infinity;
        let nextPlayer = player === 1 ? 2 : 1;

        for (let i = 0; i < validMoves.length; i++) {
          let move = validMoves[i];
          let pid = (move >> 24) & 0xff;
          state.tryPlacePiece(pid + 1, state._idxToCoord((move >> 12) & 0xfff), state._idxToCoord(move & 0xfff), false);
          workPieces[player][pid]--;

          let result = runMinimax(state, nextPlayer, alpha, beta, workPieces, endTime);
          
          workPieces[player][pid]++;
          state.undo();

          if (result.timeout) return { timeout: true };

          if (isMaximizing) {
            if (result.score > bestScore) { bestScore = result.score; bestMove = move; }
            alpha = Math.max(alpha, bestScore);
          } else {
            if (result.score < bestScore) { bestScore = result.score; bestMove = move; }
            beta = Math.min(beta, bestScore);
          }
          if (beta <= alpha) break; // Alpha-Beta Pruning
        }
        return { score: bestScore, move: bestMove };
      }

```

**2. 大幅改寫 `triggerAITurn()` 以支援三階段混合決策:**

將原來的 `triggerAITurn()` 內部的核心迴圈(`let rootState = ...` 以下)替換為以下邏輯:

```javascript
        let rootState = gameLogic.clone();
        let startTime = Date.now();
        let endTime = startTime + timeLimit;
        
        // 計算剩餘步數
        let p1Rem = piecesLeft[1][0] + piecesLeft[1][1] + piecesLeft[1][2];
        let p2Rem = piecesLeft[2][0] + piecesLeft[2][1] + piecesLeft[2][2];
        let remainingPieces = p1Rem + p2Rem;
        let workPieces = { 1: new Int8Array([piecesLeft[1][0], piecesLeft[1][1], piecesLeft[1][2]]), 2: new Int8Array([piecesLeft[2][0], piecesLeft[2][1], piecesLeft[2][2]]) };
        
        let bestMoveToPlay = null;

        // 階段 1:純 Minimax (剩餘棋子 <= pureMinimaxN)
        if (remainingPieces <= mctsPureMinimaxN) {
          box.innerText = `執行絕對預判 (Minimax)...`;
          let mmResult = runMinimax(rootState, currentPlayer, -Infinity, Infinity, workPieces, endTime);
          
          if (!mmResult.timeout && mmResult.move !== null) {
            bestMoveToPlay = mmResult.move;
          } else {
            // Minimax 超時,降級為 Simple MCTS
            box.innerText = getText('minimaxTimeout') || 'Minimax 運算超時,降級為簡單 MCTS...';
            await new Promise(r => setTimeout(r, 800));
            
            endTime = Date.now() + aiConfigs.simple.time; // 重置時間為簡單難度
            let root = new MCTSNode(null, null, rootState, currentPlayer, piecesLeft);
            while (Date.now() < endTime && !abortAiSearch) {
              if (runMCTSBatch(root, rootState, workPieces, 20) === 0 || root.isFullyExplored) break;
            }
            let bestChild = root.children.reduce((best, c) => (c.visits > (best ? best.visits : -1) ? c : best), null);
            if (bestChild) bestMoveToPlay = bestChild.move;
          }
        } 
        else {
          let root = new MCTSNode(null, null, rootState, currentPlayer, piecesLeft);
          
          // 階段 2:混合驗證 (剩餘棋子 <= heuristicN)
          if (remainingPieces <= mctsHeuristicN) {
            let mctsTimeLimit = timeLimit * 0.64; // 64% 給 MCTS
            let mctsEndTime = startTime + mctsTimeLimit;
            let lastUiTime = startTime;
            let iterations = 0;

            // 執行 MCTS
            while (Date.now() < mctsEndTime && !abortAiSearch) {
              let ran = runMCTSBatch(root, rootState, workPieces, 20);
              iterations += ran;
              if (ran === 0 || root.isFullyExplored) break;
              
              let now = Date.now();
              if (now - lastUiTime > 100) {
                box.innerText = `混合分析中...\nMCTS: ${iterations} 次\n時間: ${((now - startTime) / 1000).toFixed(1)}s`;
                await new Promise(r => setTimeout(r, 0));
                lastUiTime = now;
              }
            }

            // 取前 4 名分支進行 Minimax 驗證
            box.innerText = `最終得分驗證中 (Minimax)...`;
            await new Promise(r => setTimeout(r, 0)); // 強制渲染 UI
            
            let top4 = [...root.children].sort((a, b) => b.visits - a.visits).slice(0, 4);
            let bestVerifiedScore = currentPlayer === 1 ? -Infinity : Infinity;
            
            for (let child of top4) {
              if (Date.now() >= endTime || abortAiSearch) break;

              let pid = (child.move >> 24) & 0xff;
              rootState.tryPlacePiece(pid + 1, rootState._idxToCoord((child.move >> 12) & 0xfff), rootState._idxToCoord(child.move & 0xfff), false);
              workPieces[currentPlayer][pid]--;
              
              let mmResult = runMinimax(rootState, currentPlayer === 1 ? 2 : 1, -Infinity, Infinity, workPieces, endTime);
              
              workPieces[currentPlayer][pid]++;
              rootState.undo();

              if (!mmResult.timeout) {
                if ((currentPlayer === 1 && mmResult.score > bestVerifiedScore) || 
                    (currentPlayer === 2 && mmResult.score < bestVerifiedScore)) {
                  bestVerifiedScore = mmResult.score;
                  bestMoveToPlay = child.move;
                }
              }
            }
            
            // 如果驗證全部超時,則預設回傳 MCTS 最高拜訪數的點
            if (bestMoveToPlay === null && top4.length > 0) bestMoveToPlay = top4[0].move;

          } else {
            // 階段 3:一般 MCTS
            let lastUiTime = startTime;
            let iterations = 0;
            while (Date.now() < endTime && !abortAiSearch) {
              let ran = runMCTSBatch(root, rootState, workPieces, 20);
              iterations += ran;
              if (ran === 0 || root.isFullyExplored) break;

              let now = Date.now();
              if (now - lastUiTime > 100) {
                box.innerText = `AI 思考中...\n時間: ${((now - startTime) / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s\n次數: ${iterations}`;
                await new Promise(r => setTimeout(r, 0));
                lastUiTime = now;
              }
            }
            let bestChild = root.children.reduce((best, c) => (c.visits > (best ? best.visits : -1) ? c : best), null);
            if (bestChild) bestMoveToPlay = bestChild.move;
          }
        }

        let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
        if (bestMoveToPlay !== null) {
          box.innerText = `思考完成: ${finalTime}s`;
          let pid = (bestMoveToPlay >> 24) & 0xff;
          let p1Coord = gameLogic._idxToCoord((bestMoveToPlay >> 12) & 0xfff);
          currentSelectedPiece = pid;
          isFirstMove = false;

          let halfWidth = 31.1769;
          let isRight = gameLogic.isRight(p1Coord.x, p1Coord.y);
          let cx = isRight ? (p1Coord.x - 1) * halfWidth + 10.392 : (p1Coord.x - 1) * halfWidth + 20.784;
          let tri = { idx: p1Coord.x, N: p1Coord.y, isRight: isRight, cx: cx, cy: p1Coord.y * 18 };
          attemptDrop(tri);
        } else {
          box.innerText = `無合法著法`;
          setTimeout(() => (box.style.opacity = '0'), 2000);
        }
        isAiThinking = false;

```

這些變更完美整合了 MCTS 的廣度與 Minimax 的深度,能顯著提升 AI 在收尾階段(Endgame)的準確度。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

加入minimx算法:
當剩餘棋子數量等於或少於pureMinimaxN時,將不再使用MCTS搜尋,而是改用minimax,並用絕對分差來排序搜尋結果。

pureMinimaxN在不同等級為:簡單2,困難3,專家4,自訂5,將pureMinimaxN加入AI設置窗口中。

要用「廣度優先算法」,當時間結束時,如果minimax還沒有算完則停止運算,並挑選當前分差最大的下法。如果是AI設置窗口中的搜尋,則按分差高低排序。

在搜尋時,調用消息框的方式跟MCTS搜尋時一樣,也要實時顯示時間和搜尋量。當搜尋結束開始落子時,要顯示所用時間、總搜尋量、最佳勝負結果。

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

代码: 全选

加入minimx算法:
當剩餘棋子數量等於或少於pureMinimaxN時,將不再使用MCTS搜尋,而是改用minimax,並用絕對分差來排序搜尋結果。

pureMinimaxN在不同等級為:簡單2,困難3,專家4,自訂5,將pureMinimaxN加入AI設置窗口中。

要用「廣度優先算法」,當時間結束時,如果minimax還沒有算完則停止運算,並挑選當前分差最大的下法。如果是AI設置窗口中的搜尋,則按分差高低排序。

在搜尋時,調用消息框的方式跟MCTS搜尋時一樣,也要實時顯示時間和搜尋量。當搜尋結束開始落子時,要顯示所用時間、總搜尋量、最佳勝負結果。

需要明確給出所要修改的地方和所要替換的代碼。
gemini 3.1 pro

代码: 全选

這是一個非常棒的優化方向。當殘局棋子數量非常少時,遊戲的狀態樹會急劇縮小,此時 MCTS 的隨機 rollout 反而可能會帶來不精確的結果。改用 Minimax 搭配 Alpha-Beta 剪枝與**迭代加深 (Iterative Deepening)** (一種結合 DFS 效能與 BFS 廣度優先特性的算法,能在時間結束時隨時中斷並返回當前最優解),能保證在殘局給出「絕對精確」的必勝/必和/必敗著法。

以下是為您加入 Minimax 算法的具體修改步驟與替換代碼:

### 修改處 1:更新 `aiConfigs` 與全域變數

在 `const aiConfigs = {` 的定義中,為每個難度級別加入 `pureMinimaxN` 屬性,並在下方宣告對應的全域變數。

找到這段代碼 (約在 `<script>` 開頭不遠處):

```javascript
      const aiConfigs = {
        simple: {
          time: 12000,
          c: 2.4,
          heuristicN: 4,
          heuristicChecks: 6,

```

**將整個 `aiConfigs` 定義與其下方的全域變數替換為:**

```javascript
      const aiConfigs = {
        simple: {
          time: 12000,
          c: 2.4,
          heuristicN: 4,
          heuristicChecks: 6,
          pureMinimaxN: 2,
          icon: ' <rect x="5" y="7" width="14" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <line x1="12" y1="2" x2="12" y2="6" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="12" r="1.2" fill="currentColor" /> <circle cx="15" cy="12" r="1.2" fill="currentColor" /> '
        },
        hard: {
          time: 24000,
          c: 3.6,
          heuristicN: 8,
          heuristicChecks: 12,
          pureMinimaxN: 3,
          icon: ' <rect x="2" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="20" y="9" width="2" height="6" rx="1" fill="currentColor" /> <rect x="5" y="6" width="14" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="2" /> <circle cx="9" cy="11" r="1.2" fill="currentColor" /> <circle cx="15" cy="11" r="1.2" fill="currentColor" /> <rect x="9" y="14" width="6" height="2" fill="currentColor" /> '
        },
        expert: {
          time: 36000,
          c: 4.8,
          heuristicN: 12,
          heuristicChecks: 24,
          pureMinimaxN: 4,
          icon: '<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z" fill="currentColor"/>'
        },
        custom: {
          time: 48000,
          c: 6.0,
          heuristicN: 16,
          heuristicChecks: 48,
          pureMinimaxN: 5,
          icon: '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/>'
        },
        human: {
          icon: '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>'
        }
      };
      let isAiThinking = false;
      let abortAiSearch = false;
      let mctsExplorationConstant = 3.6;
      let mctsHeuristicN = 8;
      let mctsHeuristicChecks = 12;
      let pureMinimaxN = 3; // 新增:全局 Minimax 觸發閾值

```

### 修改處 2:在 HTML 介面加入 Minimax 閾值控制與多語言

在 HTML 代碼中找到 `<label id="label-heuristic-checks">` 的設定區塊,並在其下方插入新的 Minimax 控制行:

```html
        <div class="setting-row">
          <label id="label-heuristic-checks">預判檢查數 (局部洗牌)</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('heuristic-checks', -1, 1, 100, 0)">-</button>
            <input type="number" id="heuristic-checks" class="num-input" value="12" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('heuristic-checks', 1, 1, 100, 0)">+</button>
          </div>
        </div>

        <div class="setting-row">
          <label id="label-pure-minimax">殘局絕對預判 (Minimax) 觸發步數</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('pure-minimax-n', -1, 0, 10, 0)">-</button>
            <input type="number" id="pure-minimax-n" class="num-input" value="3" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('pure-minimax-n', 1, 0, 10, 0)">+</button>
          </div>
        </div>

```

接著找到 `const I18N = {`,在對應的中英文翻譯字典中加入:
**中文字典 (`zh: { ...`) 加入:**

```javascript
          pureMinimaxN: '殘局絕對預判 (Minimax) 觸發步數',

```

**英文字典 (`en: { ...`) 加入:**

```javascript
          pureMinimaxN: 'Endgame Minimax Trigger (Remaining Pieces)',

```

並在 `toggleLanguage()` 函數中補上這行使其支持語言切換:

```javascript
        if (document.getElementById('label-pure-minimax')) document.getElementById('label-pure-minimax').innerText = getText('pureMinimaxN');

```

### 修改處 3:更新 AI 設定套用邏輯 (`selectAiTab`, `markCustom`, `updateGlobalParams`)

找到 `selectAiTab` 函數,加入對 `pure-minimax-n` 的更新:

```javascript
      function selectAiTab(preset) {
        document.querySelectorAll('.ai-tab').forEach(tab => {
          if (tab.dataset.preset === preset) tab.classList.add('active');
          else tab.classList.remove('active');
        });
        if (aiConfigs[preset]) {
          document.getElementById('ai-time').value = aiConfigs[preset].time / 1000;
          document.getElementById('mcts-c').value = aiConfigs[preset].c.toFixed(1);
          document.getElementById('heuristic-n').value = aiConfigs[preset].heuristicN;
          document.getElementById('heuristic-checks').value = aiConfigs[preset].heuristicChecks;
          document.getElementById('pure-minimax-n').value = aiConfigs[preset].pureMinimaxN; // 新增這行
          updateGlobalParams();
        }
      }

```

找到 `markCustom` 函數並修改:

```javascript
      function markCustom() {
        let t = parseFloat(document.getElementById('ai-time').value) * 1000;
        let c = parseFloat(document.getElementById('mcts-c').value);
        let n = parseInt(document.getElementById('heuristic-n').value, 10);
        let checks = parseInt(document.getElementById('heuristic-checks').value, 10);
        let pmn = parseInt(document.getElementById('pure-minimax-n').value, 10); // 新增這行

        let matchedPreset = 'custom';
        for (let k of ['simple', 'hard', 'expert']) {
          if (
            aiConfigs[k].time === t &&
            Math.abs(aiConfigs[k].c - c) < 0.01 &&
            aiConfigs[k].heuristicN === n &&
            aiConfigs[k].heuristicChecks === checks &&
            aiConfigs[k].pureMinimaxN === pmn // 新增判斷
          ) {
            matchedPreset = k;
            break;
          }
        }
        document.querySelectorAll('.ai-tab').forEach(tab => {
          if (tab.dataset.preset === matchedPreset) tab.classList.add('active');
          else tab.classList.remove('active');
        });
        aiConfigs.custom.time = t;
        aiConfigs.custom.c = c;
        aiConfigs.custom.heuristicN = n;
        aiConfigs.custom.heuristicChecks = checks;
        aiConfigs.custom.pureMinimaxN = pmn; // 新增這行
      }

```

找到 `updateGlobalParams` 函數,加入同步:

```javascript
      function updateGlobalParams() {
        mctsExplorationConstant = parseFloat(document.getElementById('mcts-c').value);
        mctsHeuristicN = parseInt(document.getElementById('heuristic-n').value, 10);
        mctsHeuristicChecks = parseInt(document.getElementById('heuristic-checks').value, 10);
        pureMinimaxN = parseInt(document.getElementById('pure-minimax-n').value, 10); // 新增這行
        markCustom();
      }

```

### 修改處 4:插入 Minimax 與迭代加深引擎核心代碼

請在 `class MCTSNode { ... }` 類別定義的正上方,插入以下**全新的 Minimax 相關函數** (這使用迭代加深來達成隨時中斷且廣度優先的效果):

```javascript
      // === 殘局絕對計算:Iterative Deepening Minimax 引擎 ===
      let mmNodesSearched = 0;
      let mmAbort = false;

      function evaluateBoardDiff(state) {
        let scores = state.calculateScores();
        return scores.p1Score - scores.p2Score; // 永遠以 P1 視角為正
      }

      function minimax(state, depth, alpha, beta, isMaximizingPlayer, player, workPieces, timeLimit, startTime) {
        mmNodesSearched++;
        // 每 500 個節點檢查一次時間,避免鎖死主線程並允許超時中斷
        if (mmNodesSearched % 500 === 0) {
          if (Date.now() - startTime > timeLimit || abortAiSearch) mmAbort = true;
        }
        if (mmAbort) return 0;

        let moves = state.getValidMoves(player, workPieces);
        if (depth === 0 || moves.length === 0) {
          return evaluateBoardDiff(state);
        }

        let nextPlayer = player === 1 ? 2 : 1;

        if (isMaximizingPlayer) {
          let maxEval = -Infinity;
          for (let i = 0; i < moves.length; i++) {
            let move = moves[i];
            let pid = (move >> 24) & 0xff;
            state.tryPlacePiece(pid + 1, state._idxToCoord((move >> 12) & 0xfff), state._idxToCoord(move & 0xfff), false);
            workPieces[player][pid]--;

            let evalScore = minimax(state, depth - 1, alpha, beta, false, nextPlayer, workPieces, timeLimit, startTime);

            workPieces[player][pid]++;
            state.undo();
            if (mmAbort) break;

            maxEval = Math.max(maxEval, evalScore);
            alpha = Math.max(alpha, evalScore);
            if (beta <= alpha) break;
          }
          return maxEval;
        } else {
          let minEval = Infinity;
          for (let i = 0; i < moves.length; i++) {
            let move = moves[i];
            let pid = (move >> 24) & 0xff;
            state.tryPlacePiece(pid + 1, state._idxToCoord((move >> 12) & 0xfff), state._idxToCoord(move & 0xfff), false);
            workPieces[player][pid]--;

            let evalScore = minimax(state, depth - 1, alpha, beta, true, nextPlayer, workPieces, timeLimit, startTime);

            workPieces[player][pid]++;
            state.undo();
            if (mmAbort) break;

            minEval = Math.min(minEval, evalScore);
            beta = Math.min(beta, evalScore);
            if (beta <= alpha) break;
          }
          return minEval;
        }
      }

      // 迭代加深包裝器 (確保具備廣度優先中斷能力)
      async function runIterativeDeepening(rootState, rootPlayer, timeLimit, statusCallback) {
        let startTime = Date.now();
        let workPieces = {
          1: new Int8Array([piecesLeft[1][0], piecesLeft[1][1], piecesLeft[1][2]]),
          2: new Int8Array([piecesLeft[2][0], piecesLeft[2][1], piecesLeft[2][2]])
        };
        let moves = rootState.getValidMoves(rootPlayer, workPieces);
        if (moves.length === 0) return { bestMove: null, score: 0, nodes: 0, results: [] };

        let moveResults = moves.map(m => ({ move: m, score: rootPlayer === 1 ? -Infinity : Infinity }));
        mmNodesSearched = 0;
        mmAbort = false;

        let bestMove = moves[0];
        let bestScoreOverall = rootPlayer === 1 ? -Infinity : Infinity;
        let maxDepth = workPieces[1][0] + workPieces[1][1] + workPieces[1][2] +
                       workPieces[2][0] + workPieces[2][1] + workPieces[2][2];

        let lastUiTime = startTime;

        // 依深度遞增展開 (廣度優先特性)
        for (let depth = 1; depth <= maxDepth; depth++) {
          // 利用上一深度的結果進行排序,大幅提升 Alpha-Beta 剪枝效率
          moveResults.sort((a, b) => rootPlayer === 1 ? b.score - a.score : a.score - b.score);

          let alpha = -Infinity;
          let beta = Infinity;
          let currentBestMove = null;
          let currentBestScore = rootPlayer === 1 ? -Infinity : Infinity;
          let completedCurrentDepth = true;

          for (let mr of moveResults) {
            let move = mr.move;
            let pid = (move >> 24) & 0xff;
            rootState.tryPlacePiece(pid + 1, rootState._idxToCoord((move >> 12) & 0xfff), rootState._idxToCoord(move & 0xfff), false);
            workPieces[rootPlayer][pid]--;

            let nextPlayer = rootPlayer === 1 ? 2 : 1;
            let evalScore = minimax(rootState, depth - 1, alpha, beta, rootPlayer !== 1, nextPlayer, workPieces, timeLimit, startTime);

            workPieces[rootPlayer][pid]++;
            rootState.undo();

            // 實時非阻塞 UI 更新
            let now = Date.now();
            if (now - lastUiTime > 150) {
              await statusCallback(now - startTime, mmNodesSearched, depth);
              lastUiTime = now;
            }

            if (mmAbort) {
              completedCurrentDepth = false;
              break;
            }

            mr.score = evalScore; // 記錄本層分數

            if (rootPlayer === 1) {
              if (evalScore > currentBestScore) {
                currentBestScore = evalScore;
                currentBestMove = move;
              }
              alpha = Math.max(alpha, evalScore);
            } else {
              if (evalScore < currentBestScore) {
                currentBestScore = evalScore;
                currentBestMove = move;
              }
              beta = Math.min(beta, evalScore);
            }
          }

          if (completedCurrentDepth && currentBestMove !== null) {
            bestMove = currentBestMove;
            bestScoreOverall = currentBestScore;
          }

          if (mmAbort || (completedCurrentDepth && depth === maxDepth)) break;
        }

        // 最終按照最佳結果排序 (供分析面板使用)
        moveResults.sort((a, b) => rootPlayer === 1 ? b.score - a.score : a.score - b.score);

        return {
          bestMove: bestMove,
          score: bestScoreOverall,
          nodes: mmNodesSearched,
          results: moveResults
        };
      }

```

### 修改處 5:修改 `triggerAITurn()` 以觸發 Minimax

找到 `async function triggerAITurn() {` 函數。我們需要在其中加入剩餘棋子數的判斷分支。

**將整段 `triggerAITurn()` 函數替換為:**

```javascript
      async function triggerAITurn() {
        if (gameState !== 'playing' || isAiThinking) return;
        isAiThinking = true;
        abortAiSearch = false; 

        let roundStart = currentRound === 2 ? 36 : 0;
        let movesInRound = historyIndex - roundStart + 1;
        if (movesInRound === 0) {
          autoPlayFirstBlue(currentPlayer);
          isAiThinking = false;
          return;
        }

        let pType = playerTypes[currentPlayer];
        let config = aiConfigs[pType] || aiConfigs['custom'];
        let timeLimit = config.time;

        const box = document.getElementById('test-box');
        box.style.opacity = '1';

        let rootState = gameLogic.clone();
        let startTime = Date.now();

        // 計算目前剩餘總子數
        let totalPiecesLeft = piecesLeft[1].reduce((a, b) => a + b) + piecesLeft[2].reduce((a, b) => a + b);

        if (totalPiecesLeft <= pureMinimaxN) {
          // --- 使用 Minimax 進行殘局絕對預判 ---
          let res = await runIterativeDeepening(rootState, currentPlayer, timeLimit, async (timeSpent, nodes, depth) => {
            box.innerText = `Minimax 預判中...\n時間: ${(timeSpent / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s\n深度: ${depth} | 節點: ${nodes}`;
            await new Promise(r => setTimeout(r, 0));
          });

          let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
          if (res.bestMove !== null) {
            let scoreText = res.score > 0 ? `P1領先 ${res.score}` : (res.score < 0 ? `P2領先 ${-res.score}` : `平局 0`);
            // 要求:顯示所用時間、總搜尋量、最佳勝負結果
            box.innerText = `搜尋完成: ${finalTime}s\n總搜尋量: ${res.nodes}\n最佳結果: ${scoreText}`;

            let pid = (res.bestMove >> 24) & 0xff;
            let p1Idx = (res.bestMove >> 12) & 0xfff;
            let p1Coord = gameLogic._idxToCoord(p1Idx);

            currentSelectedPiece = pid;
            isFirstMove = false;

            let halfWidth = 31.1769;
            let isRight = gameLogic.isRight(p1Coord.x, p1Coord.y);
            let cx = isRight ? (p1Coord.x - 1) * halfWidth + 10.392 : (p1Coord.x - 1) * halfWidth + 20.784;

            let tri = { idx: p1Coord.x, N: p1Coord.y, isRight: isRight, cx: cx, cy: p1Coord.y * 18 };
            attemptDrop(tri);
          } else {
            box.innerText = `無合法著法`;
            setTimeout(() => (box.style.opacity = '0'), 2000);
          }
          isAiThinking = false;
          return;
        }

        // --- 正常情況:使用 MCTS ---
        let root = new MCTSNode(null, null, rootState, currentPlayer, piecesLeft);
        let lastUiTime = startTime;
        let iterations = 0;
        let workPieces = { 1: new Int8Array(3), 2: new Int8Array(3) };

        while (Date.now() - startTime < timeLimit) {
          if (abortAiSearch) {
            isAiThinking = false;
            box.style.opacity = '0';
            return;
          }

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

          let now = Date.now();
          if (now - lastUiTime > 100) {
            box.innerText = `AI 思考中...\n時間: ${((now - startTime) / 1000).toFixed(1)}s / ${(timeLimit / 1000).toFixed(0)}s\n次數: ${iterations}`;
            await new Promise(r => setTimeout(r, 0));
            lastUiTime = now;
          }
        }

        let bestChild = null, mostVisits = -1;
        for (let c of root.children) {
          if (c.visits > mostVisits) {
            mostVisits = c.visits;
            bestChild = c;
          }
        }

        let finalTime = ((Date.now() - startTime) / 1000).toFixed(1);
        if (bestChild) {
          let winRate = (((bestChild.wins / bestChild.visits + 1) / 2) * 100).toFixed(1);
          box.innerText = `思考完成: ${finalTime}s\n當前勝率: ${winRate}%`;

          let pid = (bestChild.move >> 24) & 0xff;
          let p1Idx = (bestChild.move >> 12) & 0xfff;
          let p1Coord = gameLogic._idxToCoord(p1Idx);

          currentSelectedPiece = pid;
          isFirstMove = false;

          let halfWidth = 31.1769;
          let isRight = gameLogic.isRight(p1Coord.x, p1Coord.y);
          let cx = isRight ? (p1Coord.x - 1) * halfWidth + 10.392 : (p1Coord.x - 1) * halfWidth + 20.784;

          let tri = { idx: p1Coord.x, N: p1Coord.y, isRight: isRight, cx: cx, cy: p1Coord.y * 18 };
          attemptDrop(tri);
        } else {
          box.innerText = `無合法著法`;
          setTimeout(() => (box.style.opacity = '0'), 2000);
        }
        isAiThinking = false;
      }

```

### 修改處 6:修改 `runAISearch()` 與專用的 DOM 繪製函數

找到 `async function runAISearch() {` 函數並替換,並在它上方加入一個解析 Minimax 節點專用的 `buildMinimaxNodeDOM` 函數。

**將整段 `runAISearch()` 與相關邏輯替換為:**

```javascript
      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');

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

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

        // 如果符合殘局條件,使用 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));
          });

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

          if (res.results.length === 0) {
            resDiv.innerHTML = '<div style="text-align:center; padding: 20px; color: #888;">無合法著法</div>';
          } else {
            for (let resultObj of res.results) {
              resDiv.appendChild(buildMinimaxNodeDOM(resultObj));
            }
          }
          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;
          }
        }

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

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

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

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

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

帖子 ejsoon »

當加了minimax使AI棋力提升之後,兩個「專家」的對弈:

代码: 全选

[(0,0),(1,0)] [(0,-2),(0,-1)] [(-1,-3),(0,-3)] [(1,-2),(1,-1)] [(-1,-2),(-1,-1)] [(1,1),(2,1)] [(2,2),(2,3)] [(1,3),(1,4)] [(0,3),(0,4)] [(-2,0),(-1,0)] [(0,2),(1,2)] [(-1,3),(-1,4)] [(0,5),(0,6)] [(2,-1),(2,0)] [(2,4),(2,5)] [(2,6),(3,6)] [(0,7),(0,8)] [(3,2),(3,3)] [(-2,3),(-2,4)] [(-1,7),(-1,8)] [(0,9),(0,10)] [(-1,11),(0,11)] [(-1,1),(0,1)] [(3,0),(3,1)] [(1,9),(1,10)] [(1,11),(2,11)] [(-2,2),(-1,2)] [(3,4),(3,5)] [(1,5),(1,6)] [(-2,-2),(-2,-1)] [(2,10),(3,10)] [(-1,9),(-1,10)] [(1,7),(1,8)] [(4,5),(4,6)] [(-1,-5),(-1,-4)] [(3,7),(4,7)]
Screenshot from 2026-07-20 14-05-33.png
Screenshot from 2026-07-20 14-05-33.png (27.2 KiB) 查看 147 次
附件
trirhombus2876.html.7z
(28.35 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6307
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進:
一,將P2默認的等級改為「簡單」。

P2的默認等級是「困難」,現在要改為「簡單」。

二,當切換AI時,一些變量未更改

當切換AI等級時,pureMinimaxN等變量似乎未更改。請修復。

三,當前語言應高亮

語言切換按鈕中的「中文」和「English」,當前語言應高亮。

四,給出最後一步落子提示

當有棋子落子後,在.etdrop的最後應加上一個svg-circle元素,其x,y的值等於最後一個棋子的兩個三角形中心的連線中點,半徑為3,fill為白色。

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

代码: 全选

改進:
一,將P2默認的等級改為「簡單」。

P2的默認等級是「困難」,現在要改為「簡單」。

二,當切換AI時,一些變量未更改

當切換AI等級時,pureMinimaxN等變量似乎未更改。請修復。

三,當前語言應高亮

語言切換按鈕中的「中文」和「English」,當前語言應高亮。

四,給出最後一步落子提示

當有棋子落子後,在.etdrop的最後應加上一個svg-circle元素,其x,y的值等於最後一個棋子的兩個三角形中心的連線中點,半徑為3,fill為白色。

需要明確給出所要修改的地方和所要替換的代碼。
gemini 3.1 pro

代码: 全选

沒有問題!這就為你提供這四個部分的修改代碼。請在原始檔案中尋找對應的段落並進行替換:

### 修改一:將 P2 預設的等級改為「簡單」

**1. 尋找全域變數宣告(約在第 452 行):**

```javascript
      let playerTypes = { 1: 'human', 2: 'hard' }; // human, simple, hard, expert, custom

```

**替換為:**

```javascript
      let playerTypes = { 1: 'human', 2: 'simple' }; // human, simple, hard, expert, custom

```

**2. 尋找 `DOMContentLoaded` 內的選單初始化邏輯(約在第 649 行):**

```javascript
          for (let key in tabNames) {
            let activeClass = key === 'hard' ? ' active' : ''; // 預設選中「困難」
            aiTabsContainer.innerHTML += `
              <div class="ai-tab${activeClass}" data-preset="${key}" onclick="selectAiTab('${key}')">
                <svg viewBox="0 0 24 24">${aiConfigs[key].icon}</svg>
                ${tabNames[key]}
              </div>
            `;
          }
          // 同步初始化輸入框數值為「困難」的預設值
          document.getElementById('ai-time').value = aiConfigs['hard'].time / 1000;

```

**替換為:**

```javascript
          for (let key in tabNames) {
            let activeClass = key === 'simple' ? ' active' : ''; // 預設選中「簡單」
            aiTabsContainer.innerHTML += `
              <div class="ai-tab${activeClass}" data-preset="${key}" onclick="selectAiTab('${key}')">
                <svg viewBox="0 0 24 24">${aiConfigs[key].icon}</svg>
                ${tabNames[key]}
              </div>
            `;
          }
          // 同步初始化輸入框數值為「簡單」的預設值
          document.getElementById('ai-time').value = aiConfigs['simple'].time / 1000;

```

---

### 修改二:修復切換 AI 等級時變數未更改的問題

AI 在自動思考時,原本沒有讀取 `aiConfigs` 中的完整參數,我們需要在觸發 AI 時將對應的變數覆寫進去。

**尋找 `triggerAITurn()` 函數的前半段(約在第 1630 行):**

```javascript
        let pType = playerTypes[currentPlayer];
        let config = aiConfigs[pType] || aiConfigs['custom'];
        let timeLimit = config.time;

        const box = document.getElementById('test-box');

```

**替換為:**

```javascript
        let pType = playerTypes[currentPlayer];
        let config = aiConfigs[pType] || aiConfigs['custom'];
        let timeLimit = config.time;
        
        // 切換 AI 時同步更新全域 MCTS 與 Minimax 變數
        mctsExplorationConstant = config.c;
        mctsHeuristicN = config.heuristicN;
        mctsHeuristicChecks = config.heuristicChecks;
        pureMinimaxN = config.pureMinimaxN;

        const box = document.getElementById('test-box');

```

---

### 修改三:當前語言高亮

**1. 尋找 HTML 結構中的語言切換按鈕(約在第 380 行):**

```html
    <div id="start-screen" class="fullscreen-modal">
      <div class="lang-toggle" onclick="toggleLanguage()">中文︱English</div>

```

**替換為:**

```html
    <div id="start-screen" class="fullscreen-modal">
      <div class="lang-toggle" onclick="toggleLanguage()">
        <span id="lang-zh" style="color: #0056b3; font-weight: bold;">中文</span>︱<span id="lang-en" style="color: #666; font-weight: normal;">English</span>
      </div>

```

**2. 尋找 JavaScript 中的 `toggleLanguage()` 函數(約在第 1176 行):**

```javascript
      function toggleLanguage() {
        currentLang = currentLang === 'zh' ? 'en' : 'zh';
        document.getElementById('text-title').innerText = getText('title');

```

**替換為:**

```javascript
      function toggleLanguage() {
        currentLang = currentLang === 'zh' ? 'en' : 'zh';
        
        // 更新高亮狀態
        document.getElementById('lang-zh').style.color = currentLang === 'zh' ? '#0056b3' : '#666';
        document.getElementById('lang-zh').style.fontWeight = currentLang === 'zh' ? 'bold' : 'normal';
        document.getElementById('lang-en').style.color = currentLang === 'en' ? '#0056b3' : '#666';
        document.getElementById('lang-en').style.fontWeight = currentLang === 'en' ? 'bold' : 'normal';

        document.getElementById('text-title').innerText = getText('title');

```

---

### 修改四:給出最後一步落子提示 (白色圓點)

為了確保不管是用戶點擊、AI落子還是悔棋,都能準確標記最後一步,我們將這個標記連動寫在負責更新畫面的 `updateUI()` 中。

**尋找 `updateUI()` 函數(約在第 1030 行),在其末尾添加函數呼叫,並在下方宣告提示點計算邏輯:**

```javascript
        if (restoreBtn) {
          if (currentRound === 1) {
            restoreBtn.innerHTML = `
              <path d="M4 4h16v16H4V4zm2 4v10h12V8H6z" fill="#333" />
              <path d="M12 9.5 v5 M10.5 10.5 l1.5 -1" stroke="#333" stroke-width="1.5" fill="none" stroke-linecap="round" transform="translate(0.5,1)"/>
            `;
          } else {
            restoreBtn.innerHTML = `
              <path d="M4 4h16v16H4V4zm2 4v10h12V8H6z" fill="#333" />
              <path d="M9.5 10.5 c0 -1.5 4 -1.5 4 0 c0 1.5 -4 2 -4 4 h4" stroke="#333" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0.5,1)"/>
            `;
          }
        }
      } // 這裡原本是 updateUI() 結束的地方

```

**將上述結尾替換為:**

```javascript
        if (restoreBtn) {
          if (currentRound === 1) {
            restoreBtn.innerHTML = `
              <path d="M4 4h16v16H4V4zm2 4v10h12V8H6z" fill="#333" />
              <path d="M12 9.5 v5 M10.5 10.5 l1.5 -1" stroke="#333" stroke-width="1.5" fill="none" stroke-linecap="round" transform="translate(0.5,1)"/>
            `;
          } else {
            restoreBtn.innerHTML = `
              <path d="M4 4h16v16H4V4zm2 4v10h12V8H6z" fill="#333" />
              <path d="M9.5 10.5 c0 -1.5 4 -1.5 4 0 c0 1.5 -4 2 -4 4 h4" stroke="#333" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0.5,1)"/>
            `;
          }
        }

        updateLastMoveIndicator(); // 觸發更新最後一步提示
      }

      function updateLastMoveIndicator() {
        let dropG = document.getElementById('etanidrop');
        if (!dropG) return;
        let indicator = document.getElementById('last-move-indicator');
        
        if (!indicator) {
          indicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
          indicator.setAttribute('id', 'last-move-indicator');
          indicator.setAttribute('r', '3');
          indicator.setAttribute('fill', 'white');
          indicator.setAttribute('pointer-events', 'none');
        }
        
        let roundStart = currentRound === 2 ? 36 : 0;
        if (historyIndex >= roundStart && moveHistory[historyIndex]) {
          let m = moveHistory[historyIndex];
          let halfWidth = 31.1769;
          
          let getCenter = (t) => {
            let isRight = gameLogic.isRight(t.idx, t.N);
            let cx = isRight ? (t.idx - 1) * halfWidth + 10.392 : (t.idx - 1) * halfWidth + 20.784;
            let cy = t.N * 18;
            return {cx, cy};
          };
          
          let c1 = m.t1.cx !== undefined ? m.t1 : getCenter(m.t1);
          let c2 = m.t2.cx !== undefined ? m.t2 : getCenter(m.t2);
          let midX = (c1.cx + c2.cx) / 2;
          let midY = (c1.cy + c2.cy) / 2;
          
          indicator.setAttribute('cx', midX.toFixed(3));
          indicator.setAttribute('cy', midY.toFixed(3));
          indicator.style.display = 'block';
          dropG.appendChild(indicator); // 保證附加在最後節點,不被棋子蓋住
        } else {
          if (indicator.parentNode) indicator.parentNode.removeChild(indicator);
        }
      }

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

在线用户

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