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

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

修復和改進:
一,檢查MCTS代碼
```
getBestUCTChild() {
const C = 3600; // 探索常數,配合你的遊戲分數(100, 150)量級調整
let bestValue = -Infinity;
let bestChild = null;

// 若當前節點是 AI 行動,選擇分數最大化;若是對手行動,對手會選擇讓 AI 分數最小化的路線
const isAI = this.state.player === currentPlayer;

for (const child of this.children) {
if (child.visits === 0) return child;

const expectedScore = child.totalScore / child.visits;
const exploration = C * Math.sqrt(Math.log(this.visits) / child.visits);

let uctValue;
if (isAI) {
uctValue = expectedScore + exploration; // AI 期望最大化
} else {
uctValue = -expectedScore + exploration; // 對手期望最小化 AI 分數
}

if (uctValue > bestValue) {
bestValue = uctValue;
bestChild = child;
}
}
return bestChild;
}
```

以上是其它程式的代碼,它其中包含了:
```
if (isAI) {
uctValue = expectedScore + exploration; // AI 期望最大化
} else {
uctValue = -expectedScore + exploration; // 對手期望最小化 AI 分數
}
```

那為何本程式不作這個判定?會否發生錯誤?

為何其它程式的「探索常數」會達到3600?本程式的探索常數達到多少比較合適?

二,去掉所有跟「分差縮放分母」和「節點最低訪問次數」的代碼。

「節點最低訪問次數」等同於設為1(只要去掉,默認就是1)。

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

代码: 全选

修復和改進:
一,檢查MCTS代碼
```
        getBestUCTChild() {
          const C = 3600; // 探索常數,配合你的遊戲分數(100, 150)量級調整
          let bestValue = -Infinity;
          let bestChild = null;

          // 若當前節點是 AI 行動,選擇分數最大化;若是對手行動,對手會選擇讓 AI 分數最小化的路線
          const isAI = this.state.player === currentPlayer;

          for (const child of this.children) {
            if (child.visits === 0) return child;

            const expectedScore = child.totalScore / child.visits;
            const exploration = C * Math.sqrt(Math.log(this.visits) / child.visits);

            let uctValue;
            if (isAI) {
              uctValue = expectedScore + exploration; // AI 期望最大化
            } else {
              uctValue = -expectedScore + exploration; // 對手期望最小化 AI 分數
            }

            if (uctValue > bestValue) {
              bestValue = uctValue;
              bestChild = child;
            }
          }
          return bestChild;
        }
```

以上是其它程式的代碼,它其中包含了:
```
            if (isAI) {
              uctValue = expectedScore + exploration; // AI 期望最大化
            } else {
              uctValue = -expectedScore + exploration; // 對手期望最小化 AI 分數
            }
```

那為何本程式不作這個判定?會否發生錯誤?

為何其它程式的「探索常數」會達到3600?本程式的探索常數達到多少比較合適?

二,去掉所有跟「分差縮放分母」和「節點最低訪問次數」的代碼。

「節點最低訪問次數」等同於設為1(只要去掉,默認就是1)。

