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

Re: 「我為歌狂」遊戲製作

帖子 ejsoon »

代码: 全选

<svg id="btn-maximize" style="cursor:pointer; width:32px; height:32px; fill:white;" viewBox="0 0 24 24"><path d="M4 4h6v2H6v4H4V4zm10 0h6v6h-2V6h-4V4zm6 16h-6v-2h4v-4h2v6zM4 20v-6h2v4h4v2H4z"></path></svg>
SVG preview
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5944
注册时间: 2022年 11月 18日 17:36
联系:

Re: 「我為歌狂」遊戲製作

帖子 ejsoon »

改進:
一,回合前進的問題
回合前進在行動階段是正確的,但如果回退到開局階段,之後再點擊回合前進,則會發生「遊戲板不能正確放置、立方體無故出現」等問題,查明原因。

二,AI防呆之後的問題
在以下這段代碼執行之後:
```
if (!aiPlannedActions || aiPlannedActions.length === 0) {
console.warn('MCTS 未能計算出動作,使用防呆預設');
return;
}
```
AI再也不能重啟。整個視圖也不再能旋轉和移動。

一個原因是,當AI嘗試要行動時,當前行動方已經行走了一步或多步,而AI只能是從回合開始行動。如果是這種情況,則AI應該嘗試先執行「行走撤銷」,回到回合開始的狀態。

如果還有其它可能原因,使其仍然不能行動,則應解除當前視圖旋轉控制的封鎖,允許AI重啟之後再次嘗試進行思考和行動。

三,更改AI強度按鈕的功能設置

簡單、困難、專家等級,除了時間之外,還應加上必殺深度SURE_KILL_DEPTH這個變量的設置,簡單為3,困難為5,專家為7。

button#ai-strength-trigger應加上初始為「困難等級」的時間和必殺深度data屬性。頁面刷新時,程式應讀取其data屬性並更改變量值。

需要明確給出所有要修改的地方和要替換的代碼,不用給出全部代碼。

代码: 全选

改進:
一,回合前進的問題
回合前進在行動階段是正確的,但如果回退到開局階段,之後再點擊回合前進,則會發生「遊戲板不能正確放置、立方體無故出現」等問題,查明原因。

二,AI防呆之後的問題
在以下這段代碼執行之後:
```
        if (!aiPlannedActions || aiPlannedActions.length === 0) {
          console.warn('MCTS 未能計算出動作,使用防呆預設');
          return;
        }
```
AI再也不能重啟。整個視圖也不再能旋轉和移動。

一個原因是,當AI嘗試要行動時,當前行動方已經行走了一步或多步,而AI只能是從回合開始行動。如果是這種情況,則AI應該嘗試先執行「行走撤銷」,回到回合開始的狀態。

如果還有其它可能原因,使其仍然不能行動,則應解除當前視圖旋轉控制的封鎖,允許AI重啟之後再次嘗試進行思考和行動。

三,更改AI強度按鈕的功能設置

簡單、困難、專家等級,除了時間之外,還應加上必殺深度SURE_KILL_DEPTH這個變量的設置,簡單為3,困難為5,專家為7。

button#ai-strength-trigger應加上初始為「困難等級」的時間和必殺深度data屬性。頁面刷新時,程式應讀取其data屬性並更改變量值。

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

Re: 「我為歌狂」遊戲製作

帖子 ejsoon »

gemini 3.1 pro還沒有睡:

代码: 全选

這三個問題分別涉及到狀態同步機制、防呆還原邏輯以及參數綁定。以下為需要修改及替換的代碼片段:

### 一、 修復「回合前進」在開局階段引發的遊戲板與立方體顯示異常

問題在於從「對戰階段」跳躍回退/前進到「開局階段」時,視圖沒有對遊戲板和圓柱體進行同步與透明度還原。

請找到 `restoreSnapshot(snap)` 函數,將其替換為以下代碼:

