「益智遊戲」和「抽象弈棋」
回复
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

為什麼當我停止AI思考後,就不能再次通過點擊turn-tracker切換回AI,即使用AI設置窗口切換成AI後,AI也不會開始思考?

claude sonnet 5 (low)

代码: 全选

找到問題了!在 `startAI()` 函式中,取消 AI 思考時有一個提早 return 的分支,忘記把 `aiThinking` 重設為 `false`:

```js
if (cancelAi) return;   // ← 沒有把 aiThinking 設回 false
```

這導致 `aiThinking` 永遠卡在 `true`,之後任何檢查 `!aiThinking` 的地方(點擊 turn-tracker 切換 AI、AI 設置窗口切換後觸發思考等)都會被擋住。

修復如下:已修復。現在停止 AI 思考後可以正常再次切換回 AI 了。
附件
trigolden_game2421.html.7z
(26.97 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

當兩方都是AI,一方的AI在思考時,另一方的打叉應可點擊使之變成人類。

當一方是AI且在思考時,他的打叉不應是disable。

claude sonnet 5 (low)

代码: 全选

Found both issues. Let's fix the override logic:兩個問題都已修復:

1. **非行動方切換 AI/人類不再受 `aiThinking` 影響** — 原本非行動方的打叉/打勾點擊事件都多判斷了 `!aiThinking`,但 `aiThinking` 只代表「行動方自己」是否在思考,跟另一方無關,所以會誤擋。現在拿掉這個多餘判斷,兩方都是 AI 時,思考中那方以外的另一方打叉隨時可點擊切回人類。

2. **行動方 AI 思考中的打叉不再顯示為 disabled** — 原本的視覺 disabled 狀態只根據「是否已選子/暫存棋子」判斷,AI 思考中因為還沒放暫存棋子,打叉會被加上 `disabled` 樣式(雖然功能上仍可中斷 AI,但視覺上顯示成不可點擊)。現在只要「行動方是 AI 且正在思考」,就會主動移除打叉的 `disabled` 樣式,讓它看起來、也確實可以點擊來中斷 AI。
附件
trigolden_game2422.html.7z
(27.05 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

還是不對。當一方正在行動並處於AI思考狀態時,他的打叉按鈕應是亮起的,鼠標放上去也不會變成「禁止點擊」的圖案。

claude sonnet 5 (low)

代码: 全选

Found the real root cause — these ID selectors never match the actual buttons (`btn-cross-1`/`btn-cross-2`), so the "lit up" red/green background never applies at all, disabled or not. Let's fix the selectors:找到真正的根因了,這次修了兩處:

1. **CSS 選擇器一直沒生效** — 原本的「亮起」樣式寫的是 `#btn-cross:not(.disabled)` / `#btn-check:not(.disabled)`,但實際按鈕的 id 是 `btn-cross-1`/`btn-cross-2`/`btn-check-1`/`btn-check-2`,ID 選擇器完全對不上,導致無論有沒有 `disabled` class,紅色/綠色的「亮起」背景色從未真正套用過。已改成分別對 `btn-cross-1`、`btn-cross-2`、`btn-check-1`、`btn-check-2` 生效,同時明確加上 `cursor: pointer`。

2. **時序問題** — `startAI()` 一開始把 `aiThinking` 設成 `true`,但沒有馬上呼叫 `updateUI()`,所以移除打叉 `disabled` 樣式的邏輯要等到「下一次」UI 更新才會生效,思考剛開始的那一刻按鈕仍是灰暗、鼠標顯示禁止圖案。現在在 `aiThinking = true` 之後立刻呼叫 `updateUI()`,思考一開始打叉就會亮起、鼠標也會變成可點擊的樣式。
附件
trigolden_game2423.html.7z
(27.11 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

當AI控制的一方是先手方的第一手棋,則它將隨機選擇一個棋子,隨機選擇正面或反面,隨機生成1到360內的任意角度,落下第一個棋子。

當AI控制的一方是後手方的最後一手棋,則它因為只剩一手棋而只需要落下一個棋子,完善程式中的這個邏輯。

claude sonnet 5 (low)

代码: 全选

Now let's add the two special-case helper functions and hook them into `startAI()`:Now let's verify syntax and confirm consistency:已完成,兩個特殊情況都補上了:

**先手方的第一手棋**(整局遊戲第一子,此時棋盤上還沒有任何棋子,`getAllValidMoves` 原本會直接回傳空陣列導致 AI 卡住不動)
- 新增 `placeAIFirstMove()`:從該方可用的棋子種類中隨機選一種、隨機決定正反面、隨機產生 1~360 的角度,直接落下這一子並旋轉棋盤,然後照原本流程呼叫 `actionCheck()` 完成落子。

**本方只剩最後一手棋**(例如後手方最後一子,此時只需落下一個棋子,原本的 `getAllValidMoves` 只會生成「兩子一組」的落法,永遠湊不出單子方案)
- 新增 `getAllValidSingleMoves()`:沿用原本尋找合法貼合位置的邏輯,但不做兩兩配對,回傳所有單一棋子的合法落法。
- 新增 `placeAISingleMove()`:從中隨機挑一個合法落法,落下這唯一一子並呼叫 `actionCheck()`。

在 `startAI()` 一開始就先判斷這兩種特殊情況(先判斷是否為整局第一子,再判斷本方剩餘棋子數是否為 1),符合就直接處理並結束,不會進入原本設計給「兩子一組」情境的 Minimax/MCTS 邏輯。
附件
trigolden_game2424.html.7z
(27.61 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

改進MCTS算法(明確給出所要修改的地方和所要替換的代碼)
本遊戲規則中,當有一手棋形成頂鑫結構,或這兩手棋之間形成頂鑫結構,則允許這一回合的兩手棋不落在同一個對方棋子上。

則AI在思考時,應走以下流程:

首先判斷是否至少有一個對方棋子能夠合規落下兩個己方棋子,如果沒有,直接彈出仲裁成功,如果有,則往下走。(在往後的搜尋中,如果到某方某一回合無法合規落子,則把上回合對方的這個下法直接剪枝廢棄,換另一種下法。)

之後嘗試只落一個棋子的所有可能,如果形成頂鑫結構並能給己方加分(包括雙方同時加分但己方加更多),則第二個棋子就隨機落子。如果有多種能形成頂鑫結構的方式,則全部都要往下走看哪一種最終分數最高。如果到下回合對方因不能合規落子而回溯,則只需換掉第二個棋子的下法。

如果落一個棋子不能形成頂鑫結構,則嘗試落兩個棋子到「不同的對方棋子」邊上的所有可能(其中有一個棋子落在對方上回合剛落下的棋子的邊上),看是否能形成頂鑫結構。如果有多種能形成頂鑫結構的方式,則全部都要往下走看哪一種最終分數最高。

如果都沒有可能形成頂鑫結構,則再隨機挑選之前已經得出的「合規落下兩個己方棋子到同一個對方棋子的邊上」的下法。

代码: 全选

改進MCTS算法(明確給出所要修改的地方和所要替換的代碼)
本遊戲規則中,當有一手棋形成頂鑫結構,或這兩手棋之間形成頂鑫結構,則允許這一回合的兩手棋不落在同一個對方棋子上。

則AI在思考時,應走以下流程:

首先判斷是否至少有一個對方棋子能夠合規落下兩個己方棋子,如果沒有,直接彈出仲裁成功,如果有,則往下走。(在往後的搜尋中,如果到某方某一回合無法合規落子,則把上回合對方的這個下法直接剪枝廢棄,換另一種下法。)

之後嘗試只落一個棋子的所有可能,如果形成頂鑫結構並能給己方加分(包括雙方同時加分但己方加更多),則第二個棋子就隨機落子。如果有多種能形成頂鑫結構的方式,則全部都要往下走看哪一種最終分數最高。如果到下回合對方因不能合規落子而回溯,則只需換掉第二個棋子的下法。

如果落一個棋子不能形成頂鑫結構,則嘗試落兩個棋子到「不同的對方棋子」邊上的所有可能(其中有一個棋子落在對方上回合剛落下的棋子的邊上),看是否能形成頂鑫結構。如果有多種能形成頂鑫結構的方式,則全部都要往下走看哪一種最終分數最高。

如果都沒有可能形成頂鑫結構,則再隨機挑選之前已經得出的「合規落下兩個己方棋子到同一個對方棋子的邊上」的下法。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

gemini 3.1 pro + thinking

代码: 全选

這是一個非常棒且專業的優化思路!在原本的 MCTS/Minimax 中盲目窮舉所有合法步,會讓效能大打折扣(尤其在瀏覽器單執行緒運算下)。你提出來的這套「啟發式候選步生成(Heuristic Candidate Generation)」與「提前剪枝(Pruning)」流程,完美符合了 AI 遊戲設計中的「領域知識優化(Domain Knowledge Optimization)」。

我們只需要新增一套過濾與計分的函式,並在 `startAI()` 函式原本執行搜尋前,將這套邏輯套用上去,就能讓 AI 準確執行你期望的戰術。

請依照以下兩個步驟,修改原始碼中的 JavaScript 區塊:

### 第一步:新增 AI 專用的「淨分計算」與「候選步生成」函式

請在原始碼中,找到 `async function startAI() {` 這一行。在**這行正上方**,插入以下兩段新函式:

```javascript
      // --- AI 啟發式評估分數 ---
      // 計算模擬落子後,針對特定玩家的「淨加分」
      function evaluateNetScore(allP, aiPlayer) {
        let dingMap = new Map();
        let netScore = 0;
        let simScoredVictims = new Set(scoredVictims); // 複製當前已計分的點避免污染全域

        for (let A of allP) {
          for (let B of allP) {
            if (A.id === B.id) continue;
            let dingVertices = [];
            for (let v of A.vertices) {
              let isDing = false;
              for (let e = 0; e < 4; e++) {
                if (pointOnOpenSegment(v, B.vertices[e], B.vertices[(e + 1) % 4])) {
                  isDing = true; break;
                }
              }
              if (isDing) dingVertices.push(v);
            }
            if (dingVertices.length > 0) {
              let treeRes = getTreeDistance(A, B, allP);
              if (treeRes && treeRes.dist >= 0) {
                for (let v of dingVertices) {
                  let key = B.id + '_' + v.join('_');
                  if (!simScoredVictims.has(key)) {
                    if (!dingMap.has(key)) dingMap.set(key, []);
                    dingMap.get(key).push({ dinger: A, victim: B, dist: treeRes.dist });
                  }
                }
              }
            }
          }
        }

        dingMap.forEach((dings, key) => {
          let minDist = Infinity;
          let bestDing = null;
          for (let ding of dings) {
            if (ding.dist < minDist) { minDist = ding.dist; bestDing = ding; }
          }
          if (bestDing) {
            let score = bestDing.dist;
            if (score > 0) {
              let scorer = 0;
              if (SCORING_MODE === 0) scorer = bestDing.dinger.owner;
              else if (SCORING_MODE === 1) scorer = bestDing.victim.owner;

              if (scorer === aiPlayer) netScore += score;
              else if (scorer > 0 && scorer !== aiPlayer) netScore -= score;
            }
          }
        });
        return netScore;
      }

      // --- AI 專用候選步生成(實踐頂鑫特例與 fallback 機制) ---
      function getAICandidateMoves(player) {
        let standardPairs = getAllValidMoves(player);

        // 1. 判斷是否至少有一個對方棋子能夠合規落下兩個己方棋子 (沒有就回傳仲裁)
        if (standardPairs.length === 0) {
          return { type: 'arbitration', moves: [] };
        }

        let allSingleMoves = getAllValidSingleMoves(player);
        let tempAllPieces = pieces.concat(tempPieces);
        let scoringSinglePairs = [];

        // 2. 嘗試只落一個棋子,看是否形成頂鑫結構且己方淨加分 > 0
        for (let g1 of allSingleMoves) {
          let g1Copy = JSON.parse(JSON.stringify(g1));
          g1Copy.id = 'sim_1';
          let simPieces1 = tempAllPieces.concat([g1Copy]);

          let netScore = evaluateNetScore(simPieces1, player);
          if (netScore > 0) {
            // 如果形成頂鑫並加分,第二個棋子可落在任意(甚至不同的)對方棋子上
            for (let g2 of allSingleMoves) {
              if (g1.svgId === g2.svgId && piecesCount[g1.svgId] < 2) continue;
              if (isValidGhost(g2, simPieces1)) {
                let g2Copy = JSON.parse(JSON.stringify(g2));
                g2Copy.id = 'sim_2';
                scoringSinglePairs.push([g1Copy, g2Copy]);
              }
            }
          }
        }

        if (scoringSinglePairs.length > 0) {
          return { type: 'scoring_single', moves: scoringSinglePairs }; // 有找到單子得分的就優先往下走
        }

        let diffTargetPairs = [];
        let lastOppIds = lastTurnPieces.filter(p => p.owner !== player).map(p => p.id);

        // 3. 落兩個棋子到「不同的對方棋子」邊上(其中一個必須落在對方上回合的棋子上)
        for (let i = 0; i < allSingleMoves.length; i++) {
          let g1 = allSingleMoves[i];
          if (!lastOppIds.includes(g1.targetId)) continue;

          let g1Copy = JSON.parse(JSON.stringify(g1));
          g1Copy.id = 'sim_1';
          let simPieces1 = tempAllPieces.concat([g1Copy]);

          for (let j = 0; j < allSingleMoves.length; j++) {
            let g2 = allSingleMoves[j];
            if (g1.targetId === g2.targetId) continue;
            if (g1.svgId === g2.svgId && piecesCount[g1.svgId] < 2) continue;

            if (isValidGhost(g2, simPieces1)) {
              let g2Copy = JSON.parse(JSON.stringify(g2));
              g2Copy.id = 'sim_2';
              let simPieces2 = simPieces1.concat([g2Copy]);

              // 兩個棋子落下後若形成頂鑫結構且己方得分,則保留此種配對
              let netScore = evaluateNetScore(simPieces2, player);
              if (netScore > 0) {
                diffTargetPairs.push([g1Copy, g2Copy]);
              }
            }
          }
        }

        if (diffTargetPairs.length > 0) {
          return { type: 'scoring_double', moves: diffTargetPairs }; // 保留不同棋子但能得分的雙子組合
        }

        // 4. 如果都沒有可能,則隨機挑選原本得出的「同一個對方棋子」的下法給後續隨機或評估
        return { type: 'standard', moves: standardPairs };
      }

```

---

### 第二步:替換 `startAI()` 內部的搜尋邏輯

在 `async function startAI() {` 函式中,找到以下區段(大約在 MCTS 開始前抓取 `validMoves` 的地方):

```javascript
        // 取得所有合法步 (使用原本的 getAllValidMoves)
        let validMoves = getAllValidMoves(currentPlayer);
        if (validMoves.length === 0) {
          aiThinking = false;
          return; // 無步可走,可能需要觸發仲裁
        }

```

**請將上述程式碼,一直往下選取到包含 `if (useMinimax) { ... } else { ... }` 整個區塊結束(即在 `let totalTime = ...` 這行之上),並將其全部替換為以下程式碼:**

```javascript
        // 取得所有合法步 (改用新的啟發式過濾,實踐頂鑫結構的落子特例與優先級)
        let aiCandidates = getAICandidateMoves(currentPlayer);
        
        // 判斷是否對方連合規落下兩個己方棋子的機會都不給
        if (aiCandidates.type === 'arbitration') {
          aiThinking = false;
          // 直接彈出仲裁成功,自動判對方負
          requestArbitration();
          setTimeout(executeArbitration, 500); 
          return;
        }

        let validMoves = aiCandidates.moves;
        if (validMoves.length === 0) {
          aiThinking = false;
          return;
        }

        let bestMove = validMoves[0];
        let expectedValue = 0;
        let oppPlayer = currentPlayer === 1 ? 2 : 1;

        if (useMinimax) {
          let bestScore = -Infinity;
          let tempAllPieces = pieces.concat(tempPieces);

          for (let i = 0; i < validMoves.length; i++) {
            if (cancelAi) break;
            let move = validMoves[i];
            searchCount++;

            let g1Copy = JSON.parse(JSON.stringify(move[0]));
            let g2Copy = JSON.parse(JSON.stringify(move[1]));
            g1Copy.id = 'sim_1';
            g2Copy.id = 'sim_2';
            let simPieces = tempAllPieces.concat([g1Copy, g2Copy]);

            // 模擬落子後的「己方原本總分 + 淨加分」
            let netScore = evaluateNetScore(simPieces, currentPlayer);
            let myScore = scores[currentPlayer] + netScore;

            // 剪枝預判:檢查下回合對方是否因不能合規落子而回溯
            // 技巧:暫存全域狀態,讓 getAllValidMoves 以為已經落子
            let originalPieces = pieces;
            let originalTempPieces = tempPieces;
            pieces = simPieces;
            tempPieces = [];
            let oppCanMove = getAllValidMoves(oppPlayer).length > 0;
            pieces = originalPieces; // 復原全域狀態
            tempPieces = originalTempPieces;

            if (!oppCanMove) {
              // 剪枝廢棄對方未來的可能:對方無法合規落子,此步必勝!直接換掉其他下法。
              bestScore = Infinity;
              bestMove = move;
              expectedValue = Infinity;
              break; 
            }

            if (myScore > bestScore) {
              bestScore = myScore;
              bestMove = move;
              expectedValue = bestScore;
            }

            if (performance.now() - startTime > 100) {
              showMessage(`Minimax Depth: ${turnsLeft} | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s`, 0, true);
              await new Promise(r => setTimeout(r, 0)); // 讓出主執行緒避免畫面卡死
            }
          }
        } else {
          // MCTS 邏輯
          let rootNode = { visits: 0, wins: 0, children: [] };
          validMoves.forEach(m => rootNode.children.push({ move: m, visits: 0, wins: 0 }));
          let tempAllPieces = pieces.concat(tempPieces);
          
          while (performance.now() - startTime < timeLimit && !cancelAi) {
            let bestUcb = -Infinity;
            let selectedNode = rootNode.children[0];

            for (let child of rootNode.children) {
              if (child.visits === 0) {
                selectedNode = child;
                break;
              }
              let ucb = (child.wins / child.visits) + cValue * Math.sqrt(Math.log(rootNode.visits) / child.visits);
              if (ucb > bestUcb) {
                bestUcb = ucb;
                selectedNode = child;
              }
            }

            // Simulation (模擬並實踐強制必勝的剪枝)
            let move = selectedNode.move;
            let g1Copy = JSON.parse(JSON.stringify(move[0]));
            let g2Copy = JSON.parse(JSON.stringify(move[1]));
            g1Copy.id = 'sim_1'; g2Copy.id = 'sim_2';
            let simPieces = tempAllPieces.concat([g1Copy, g2Copy]);

            let originalPieces = pieces;
            let originalTempPieces = tempPieces;
            pieces = simPieces;
            tempPieces = [];
            let oppCanMove = getAllValidMoves(oppPlayer).length > 0;
            pieces = originalPieces;
            tempPieces = originalTempPieces;

            let simVal = 0;
            if (!oppCanMove) {
              simVal = 100; // 對方無步可走,給予極大權重(強制引導 AI 走這步)
            } else {
              let netScore = evaluateNetScore(simPieces, currentPlayer);
              // 正規化分數以適合 MCTS (防止出現負值權重)
              simVal = netScore > 0 ? 1 + netScore * 0.1 : (netScore < 0 ? 0 : 0.5); 
            }

            // Backpropagation
            selectedNode.visits++;
            selectedNode.wins += simVal;
            rootNode.visits++;
            searchCount++;

            if (searchCount % 10 === 0) {
              showMessage(`MCTS | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s | Searches: ${searchCount}`, 0, true);
              await new Promise(r => setTimeout(r, 0));
            }
          }

          let bestChild = rootNode.children.reduce((max, node) => (node.visits > max.visits ? node : max), rootNode.children[0]);
          bestMove = bestChild.move;
          expectedValue = bestChild.wins / bestChild.visits || 0;
        }

```

### 為什麼這樣改會解決你的問題?

* **層級化候選池 (Fallback Hierarchy):** 新增的 `getAICandidateMoves` 徹底遵循了你的邏輯。它會優先找出「只放一顆就能得分」的棋子並任意配對第二顆;若無,則尋找「兩顆落在不同棋子上」並得分的配置;最後才是常規合法步,這讓 AI 思考時的方向變得極其銳利。
* **提前剪枝 (Forward Pruning):** 由於在迴圈內,我們把 `simPieces` 替換進全域暫存,直接對對手跑了一次 `getAllValidMoves(oppPlayer)`。如果發現對手完全沒有合法步走,AI 就會將這手棋評定為 `Infinity`(必勝),瞬間中斷搜尋,直接走這一步等著贏。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

claude sonnet 5 先high再補medium

代码: 全选

Syntax 檢查通過。現在確認 `startAI` 的整體流程串接正確,並看一下最終結果。結構正確,銜接良好。現在把改好的檔案交給你,並附上完整說明。## 修改摘要

我已將檔案改好並輸出為 **trigolden_game2424_MCTS改進版.html**。以下是完整的修改清單。

### 修改一:新增一整組輔助函式(插入點)

**位置**:`function getAllValidSingleMoves(player) {...}` 結束後、`function placeAIFirstMove(player) {` 之前

**做法**:插入以下新函式(共 9 個),不刪除任何原有程式碼:

| 函式 | 作用 |
|---|---|
| `withHypotheticalPieces()` | 暫時把候選棋子併入全域 `pieces` 執行檢查後還原 |
| `simulateScoreGain()` | 純模擬計算某落子組合能讓雙方各加多少分(邏輯與 `actionCheck()` 一致但不寫入全域狀態) |
| `hasLegalContinuation()` | 檢查某落子後,對方下一手是否還有「同一棋子」合規下法(用於剪枝) |
| `getScoringSingleCandidates()` | **步驟二**:篩選出單子即可形成頂鑫結構且己方加分的候選 |
| `getRandomSecondGhost()` | 為已選定的第一子隨機挑一個合法第二子(可落不同對方棋子) |
| `getGhostsForOppPiece()` | 產生某對方棋子邊上所有合法棋子(供跨目標配對用) |
| `getCrossTargetScoringCandidates()` | **步驟三**:兩子落不同對方棋子(其一須為對方上一手棋子)且形成頂鑫結構的所有組合 |
| `buildAIExceptionMoves()` | 整合步驟二、三,產生例外候選清單 |
| `pickRandomSameTargetMove()` | **步驟四**:同一棋子合規下法中隨機挑一種(優先避開讓對方卡死的選項) |
| `aiTriggerArbitrationSuccess()` | **步驟一**:無合規下法時自動判定仲裁成功(呼叫 `originalActionCross` 而非被 hook 過的版本,避免誤判成人類打斷 AI) |

### 修改二:替換 `startAI()` 中的候選步伐取得邏輯

**原代碼**:
```js
let validMoves = getAllValidMoves(currentPlayer);
if (validMoves.length === 0) {
  aiThinking = false;
  return;
}
```
**替換為**:呼叫步驟一~四的完整流程(仲裁檢查 → 例外候選 → 同一棋子保底)。

### 修改三:修正 Minimax 評分邏輯

**原代碼有 bug**:`myScore = scores[currentPlayer]` 完全沒模擬落子,导致永遠選第一個候選。已改為用 `simulateScoreGain()` 計算真正的淨得分。

### 修改四:修正 MCTS 模擬(Simulation)步驟

**原代碼**:`let simVal = Math.random();`(純亂數佔位,等於瞎猜)
已改為用 `simulateScoreGain()` 算出淨得分差,經 Sigmoid 正規化為 0~1 勝率值餵給 UCB1。

---

**注意**:第三、四點修改雖然不是你列出的四步驟流程本身,但若不修正,即使步驟一~四挑出了正確的候選清單,後續的 Minimax/MCTS 也只會隨機/固定選擇,無法真正做到「全部往下走看哪一種最終分數最高」,所以一併修正了。若你只想要步驟一~四的候選邏輯而不想動 Minimax/MCTS 評分部分,告訴我,我可以把修改三、四還原。
附件
trigolden_game2428.html.7z
(31.32 KiB) 已下载 2 次
trigolden_game2424_MCTS改進版.html.7z
(30.26 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

修復和改進
一,語言顯示問題

在AI思考時的消息框中的文字,應該都要改成當前的語言,尤其是做MCTS搜尋時。比如MCTS應該翻譯成「蒙地卡羅樹搜尋」,time是「用時」,等等。

二,棋譜中的棋子序號仍然不對

由於一局總共只有54個棋子,所以序號不應超出54。而棋譜中仍然會出現超過54的棋子,再次導入則會發生錯誤。

原因可能是當把落下的棋子拾回,再放下,它同棋子id就會增長。應把本回合新增的兩個棋子id等於「場上棋子總數」和「場上棋子總數+1」。同時檢查還有沒有其它可能原因。

三,AI會主動送分

AI會有可能落下一子,形成的頂鑫結構是在給對方加分,這是不對的。只有在「給對方加分的同時自己能加更多的分」的情況下,才能給對方主動加分。

檢查代碼,分析原因並修復。

回答要求:

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

代码: 全选

修復和改進
一,語言顯示問題

在AI思考時的消息框中的文字,應該都要改成當前的語言,尤其是做MCTS搜尋時。比如MCTS應該翻譯成「蒙地卡羅樹搜尋」,time是「用時」,等等。

二,棋譜中的棋子序號仍然不對

由於一局總共只有54個棋子,所以序號不應超出54。而棋譜中仍然會出現超過54的棋子,再次導入則會發生錯誤。

原因可能是當把落下的棋子拾回,再放下,它同棋子id就會增長。應把本回合新增的兩個棋子id等於「場上棋子總數」和「場上棋子總數+1」。同時檢查還有沒有其它可能原因。

三,AI會主動送分

AI會有可能落下一子,形成的頂鑫結構是在給對方加分,這是不對的。只有在「給對方加分的同時自己能加更多的分」的情況下,才能給對方主動加分。

檢查代碼,分析原因並修復。

回答要求:

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

代码: 全选

這三個問題分別涉及到 UI 多語言適配、遊戲資料結構的一致性,以及 AI 評估函數的邏輯缺陷。

以下是針對這三個問題的修復方案與替換代碼。

### 一、語言顯示問題

需要將 AI 思考中(尤其是 Minimax 與 MCTS 搜尋時)輸出的進度訊息支援雙語切換。

**請找到 `startAI()` 函數中的 Minimax 區塊:**

```javascript
            if (performance.now() - startTime > 100) {
              showMessage(
                `Minimax Depth: ${maxDepth} | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s`,
                0,
                true
              );
              await new Promise(r => setTimeout(r, 0));
            }

```

**替換為:**

```javascript
            if (performance.now() - startTime > 100) {
              let timeStr = ((performance.now() - startTime) / 1000).toFixed(1);
              let msg = currentLang === 'zh'
                ? `極小化極大演算法 | 深度: ${maxDepth} | 用時: ${timeStr}秒`
                : `Minimax | Depth: ${maxDepth} | Time: ${timeStr}s`;
              showMessage(msg, 0, true);
              await new Promise(r => setTimeout(r, 0));
            }

```

**接著,在同一個函數內找到 MCTS 區塊:**

```javascript
            // UI 更新頻率控制
            if (searchCount % 10 === 0) {
              showMessage(
                `MCTS | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s | Searches: ${searchCount}`,
                0,
                true
              );
              await new Promise(r => setTimeout(r, 0)); // 讓出主執行緒避免卡頓
            }

```

**替換為:**

```javascript
            // UI 更新頻率控制
            if (searchCount % 10 === 0) {
              let timeStr = ((performance.now() - startTime) / 1000).toFixed(1);
              let msg = currentLang === 'zh'
                ? `蒙地卡羅樹搜尋 | 用時: ${timeStr}秒 | 搜尋次數: ${searchCount}`
                : `MCTS | Time: ${timeStr}s | Searches: ${searchCount}`;
              showMessage(msg, 0, true);
              await new Promise(r => setTimeout(r, 0)); // 讓出主執行緒避免卡頓
            }

```

**最後找到搜尋結束的提示:**

```javascript
        let totalTime = ((performance.now() - startTime) / 1000).toFixed(1);
        let valStr = useMinimax ? `Score: ${expectedValue}` : `WinRate: ${(expectedValue * 100).toFixed(1)}%`;
        showMessage(`Done | Time: ${totalTime}s | Searches: ${searchCount} | ${valStr}`, 3000, true);

```

**替換為:**

```javascript
        let totalTime = ((performance.now() - startTime) / 1000).toFixed(1);
        let valStr = useMinimax 
          ? (currentLang === 'zh' ? `分數: ${expectedValue}` : `Score: ${expectedValue}`) 
          : (currentLang === 'zh' ? `勝率: ${(expectedValue * 100).toFixed(1)}%` : `WinRate: ${(expectedValue * 100).toFixed(1)}%`);
        let msg = currentLang === 'zh'
          ? `完成 | 用時: ${totalTime}秒 | 搜尋次數: ${searchCount} | ${valStr}`
          : `Done | Time: ${totalTime}s | Searches: ${searchCount} | ${valStr}`;
        showMessage(msg, 3000, true);

```

---

### 二、棋子序號 (ID) 超出 54 的問題

由於頻繁拾起、放下、以及生成預覽殘影都會消耗 `nextPieceId` 計數器,導致最終產生的序號超出理論上限。我們應該在按下打勾(`actionCheck`)真正確認落子時,**嚴格根據當前場上棋子總數 (`pieces.length`) 強制重新編號**,並徹底廢除全域變數 `nextPieceId`。

**1. 刪除全域變數 `nextPieceId` 及其相關重置邏輯**
請在代碼中**刪除**以下包含 `nextPieceId` 的行(共 4 處):

* 全域宣告:`let nextPieceId = 1;`
* `resetGame()` 內:`nextPieceId = 1;`
* `undo()` 內:`nextPieceId = pieces.length + 1;`
* `redo()` 內:`nextPieceId = pieces.length + 1;`

**2. 替換 `commitGhost` 中的 ID 賦值**

```javascript
        gp.id = nextPieceId++; // 二、改用計數器確保唯一,避免拾回重下後與另一暫存棋子撞號

```

**替換為:**

```javascript
        let maxId = pieces.length;
        for (let tp of tempPieces) { if (tp.id > maxId) maxId = tp.id; }
        gp.id = maxId + 1;

```

**3. 替換所有其它 `id: nextPieceId++` 或 `g.id = nextPieceId++**`
請在以下五個函數中,將對應的 `nextPieceId++` 替換為 `pieces.length + 1` 或 `pieces.length + 2`:

* `placeAIFirstMove` 內:`id: pieces.length + 1,`
* `placeAISingleMove` 內:`g.id = pieces.length + 1;`
* `startAI` 內:將 `g1.id = nextPieceId++; g2.id = nextPieceId++;` 替換為 `g1.id = pieces.length + 1; g2.id = pieces.length + 2;`
* `selectTile` (首回合邏輯) 內:`id: pieces.length + 1,`
* `confirmImport` (首回合) 內:`id: pieces.length + 1,`
* `confirmImport` (後續回合) 內:
```javascript
let maxId = pieces.length;
for (let tp of tempPieces) { if (tp.id > maxId) maxId = tp.id; }
let p = {
  id: maxId + 1,

```



**4. 在 `actionCheck` 中強制重編號(最關鍵的一步)**
**請找到 `actionCheck(that = false)` 函數:**

```javascript
      function actionCheck(that = false) {
        if (that && that.classList.contains('disabled')) return;

        allRings = [];

```

**替換為:**

```javascript
      function actionCheck(that = false) {
        if (that && that.classList.contains('disabled')) return;

        // 嚴格依照場上已有棋子總數重新編號,確保送入 pieces 與棋譜的 ID 連續且不超過上限
        for (let i = 0; i < tempPieces.length; i++) {
          tempPieces[i].id = pieces.length + i + 1;
        }

        allRings = [];

```

---

### 三、AI 主動送分的問題

原本在步驟四 (保底落子) 時,AI 只是隨機挑選了任意不卡死對方的下法,並未把這些保底下法送進 Minimax/MCTS 去驗算,導致有極大機率亂下並送分。同時,在單子判斷中,過濾條件也不夠嚴謹。

**1. 修改步驟二的單子過濾條件**
**請找到 `getScoringSingleCandidates` 函數內的判斷:**

```javascript
          if (gain[player] > 0 && gain[player] >= gain[opp]) {
            result.push(g);
          }

```

**替換為(加上給對方加分時,自己必須加更多分的限制):**

```javascript
          if (gain[player] > 0 && (gain[opp] === 0 || gain[player] > gain[opp])) {
            result.push(g);
          }

```

**2. 將隨機保底落子改為過濾送分下法,並全數交給搜尋引擎**
**請找到整個 `pickRandomSameTargetMove` 函數,並將它替換為新的過濾函數:**

```javascript
      async function getFilteredSameTargetMoves(sameTargetMoves, player, opp) {
        let safeMoves = [];
        for (let idx = 0; idx < sameTargetMoves.length; idx++) {
          if (cancelAi) break;
          let m = sameTargetMoves[idx];

          let gc1 = Object.assign({}, m[0], { id: -70001 });
          let gc2 = Object.assign({}, m[1], { id: -70002 });
          let gain = simulateScoreGain([gc1, gc2]);

          // 不主動送分:若給對方加分的同時,己方加分沒有大於對方,則拋棄此下法
          if (gain[opp] > 0 && gain[player] <= gain[opp]) {
            continue;
          }

          if (hasLegalContinuation([m[0], m[1]], opp)) {
            safeMoves.push(m);
          }
          await aiMaybeYield(
            currentLang === 'zh'
              ? `AI 思考中:步驟四/從同一棋子下法中篩選保底選項 (${idx + 1}/${sameTargetMoves.length})`
              : `AI thinking: Step4/filtering fallback same-target moves (${idx + 1}/${sameTargetMoves.length})`
          );
        }
        
        // 如果安全且不送分的下法全部被濾光了,退而求其次只保證不會死局(避免AI無法下子卡死)
        if (safeMoves.length === 0) {
          for (let idx = 0; idx < sameTargetMoves.length; idx++) {
            if (cancelAi) break;
            let m = sameTargetMoves[idx];
            if (hasLegalContinuation([m[0], m[1]], opp)) safeMoves.push(m);
          }
        }
        return safeMoves.length > 0 ? safeMoves : sameTargetMoves;
      }

```

**3. 更新 `startAI()` 使用上述新函數**
**請找到 `startAI()` 內原本的步驟四區塊:**

```javascript
        let validMoves;
        if (exceptionMoves.length > 0) {
          // 有一種以上能形成頂鑫結構的下法,全部往下走,交給 Minimax / MCTS 搜尋比較最終分數最高者
          validMoves = exceptionMoves;
        } else {
          // ===== 步驟四:都無法形成頂鑫結構 → 隨機挑選一種「落在同一對方棋子」的下法 =====
          let oppPlayerNum = currentPlayer === 1 ? 2 : 1;
          validMoves = [await pickRandomSameTargetMove(sameTargetMoves, oppPlayerNum)];
        }

```

**替換為(這樣一來,大量保底下法也會送進 MCTS/Minimax 進行智力評估):**

```javascript
        let validMoves;
        let oppPlayerNum = currentPlayer === 1 ? 2 : 1;
        if (exceptionMoves.length > 0) {
          // 有一種以上能形成頂鑫結構的下法,全部往下走,交給 Minimax / MCTS 搜尋比較最終分數最高者
          validMoves = exceptionMoves;
        } else {
          // ===== 步驟四:都無法形成頂鑫結構 → 從「落在同一對方棋子」的下法中篩選 =====
          let candidateMoves = await getFilteredSameTargetMoves(sameTargetMoves, currentPlayer, oppPlayerNum);
          
          // 為了防止 Minimax 在選項過多時跑太久卡死,打亂並取前 20 種;MCTS 無此限制則全拿
          if (useMinimax && candidateMoves.length > 20) {
             candidateMoves = candidateMoves.sort(() => Math.random() - 0.5).slice(0, 20);
          }
          validMoves = candidateMoves;
        }

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

Re: 一個新的遊戲創意

帖子 ejsoon »

還是claude直接幫改好的香啊〜

代码: 全选

Now issue 2 — the piece-numbering fix at the single commit choke point (`actionCheck`):Now issue 3 — AI voluntarily feeding points to the opponent:Now the more serious gap: after picking a self-favorable first piece `g1`, the randomly-chosen second piece `g2` is never checked against the *combined* score outcome:Now step 3 — the cross-target candidate generator, which only checks whether *a* structure forms, not who benefits:Syntax is valid. Now let me save the fixed file and do a final review of all changes.All fixes are in. Let me copy the file to outputs and present it.## 修改摘要

### 一、AI 思考訊息未翻譯

三處硬編碼英文字串,改成依 `currentLang` 顯示中/英:

**Minimax 進度訊息**(原約 2580 行)
```js
// 原
`Minimax Depth: ${maxDepth} | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s`,
// 改
currentLang === 'zh'
  ? `極小化極大搜尋 | 深度: ${maxDepth} | 用時: ${((performance.now() - startTime) / 1000).toFixed(1)}s`
  : `Minimax | Depth: ${maxDepth} | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s`,
```

**MCTS 進度訊息**
```js
// 原
`MCTS | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s | Searches: ${searchCount}`,
// 改
currentLang === 'zh'
  ? `蒙地卡羅樹搜尋 | 用時: ${((performance.now() - startTime) / 1000).toFixed(1)}s | 搜尋次數: ${searchCount}`
  : `MCTS | Time: ${((performance.now() - startTime) / 1000).toFixed(1)}s | Searches: ${searchCount}`,
```

**完成訊息**(`Done | Time... | Score/WinRate`)也同樣改成雙語(分數/勝率/完成/用時/搜尋次數)。

### 二、棋子序號會超過 54

**根因**:`nextPieceId` 是一個「只會遞增、不會回收」的全域計數器。回合中把已落下的暫存棋子拾回(`pickupTempPiece`)再重下,會再次呼叫 `nextPieceId++` 拿到新的、更大的號碼,即使場上實際棋子數沒有增加。這個號碼被寫進棋譜(`parentId`),但重新匯入棋譜時是依「場上棋子總數」依序重建 id,兩者對不上,於是出現「超過 54」及「導入報錯」。

**修法**:在真正落子的唯一入口 `actionCheck()` 開頭,落子送進 `pieces` 之前,依 `tempPieces` 目前順序**重新賦予**序號:

```js
function actionCheck(that = false) {
  if (that && that.classList.contains('disabled')) return;

  tempPieces.forEach((tp, idx) => {
    tp.id = pieces.length + idx + 1;
  });

  allRings = [];
  let allNewP = pieces.concat(tempPieces);
  ...
```

這樣無論回合中拾回重下幾次,最終序號永遠等於「場上棋子總數 +1/+2」,落在 1~54 內,且與匯出棋譜時的順序一致。另外把 `commitGhost()` 裡的 `gp.id = nextPieceId++` 也一併改成同樣公式,減少中途出現的暫時性大號碼:

```js
gp.id = pieces.length + tempPieces.length + 1;
```

(其餘呼叫 `nextPieceId++` 的地方最終都會流經 `actionCheck` 被覆蓋,不影響正確性,故未逐一更動以降低風險。)

### 三、AI 會主動送分

檢查後找到**兩個**漏洞:

1. **步驟三(跨對方棋子組合)完全沒檢查誰得分**:`getCrossTargetScoringCandidates` 只用 `simulateScoringTriGolden` 確認「有沒有形成頂鑫結構」,不管這個結構是幫自己還是幫對方加分。
2. **步驟二第二子沒有整手覆核**:選到自己能加分的第一子後,第二子是 `getRandomSecondGhost` 隨機挑的,只驗證合法性,沒檢查這個隨機第二子會不會自己另外形成一個「送分給對方」的結構。

**修法**(三處,皆改用 `simulateScoreGain` 算出真實得分,並要求己方嚴格多於對方):

```js
// 步驟二單子篩選(原用 >=,改為嚴格 >)
if (gain[player] > gain[opp]) {
  result.push(g);
}
```

```js
// 步驟二配第二子後,用「兩子合併」的實際得分重新驗證
let gc1 = Object.assign({}, g1, { id: -70001 });
let gc2 = Object.assign({}, g2, { id: -70002 });
let pairGain = simulateScoreGain([gc1, gc2]);
if (pairGain[player] > pairGain[opp] && hasLegalContinuation([g1, g2], opp)) {
  exceptionMoves.push([g1, g2]);
  found = true;
}
```

```js
// 步驟三:改用實際得分而非「是否形成結構」
let pairGain = simulateScoreGain([gc1, gc2]);
if (pairGain[player] > pairGain[oppPlayerNum]) {
  results.push([g1, g2]);
}
```
(因原函式內沒有代表對手玩家編號的變數,額外在函式開頭加了 `let oppPlayerNum = player === 1 ? 2 : 1;`。)

步驟四(保底、兩子必須落在同一對方棋子)維持不動——那是「找不到任何有利下法時被迫落子」的情況,本來就可能不得已送分,不屬於本次要修的「主動送分」。

所有修改已跑過 `node --check` 語法驗證通過,檔案已附上供下載。
把所有的不同語言全部寫進i18n中。

代码: 全选

Let me view each site in full context to plan the i18n keys with placeholders.Now the remaining sites (game info dialog, end-game dialog, rotate, copy):Now let's replace each usage site to use `t()`.Now the arbitration nav, game info dialog, end-game dialogs, rotate label, and copy message:Now the end-game dialogs:Good — that's `getGhostsForOppPiece`, a separate function that never calls the `t()` i18n helper. No collision. Let's now fix the rotate label and copied message.Now let's verify no stray `currentLang === '...' ? ... : ...` ternaries remain for display text, and validate syntax.All converted. Now syntax check and confirm the `i18n['arb-ways']` pattern (already an i18n key) still works, plus verify braces balance:Good. Now let's also double check the `t()` function isn't shadowed where it's called (e.g., inside `checkEndGame`, `restoreWindow`, `arbNav`), and run a broader static check for any leftover raw Chinese/English literal strings shown to the user via `showMessage(...)` with `directString: true` that aren't yet i18n-ized.All `showMessage` calls now route through i18n (either directly with an i18n key, or via the `t()` helper). Let me do a final full-file check and re-export.All 26 previously-hardcoded strings are now backed by i18n keys. Let's save the final file.## 修改摘要

把所有原本用 `currentLang === 'zh' ? ... : ...` 寫死在程式碼裡的雙語字串,全部搬進 `i18n` 物件,並新增一個帶佔位符替換功能的輔助函式 `t(key, vars)`。

**新增輔助函式**(放在 `i18n` 物件定義之後):
```js
function t(key, vars) {
  let str = i18n[key][currentLang];
  if (vars) {
    for (const k in vars) {
      str = str.split('{' + k + '}').join(vars[k]);
    }
  }
  return str;
}
```

**`i18n` 物件新增的 26 個 key**(動態內容用 `{變數名}` 佔位符),涵蓋:
- AI 思考/打斷訊息:`ai-stopped`、`ai-thinking-ellipsis`、`ai-thinking-step1~4`
- Minimax/MCTS 搜尋進度與結果:`ai-minimax-progress`、`ai-mcts-progress`、`ai-score-label`、`ai-winrate-label`、`ai-done`
- 仲裁導覽:`arb-nav-info`
- 玩家稱謂:`player-first`、`player-second`
- 遊戲資訊 / 結算對話框:`game-info-title`、`game-info-msg`、`game1-end-title`、`game1-end-msg`、`game1-end-next-btn`、`match-end-title`、`match-end-msg`、`match-result-p1/p2/draw`、`match-end-again-btn`
- 其他:`rotate-label`(旋轉角度顯示)、`copied`(複製棋譜提示)

**呼叫端範例**(原本三元運算式改為呼叫 `t()`):
```js
// 原
showMessage(currentLang === 'zh' ? '已停止 AI 思考' : 'AI Stopped', 2000, true);
// 改
showMessage(t('ai-stopped'), 2000, true);
```
```js
// 原(帶動態數字)
currentLang === 'zh'
  ? `第 ${arbCurrentIndex + 1} 種放置方式,一共有 ${arbValidMoves.length} 種放置方式`
  : `Move ${arbCurrentIndex + 1} of ${arbValidMoves.length} valid moves`
// 改
t('arb-nav-info', { cur: arbCurrentIndex + 1, total: arbValidMoves.length })
```
```js
// 結算對話框的勝負結果字串也拆成獨立 key,用巢狀組合
let resultKey = t1 > t2 ? 'match-result-p1' : t2 > t1 ? 'match-result-p2' : 'match-result-draw';
let msg = t('match-end-msg', { t1: t1, t2: t2, result: t(resultKey) });
```

檢查後確認:
- 全檔已無任何殘留的 `currentLang === 'zh'` / `currentLang === 'en'` 三元字串(`grep` 結果為 0)。
- 所有 `showMessage(...)` 呼叫要麼直接傳既有 i18n key,要麼透過 `t()`,沒有裸露的中/英文字串。
- `for (let t of availTypes)` 這類迴圈變數 `t` 都在各自函式的區塊作用域內,不會與新的 `t()` 輔助函式衝突(已逐一確認所在函式未同時呼叫 `t(key)`)。
- `node --check` 語法驗證通過。

檔案已更新並附上供下載。
附件
trigolden_game243.html.7z
(32.54 KiB) 已下载 2 次
上次由 ejsoon 在 2026年 8月 10日 08:29,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

從第二局退到第一局,不見棋子。

第二局後手方是AI,他應能自動下第一手棋
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

檢查步驟二的代碼邏輯,應該僅包含「如果一手棋使雙方都得分,必須是己方得分更多」,如果第一手棋已經形能頂鑫結構並得分, 則第二手棋也應像第一手棋那樣,首先尋找使己方得分的頂鑫結構,不能出現「第一手棋得分之後第二手棋失分」的情形。

claude

代码: 全选

Now update the caller in `buildAIExceptionMoves` (step 2) to use the new active-search function instead of the random-then-retry loop:No leftover references. Let me quickly sanity-check the JS syntax with node.JS 語法檢查通過。修改摘要:

**問題**:原本步驟二在第一子(`g1`)已確定「單獨加分」後,第二子是用 `getRandomSecondGhost` **隨機**挑一個合法落點,下完才用 `simulateScoreGain` 驗證兩子合計是否仍「己方多於對方」,不合格就重抽(最多 15 次)。這代表第二子的搜尋本身完全沒有「找加分結構」的意圖,只是撞運氣,撞不到就整個放棄這個原本會加分的 `g1`,也可能在 15 次內恰好撞到一個「打平或倒扣」都不會發生但效率很差、且邏輯上並非「主動尋找」的解法。

**修改**:
1. 新增 `getAllSecondGhostCandidates`:把原本隨機挑一個的邏輯改為列舉**所有**合法第二子候選(沿用原本的落點窮舉邏輯,只是不在最後做隨機挑選)。
2. 新增 `getScoringSecondGhost`:在所有候選中,先篩選出「與 `g1` 合計後己方嚴格多於對方」的候選(這就是唯一合格標準,對應你說的「如果一手棋使雙方都得分,必須是己方得分更多」),再從中優先挑「第二子自己也單獨為己方加分」的(真正做到「像第一手棋那樣先找加分結構」),沒有的話才退而選「不加分但也不倒扣」的候選;若完全沒有合格候選則回傳 `null`。
3. `buildAIExceptionMoves` 步驟二的迴圈改為直接呼叫 `getScoringSecondGhost`,拿到即驗證後續合規(`hasLegalContinuation`),拿不到就放棄這個 `g1`——不再有「重抽 15 次撞運氣」的路徑,也不會出現「第一手得分、第二手倒送分給對方」的情形。修改後的檔案已輸出,可直接查看差異或在遊戲中測試 AI 行為。
https://ejsoon.vip/wp-content/uploads/2 ... e2431.html
附件
trigolden_game2431.html.7z
(32.79 KiB) 已下载 1 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

如果從第二局開始,回退到第一局結束,棋盤上一個棋子也沒有,也不自動彈出「第一局結束」的窗口提示。

當從第一局結束,點開始第二局時,如果綠方(第一局的後手方)是AI,它不會自動下第一手棋。應該跟第一局先手方那樣,隨機下第一手棋。

claude sonnet 5 (low)

代码: 全选

Now let's implement the fix: switch `pieces` to a full snapshot stored in each history entry, instead of incremental pop/push.Now update `undo()` to restore `pieces` directly from the snapshot instead of incrementally popping:Now update `redo()` similarly:Good. Now let's check the undo() context to verify it's consistent too:Good, that's consistent — the first `saveState()` call (in `startGame()`) will push index 0 with an empty pieces snapshot. Now let's fix bug #2: AI not auto-playing first move for game 2's starting player.Now let's syntax-check the file again:Syntax checks pass. Let's also confirm there's no other place still relying on the removed pop/push semantics (e.g. any other reference to `piecesToAdd` array being needed for extra logic) and double check `undo`/`redo` full context once more.All consistent.## 修改摘要

**問題根源(第一個 bug)**:`gameHistory` 裡除了 `pieces` 之外的所有欄位(分數、棋子數、回合數…)都是「每步存一份完整快照」,唯獨 `pieces` 棋盤本身是用「逐步 push/pop 疊加 `newPieces` 差異」來反推。這在正常對局中沒問題,但 `nextGame()` 開始第二局時會呼叫 `resetGame()` 把 `pieces` 直接清空——這個清空動作沒有被記錄成任何「差異」,於是 undo 往回跳到第一局結束那個節點時,逐步 pop 的機制完全對不上,導致棋盤變空、且因為狀態同步邏輯是靠 `currentDialogMode` 驅動彈窗,中間斷層也連帶讓「第一局結束」的彈窗判斷失準。

**修法**:讓 `pieces` 也比照其他欄位改成「每步存完整快照」(`piecesSnapshot`),`undo()`/`redo()` 直接用目標節點的快照整份還原,不再依賴逐步疊加差異。這樣無論是否跨局、是否經過仲裁或整盤清空,都能正確還原,`checkEndGame()` 也就能在對的時機正確觸發彈窗。

**第二個 bug**:`nextGame()` 在切換到第二局、交換先後手後只呼叫了 `resetGame()` + `saveState()`,沒有像每手棋下完後的 `actionCheck` 那樣呼叫 `checkAndTriggerAI()`,所以即使新的先手方是 AI,也不會自動觸發它下第一手棋。已在 `resetGame(false)` 之後補上 `setTimeout(checkAndTriggerAI, 1500)`,與其他觸發 AI 的地方保持一致的延遲時間。

兩處修改都已通過 Node.js 語法檢查。
https://ejsoon.vip/wp-content/uploads/2 ... e2432.html
附件
trigolden_game2432.html.7z
(33.1 KiB) 尚未被下载
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6490
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

如果選擇規則是鑫棋計分,則記譜時在先手第一手棋的前面加7,包括第二局的第一手棋。比如7529指的是鑫棋計分、第三個棋子正面、旋轉29度。

claude sonnet 5 (low)

代码: 全选

So `SCORING_MODE = 0` is 頂棋計分 (dinger scoring), `SCORING_MODE = 1` is 鑫棋計分 (xin scoring). Now let's look at `getGameNotation`.Good, `setScoringMode` is safe to call during import. Now let's implement both changes.Now update the import parser to strip and interpret the `7` prefix on each game's first move token:Good — non-game-start branch further down still correctly uses `token[1]`, `token.substring(2,...)`, `token[token.length-1]` since it's untouched by the prefix logic. Let's syntax-check the full file.语法檢查通過。## 修改摘要

**`getGameNotation`(匯出棋譜)**:每一局各自呼叫一次此函式,`i === 0` 就是該局的先手第一手棋。改成當 `SCORING_MODE === 1`(鑫棋計分)時,在第一手棋的記譜前面加上 `7`;`SCORING_MODE === 0`(頂棋計分)則不加任何前綴,維持原樣。由於每一局都是各自呼叫,這個判斷自然涵蓋「第二局的第一手棋」。

例如 `7529`:`7` = 鑫棋計分標記,`5` = 第三種棋子正面(C,未翻面),`29` = 旋轉 29 度。

**`confirmImport`(匯入棋譜)**:解析時只要偵測到是「每局的第一手棋」(`isGameStart`),就先檢查開頭字元是否為 `7`:
- 若是,呼叫 `setScoringMode(1)` 還原為鑫棋計分,並剝除這個 `7` 再繼續解析棋子類型與角度。
- 若不是,呼叫 `setScoringMode(0)` 還原為頂棋計分。

其餘非第一手棋的 token 格式不受影響,仍照原本方式解析。
https://ejsoon.vip/wp-content/uploads/2 ... e2433.html
附件
trigolden_game2433.html.7z
(33.41 KiB) 尚未被下载
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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