代码: 全选
這些修改非常合理,能顯著提升遊戲的體驗與 AI 的可用性。
首先,為你解答關於「必殺深度」的疑問:
**你的定義是完全正確的。** 在你的程式碼中,`minimaxEndgame` 每次遞迴向下探索時,呼叫的是 `applyTurnToState(state, move)`,而這個 `move` 是由 `generateAllValidTurns` 生成的。由於 `generateAllValidTurns` 會一口氣將「三次行走加上可能的話筒放置」打包成一個完整的「回合節點(Node)」,因此**這裡的深度(Depth)代表的確實是完整的「回合數」**,而不是單一步驟(行走步數)。
以下是針對你提出的四項需求,對應的程式碼替換指南:
### 一、 修改 HTML:AI 強度選單與設置面板
請找到 `<div id="ai-strength-options" class="custom-select-options">` 到 `` 的部分,並替換為以下程式碼(這會更新 1s, 4s, 7s 與新增自定義選項,加上 +/- 按鈕,並移除必殺開關):
```html
<div id="ai-strength-options" class="custom-select-options">
<div class="csinger-option strength-option" data-value="1000" 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>
簡單 (1s)
</div>
<div class="csinger-option strength-option" data-value="4000" 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>
困難 (4s)
</div>
<div class="csinger-option strength-option" data-value="7000" 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>
專家 (7s)
</div>
<div class="csinger-option strength-option" id="opt-custom-ai" data-value="9000" data-depth="9">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
自定義 (9s)
</div>
</div>
</div>
<div class="divider"></div>
<button id="btn-walk-undo" class="hist-btn" title="行走撤銷">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C20.76 11.03 17.03 8 12.5 8z"/></svg>
</button>
<button id="btn-walk-redo" class="hist-btn" title="行走重做">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" transform="scale(-1,1)"><path d="M11.5 8C16.03 8 19.76 11.03 21 14.72l-2.37.78c-1.05-3.19-4.06-5.5-7.6-5.5-1.95 0-3.73.72-5.12 1.88L9.53 16H.53V7l3.6 3.6C5.98 8.99 8.38 8 11.5 8z"/></svg>
</button>
<button id="btn-turn-undo" class="hist-btn" title="回合後退">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<button id="btn-turn-redo" class="hist-btn" title="回合前進">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M4 11h12.17l-5.59-5.59L12 4l8 8-8 8-1.41-1.41L16.17 13H4v-2z"/></svg>
</button>
<div class="divider"></div>
<div class="custom-select-container">
<button id="btn-io-trigger" class="hist-btn custom-select-trigger" title="局面導入導出">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
</button>
<div id="io-options" class="custom-select-options">
<div class="csinger-option custom-option" id="opt-export-moves">導出棋譜</div>
<div class="csinger-option custom-option" id="opt-export-state">導出局面</div>
<div class="csinger-option custom-option" id="opt-import">導入棋局</div>
</div>
</div>
<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; display: flex; justify-content: center; align-items: center; gap: 15px;" id="modal-title-container">
<span id="modal-title">設置</span>
<span class="lang-toggle-text" id="modal-lang-toggle">
<span id="lang-zh" class="lang-active">中文</span> | <span id="lang-en">En</span>
</span>
</h3>
<div class="setting-row">
<label id="lbl-think-time">思考時間 (秒):</label>
<div style="display:flex; align-items:center; gap:5px;">
<button id="btn-time-minus" style="padding:4px 10px; cursor:pointer; background:#34495e; color:white; border:1px solid #7f8c8d; border-radius:4px;">-</button>
<input type="number" id="inp-think-time" step="1" min="1" max="24" style="text-align:center;">
<button id="btn-time-plus" style="padding:4px 10px; cursor:pointer; background:#34495e; color:white; border:1px solid #7f8c8d; border-radius:4px;">+</button>
</div>
</div>
<div class="setting-row">
<label id="lbl-sure-kill">必殺深度:</label>
<div style="display:flex; align-items:center; gap:5px;">
<button id="btn-depth-minus" style="padding:4px 10px; cursor:pointer; background:#34495e; color:white; border:1px solid #7f8c8d; border-radius:4px;">-</button>
<input type="number" id="inp-sure-kill" step="1" min="1" max="18" style="text-align:center;">
<button id="btn-depth-plus" style="padding:4px 10px; cursor:pointer; background:#34495e; color:white; border:1px solid #7f8c8d; border-radius:4px;">+</button>
</div>
</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>
```
*(同時請將預設 AI 強度變數改為:`let aiStrength = 4000;` 位於 `<script type="module">` 內約略 584 行附近)*
---
### 二、 更新 AI 思考狀態(中英切換)
在 `script` 區塊中,尋找 `finishAIAction`、`doOpeningAI` 和 `doMCTSAI` 這三個函數,並替換為以下邏輯以支援動態語系與搜尋狀態即時刷新:
```javascript
function finishAIAction(selectedMarker, timeTaken, winRateText) {
const statusDiv = document.getElementById('ai-status');
const endText = window.currentLang === 'zh' ? '思考結束' : 'Thought Complete';
const timeText = window.currentLang === 'zh' ? '用時' : 'Time';
const winText = window.currentLang === 'zh' ? '勝率' : 'Win Rate';
statusDiv.innerHTML = `${endText}<br>${timeText}: ${timeTaken}s<br>${winText}: ${winRateText}`;
setTimeout(() => {
document.body.style.pointerEvents = 'auto';
isAIThinking = false;
if (selectedMarker) {
let activeMarker = markers.find(
m =>
Math.abs(m.position.x - selectedMarker.position.x) < 1 &&
Math.abs(m.position.z - selectedMarker.position.z) < 1
);
if (activeMarker) activeMarker.userData.onClick();
}
}, 800);
}
function doOpeningAI() {
let startTime = performance.now();
let selectedMarker = null;
if ((currentPhaseFn === startPhase4 || currentPhaseFn === startPhase5 || currentPhaseFn === startPhase6) && markers.length === 3) {
let m1, m2;
for (let i = 0; i < 3; i++) {
for (let j = i + 1; j < 3; j++) {
let pos1 = { c: Math.round(markers[i].position.x / 60 + 3.5), r: Math.round(markers[i].position.z / 60 + 3.5) };
let pos2 = { c: Math.round(markers[j].position.x / 60 + 3.5), r: Math.round(markers[j].position.z / 60 + 3.5) };
if (pos1.c === pos2.c || pos1.r === pos2.r) {
m1 = markers[i]; m2 = markers[j]; break;
}
}
if (m1) break;
}
if (m1 && m2) selectedMarker = Math.random() < 0.5 ? m1 : m2;
}
if (!selectedMarker) selectedMarker = markers[Math.floor(Math.random() * markers.length)];
let timeTaken = ((performance.now() - startTime) / 1000).toFixed(1);
finishAIAction(selectedMarker, timeTaken, window.currentLang === 'zh' ? 'N/A (開局)' : 'N/A (Opening)');
}
// (在 doMCTSAI 內部,將迴圈的狀態刷新部分替換為:)
if (iterations % 50 === 0) {
const elapsedSeconds = ((performance.now() - startTime) / 1000).toFixed(1);
if (statusDiv) {
const thinkingText = window.currentLang === 'zh' ? 'AI 思考中...' : 'AI Thinking...';
const costText = window.currentLang === 'zh' ? '耗時' : 'Time';
const searchText = window.currentLang === 'zh' ? '搜尋次數' : 'Searches';
statusDiv.innerHTML = `${thinkingText}<br>
<span style="font-size: 0.9em; color: #ccc;">
${costText}: ${elapsedSeconds}s | ${searchText}: ${iterations}
</span>`;
}
const searchingMsgDiv = document.getElementById('searching-message');
if (searchingMsgDiv && searchingMsgDiv.style.display === 'block') {
const modalSearchText = window.currentLang === 'zh' ? '搜尋中' : 'Searching';
searchingMsgDiv.innerText = `${modalSearchText}... (${elapsedSeconds}s, ${iterations} iter)`;
}
await new Promise(resolve => setTimeout(resolve, 0));
}
```
---
### 三、 更新話筒放置邏輯(第一步等於第三步)
找到 `showMicrophonePlacementOptions()`,並替換為以下程式碼:
```javascript
function showMicrophonePlacementOptions() {
clearMarkers();
const currentMicsCount = microphones.filter(m => m.userData.color === currentPlayer).length;
if (currentMicsCount >= 4) {
switchTurn();
return;
}
let targetSpots = [];
const activeCube = cubes.find(q => q.userData.color === currentPlayer);
const currentC = activeCube.userData.col;
const currentR = activeCube.userData.row;
// 規則:如果第一次停留點等於第三次停留點,則只能在第二次停留點放話筒
const isStop1SameAsStop3 = turnStopPositions.length > 0 &&
turnStopPositions[0].c === currentC &&
turnStopPositions[0].r === currentR;
if (isStop1SameAsStop3 && turnStopPositions.length > 1) {
targetSpots.push(turnStopPositions[1]);
} else {
turnStopPositions.forEach(p => {
if (!targetSpots.some(ts => ts.c === p.c && ts.r === p.r)) targetSpots.push(p);
});
}
let availableSpots = targetSpots.filter(p => {
return !microphones.some(
m => m.userData.color === currentPlayer && m.userData.col === p.c && m.userData.row === p.r
);
});
// 若規定位置已被己方話筒佔據,或無合法停留點,直接結束行動
if (availableSpots.length === 0) {
switchTurn();
return;
}
const msgText = window.currentLang === 'zh'
? `${currentPlayer === 'blue' ? '藍方' : '綠方'}行動:選擇在停留點放置話筒`
: `${currentPlayer === 'blue' ? 'Blue' : 'Green'} Turn: Place mic on valid stop`;
showMessage(msgText);
const micMarkerColor = currentPlayer === 'blue' ? 0xff4444 : 0x800080;
availableSpots.forEach(spot => {
createMarker(spot.c, spot.r, micMarkerColor, () => {
pushAction();
recordMove(spot.c, spot.r);
clearMarkers();
createMicrophoneMesh(currentPlayer, spot.c, spot.r);
switchTurn();
}, 6);
});
triggerAIIfReady();
}
```
---
### 四、 強制必殺算法與修正自動跑完回歸設定
將 `planFullTurnActionsMCTS` 中關於判斷 `window.useSureKillInSearch` 的分支完全移除,強制套用。並更新 `checkAndRunAI()` 與設定相關的事件監聽。
```javascript
// 【請替換 checkAndRunAI 和 triggerAIIfReady 函數】
function triggerAIIfReady() {
const statusDiv = document.getElementById('ai-status');
if (markers.length > 0 && !isAIThinking) {
// 加入 aiPlannedActions.length > 0 作為觸發條件,確保搜尋結果能連續執行完畢
const shouldRun = (currentTurnColor === 'blue' && isBlueAI) || (currentTurnColor === 'green' && isGreenAI) || (gameplayActive && aiPlannedActions.length > 0);
if (shouldRun) {
checkAndRunAI();
} else {
statusDiv.style.display = 'none';
}
}
}
function checkAndRunAI() {
if (markers.length === 0 || isAIThinking) return;
const shouldRun = (currentTurnColor === 'blue' && isBlueAI) || (currentTurnColor === 'green' && isGreenAI) || (gameplayActive && aiPlannedActions.length > 0);
if (!shouldRun) return;
if (gameplayActive && aiPlannedActions.length > 0) {
const nextAction = aiPlannedActions.shift();
const targetMarker = markers.find(m => {
const mc = Math.round(m.position.x / 60 + 3.5);
const mr = Math.round(m.position.z / 60 + 3.5);
return mc === nextAction.c && mr === nextAction.r;
});
if (targetMarker) {
setTimeout(() => { targetMarker.userData.onClick(); }, 400);
} else {
aiPlannedActions = [];
}
return;
}
isAIThinking = true;
const statusDiv = document.getElementById('ai-status');
statusDiv.style.display = 'block';
statusDiv.innerHTML = window.currentLang === 'zh' ? 'AI 思考中...' : 'AI Thinking...';
document.body.style.pointerEvents = 'none';
if (!gameplayActive) doOpeningAI();
else doMCTSAI();
}
// 【請替換 planFullTurnActionsMCTS 內必敗必殺判斷區塊】
async function planFullTurnActionsMCTS(timeLimit) {
// ... (保持前面的變數定義 rootState)
const actualSureKillDepth = window.customSureKillDepth || SURE_KILL_DEPTH;
let isSureLoss = false;
// 強制執行必殺與必敗搜尋
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;
}
const rootNode = new MCTSNode(null, rootState, null);
// ... (保持後面的 MCTS while 迴圈不變)
```
**更新語言與設置按鈕邏輯(放在檔案末端的事件綁定區):**
```javascript
function updateUIAfterLangChange() {
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';
document.getElementById('lbl-think-time').innerText = window.currentLang === 'zh' ? '思考時間 (秒):' : 'Think Time (s):';
document.getElementById('lbl-sure-kill').innerText = window.currentLang === 'zh' ? '必殺深度:' : 'Sure-kill Depth:';
document.getElementById('lang-zh').className = window.currentLang === 'zh' ? 'lang-active' : '';
document.getElementById('lang-en').className = window.currentLang === 'en' ? 'lang-active' : '';
// 即時刷新客製化選項文字
let customOpt = document.getElementById('opt-custom-ai');
if (customOpt) {
const val = customOpt.getAttribute('data-value') / 1000;
customOpt.innerHTML = `<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> ${window.currentLang === 'zh' ? '自定義' : 'Custom'} (${val}s)`;
}
if (uiMsg.innerText) showMessage(uiMsg.innerText);
if (pagedSearchResults.length > 0) renderSearchResults();
}
// 替換關閉設置視窗與搜尋的點擊事件
document.getElementById('btn-modal-close').addEventListener('click', () => {
const tTime = parseInt(document.getElementById('inp-think-time').value);
const dDepth = parseInt(document.getElementById('inp-sure-kill').value);
window.customThinkTime = tTime || null;
window.customSureKillDepth = dDepth || null;
const msTime = window.customThinkTime ? window.customThinkTime * 1000 : aiStrength;
const currentDepth = window.customSureKillDepth ? window.customSureKillDepth : SURE_KILL_DEPTH;
let customOpt = document.getElementById('opt-custom-ai');
customOpt.setAttribute('data-value', msTime);
customOpt.setAttribute('data-depth', currentDepth);
customOpt.innerHTML = `<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> ${window.currentLang === 'zh' ? '自定義' : 'Custom'} (${msTime/1000}s)`;
customOpt.onclick = () => {
aiStrength = msTime; SURE_KILL_DEPTH = currentDepth;
aiStrengthTrigger.innerHTML = customOpt.querySelector('svg').outerHTML;
aiStrengthTrigger.setAttribute('data-value', aiStrength);
aiStrengthTrigger.setAttribute('data-depth', SURE_KILL_DEPTH);
aiStrengthOptions.classList.remove('open');
};
const isDefault1 = msTime === 1000 && currentDepth === 3;
const isDefault2 = msTime === 4000 && currentDepth === 5;
const isDefault3 = msTime === 7000 && currentDepth === 7;
if (!isDefault1 && !isDefault2 && !isDefault3) {
aiStrength = msTime; SURE_KILL_DEPTH = currentDepth;
aiStrengthTrigger.innerHTML = customOpt.querySelector('svg').outerHTML;
aiStrengthTrigger.setAttribute('data-value', aiStrength);
aiStrengthTrigger.setAttribute('data-depth', SURE_KILL_DEPTH);
}
modal.style.display = 'none';
restoreAIHighlights();
});
document.getElementById('btn-modal-search').addEventListener('click', async () => {
if (!gameplayActive || isAIThinking) return;
// 點擊後立刻清空既有項目
document.getElementById('search-results').innerHTML = '';
pagedSearchResults = [];
document.getElementById('page-info').innerText = '1 / 1';
const msgDiv = document.getElementById('searching-message');
msgDiv.style.display = 'block';
msgDiv.innerText = window.currentLang === 'zh' ? '搜尋中,請稍候...' : 'Searching...';
window.customThinkTime = parseInt(document.getElementById('inp-think-time').value) || null;
window.customSureKillDepth = parseInt(document.getElementById('inp-sure-kill').value) || null;
const timeLimit = (window.customThinkTime ? window.customThinkTime * 1000 : aiStrength);
aiPlannedActions = [];
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();
});
// 初始化輸入框加減邏輯
const setupNumberInput = (inputId, minusId, plusId, min, max) => {
const inp = document.getElementById(inputId);
document.getElementById(minusId).addEventListener('click', () => {
let val = parseInt(inp.value);
if (isNaN(val)) val = min;
if (val > min) inp.value = val - 1;
});
document.getElementById(plusId).addEventListener('click', () => {
let val = parseInt(inp.value);
if (isNaN(val)) val = min;
if (val < max) inp.value = val + 1;
});
inp.addEventListener('change', () => {
let val = parseInt(inp.value);
if (isNaN(val) || val < min) inp.value = min;
if (val > max) inp.value = max;
});
};
setupNumberInput('inp-think-time', 'btn-time-minus', 'btn-time-plus', 1, 24);
setupNumberInput('inp-sure-kill', 'btn-depth-minus', 'btn-depth-plus', 1, 18);
```