```javascript
      function restoreSnapshot(snap) {
        currentPlayer = snap.currentPlayer;
        walkCount = snap.walkCount;
        lastDirection = snap.lastDirection;
        turnStopPositions = snap.turnStopPositions;
        gameplayActive = snap.gameplayActive;

        // 恢復棋譜狀態
        globalMoveHistory = [...snap.globalMoveHistory];
        currentTurnCoords = [...snap.currentTurnCoords];

        clearMarkers();

        // 【修改1】提取共同的修復與同步邏輯,確保所有階段都能正確重置遊戲板與圓柱
        const fixOpacity = m => {
          if (Array.isArray(m.material))
            m.material.forEach(mat => {
              mat.opacity = 1;
              mat.transparent = false;
            });
          else {
            m.material.opacity = 1;
            m.material.transparent = false;
          }
        };

        // ① 總是同步遊戲板 (boardMeshes)
        boardMeshes.forEach(mesh => {
          if (!snap.boardMeshesRefs.includes(mesh)) scene.remove(mesh);
        });
        snap.boardMeshesRefs.forEach(mesh => {
          if (!boardMeshes.includes(mesh)) scene.add(mesh);
          fixOpacity(mesh); // 修復透明度與動畫殘留
        });
        boardMeshes = [...snap.boardMeshesRefs];

        // ② 總是同步圓柱 (cylinderMeshes)
        cylinderMeshes.forEach(mesh => {
          if (!snap.cylinderMeshesRefs.includes(mesh)) scene.remove(mesh);
        });
        snap.cylinderMeshesRefs.forEach(mesh => {
          if (!cylinderMeshes.includes(mesh)) scene.add(mesh);
          fixOpacity(mesh);
        });
        cylinderMeshes = [...snap.cylinderMeshesRefs];

        // 開局階段:利用 3D 參照比對,完美支援開局的進退
        if (!gameplayActive) {
          currentPhaseFn = snap.currentPhaseFn;
          blueCornerType = snap.blueCornerType;
          orangeCornerType = snap.orangeCornerType;
          placedEdges = { ...snap.placedEdges };
          placedCorners = { ...snap.placedCorners };
          edgeTurn = snap.edgeTurn;
          cylindersData = JSON.parse(JSON.stringify(snap.cylindersData));

          // ③ 同步開局可能放置的立方體 (cubes)
          cubes.forEach(mesh => {
            if (!snap.openingCubesRefs.includes(mesh)) scene.remove(mesh);
          });
          snap.openingCubesRefs.forEach(mesh => {
            if (!cubes.includes(mesh)) scene.add(mesh);
            fixOpacity(mesh);
          });
          cubes = [...snap.openingCubesRefs];

          // 重新觸發當前步驟的點擊提示圈
          if (currentPhaseFn) currentPhaseFn();
          return; // 開局階段處理完畢,直接返回
        }

        // 以下為原本【對戰階段】的還原邏輯(保持不變)...
        cubes.forEach(c => scene.remove(c));
        cubes = [];
        microphones.forEach(m => scene.remove(m));
        microphones = [];

        snap.cubes.forEach(sCube => {
          placeCube(sCube.col, sCube.row, sCube.color);
          const newCube = cubes[cubes.length - 1];
          newCube.userData.score = sCube.score;
          newCube.userData.currentNumber = sCube.currentNumber;
          newCube.userData.moving = false;
          newCube.userData.animating = false;
          newCube.quaternion.copy(sCube.quat);

          const onOwnMic = snap.microphones.some(
            m => m.color === sCube.color && m.col === sCube.col && m.row === sCube.row
          );
          const targetY = onOwnMic ? itemY + 5 : itemY;
          newCube.position.y = targetY;
          newCube.userData.targetY = targetY;
        });

        snap.microphones.forEach(sMic => {
          createMicrophoneMesh(sMic.color, sMic.col, sMic.row);
        });

        if (snap.subPhase === 'walk') {
          showWalkOptions();
        } else {
          showMicrophonePlacementOptions();
        }
      }

```

---

### 二、 修復 AI 防呆後的死鎖問題並嘗試自動撤銷

當 AI 因為中途啟動或遇到邊界條件無法計算出路徑時,需先解除封鎖並嘗試自動回退一步讓其重新啟動計算。

請找到 `doMCTSAI()` 函數,替換其中的這段防呆邏輯:

**將原本的:**

```javascript
        if (!aiPlannedActions || aiPlannedActions.length === 0) {
          console.warn('MCTS 未能計算出動作,使用防呆預設');
          return;
        }

```

**替換為:**

```javascript
        if (!aiPlannedActions || aiPlannedActions.length === 0) {
          console.warn('MCTS 未能計算出動作,嘗試復原狀態');
          resetAIState(); // 【修改2】解除 AI 思考狀態及畫面操作鎖定
          
          // 如果不在回合起點,嘗試執行一次行走撤銷
          if (walkCount > 1 || (markers.length > 0 && markers[0].position.y === 6)) {
            setTimeout(() => {
              walkUndo();
            }, 300);
          }
          return;
        }

```

---

### 三、 增強 AI 強度按鈕的屬性連動功能

需要將時間強度與 `SURE_KILL_DEPTH` 綁定,在 HTML 增加資料屬性,並在讀取與點擊事件中更新。

#### 1. 修改 HTML 結構中的 `custom-select-container`

請替換 `<div class="custom-select-container">` 與其內部的按鈕和選項:

```html
      <div class="custom-select-container">
        <button id="ai-strength-trigger" class="hist-btn custom-select-trigger" title="AI 強度" data-value="3600" data-depth="5">
          <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
            <path
              d="M12,2 A10,10 0 1,0 22,12 A10,10 0 0,0 12,2 Z M15,14 H9 V10 H15 Z M12,6 A2,2 0 1,1 10,8 A2,2 0 0,1 12,6 Z"
            />
          </svg>
        </button>
        <div id="ai-strength-options" class="custom-select-options">
          <div class="csinger-option strength-option" data-value="1200" data-depth="3">
            <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
              <path
                d="M12,2 A10,10 0 1,0 22,12 A10,10 0 0,0 12,2 Z M14,14 H10 V12 H14 Z M12,8 A1.5,1.5 0 1,1 10.5,9.5 A1.5,1.5 0 0,1 12,8 Z"
              />
            </svg>
            簡單 (1.2s)
          </div>
          <div class="csinger-option strength-option" data-value="3600" data-depth="5">
            <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
              <path
                d="M12,2 A10,10 0 1,0 22,12 A10,10 0 0,0 12,2 Z M15,14 H9 V10 H15 Z M12,6 A2,2 0 1,1 10,8 A2,2 0 0,1 12,6 Z"
              />
            </svg>
            困難 (3.6s)
          </div>
          <div class="csinger-option strength-option" data-value="7200" data-depth="7">
            <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
              <path
                d="M12,2 A10,10 0 1,0 22,12 A10,10 0 0,0 12,2 Z M16,15 H8 V9 H16 Z M12,4 A2.5,2.5 0 1,1 9.5,6.5 A2.5,2.5 0 0,1 12,4 Z"
              />
            </svg>
            專家 (7.2s)
          </div>
        </div>
      </div>

```

#### 2. 初始化 `SURE_KILL_DEPTH` 變數

在 JavaScript 區塊中尋找 `let SURE_KILL_DEPTH = 3;`,將其預設值修改為 5 以配合預設的困難模式:

```javascript
      // =========================================================
      // MCTS 核心演算法 (Async 入口)
      // =========================================================
      let SURE_KILL_DEPTH = 5; // 【修改3】必勝計算深度預設改為 5

```

