代码: 全选
以下是針對您的三個需求所對應的代碼修改。請在您的專案檔案中尋找對應的段落並進行替換:
### 一、 獨立 AI 按鈕事件與即時中斷機制
我們將新增一個 `aiActionTimeout` 變數來記錄 AI 延遲點擊事件,並加入 `abortAITurn` 函式來中斷思考和撤銷尚未確認的回合,同時改寫兩個 AI 按鈕的事件綁定。
**1. 增加 `aiActionTimeout` 全域變數**
尋找(大約在行號 385 附近):
```javascript
let isBlueAI = false;
let isGreenAI = true; // 預設綠方為 AI
let blueAiTimeout = null; // 記錄藍隊 AI 延遲定時器
let greenAiTimeout = null; // 記錄綠隊 AI 延遲定時器
let aiStrength = 4000; // 預設困難
```
替換為:
```javascript
let isBlueAI = false;
let isGreenAI = true; // 預設綠方為 AI
let blueAiTimeout = null; // 記錄藍隊 AI 延遲定時器
let greenAiTimeout = null; // 記錄綠隊 AI 延遲定時器
let aiActionTimeout = null; // 記錄 AI 動作執行的定時器
let aiStrength = 4000; // 預設困難
```
**2. 加入中斷函數並改寫按鈕事件**
尋找(大約在行號 1520 附近):
```javascript
btnAiBlue.addEventListener('click', () => {
isBlueAI = !isBlueAI;
btnAiBlue.classList.toggle('active-blue', isBlueAI);
triggerAIIfReady();
});
btnAiGreen.addEventListener('click', () => {
isGreenAI = !isGreenAI;
btnAiGreen.classList.toggle('active-green', isGreenAI);
triggerAIIfReady();
});
```
替換為:
```javascript
function abortAITurn(color) {
if (currentPlayer !== color) return; // 僅當中斷當前行動方的 AI 時才撤銷
isAIThinking = false;
aiPlannedActions = [];
if (aiActionTimeout) {
clearTimeout(aiActionTimeout);
aiActionTimeout = null;
}
const statusDiv = document.getElementById('ai-status');
if (statusDiv) statusDiv.style.display = 'none';
document.body.style.pointerEvents = 'auto';
// 如果在正式對戰中,且已經走了幾步(或是正處於放話筒階段),則撤銷回回合起點
if (gameplayActive && (walkCount > 1 || currentSubPhase !== 'walk' || (markers.length > 0 && markers[0].position.y === 6))) {
setTimeout(() => {
turnUndo();
}, 100);
}
}
btnAiBlue.addEventListener('click', () => {
isBlueAI = !isBlueAI;
btnAiBlue.classList.toggle('active-blue', isBlueAI);
if (!isBlueAI) {
abortAITurn('blue');
} else {
triggerAIIfReady();
}
});
btnAiGreen.addEventListener('click', () => {
isGreenAI = !isGreenAI;
btnAiGreen.classList.toggle('active-green', isGreenAI);
if (!isGreenAI) {
abortAITurn('green');
} else {
triggerAIIfReady();
}
});
```
**3. 將原有定時器改用 `aiActionTimeout` 紀錄與中斷防護**
尋找 `checkAndRunAI()` 中的:
```javascript
if (targetMarker) {
setTimeout(() => {
targetMarker.userData.onClick();
}, 400);
} else {
```
替換為:
```javascript
if (targetMarker) {
aiActionTimeout = setTimeout(() => {
targetMarker.userData.onClick();
}, 400);
} else {
```
尋找 `finishAIAction()` 中的:
```javascript
setTimeout(() => {
document.body.style.pointerEvents = 'auto';
isAIThinking = false;
```
替換為:
```javascript
aiActionTimeout = setTimeout(() => {
document.body.style.pointerEvents = 'auto';
isAIThinking = false;
```
尋找 `doMCTSAI()` 中的:
```javascript
// 接收新的回傳結構
const result = await planFullTurnActionsMCTS(timeLimit);
aiPlannedActions = result.sequence;
```
替換為:
```javascript
// 接收新的回傳結構
const result = await planFullTurnActionsMCTS(timeLimit);
if (!isAIThinking) return; // 如果在中途被手動關閉,則立刻放棄後續執行
aiPlannedActions = result.sequence;
```
---
### 二、 佈局階段英文翻譯適配修正
尋找 `tMsg(msg)` 函數內的這段舊式正則表達(大約在行號 275 附近):
```javascript
if (msg.includes('選擇並放置角遊戲板(左上角或右上角)')) {
return translated.replace(
/(.+)行動:選擇並放置角遊戲板(左上角或右上角)/,
'$1 Turn: Place corner board (Top-Left or Top-Right)'
);
}
if (msg.includes('選擇並放置角遊戲板')) {
return translated.replace(/(.+)行動:選擇並放置角遊戲板/, '$1 Turn: Select & place corner board');
}
if (msg.includes('放置對角遊戲板')) {
return translated.replace(/(.+)行動:放置對角遊戲板/, '$1 Turn: Place opposite corner');
}
if (msg.includes('在中心板上放置一個圓柱體 (避開同行/同列/斜相鄰)')) {
return translated.replace(
/(.+)行動:在中心板上放置一個圓柱體 \(避開同行\/同列\/斜相鄰\)/,
'$1 Turn: Place cylinder on center board (Avoid row/col/diag)'
);
}
if (msg.includes('在中心板上放置一個圓柱體')) {
return translated.replace(/(.+)行動:在中心板上放置一個圓柱體/, '$1 Turn: Place cylinder on center board');
}
if (msg.includes('放置邊遊戲板及圓柱體')) {
return translated.replace(/(.+)行動:放置邊遊戲板及圓柱體/, '$1 Turn: Place edge board & cylinder');
}
if (msg.includes('放置本方歌手')) {
return translated.replace(/(.+)行動:放置本方歌手/, '$1 Turn: Place own singer');
}
if (msg.includes('開局階段結束!即將進入正式對戰模式...')) {
return 'Opening Phase Ended! Entering Battle Mode...';
}
```
替換為:
```javascript
if (msg.includes('布局階段:')) {
if (msg.includes('放置角遊戲板')) {
return translated.replace(/布局階段:(.+)放置角遊戲板/, 'Placement Phase: $1 places corner board');
}
if (msg.includes('在中遊戲板上放置一個圓柱體 (避免共線以及斜連)')) {
return translated.replace(/布局階段:(.+)在中遊戲板上放置一個圓柱體 \(避免共線以及斜連\)/, 'Placement Phase: $1 places cylinder on center board (avoid colinear/diag)');
}
if (msg.includes('在中遊戲板上放置一個圓柱體')) {
return translated.replace(/布局階段:(.+)在中遊戲板上放置一個圓柱體/, 'Placement Phase: $1 places cylinder on center board');
}
if (msg.includes('放置邊遊戲板及圓柱體')) {
return translated.replace(/布局階段:(.+)放置邊遊戲板及圓柱體/, 'Placement Phase: $1 places edge board & cylinder');
}
if (msg.includes('放置本方歌手')) {
return translated.replace(/布局階段:(.+)放置本方歌手/, 'Placement Phase: $1 places own singer');
}
}
if (msg.includes('布局階段結束!即將進入行動階段...')) {
return 'Placement Phase Ended! Entering Action Phase...';
}
```
---
### 三、 增加對平局(Tie)的判斷與描述
**1. 讓 Minimax 和 RandomPlayout 優先識別平局**
尋找 `minimaxEndgame()` 函數的前幾行:
```javascript
function minimaxEndgame(state, depth, isAI, alpha = -Infinity, beta = Infinity, deadline = Infinity) {
// 如果執行時間已超過分配的截止時間,強制退出,避免卡死主執行緒
if (performance.now() > deadline) return { val: 0, move: null, timeout: true };
if (state.aiScore >= 6) return { val: 10000 + state.aiScore * 500 - state.oppScore * 500 + depth, move: null };
if (state.oppScore >= 6)
return { val: -10000 + state.aiScore * 500 - state.oppScore * 500 - depth, move: null };
```
替換為:
```javascript
function minimaxEndgame(state, depth, isAI, alpha = -Infinity, beta = Infinity, deadline = Infinity) {
// 如果執行時間已超過分配的截止時間,強制退出,避免卡死主執行緒
if (performance.now() > deadline) return { val: 0, move: null, timeout: true };
// 優先判斷平局 (雙方同時達到6分)
if (state.aiScore >= 6 && state.oppScore >= 6) return { val: 0 + state.scoreDiff, move: null, isTie: true };
if (state.aiScore >= 6) return { val: 10000 + state.aiScore * 500 - state.oppScore * 500 + depth, move: null };
if (state.oppScore >= 6)
return { val: -10000 + state.aiScore * 500 - state.oppScore * 500 - depth, move: null };
```
尋找 `simulateRandomPlayout()` 函數的前幾行:
```javascript
function simulateRandomPlayout(state, aiColor, depthLimit) {
let currState = state;
for (let i = 0; i < depthLimit; i++) {
if (currState.aiScore >= 6)
return 10000 + currState.scoreDiff + currState.aiScore * 500 - currState.oppScore * 500;
if (currState.oppScore >= 6)
return -10000 + currState.scoreDiff + currState.aiScore * 500 - currState.oppScore * 500;
```
替換為:
```javascript
function simulateRandomPlayout(state, aiColor, depthLimit) {
let currState = state;
for (let i = 0; i < depthLimit; i++) {
if (currState.aiScore >= 6 && currState.oppScore >= 6)
return 0 + currState.scoreDiff; // 優先判斷平局
if (currState.aiScore >= 6)
return 10000 + currState.scoreDiff + currState.aiScore * 500 - currState.oppScore * 500;
if (currState.oppScore >= 6)
return -10000 + currState.scoreDiff + currState.aiScore * 500 - currState.oppScore * 500;
```
**2. 將平局結果導出至 UI 文本與 Modal 顯示**
尋找 `planFullTurnActionsMCTS()`:
```javascript
const actualSureKillDepth = SURE_KILL_DEPTH;
let isSureLoss = false;
await new Promise(resolve => setTimeout(resolve, 30));
// 1. 將所有時間分配給 Minimax 必殺搜尋
const minimaxDeadline = performance.now() + timeLimit;
const endgameRes = minimaxEndgame(rootState, actualSureKillDepth, true, -Infinity, Infinity, minimaxDeadline);
if (!endgameRes.timeout) {
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;
}
}
```
替換為:
```javascript
const actualSureKillDepth = SURE_KILL_DEPTH;
let isSureLoss = false;
let isSureTie = false;
await new Promise(resolve => setTimeout(resolve, 30));
// 1. 將所有時間分配給 Minimax 必殺搜尋
const minimaxDeadline = performance.now() + timeLimit;
const endgameRes = minimaxEndgame(rootState, actualSureKillDepth, true, -Infinity, Infinity, minimaxDeadline);
if (!endgameRes.timeout) {
if (endgameRes.val > 5000 && endgameRes.move) {
return { sequence: endgameRes.move.sequence, isSureKill: true, isSureTie: false, isSureLoss: false, val: endgameRes.val };
} else if (endgameRes.isTie || (endgameRes.val === rootState.scoreDiff && endgameRes.move && Math.abs(endgameRes.val) < 1000)) {
isSureTie = true; // 搜尋到強制平局
} else if (endgameRes.val < -5000) {
isSureLoss = true;
}
}
```
並在 `planFullTurnActionsMCTS()` 最後的回傳處將 `isSureTie` 加上:
```javascript
return {
sequence: bestChild ? bestChild.actionSequence : fallbackSequence,
rootNode: rootNode,
isSureKill: false,
isSureTie: isSureTie,
isSureLoss: isSureLoss
};
```
尋找 `doMCTSAI()` 內的文本提示:
```javascript
if (result.isSureKill) {
winRateText = window.currentLang === 'zh' ? '100% (必勝局面)' : '100% (Sure Win)';
} else if (result.isSureLoss) {
winRateText = window.currentLang === 'zh' ? '0% (必敗局面)' : '0% (Sure Loss)';
} else if (result.rootNode) {
```
替換為:
```javascript
if (result.isSureKill) {
winRateText = window.currentLang === 'zh' ? '100% (必勝局面)' : '100% (Sure Win)';
} else if (result.isSureTie) {
winRateText = window.currentLang === 'zh' ? '平局保底 (強制平手)' : 'Tie Guard (Sure Tie)';
} else if (result.isSureLoss) {
winRateText = window.currentLang === 'zh' ? '0% (必敗局面)' : '0% (Sure Loss)';
} else if (result.rootNode) {
```
尋找 UI 彈窗中的搜尋最佳行動 `btn-modal-search` 結果串接邏輯:
```javascript
if (res.isSureKill) {
allCandidates.unshift({ seq: res.sequence, winRate: 100, isSureKill: true, isSureLoss: false });
}
```
替換為:
```javascript
if (res.isSureKill) {
allCandidates.unshift({ seq: res.sequence, winRate: 100, isSureKill: true, isSureTie: false, isSureLoss: false });
} else if (res.isSureTie) {
allCandidates.unshift({ seq: res.sequence, winRate: 50, isSureKill: false, isSureTie: true, isSureLoss: false });
}
```
接著在 `renderSearchResults` 中找到這行:
```javascript
let stateTxt = item.isSureKill ? '必勝/Win' : item.isSureLoss ? '必敗/Loss' : `${item.winRate.toFixed(1)}%`;
```
替換為:
```javascript
let stateTxt = item.isSureKill ? '必勝/Win' : item.isSureTie ? '必平/Tie' : item.isSureLoss ? '必敗/Loss' : `${item.winRate.toFixed(1)}%`;
```
**3. 更新對戰結束時 (`triggerRoundEnd`) 的單局平手文字**
尋找:
```javascript
let roundWinner = blueFinal > greenFinal ? '藍方' : greenFinal > blueFinal ? '綠方' : '平手';
if (window.currentLang === 'en') {
roundWinner = roundWinner === '藍方' ? 'Blue' : roundWinner === '綠方' ? 'Green' : 'Draw';
}
const overlay = document.createElement('div');
overlay.id = 'round-end-overlay';
overlay.style.cssText =
'position:absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.85); display:flex; flex-direction:column; justify-content:center; align-items:center; color:#fff; font-family:sans-serif; z-index:100;';
const titleText =
window.currentLang === 'zh'
? `第 ${gameRound} 局結束!本局勝者:${roundWinner}`
: `Round ${gameRound} Ended! Winner: ${roundWinner}`;
```
替換為:
```javascript
let roundWinner = blueFinal > greenFinal ? '藍方' : greenFinal > blueFinal ? '綠方' : '平手';
if (window.currentLang === 'en') {
roundWinner = roundWinner === '藍方' ? 'Blue' : roundWinner === '綠方' ? 'Green' : 'Draw';
}
const overlay = document.createElement('div');
overlay.id = 'round-end-overlay';
overlay.style.cssText =
'position:absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.85); display:flex; flex-direction:column; justify-content:center; align-items:center; color:#fff; font-family:sans-serif; z-index:100;';
let titleText = "";
if (roundWinner === '平手' || roundWinner === 'Draw') {
titleText = window.currentLang === 'zh'
? `第 ${gameRound} 局結束!雙方平局!`
: `Round ${gameRound} Ended! It's a Draw!`;
} else {
titleText = window.currentLang === 'zh'
? `第 ${gameRound} 局結束!本局勝者:${roundWinner}`
: `Round ${gameRound} Ended! Winner: ${roundWinner}`;
}
```