設置窗口第一項為語言,可在「中文︱Englosh」之間切換,當切換成英文時,所有message提示文字、按鈕(如「開始遊戲」)、ai-status窗口的文字,都要改成英文。
當搜尋到限定時間,則把當前所搜尋到的結果(當前方的本回合行動),通過列表展示在下方,附上其對應的勝率(包括是否必殺或必敗),最優選排第一,每頁最多5個,超出需翻頁。列表提供上下翻頁按鈕。
代码: 全选
這裡為你提供改進的代碼片段。請根據以下分類,將代碼替換或新增到你原本的 `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(); }});
```