#### 3. 修改按鈕事件監聽器

在主迴圈事件綁定的末尾處(`document.querySelectorAll('.strength-option')` 的事件),加入深度值的讀取與賦值:

```javascript
      // 初始化頁面時讀取預設設定
      if (aiStrengthTrigger) {
         aiStrength = parseInt(aiStrengthTrigger.getAttribute('data-value') || '3600');
         SURE_KILL_DEPTH = parseInt(aiStrengthTrigger.getAttribute('data-depth') || '5');
      }

      document.querySelectorAll('.strength-option').forEach(opt => {
        opt.addEventListener('click', () => {
          aiStrength = parseInt(opt.getAttribute('data-value'));
          SURE_KILL_DEPTH = parseInt(opt.getAttribute('data-depth')); // 【修改4】同步必殺深度
          
          aiStrengthTrigger.innerHTML = opt.querySelector('svg').outerHTML;
          // 同步更新 Trigger 的屬性
          aiStrengthTrigger.setAttribute('data-value', aiStrength);
          aiStrengthTrigger.setAttribute('data-depth', SURE_KILL_DEPTH);
          
          aiStrengthOptions.classList.remove('open');
        });
      });

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

Re: 「我為歌狂」遊戲製作

帖子 ejsoon »

改進:
一,回合後退和前進的問題:
當行動階段,雙方都已經進行了幾個回合之後,點擊「回合後退」和「回合前進」,會出現幾個錯誤。

一是當回到布局階段的最後一步時,藍方的立方體已放置但位置不對。

二是當回到布局階段的某一回合時,只要點擊「回合前進」,則會立即回到行動階段的開始,這是不對的,應能自由在布局階段來回進退。

三是當從布局階段回到行動階段的某個回合時,發現「行動標示」放置的地方是錯誤的,比如有圓柱體阻擋不應能穿過去。

二,手機端適配問題

手機端的div#history-controls確實是把寬度調整至95%,但是裡面的按鈕並不能完全顯示。

在保持每個按鈕的高寬比為1:1的前提下,縮小按鈕,以使所有的按鈕都能全部顯示。為中間的「<div style="width: 2px; background: #7f8c8d; margin: 0 5px"></div>」增加類名,並在手機端的時候隱藏。

需要明確給出所有要修改的地方和要替換的代碼,不用給出全部代碼。

代码: 全选

改進:
一,回合後退和前進的問題:
當行動階段,雙方都已經進行了幾個回合之後,點擊「回合後退」和「回合前進」,會出現幾個錯誤。

一是當回到布局階段的最後一步時,藍方的立方體已放置但位置不對。

二是當回到布局階段的某一回合時,只要點擊「回合前進」,則會立即回到行動階段的開始,這是不對的,應能自由在布局階段來回進退。

三是當從布局階段回到行動階段的某個回合時,發現「行動標示」放置的地方是錯誤的,比如有圓柱體阻擋不應能穿過去。

二,手機端適配問題

手機端的div#history-controls確實是把寬度調整至95%,但是裡面的按鈕並不能完全顯示。

在保持每個按鈕的高寬比為1:1的前提下,縮小按鈕,以使所有的按鈕都能全部顯示。為中間的「<div style="width: 2px; background: #7f8c8d; margin: 0 5px"></div>」增加類名,並在手機端的時候隱藏。

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

Re: 「我為歌狂」遊戲製作

帖子 ejsoon »

gemini 3.1 pro將睡到20:52

代码: 全选

沒問題!我已經幫你找出這兩個主要問題的癥結點了。

這幾個 Bug 的核心原因在於:

1. **撤銷/重做邏輯缺陷:** `turnRedo` 在搜尋目標時強制跳過了「佈局階段(`gameplayActive = false`)」的所有步驟;而 `restoreSnapshot` 在還原「行動階段」的快照時,遺漏了更新 `cylindersData`(導致穿透圓柱體),也沒有在佈局階段正確重設立方體的位置。
2. **手機版 UI 擁擠:** 原本按鈕的內邊距(Padding)在小螢幕上佔據太多空間,且中間的分隔線壓縮了按鈕,導致無法維持 1:1 的完整顯示。

以下是針對這兩個問題,具體需要替換的代碼片段(請直接尋找對應的區塊並覆蓋):

### 一、 手機端 UI 與分隔線適配修改

**1. CSS 樣式替換:**
請找到 `<style>` 標籤中原本的 `.ai-toggle svg` 與 `@media (max-width: 768px)` 區塊,將其替換為以下代碼,加入新的 `.divider` 類別並優化手機端按鈕:

```css
      .ai-toggle svg {
        pointer-events: none;
      }
      /* 新增分隔線類別 */
      .divider {
        width: 2px;
        background: #7f8c8d;
        margin: 0 5px;
      }
      @media (max-width: 768px) {
        #history-controls {
          width: 100%;
          justify-content: center;
          gap: 4px; /* 縮小間距 */
          bottom: 10px;
          flex-wrap: nowrap; /* 強制不換行 */
        }
        .hist-btn {
          padding: 0;
          /* 強制 1:1 比例並縮小 */
          width: 36px;
          height: 36px;
        }
        .hist-btn svg {
          width: 20px;
          height: 20px;
        }
        .divider {
          display: none; /* 手機端隱藏分隔線以節省空間 */
        }
        #message {
          font-size: 16px;
          top: 10px;
        }
        #btn-start {
          padding: 15px 30px;
          font-size: 20px;
        }
      }

```

**2. HTML 按鈕分隔線替換:**
請在 HTML 結構中找到兩處 `#history-controls` 裡面的分隔線:
`<div style="width: 2px; background: #7f8c8d; margin: 0 5px"></div>`
將它們全部替換為:

```html
      <div class="divider"></div>

```

---

### 二、 歷史記錄進退場邏輯修改