需要明確給出所要修改的地方和所要替換的代碼。
附件
trirhombus2772.html.7z
(25.9 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6234
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

gemini 3.1 pro

代码: 全选

這是一個非常棒的觀察!你注意到了不同 MCTS(蒙地卡羅樹搜尋)實作在細節上的差異。我會先為你解答這兩個關於演算法邏輯的疑惑,接著提供明確的代碼修改指南,幫你把不需要的參數清理乾淨。

### 一、 關於 MCTS 演算法邏輯的解答

#### 1. 為何本程式沒有使用 `isAI` 的正負號判定?會發生錯誤嗎?

**不會發生錯誤。** 這是因為這份代碼採用了類似 **Negamax(負極大值)** 的概念,將正負號的翻轉實作在**反向傳播(Backpropagation)** 階段,而不是**選擇(Selection)** 階段。

在你的代碼第 1308 行附近(反向傳播階段),有這樣一段邏輯:

```javascript
let diff = p1SimScore - p2SimScore;
let curr = node;
while (curr !== null) {
  curr.visits++;
  if (curr.player === 1) {
    curr.wins += -diff;  // 注意這裡的反轉
  } else {
    curr.wins += diff;
  }
  curr = curr.parent;
}

```

因為在反向傳播時,已經根據該節點是哪位玩家的行動(`curr.player`),把對他不利的分差取了負數(`-diff`),所以存儲在節點裡的 `wins`(實際上是累計分差)已經是從**該節點玩家的視角**來看的期望值了。

因此,在選擇階段計算 UCT 時,直接取 `c.wins / c.visits` 就能保證雙方都在追求「自己的利益最大化」,自然就不需要在計算 UCT 時再用 `isAI` 去判斷一次了。這是一種非常優雅且常見的寫法。

#### 2. 探索常數 $C$ 為什麼在別人的程式是 3600?本程式該設多少?

UCT 的公式為:


$$UCT = \text{期望分數} + C \times \sqrt{\frac{\ln(N)}{n_i}}$$

公式分為兩部分:「開發(期望分數)」與「探索(後面的平方根項)」。**探索常數 $C$ 的作用,是將「探索項」放大到與「開發項」相同的量級**,讓兩者能互相抗衡。

* **為何別人的 $C=3600$?** 因為在那個遊戲中,最終的分數計算可能高達數百或數千(例如 $100$、$150$ 甚至更高)。如果 $C$ 太小(例如 $1.414$),探索項計算出來可能只有 $2$ 或 $3$,這在幾百倍的期望分數面前微不足道,AI 就會變成純貪婪算法,失去探索能力。
* **三菱棋(Trirhombus)的 $C$ 該設多少?** 三菱棋的每次模擬分差通常都在個位數或十位數(大約在 $-20$ 到 $20$ 之間)。因此,探索常數不需要設到幾千。預設的 $C = 3.6$ 到 $C = 5.0$ 左右是非常合理且合適的區間,足以平衡這款遊戲的分差量級。

---

### 二、 代碼修改指南(移除不必要的參數)

我們將把「分差縮放分母」與「節點最低訪問次數」相關的 UI 與邏輯徹底移除。請按照以下步驟尋找並替換代碼。

#### 1. 移除 HTML UI 介面

請在 HTML 中找到以下區塊(約在 `<div id="ai-modal">` 內部),並**將這兩段完整的 `<div class="setting-row">...</div>` 直接刪除**:

**請刪除這段(節點最低訪問次數):**

```html
        <div class="setting-row">
          <label id="label-mcts-v">節點最低訪問次數</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('mcts-v', -1, 1, 50, 0)">-</button>
            <input type="number" id="mcts-v" class="num-input" value="2" step="1" onchange="updateGlobalParams()" />
            <button class="num-btn" onclick="adjustGlobalParam('mcts-v', 1, 1, 50, 0)">+</button>
          </div>
        </div>

```

**並刪除這段(分差縮放分母):**

```html
        <div class="setting-row">
          <label id="label-mcts-scale">分差縮放分母:</label>
          <div class="num-ctrl">
            <button class="num-btn" onclick="adjustGlobalParam('mcts-scale', -1, 1, 50, 0)">-</button>
            <input
              type="number"
              id="mcts-scale"
              class="num-input"
              value="20"
              step="1"
              onchange="updateGlobalParams()"
            />
            <button class="num-btn" onclick="adjustGlobalParam('mcts-scale', 1, 1, 50, 0)">+</button>
          </div>
        </div>

```

#### 2. 移除多國語言字典 (I18N) 裡的定義

找到 `const I18N = {` 的設定,將 `zh` 與 `en` 裡面對應的鍵值刪除。

**在 `zh` 區塊中,刪除這兩行:**

```javascript
          mctsV: '節點最低訪問次數',
          mctsScale: '分差縮放分母',

```

**在 `en` 區塊中,刪除這兩行:**

```javascript
          mctsV: 'Minimum Node Visits',
          mctsScale: 'Score Scaling Denominator',

```

#### 3. 移除 `toggleLanguage()` 函數中的更新邏輯

找到 `toggleLanguage()` 函數,**刪除這兩行**:

```javascript
        document.getElementById('label-mcts-v').innerText = getText('mctsV');
        document.getElementById('label-mcts-scale').innerText = getText('mctsScale');

```

#### 4. 移除全域變數

找到宣告全域變數的地方(約在最上方或 AI 變數區),將 `mctsMinVisits` 移除。

**原代碼:**

```javascript
      let mctsExplorationConstant = 1.414;
      let mctsMinVisits = 1;

```

**替換為:**

```javascript
      let mctsExplorationConstant = 1.414;

```

#### 5. 修改 MCTS 核心:Selection 邏輯

找到 `executeMCTSSearch` 函數內部的 Selection 階段。將原本使用 `mctsMinVisits` 的判斷,改為最標準的 `c.visits === 0`。

**原代碼:**

```javascript
              for (let c of node.children) {
                let uct;
                if (c.visits < mctsMinVisits) {
                  uct = 10000 + Math.random();
                } else {
                  // 取代原有的勝率百分比,直接使用真實分差期望值 + 探索項
                  let expectedScore = c.wins / c.visits;
                  uct = expectedScore + mctsExplorationConstant * Math.sqrt(Math.log(node.visits) / c.visits);
                }
                if (uct > bestUCT) {

```

**替換為:**

```javascript
              for (let c of node.children) {
                let uct;
                if (c.visits === 0) {
                  uct = 10000 + Math.random();
                } else {
                  // 取代原有的勝率百分比,直接使用真實分差期望值 + 探索項
                  let expectedScore = c.wins / c.visits;
                  uct = expectedScore + mctsExplorationConstant * Math.sqrt(Math.log(node.visits) / c.visits);
                }
                if (uct > bestUCT) {

```

#### 6. 修改 `updateGlobalParams` 函數

最後,清理更新參數的函數,移除抓取 `mcts-v` 的邏輯。

**原代碼:**

```javascript
      function updateGlobalParams() {
        mctsExplorationConstant = parseFloat(document.getElementById('mcts-c').value);
        mctsMinVisits = parseInt(document.getElementById('mcts-v').value, 10);
        markCustom(); // 切換到自訂標籤
      }

```

**替換為:**

```javascript
      function updateGlobalParams() {
        mctsExplorationConstant = parseFloat(document.getElementById('mcts-c').value);
        markCustom(); // 切換到自訂標籤
      }

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

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

修復和改進:
改進rolloutPolicy

不應計算每一步的即時分差,每次都要往下探索至局終。

不要每次都絕對選擇Top-K,設定一個機率(如0.3)隨機落子。

二,在Backpropagation中引進度獎勵(Progressive Bonus)

給予後期得分的權重獎勵,使一條路線在後期即深層網路得分價值略高於淺層網路。

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

代码: 全选

修復和改進:
改進rolloutPolicy

不應計算每一步的即時分差,每次都要往下探索至局終。

不要每次都絕對選擇Top-K,設定一個機率(如0.3)隨機落子。

二,在Backpropagation中引進度獎勵(Progressive Bonus)

給予後期得分的權重獎勵,使一條路線在後期即深層網路得分價值略高於淺層網路。

需要明確給出所要修改的地方和所要替換的代碼。
附件
trirhombus2773.html.7z
(25.72 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6234
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

gemini 3.1 pro要睡到8:52

為什麼gemini3.1 pro至今沒有換下去,因為它確實能打,確實好用。

代码: 全选

這兩個修改能大幅改善 MCTS 在後期路線的判斷能力,也能跳出局部最佳解。

以下是針對你提供的 `trirhombus2773.html` 需要修改的地方與替換的程式碼:

### 第一部分:改進 `rolloutPolicy` 函數

這裡我們加入了 $0.3$ 的隨機探索機率(Epsilon-Greedy),並且在評估候選步時,不再只看「即時分差」,而是複製當前狀態並**隨機模擬至局終**來評估該步的最終潛力。考慮到模擬至局終較耗費效能,稍微調降了候選池的大小。

**請找到以下程式碼(約在檔案中後段):**

```javascript
      // === Top-K Random Rollout 策略 ===
      function rolloutPolicy(simState, simPlayer, workPieces, moves) {
        // 為了效能,我們不評估所有合法步,而是隨機抽取最多 8 步作為候選池
        const M = Math.min(8, moves.length);
        const K = Math.min(3, M); // 從候選池中挑選期望值最高的前 3 名

        let candidates = [];
        let movesCopy = [...moves];
        for (let i = 0; i < M; i++) {
          let rIdx = Math.floor(Math.random() * movesCopy.length);
          candidates.push(movesCopy.splice(rIdx, 1)[0]);
        }

        let evaluated = [];
        for (let m of candidates) {
          let simPid = (m >> 24) & 0xff;
          let simP1Idx = (m >> 12) & 0xfff;
          let simP2Idx = m & 0xfff;

          // 模擬落子
          simState.tryPlacePiece(simPid + 1, simState._idxToCoord(simP1Idx), simState._idxToCoord(simP2Idx), false);
          workPieces[simPlayer][simPid]--;

          // 評估即時分差 (作為期望值的簡易替代)
          let scores = simState.calculateScores();
          let scoreDiff = simPlayer === 1 ? scores.p1Score - scores.p2Score : scores.p2Score - scores.p1Score;

          // 復原盤面與可用棋子數
          simState.undo();
          workPieces[simPlayer][simPid]++;

          evaluated.push({ move: m, score: scoreDiff });
        }

        // 依分差降序排序,取前 K 名
        evaluated.sort((a, b) => b.score - a.score);
        let topK = evaluated.slice(0, K);

        // 再從前 K 名中隨機挑選 1 步 (加上隨機性,避免陷入局部最佳)
        return topK[Math.floor(Math.random() * topK.length)].move;
      }

```

**將其替換為以下程式碼:**

```javascript
      // === 改進版 Top-K Random Rollout 策略 ===
      function rolloutPolicy(simState, simPlayer, workPieces, moves) {
        // 1. 設定 30% (0.3) 的機率直接完全隨機落子,跳出局部最佳 (Epsilon-Greedy)
        if (Math.random() < 0.3) {
          return moves[Math.floor(Math.random() * moves.length)];
        }

        // 2. 為了效能平衡,由於需探索至局終,將候選池縮小至最多 4 步,取前 2 名
        const M = Math.min(4, moves.length);
        const K = Math.min(2, M); 

        let candidates = [];
        let movesCopy = [...moves];
        for (let i = 0; i < M; i++) {
          let rIdx = Math.floor(Math.random() * movesCopy.length);
          candidates.push(movesCopy.splice(rIdx, 1)[0]);
        }

        let evaluated = [];
        for (let m of candidates) {
          let simPid = (m >> 24) & 0xff;
          let simP1Idx = (m >> 12) & 0xfff;
          let simP2Idx = m & 0xfff;

          // 深度拷貝當前狀態,用於模擬至局終 (避免頻繁 undo 的複雜度)
          let tempState = simState.clone();
          let tempPieces = {
            1: new Int8Array(workPieces[1]),
            2: new Int8Array(workPieces[2])
          };

          tempState.tryPlacePiece(simPid + 1, tempState._idxToCoord(simP1Idx), tempState._idxToCoord(simP2Idx), false);
          tempPieces[simPlayer][simPid]--;

          let currentSimPlayer = simPlayer === 1 ? 2 : 1;

          // 不看即時分數,持續隨機落子直到局終
          while (true) {
            let tempMoves = tempState.getValidMoves(currentSimPlayer, tempPieces);
            if (tempMoves.length === 0) break;

            let randomMove = tempMoves[Math.floor(Math.random() * tempMoves.length)];
            let rPid = (randomMove >> 24) & 0xff;
            let rP1Idx = (randomMove >> 12) & 0xfff;
            let rP2Idx = randomMove & 0xfff;

            tempState.tryPlacePiece(rPid + 1, tempState._idxToCoord(rP1Idx), tempState._idxToCoord(rP2Idx), false);
            tempPieces[currentSimPlayer][rPid]--;
            currentSimPlayer = currentSimPlayer === 1 ? 2 : 1;
          }

          // 評估最終局終分差
          let scores = tempState.calculateScores();
          let scoreDiff = simPlayer === 1 ? scores.p1Score - scores.p2Score : scores.p2Score - scores.p1Score;

          evaluated.push({ move: m, score: scoreDiff });
        }

        // 依分差降序排序,取前 K 名
        evaluated.sort((a, b) => b.score - a.score);
        let topK = evaluated.slice(0, K);

        // 再從前 K 名中隨機挑選 1 步
        return topK[Math.floor(Math.random() * topK.length)].move;
      }

```

---

### 第二部分:在 Simulation 與 Backpropagation 引進進度獎勵

這裡我們在 Simulation 階段計算推演的深度 (`rolloutDepth`),並在 Backpropagation 階段結合 MCTS 樹的深度 (`currDepth`) 產生一個總深度。總深度越深,我們給予的得分權重加成越高(每深一步增加 1% 的價值),藉此鼓勵 AI 選擇能延伸到更深層網路或能存活更久的路線。

**請在 `executeMCTSSearch` 函數中,找到以下程式碼:**

```javascript
            // 3. Simulation (引入 Top-K Random)
            let p1SimScore, p2SimScore;
            if (node.isTerminal) {
              let scores = simState.calculateScores();
              p1SimScore = scores.p1Score;
              p2SimScore = scores.p2Score;
            } else {
              while (true) {
                let moves = simState.getValidMoves(simPlayer, workPieces);
                if (moves.length === 0) break;

                // 使用 Top-K Random 取代原本的完全隨機
                let m = rolloutPolicy(simState, simPlayer, workPieces, moves);

                let simPid = (m >> 24) & 0xff;
                let simP1Idx = (m >> 12) & 0xfff;
                let simP2Idx = m & 0xfff;
                simState.tryPlacePiece(
                  simPid + 1,
                  simState._idxToCoord(simP1Idx),
                  simState._idxToCoord(simP2Idx),
                  false
                );
                workPieces[simPlayer][simPid]--;
                simPlayer = simPlayer === 1 ? 2 : 1;
              }
              let scores = simState.calculateScores();
              p1SimScore = scores.p1Score;
              p2SimScore = scores.p2Score;
            }

            // 4. Backpropagation (去除歸一化,直接傳遞真實分差)
            let diff = p1SimScore - p2SimScore;
            let curr = node;
            while (curr !== null) {
              curr.visits++;
              if (curr.player === 1) {
                curr.wins += -diff;
              } else {
                curr.wins += diff;
              }
              curr = curr.parent;
            }
            iterations++;

```

**將其替換為以下程式碼:**

```javascript
            // 3. Simulation (引入 Top-K Random 與進度追蹤)
            let p1SimScore, p2SimScore;
            let rolloutDepth = 0; // 新增:紀錄 Simulation 往下探索的深度
            if (node.isTerminal) {
              let scores = simState.calculateScores();
              p1SimScore = scores.p1Score;
              p2SimScore = scores.p2Score;
            } else {
              while (true) {
                let moves = simState.getValidMoves(simPlayer, workPieces);
                if (moves.length === 0) break;

                // 使用改良版的 rolloutPolicy
                let m = rolloutPolicy(simState, simPlayer, workPieces, moves);

                let simPid = (m >> 24) & 0xff;
                let simP1Idx = (m >> 12) & 0xfff;
                let simP2Idx = m & 0xfff;
                simState.tryPlacePiece(
                  simPid + 1,
                  simState._idxToCoord(simP1Idx),
                  simState._idxToCoord(simP2Idx),
                  false
                );
                workPieces[simPlayer][simPid]--;
                simPlayer = simPlayer === 1 ? 2 : 1;
                rolloutDepth++; // 累加探索深度
              }
              let scores = simState.calculateScores();
              p1SimScore = scores.p1Score;
              p2SimScore = scores.p2Score;
            }

            // 4. Backpropagation (引進進度獎勵 Progressive Bonus)
            let diff = p1SimScore - p2SimScore;
            
            // 計算總深度:MCTS 樹內深度 (currDepth) + Rollout 隨機延伸深度 (rolloutDepth)
            let totalDepth = currDepth + rolloutDepth;
            // 設定權重獎勵:每多推進一手,價值提升 1% (可依實際遊戲節奏調整 0.01 的常數)
            let progressiveBonus = 1 + (totalDepth * 0.01);
            let weightedDiff = diff * progressiveBonus;

            let curr = node;
            while (curr !== null) {
              curr.visits++;
              if (curr.player === 1) {
                curr.wins += -weightedDiff;
              } else {
                curr.wins += weightedDiff;
              }
              curr = curr.parent;
            }
            iterations++;

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

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

修復和改進:
一,展示每個分支能拿到的最高分差

在AI設置窗口的搜尋結果,給出每個分支能拿到的最高分差,它應該是一個整數,可能是正數或負數或0。

二,改進rolloutPolicy

去掉Top-K Random和Epsilon-Greedy,起初完全隨機,直到分差期望的差距大到一定的值,才開始偏向分差期望高的分支。

這段代碼「uct = 10000 + Math.random();」我不理解,如果是想表示無窮大,那直接用Infinity不是更好?

三,去掉Backpropagation中的進度獎勵(Progressive Bonus)

之前「給予後期得分的權重獎勵,使一條路線在後期即深層網路得分價值略高於淺層網路。」但是後來發現沒什麼用,現在要把它去掉。

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

代码: 全选

修復和改進:
一,展示每個分支能拿到的最高分差

在AI設置窗口的搜尋結果,給出每個分支能拿到的最高分差,它應該是一個整數,可能是正數或負數或0。

二,改進rolloutPolicy

去掉Top-K Random和Epsilon-Greedy,起初完全隨機,直到分差期望的差距大到一定的值,才開始偏向分差期望高的分支。

這段代碼「uct = 10000 + Math.random();」我不理解,如果是想表示無窮大,那直接用Infinity不是更好?

三,去掉Backpropagation中的進度獎勵(Progressive Bonus)

之前「給予後期得分的權重獎勵,使一條路線在後期即深層網路得分價值略高於淺層網路。」但是後來發現沒什麼用,現在要把它去掉。

需要明確給出所要修改的地方和所要替換的代碼。
附件
trirhombus2774.html.7z
(26.12 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6234
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

claude用一下
附件
trirhombus2774claude2.html.7z
(26.82 KiB) 已下载 3 次
trirhombus2774claude.html.7z
(26.23 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6234
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

修復和改進:
一,展示每個分支能拿到的最高分差

在AI設置窗口的搜尋結果,給出每個分支能拿到的最高分差,它應該是一個整數,可能是正數或負數或0。

正確理解我說的「最高分差」。舉個例子,如果我方的某一步棋最高可以高出對方3分,則對方接下來的一步棋的所有可能中最多只能使我方比他高出3分。

二,改進rolloutPolicy

去掉Top-K Random和Epsilon-Greedy,起初完全隨機,直到分差期望的差距大到一定的值,才開始偏向分差期望高的分支。

這段代碼「uct = 10000 + Math.random();」我不理解,如果是想表示無窮大,那直接用Infinity不是更好?

三,去掉Backpropagation中的進度獎勵(Progressive Bonus)

之前「給予後期得分的權重獎勵,使一條路線在後期即深層網路得分價值略高於淺層網路。」但是後來發現沒什麼用,現在要把它去掉。

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

代码: 全选

修復和改進:
一,展示每個分支能拿到的最高分差

在AI設置窗口的搜尋結果,給出每個分支能拿到的最高分差,它應該是一個整數,可能是正數或負數或0。

正確理解我說的「最高分差」。舉個例子,如果我方的某一步棋最高可以高出對方3分,則對方接下來的一步棋的所有可能中最多只能使我方比他高出3分。

二,改進rolloutPolicy

去掉Top-K Random和Epsilon-Greedy,起初完全隨機,直到分差期望的差距大到一定的值,才開始偏向分差期望高的分支。

這段代碼「uct = 10000 + Math.random();」我不理解,如果是想表示無窮大,那直接用Infinity不是更好?

三,去掉Backpropagation中的進度獎勵(Progressive Bonus)

之前「給予後期得分的權重獎勵,使一條路線在後期即深層網路得分價值略高於淺層網路。」但是後來發現沒什麼用,現在要把它去掉。

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

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

gemini 3.1 pro

代码: 全选

這三個要求都非常合理,能讓 MCTS 引擎的邏輯更純粹且有效率。

在開始修改之前,先解答你關於 `uct = 10000 + Math.random();` 的疑惑:
如果你直接給未探索節點設為 `Infinity`,當有多個未探索節點同時都是 `Infinity` 時,程式在比對大小(尋找最大值)時會把它們視為相等,通常會導致系統「永遠固定選擇陣列中的第一個節點」來展開,這會讓搜尋樹在初期嚴重向左傾斜,失去探索的多樣性。加上一個微小的 `Math.random()` 就能確保這些未探索節點雖然擁有極高優先級,但彼此之間會隨機打亂,這在 MCTS 實作中是個非常經典且實用的除錯小技巧。

以下是針對你的三個需求,明確需要替換的代碼片段:

### 1. 修改 `rolloutPolicy` (拔除 Epsilon-Greedy 與 Top-K)

將原本的 `rolloutPolicy` 函數整段替換為以下代碼。這個版本會在初期完全隨機,而在遊戲進入中後期且「隨機抽樣的走法中,即時分差大於等於 2」時,才會貪婪地選擇高分分支,大幅減少模擬運算量。

```javascript
// === Top-K Random, Epsilon-Greedy Rollout 策略 (已替換為動態分差貪婪策略) ===
function rolloutPolicy(simState, simPlayer, workPieces, moves) {
  // 起初完全隨機
  let randomMove = moves[Math.floor(Math.random() * moves.length)];

  // 計算當前遊戲步數 (總共 36 步)
  let movesPlayed = 36 - (workPieces[1][0] + workPieces[1][1] + workPieces[1][2] + workPieces[2][0] + workPieces[2][1] + workPieces[2][2]);

  // 早期完全隨機,避免在此階段進行耗時評估 (例如前 20 步)
  if (movesPlayed < 20) {
    return randomMove;
  }

  // 後期:為效能考量,僅隨機抽樣最多 3 個著法來評估「即時分差」
  const M = Math.min(3, moves.length);
  let bestMove = randomMove;
  let bestDiff = -Infinity;
  let worstDiff = Infinity;

  for (let i = 0; i < M; i++) {
    let rIdx = Math.floor(Math.random() * moves.length);
    let m = moves[rIdx];

    let simPid = (m >> 24) & 0xff;
    let simP1Idx = (m >> 12) & 0xfff;
    let simP2Idx = m & 0xfff;

    // 建立暫存盤面測試即時分數
    let tempState = simState.clone();
    tempState.tryPlacePiece(simPid + 1, tempState._idxToCoord(simP1Idx), tempState._idxToCoord(simP2Idx), false);
    let scores = tempState.calculateScores();
    let diff = simPlayer === 1 ? scores.p1Score - scores.p2Score : scores.p2Score - scores.p1Score;

    if (diff > bestDiff) {
      bestDiff = diff;
      bestMove = m;
    }
    if (diff < worstDiff) {
      worstDiff = diff;
    }
  }

  // 直到分差期望的差距大到一定的值 (這裡設定為 2 分以上),才開始偏向高分分支
  if (bestDiff - worstDiff >= 2) {
    return bestMove;
  } else {
    return randomMove; // 差距不大則保持隨機
  }
}

```

---

### 2. 去除進度獎勵,並在反向傳播中記錄最高分差 (Minimax 基礎)

找到 `executeMCTSSearch` 函數中的 **第 4 步 Backpropagation**,將該段替換如下。我們移除了 `progressiveBonus`,並利用 MCTS 的特性順便記錄每個節點在模擬中所遇到的最大/最小分差(`mmDiff`),供稍後計算最高分差使用。

```javascript
            // 4. Backpropagation (移除進度獎勵,並紀錄 Minimax 模擬極值)
            let diff = p1SimScore - p2SimScore; // 絕對分差 (P1 - P2)

            let curr = node;
            while (curr !== null) {
              curr.visits++;
              if (curr.player === 1) {
                // 如果是 P1 落子,我們希望最大化 P1 - P2
                curr.wins += -diff;
                if (curr.mmDiff === undefined) curr.mmDiff = diff;
                else curr.mmDiff = Math.max(curr.mmDiff, diff);
              } else {
                // 如果是 P2 落子,P2 會試圖最小化 P1 - P2
                curr.wins += diff;
                if (curr.mmDiff === undefined) curr.mmDiff = diff;
                else curr.mmDiff = Math.min(curr.mmDiff, diff);
              }
              curr = curr.parent;
            }
            iterations++;

```

---

### 3. 計算並在 UI 中展示「最高分差」

為了完美實現「對方接下來的一步棋的所有可能中最多只能使我方高出 X 分」的 Minimax 邏輯,我們需要在 AI 搜尋結束後,由樹根往下遞迴計算一次極大極小值,並呈現在畫面上。

**第一處修改**:找到 `runAISearch()` 函數,在 `let { root, iterations, timeElapsed } = await executeMCTSSearch(timeLimit, true);` 這行之後,排序邏輯之前,插入以下這段遞迴計算代碼:

```javascript
        // --- 插入在 runAISearch 中,await executeMCTSSearch 之後 ---
        if (abortAiSearch) {
          statusDiv.innerHTML = `⚠️ 搜尋已中斷`;
          btn.disabled = false;
          btn.style.opacity = '1';
          return;
        }

        // 計算整數最高分差 (Minimax 演算法應用於 MCTS 樹)
        function computeTreeMinimax(n) {
          if (n.isTerminal) {
            n.treeMinimax = n.terminalScore;
            return n.treeMinimax;
          }
          if (n.children.length === 0) {
            // 葉節點回傳反向傳播記錄到的極端值
            n.treeMinimax = n.mmDiff !== undefined ? n.mmDiff : 0;
            return n.treeMinimax;
          }
          if (n.player === 1) { // P1 取 Max
            let maxVal = -Infinity;
            for (let c of n.children) maxVal = Math.max(maxVal, computeTreeMinimax(c));
            n.treeMinimax = maxVal;
            return maxVal;
          } else { // P2 取 Min
            let minVal = Infinity;
            for (let c of n.children) minVal = Math.min(minVal, computeTreeMinimax(c));
            n.treeMinimax = minVal;
            return minVal;
          }
        }
        computeTreeMinimax(root);
        // --- 插入結束 ---

        statusDiv.innerHTML = `✅ 搜尋完成!總耗時: ${(timeElapsed / 1000).toFixed(1)}s / 總次數: ${iterations}`;

```

**第二處修改**:找到 `buildTreeNodeDOM(node, isFirstLevel, evalPlayer)` 函數中的 UI 拼接部分(大約在 `let displayColor = expected > 0 ? '#007b00' : ...` 附近),替換成以下內容以顯示「最高」分差:

```javascript
        // 直接計算預期真實分差
        let expected = node.visits > 0 ? node.wins / node.visits : 0;
        if (node.parent && node.parent.player !== evalPlayer) {
          expected = -expected;
        }

        // 讀取剛才遞迴計算出的樹狀 Minimax 最高分差
        let maxDiffVal = node.treeMinimax !== undefined ? node.treeMinimax : 0;
        let displayMaxDiff = evalPlayer === 1 ? maxDiffVal : -maxDiffVal;
        
        let displayColor = expected > 0 ? '#007b00' : expected < 0 ? '#d00000' : '#666';
        let displayColorMax = displayMaxDiff > 0 ? '#007b00' : displayMaxDiff < 0 ? '#d00000' : '#666';

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

        let hasChildren = node.children && node.children.length > 0;
        let expandIcon = hasChildren ? '▶' : ' ';

        header.innerHTML = `
          <span class="tree-expand" style="width: 20px; display: inline-block; text-align: center;">${expandIcon}</span>
          <span style="width: 130px; display: inline-block;">${pieceName} ${notation}</span>
          <span style="color: ${displayColor}; width: 90px; display: inline-block; font-weight: bold;">期望: ${expected.toFixed(1)}</span>
          <span style="color: ${displayColorMax}; width: 80px; display: inline-block; font-weight: bold;">最高: ${displayMaxDiff}</span>
          <span style="color: #666;">次數: ${node.visits}</span>
        `;

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

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

修復和改進:
一,將分差差距值放至AI思考設置窗口

分差期望差距值增加到AI思考設置的窗口中,使之可以調整,步進是0.1。

二,改變selection策略

用50%的比例「展開新子節點」或「往已存在的子節點繼續探索。

三,檢查代碼和說明

為何我發現分差差距小於2的分支,visit數量會相差很大?

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

代码: 全选

修復和改進:
一,將分差差距值放至AI思考設置窗口

分差期望差距值增加到AI思考設置的窗口中,使之可以調整,步進是0.1。

二,改變selection策略

用50%的比例「展開新子節點」或「往已存在的子節點繼續探索。

三,檢查代碼和說明

為何我發現分差差距小於2的分支,visit數量會相差很大?

需要明確給出所要修改的地方和所要替換的代碼。
附件
trirhombus2775.html.7z
(26.2 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6234
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

跟2768下,又和一局。是否可以認為,現在想贏一局AI已經有點難了?
附件
Screenshot from 2026-07-07 12-31-45.png
Screenshot from 2026-07-07 12-31-45.png (199.54 KiB) 还没查看
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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