**1. `restoreSnapshot` 的還原修復:**
請找到 `function restoreSnapshot(snap)` 中「`// ② 總是同步圓柱`」與「`// 以下為原本【對戰階段】的還原邏輯`」之間的程式碼,替換成以下修復邏輯(確保 `cylindersData` 無論哪一階段都還原,並修正佈局期的立方體位置):

```javascript
        // ② 總是同步圓柱 (cylinderMeshes)
        cylinderMeshes.forEach(mesh => {
          if (!snap.cylinderMeshesRefs.includes(mesh)) scene.remove(mesh);
        });
        snap.cylinderMeshesRefs.forEach(mesh => {
          if (!cylinderMeshes.includes(mesh)) scene.add(mesh);
          fixOpacity(mesh);
        });
        cylinderMeshes = [...snap.cylinderMeshesRefs];

        // 【修復:不論是開局還是對戰階段,都必須還原這些變數,確保回到行動階段時有正確的障礙物資料】
        currentPhaseFn = snap.currentPhaseFn;
        blueCornerType = snap.blueCornerType;
        orangeCornerType = snap.orangeCornerType;
        placedEdges = { ...snap.placedEdges };
        placedCorners = { ...snap.placedCorners };
        edgeTurn = snap.edgeTurn;
        cylindersData = JSON.parse(JSON.stringify(snap.cylindersData));

        // 開局階段:利用 3D 參照比對,完美支援開局的進退
        if (!gameplayActive) {
          // ③ 同步開局可能放置的立方體 (cubes)
          cubes.forEach(mesh => {
            if (!snap.openingCubesRefs.includes(mesh)) scene.remove(mesh);
          });
          snap.openingCubesRefs.forEach(mesh => {
            if (!cubes.includes(mesh)) scene.add(mesh);
            fixOpacity(mesh);
          });
          cubes = [...snap.openingCubesRefs];

          // 【修復:修正回到開局階段最後一步時立方體位置錯亂的問題】
          cubes.forEach(cube => {
            let sCube = snap.cubes.find(sc => sc.color === cube.userData.color);
            if (sCube) {
              cube.userData.col = sCube.col;
              cube.userData.row = sCube.row;
              const pos = getCellWorldPos(sCube.col, sCube.row);
              cube.position.set(pos.x, itemY, pos.z);
              cube.quaternion.copy(sCube.quat);
              cube.userData.moving = false;
              cube.userData.animating = false;
            }
          });

          // 重新觸發當前步驟的點擊提示圈
          if (currentPhaseFn) currentPhaseFn();
          return; // 開局階段處理完畢,直接返回
        }

        // 以下為原本【對戰階段】的還原邏輯(保持不變)...

```

**2. `turnUndo` 與 `turnRedo` 階段分離處理:**
請找到 `function turnUndo()` 與 `function turnRedo()` 兩個函式,替換為以下邏輯(如果在佈局階段點擊,將自動降級為執行「單步進退」,允許自由來回):

```javascript
      function turnUndo() {
        // 【修復:在佈局階段,回合後退等同於單步撤銷】
        if (!gameplayActive) {
          if (historyUndoStack.length === 0) return;
          historyRedoStack.push(saveSnapshot());
          restoreSnapshot(historyUndoStack.pop());
          updateUndoRedoButtons();
          return;
        }

        let targetIdx = -1;
        for (let i = historyUndoStack.length - 1; i >= 0; i--) {
          let snap = historyUndoStack[i];
          if (
            !snap.gameplayActive ||
            (snap.currentPlayer !== currentPlayer && snap.walkCount === 1 && snap.subPhase === 'walk')
          ) {
            targetIdx = i;
            break;
          }
        }
        if (targetIdx === -1) return;

        historyRedoStack.push(saveSnapshot());
        while (historyUndoStack.length > targetIdx + 1) {
          historyRedoStack.push(historyUndoStack.pop());
        }
        restoreSnapshot(historyUndoStack.pop());
        updateUndoRedoButtons();
      }

      function turnRedo() {
        // 【修復:在佈局階段,回合前進等同於單步重做,自由來回進退】
        if (!gameplayActive) {
          if (historyRedoStack.length === 0) return;
          historyUndoStack.push(saveSnapshot());
          restoreSnapshot(historyRedoStack.pop());
          updateUndoRedoButtons();
          return;
        }

        let targetIdx = -1;
        // 尋找下一個回合的起點
        for (let i = historyRedoStack.length - 1; i >= 0; i--) {
          let snap = historyRedoStack[i];
          if (
            snap.gameplayActive &&
            (!gameplayActive || snap.currentPlayer !== currentPlayer) &&
            snap.walkCount === 1 &&
            snap.subPhase === 'walk'
          ) {
            targetIdx = i;
            break;
          }
        }

        if (targetIdx === -1) {
          return;
        }

        historyUndoStack.push(saveSnapshot());
        while (historyRedoStack.length > targetIdx + 1) {
          historyUndoStack.push(historyRedoStack.pop());
        }
        restoreSnapshot(historyRedoStack.pop());
        updateUndoRedoButtons();
      }

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

Re: 「我為歌狂」遊戲製作

帖子 ejsoon »

改進:
一,調整AI必殺優先選擇策略

當一方有必殺時,應優先選擇己方與對方結局分數相差最大的行動,如果一樣,則優先選擇結局回合數最少的行動。

反之,當一方已經必敗時,他應優先選擇與對方結局分數相差最小的行動,如果一樣,則優先選擇結局回合數最多的行動。

二,新增「設置按鈕」
在「導出按鈕」右方新增「設置按鈕」,當點擊時,彈出一個「設置窗口」。

設置窗口第一項為語言,可在「中文︱Englosh」之間切換,當切換成英文時,所有message提示文字、按鈕(如「開始遊戲」)、ai-status窗口的文字,都要改成英文。

第二項為AI設置,可設置:
思考時間:單位是秒,精確至0.1,默認為當前AI強度所對應的秒數。
必殺深度:大於0的整數,默認為當前AI強度所對應的必殺深度。
在搜尋時調用必殺算法:勾選項,默認不選。

下方為一個「搜尋按鈕」,當點擊時,程式將對當前行動方進行搜尋,下方將有一個.searchingmessage即時顯示當前搜尋結果。

當搜尋到限定時間,則把當前所搜尋到的結果(當前方的本回合行動),通過列表展示在下方,附上其對應的勝率(包括是否必殺或必敗),最優選排第一,每頁最多5個,超出需翻頁。列表提供上下翻頁按鈕。

當點擊列表的其中一項,則本窗口關閉,同時場上當前方將按此行動。

三,開始頁和結束頁的改進
在一開始的頁面,在「開始遊戲」下方增加一個「語言切換」按鈕,可將遊戲語言進行中英文的切換。

在每一局結束時的文字提示頁,以及最後終局文字提示頁,都增加一個「棋譜文件下載按鈕」,點擊將下載當前棋譜文件。

需要明確給出所要修改的地方和所要替換的代碼,不用給出全部代碼。

代码: 全选

改進:
一,調整AI必殺優先選擇策略

當一方有必殺時,應優先選擇己方與對方結局分數相差最大的行動,如果一樣,則優先選擇結局回合數最少的行動。

反之,當一方已經必敗時,他應優先選擇與對方結局分數相差最小的行動,如果一樣,則優先選擇結局回合數最多的行動。

二,新增「設置按鈕」
在「導出按鈕」右方新增「設置按鈕」,當點擊時,彈出一個「設置窗口」。

設置窗口第一項為語言,可在「中文︱Englosh」之間切換,當切換成英文時,所有message提示文字、按鈕(如「開始遊戲」)、ai-status窗口的文字,都要改成英文。

第二項為AI設置,可設置:
思考時間:單位是秒,精確至0.1,默認為當前AI強度所對應的秒數。
必殺深度:大於0的整數,默認為當前AI強度所對應的必殺深度。
在搜尋時調用必殺算法:勾選項,默認不選。

下方為一個「搜尋按鈕」,當點擊時,程式將對當前行動方進行搜尋,下方將有一個.searchingmessage即時顯示當前搜尋結果。

當搜尋到限定時間,則把當前所搜尋到的結果(當前方的本回合行動),通過列表展示在下方,附上其對應的勝率(包括是否必殺或必敗),最優選排第一,每頁最多5個,超出需翻頁。列表提供上下翻頁按鈕。

當點擊列表的其中一項,則本窗口關閉,同時場上當前方將按此行動。

三,開始頁和結束頁的改進
在一開始的頁面,在「開始遊戲」下方增加一個「語言切換」按鈕,可將遊戲語言進行中英文的切換。

在每一局結束時的文字提示頁,以及最後終局文字提示頁,都增加一個「棋譜文件下載按鈕」,點擊將下載當前棋譜文件。

需要明確給出所要修改的地方和所要替換的代碼,不用給出全部代碼。

gemini 3.1 pro 起來工作啦

代码: 全选

這裡為你提供改進的代碼片段。請根據以下分類,將代碼替換或新增到你原本的 `csinger2435.html` 檔案對應位置中。

### 一、CSS 樣式新增

請在 `<style>` 標籤的最底部(`</style>` 之前)加入以下樣式,用於設定面板和列表:

```css
/* 設定面板與搜尋結果樣式 */
#settings-modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 90%;
  max-width: 400px;
  background-color: #2c3e50;
  border: 2px solid #bdc3c7;
  border-radius: 10px;
  padding: 20px;
  color: white;
  z-index: 200;
  display: none;
  flex-direction: column;
  gap: 15px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.8);
  font-family: sans-serif;
}
.setting-row { display: flex; justify-content: space-between; align-items: center; }
.setting-row input[type="number"] { width: 60px; padding: 5px; border-radius: 4px; border: 1px solid #ccc; }
#search-results { display: flex; flex-direction: column; gap: 5px; max-height: 200px; overflow-y: auto; }
.search-item { background: #34495e; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; border: 1px solid #7f8c8d; font-size: 14px;}
.search-item:hover { background: #4e6a85; }
.pagination { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
.pagination button { padding: 5px 10px; cursor: pointer; border-radius: 4px; border: none; background: #3498db; color: white;}
.pagination button:disabled { background: #7f8c8d; cursor: not-allowed; }
#btn-lang-toggle { position: absolute; top: 70%; left: 50%; transform: translateX(-50%); padding: 10px 20px; font-size: 18px; border-radius: 8px; cursor: pointer; background: #8e44ad; color: white; border: none; z-index: 10; pointer-events: auto; }

```

### 二、HTML 結構新增與替換

**1. 開始畫面按鈕:** 在 `<div id="ui-layer">` 中新增語言切換按鈕。

```html
    <div id="ui-layer">
      <button id="btn-start">開始遊戲</button>
      <button id="btn-lang-toggle">語言:中文 | Switch to English</button> </div>

```

**2. 設置按鈕與面板:** 在 `<div id="history-controls">` 內,緊接著 `io-options` 區塊後面新增「設置按鈕」,並在外層新增面板 HTML。

```html
      <input type="file" id="file-import" accept=".txt" style="display: none" />
      
      <button id="btn-settings" class="hist-btn" title="設置">
        <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
      </button>
    </div>

    <div id="settings-modal">
      <h3 style="margin:0; text-align:center;" id="modal-title">設置 / Settings</h3>
      
      <div class="setting-row">
        <label id="lbl-think-time">思考時間 (秒):</label>
        <input type="number" id="inp-think-time" step="0.1">
      </div>
      <div class="setting-row">
        <label id="lbl-sure-kill">必殺深度:</label>
        <input type="number" id="inp-sure-kill" step="1" min="1">
      </div>
      <div class="setting-row">
        <label id="lbl-use-minimax">搜尋時調用必殺算法:</label>
        <input type="checkbox" id="inp-use-minimax">
      </div>
      
      <button id="btn-modal-search" style="padding:10px; background:#e67e22; color:white; border:none; border-radius:5px; cursor:pointer;">搜尋當前最佳行動</button>
      <div id="searching-message" style="color:#f1c40f; text-align:center; display:none;">搜尋中...</div>
      
      <div id="search-results"></div>
      
      <div class="pagination">
        <button id="btn-page-prev">上一頁</button>
        <span id="page-info">1 / 1</span>
        <button id="btn-page-next">下一頁</button>
      </div>
      
      <button id="btn-modal-close" style="padding:10px; background:#7f8c8d; color:white; border:none; border-radius:5px; cursor:pointer;">關閉</button>
    </div>

```

### 三、JavaScript 翻譯系統與全局變數新增

在 `<script type="module">` 內最上方(`import` 下方)加入:

```javascript
      // 全局設置變數
      window.currentLang = 'zh';
      window.customThinkTime = null; // null代表跟隨AI強度
      window.customSureKillDepth = null; // null代表跟隨AI強度
      window.useSureKillInSearch = false;
      
      let pagedSearchResults = [];
      let currentPage = 0;
      const RESULTS_PER_PAGE = 5;

      // 簡易在地化系統:攔截中文並轉譯
      const i18n = {
        '開始遊戲': 'Start Game',
        '結束遊戲顯示總分': 'End Game & Show Scores',
        '當前場地再戰一局': 'Rematch (Same Board)',
        '雙方換先再戰一局': 'Rematch (Swap First)',
        '查看最終贏家及總分': 'View Final Winner & Scores',
        '重新開始整個遊戲': 'Restart Entire Game',
        '導出當前棋譜': 'Download Move History'
      };

      function tMsg(msg) {
        if (window.currentLang === 'zh') return msg;
        // 動態翻譯處理
        if (msg.includes('局開局:中心遊戲板就位')) return msg.replace(/第 (\d+) 局開局:中心遊戲板就位/, 'Round $1 Start: Center boards ready');
        if (msg.includes('選擇並放置角遊戲板(左上角或右上角)')) return msg.replace(/(.+)行動:選擇並放置角遊戲板(左上角或右上角)/, '$1 Turn: Place corner board (Top-Left or Top-Right)');
        if (msg.includes('選擇並放置角遊戲板')) return msg.replace(/(.+)行動:選擇並放置角遊戲板/, '$1 Turn: Select & place corner board');
        if (msg.includes('放置對角遊戲板')) return msg.replace(/(.+)行動:放置對角遊戲板/, '$1 Turn: Place opposite corner');
        if (msg.includes('在中心板上放置一個圓柱體 (避開同行/同列/斜相鄰)')) return msg.replace(/(.+)行動:在中心板上放置一個圓柱體 \(避開同行\/同列\/斜相鄰\)/, '$1 Turn: Place cylinder on center board (Avoid row/col/diag)');
        if (msg.includes('在中心板上放置一個圓柱體')) return msg.replace(/(.+)行動:在中心板上放置一個圓柱體/, '$1 Turn: Place cylinder on center board');
        if (msg.includes('放置邊遊戲板及圓柱體')) return msg.replace(/(.+)行動:放置邊遊戲板及圓柱體/, '$1 Turn: Place edge board & cylinder');
        if (msg.includes('放置本方歌手')) return msg.replace(/(.+)行動:放置本方歌手/, '$1 Turn: Place own singer');
        if (msg.includes('開局階段結束!即將進入正式對戰模式...')) return 'Opening Phase Ended! Entering Battle Mode...';
        if (msg.includes('次行走無路可走')) return msg.replace(/(.+)第 (\d+) 次行走無路可走/, '$1 has no valid moves for walk $2');
        if (msg.includes('次行走')) return msg.replace(/輪到 (.+) 行動:第 (\d+) 次行走/, "$1's Turn: Walk $2");
        if (msg.includes('選擇在第一次或第二次停靠點放置話筒')) return msg.replace(/(.+)行動:選擇在第一次或第二次停靠點放置話筒/, "$1's Turn: Place mic on 1st or 2nd stop");
        
        msg = msg.replace(/藍方/g, 'Blue').replace(/綠方/g, 'Green');
        return i18n[msg] || msg;
      }

      // 改寫原本的 showMessage 來支援多國語系
      const originalShowMessage = showMessage;
      showMessage = function(msg) {
        // 先處理內部狀態紀錄
        if (msg.includes('藍方') || msg.includes('Blue')) currentTurnColor = 'blue';
        else if (msg.includes('綠方') || msg.includes('Green')) currentTurnColor = 'green';
        // 翻譯顯示
        uiMsg.style.display = 'block';
        uiMsg.innerText = tMsg(msg);
      }

```

### 四、開始與結算畫面(增加語言切換與棋譜下載按鈕)

**1. 替換 `triggerRoundEnd()` 中創建按鈕的部分:** 尋找 `const btnNo = createOverlayButton('結束遊戲顯示總分', () => {` 前後。

```javascript
        // 【替換 triggerRoundEnd 內部按鈕新增部分】
        const btnNo = createOverlayButton(tMsg('結束遊戲顯示總分'), () => {
          globalMoveHistory.push('1');
          document.body.removeChild(overlay);
          showFinalGameSummary();
        });
        const btnSame = createOverlayButton(tMsg('當前場地再戰一局'), () => {
          globalMoveHistory.push('2');
          document.body.removeChild(overlay);
          gameRound = 2;
          reinitNextRound(true);
        });
        const btnNew = createOverlayButton(tMsg('雙方換先再戰一局'), () => {
          globalMoveHistory.push('3');
          document.body.removeChild(overlay);
          gameRound = 2;
          reinitNextRound(false);
        });
        const btnDownload = createOverlayButton(tMsg('導出當前棋譜'), () => {
          document.getElementById('opt-export-moves').click();
        });

        overlay.appendChild(title);
        const scoreInfo = document.createElement('p');
        scoreInfo.style.fontSize = '22px';
        scoreInfo.innerText = window.currentLang === 'zh' ? `藍方單局得分:${blueFinal} | 綠方單局得分:${orangeFinal}` : `Blue Score: ${blueFinal} | Green Score: ${orangeFinal}`;
        overlay.appendChild(scoreInfo);

        if (gameRound === 1) {
          overlay.appendChild(btnNo);
          overlay.appendChild(btnSame);
          overlay.appendChild(btnNew);
        } else {
          const btnEnd = createOverlayButton(tMsg('查看最終贏家及總分'), () => {
            document.body.removeChild(overlay);
            showFinalGameSummary();
          });
          overlay.appendChild(btnEnd);
        }
        overlay.appendChild(btnDownload); // 增加下載按鈕
        document.body.appendChild(overlay);

```

**2. 替換 `showFinalGameSummary()`:**

```javascript
        // 【在 showFinalGameSummary() 中 btnRestartAll 下方加入下載按鈕】
        const btnRestartAll = createOverlayButton(tMsg('重新開始整個遊戲'), () => {
          document.body.removeChild(overlay);
          gameRound = 1;
          blueTotalScore = 0;
          orangeTotalScore = 0;
          reinitNextRound(false);
        });
        overlay.appendChild(btnRestartAll);

        const btnDownloadFinal = createOverlayButton(tMsg('導出當前棋譜'), () => {
          document.getElementById('opt-export-moves').click();
        });
        overlay.appendChild(btnDownloadFinal);

```

### 五、全新的 Minimax 必殺/必敗計算策略

刪除原本的 `checkSureKill` 和 `checkSureLoss` 函數,替換為這一個統合且符合最佳化策略(最大化分數差及回合控制)的 `minimaxEndgame` 函數:

```javascript
      // =========================================================
      // [替換] 改良版 Minimax 必殺與必敗搜尋
      // =========================================================
      function minimaxEndgame(state, depth, isAI, alpha = -Infinity, beta = Infinity) {
        // 評分標準:勝負權重(±10000) + 分數差權重(*100) + 回合深度權重
        // 贏時:分數差越大越好,深度(剩餘步數)越大越好 (代表最快獲勝)
        // 輸時:分數差越大越好(少輸一點),深度越小越好 (代表盡量拖延回合)
        if (state.aiScore >= 6) return { val: 10000 + (state.aiScore - state.oppScore) * 100 + depth, move: null };
        if (state.oppScore >= 6) return { val: -10000 + (state.aiScore - state.oppScore) * 100 - depth, move: null };
        if (depth <= 0) return { val: 0, move: null };

        const moves = generateAllValidTurns(state);
        if (moves.length === 0) return { val: 0, move: null };

        let bestMove = null;
        let bestVal = isAI ? -Infinity : Infinity;

        for (let move of moves) {
          const nextState = applyTurnToState(state, move);
          const res = minimaxEndgame(nextState, depth - 1, !isAI, alpha, beta);

          if (isAI) {
            if (res.val > bestVal) { bestVal = res.val; bestMove = move; }
            alpha = Math.max(alpha, bestVal);
          } else {
            if (res.val < bestVal) { bestVal = res.val; bestMove = move; }
            beta = Math.min(beta, bestVal);
          }
          if (beta <= alpha) break; // Alpha-beta 剪枝
        }
        return { val: bestVal, move: bestMove };
      }

```

然後,在 **`planFullTurnActionsMCTS`** 函數的頂部(定義 `rootState` 之後),替換原本必殺檢查的段落:

```javascript
        // 【替換 planFullTurnActionsMCTS 的必殺檢查邏輯】
        const actualSureKillDepth = window.customSureKillDepth || SURE_KILL_DEPTH;
        let isSureLoss = false;
        
        // 如果允許在搜尋時調用,或者僅判斷必敗
        if (window.useSureKillInSearch) {
           const endgameRes = minimaxEndgame(rootState, actualSureKillDepth, true);
           if (endgameRes.val > 5000 && endgameRes.move) {
             return { sequence: endgameRes.move.sequence, isSureKill: true, isSureLoss: false, val: endgameRes.val };
           } else if (endgameRes.val < -5000) {
             isSureLoss = true;
           }
        } else {
           // 舊機制:預設不開啟耗時的全展開必殺搜尋,只做基礎判定 (避免網頁卡頓)
           const testLoss = minimaxEndgame(rootState, actualSureKillDepth, true);
           if (testLoss.val < -5000) isSureLoss = true;
           else if (testLoss.val > 5000 && testLoss.move) {
             return { sequence: testLoss.move.sequence, isSureKill: true, isSureLoss: false, val: testLoss.val };
           }
        }

```

### 六、設置面板與擴展搜尋邏輯的綁定

在 `<script>` 尾端 `animate();` 的上方,加入這些新事件監聽與搜尋功能:

```javascript
      // ==========================================
      // [新增] 語言切換與設置面板邏輯
      // ==========================================
      document.getElementById('btn-lang-toggle').addEventListener('click', () => {
        window.currentLang = window.currentLang === 'zh' ? 'en' : 'zh';
        document.getElementById('btn-start').innerText = window.currentLang === 'zh' ? '開始遊戲' : 'Start Game';
        document.getElementById('btn-lang-toggle').innerText = window.currentLang === 'zh' ? '語言:中文 | Switch to English' : 'Language: English | 切換至中文';
        document.getElementById('opt-export-moves').innerText = window.currentLang === 'zh' ? '導出棋譜' : 'Export Moves';
        document.getElementById('opt-export-state').innerText = window.currentLang === 'zh' ? '導出局面' : 'Export FEN';
        document.getElementById('opt-import').innerText = window.currentLang === 'zh' ? '導入棋局' : 'Import Game';
        document.getElementById('modal-title').innerText = window.currentLang === 'zh' ? '設置' : 'Settings';
        document.getElementById('btn-modal-search').innerText = window.currentLang === 'zh' ? '搜尋當前最佳行動' : 'Search Best Move';
        document.getElementById('btn-modal-close').innerText = window.currentLang === 'zh' ? '關閉' : 'Close';
        
        // 即時刷新當前提示
        if (uiMsg.innerText) showMessage(uiMsg.innerText);
      });

      const modal = document.getElementById('settings-modal');
      document.getElementById('btn-settings').addEventListener('click', () => {
        modal.style.display = 'flex';
        document.getElementById('inp-think-time').value = window.customThinkTime || (aiStrength / 1000);
        document.getElementById('inp-sure-kill').value = window.customSureKillDepth || SURE_KILL_DEPTH;
        document.getElementById('inp-use-minimax').checked = window.useSureKillInSearch;
      });

      document.getElementById('btn-modal-close').addEventListener('click', () => {
        window.customThinkTime = parseFloat(document.getElementById('inp-think-time').value) || null;
        window.customSureKillDepth = parseInt(document.getElementById('inp-sure-kill').value) || null;
        window.useSureKillInSearch = document.getElementById('inp-use-minimax').checked;
        modal.style.display = 'none';
      });

      document.getElementById('btn-modal-search').addEventListener('click', async () => {
        if (!gameplayActive || isAIThinking) return;
        const msgDiv = document.getElementById('searching-message');
        msgDiv.style.display = 'block';
        msgDiv.innerText = window.currentLang === 'zh' ? '搜尋中,請稍候...' : 'Searching...';
        
        window.customThinkTime = parseFloat(document.getElementById('inp-think-time').value) || null;
        window.customSureKillDepth = parseInt(document.getElementById('inp-sure-kill').value) || null;
        window.useSureKillInSearch = document.getElementById('inp-use-minimax').checked;
        
        const timeLimit = (window.customThinkTime ? window.customThinkTime * 1000 : aiStrength);
        
        // 清除先前序列以重新搜尋
        aiPlannedActions = [];
        
        // 執行 MCTS 搜尋
        const res = await planFullTurnActionsMCTS(timeLimit);
        msgDiv.style.display = 'none';
        
        // 解析並排序多個候選結果
        let allCandidates = [];
        if (res.rootNode) {
          allCandidates = res.rootNode.children.map(c => {
             const expectedScore = c.totalScore / c.visits;
             const winRate = (1 / (1 + Math.exp(-expectedScore / 200))) * 100;
             return { seq: c.actionSequence, winRate: winRate, isSureKill: false, isSureLoss: res.isSureLoss };
          }).sort((a,b) => b.winRate - a.winRate);
        }
        
        // 如果觸發必殺,強制置頂
        if (res.isSureKill) {
          allCandidates.unshift({ seq: res.sequence, winRate: 100, isSureKill: true, isSureLoss: false });
        }
        
        pagedSearchResults = allCandidates;
        currentPage = 0;
        renderSearchResults();
      });

      function renderSearchResults() {
        const container = document.getElementById('search-results');
        container.innerHTML = '';
        if (pagedSearchResults.length === 0) {
           container.innerHTML = `<div style="text-align:center;">${window.currentLang === 'zh'?'無合法結果':'No results'}</div>`;
           return;
        }
        
        const start = currentPage * RESULTS_PER_PAGE;
        const pageItems = pagedSearchResults.slice(start, start + RESULTS_PER_PAGE);
        
        pageItems.forEach((item, index) => {
          const div = document.createElement('div');
          div.className = 'search-item';
          let stateTxt = item.isSureKill ? '必勝/Win' : (item.isSureLoss ? '必敗/Loss' : `${item.winRate.toFixed(1)}%`);
          let moveTxt = item.seq.map(s => `${String.fromCharCode(65+s.c)}${s.r+1}`).join('->');
          div.innerText = `${start + index + 1}. [${stateTxt}] ${moveTxt}`;
          
          div.onclick = () => {
             aiPlannedActions = [...item.seq];
             modal.style.display = 'none';
             if (!isAIThinking) {
               // 觸發人類的模擬點擊執行此行動
               isAIThinking = false;
               checkAndRunAI(); 
             }
          };
          container.appendChild(div);
        });
        
        document.getElementById('page-info').innerText = `${currentPage + 1} / ${Math.ceil(pagedSearchResults.length / RESULTS_PER_PAGE) || 1}`;
        document.getElementById('btn-page-prev').disabled = currentPage === 0;
        document.getElementById('btn-page-next').disabled = (currentPage + 1) * RESULTS_PER_PAGE >= pagedSearchResults.length;
      }

      document.getElementById('btn-page-prev').addEventListener('click', () => { if(currentPage > 0){ currentPage--; renderSearchResults(); }});
      document.getElementById('btn-page-next').addEventListener('click', () => { if((currentPage + 1) * RESULTS_PER_PAGE < pagedSearchResults.length){ currentPage++; renderSearchResults(); }});

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

在线用户

正浏览此版面之用户: Amazon [Bot], Majestic-12 [Bot] 和 2 访客