Re: 「我為歌狂」遊戲製作
发表于 : 2026年 6月 18日 13:01
gemini 3.1 pro
代码: 全选
這三個問題分別涉及到回合歷史陣列的紀錄時機、跨對戰局數重做時的判定邏輯、以及介面和多國語系的微調。
以下為具體的修正步驟和需要替換的程式碼:
### 一、修復回合後退/前進、取消 AI 高亮與歷史紀錄漏寫問題
**1. 新增 `disableAI` 函數並套用到所有的進退按鈕中**
在代碼中找到 `function walkUndo()` 及其周圍的函數,替換為以下內容,確保任何人工撤銷或前進都會自動關閉 AI:
```javascript
function disableAI() {
isBlueAI = false;
isGreenAI = false;
document.getElementById('btn-ai-blue').classList.remove('active-blue');
document.getElementById('btn-ai-green').classList.remove('active-green');
}
function walkUndo() {
disableAI(); // 取消AI高亮
if (historyUndoStack.length === 0) return;
if (gameplayActive) {
let lastSnap = historyUndoStack[historyUndoStack.length - 1];
if (lastSnap.currentPlayer !== currentPlayer || !lastSnap.gameplayActive) return;
}
historyRedoStack.push(saveSnapshot());
restoreSnapshot(historyUndoStack.pop());
updateUndoRedoButtons();
}
function walkRedo() {
disableAI(); // 取消AI高亮
if (historyRedoStack.length === 0) return;
if (gameplayActive) {
let nextSnap = historyRedoStack[historyRedoStack.length - 1];
if (nextSnap.currentPlayer !== currentPlayer || !nextSnap.gameplayActive) return;
}
historyUndoStack.push(saveSnapshot());
restoreSnapshot(historyRedoStack.pop());
updateUndoRedoButtons();
}
function turnUndo() {
disableAI(); // 取消AI高亮
// 在開局階段,回合後退就等同於單步撤銷
if (!gameplayActive && !cubes.some(c => c.userData.score >= 6)) {
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.cubesData.some(c => c.score >= 6)) {
targetIdx = i;
break;
}
// 我們在尋找一個「回合起點」的快照
if (snap.gameplayActive && snap.walkCount === 1 && snap.subPhase === 'walk') {
if (!gameplayActive) {
// 若當前在「遊戲結束」狀態,找到的第一個回合起點就是引發勝利的那一回合起點
targetIdx = i;
break;
} else if (walkCount > 1 || markers.some(m => m.position.y === 6)) {
// 處於回合中途,我們要退回到「當前回合」的起點
if (snap.currentPlayer === currentPlayer) {
targetIdx = i;
break;
}
} else {
// 處於回合起點,我們要退回到「上一回合」的起點
if (snap.currentPlayer !== currentPlayer) {
targetIdx = i;
break;
}
}
}
}
if (targetIdx === -1) return;
historyRedoStack.push(saveSnapshot());
while (historyUndoStack.length > targetIdx + 1) {
historyRedoStack.push(historyUndoStack.pop());
}
restoreSnapshot(historyUndoStack.pop());
updateUndoRedoButtons();
}
```
**2. 修改 `turnRedo` 邏輯解決跨局卡死的問題**
找到 `function turnRedo()` 並將其**完全替換**為以下邏輯(新增了 `isNextOpening` 判定):
```javascript
function turnRedo() {
disableAI(); // 取消AI高亮
// 在開局階段,或者是重做堆疊的第一個是開局階段 (例如從結算畫面進入下一局)
let isNextOpening = historyRedoStack.length > 0 && !historyRedoStack[historyRedoStack.length - 1].gameplayActive && !historyRedoStack[historyRedoStack.length - 1].cubesData.some(c => c.score >= 6);
if ((!gameplayActive && !cubes.some(c => c.userData.score >= 6)) || isNextOpening) {
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 && snap.cubesData.some(c => c.score >= 6)) {
targetIdx = i;
break;
}
if (snap.gameplayActive && snap.walkCount === 1 && snap.subPhase === 'walk') {
if (!gameplayActive) {
break; // 遊戲若已結束無法再往前重做回合
} else if (walkCount > 1 || markers.some(m => m.position.y === 6)) {
// 處於回合中途,重做到「下一回合」的起點
if (snap.currentPlayer !== currentPlayer) {
targetIdx = i;
break;
}
} else {
// 處於回合起點,重做到「下一回合」的起點
if (snap.currentPlayer !== currentPlayer) {
targetIdx = i;
break;
}
}
}
}
if (targetIdx === -1) return;
historyUndoStack.push(saveSnapshot());
while (historyRedoStack.length > targetIdx + 1) {
historyUndoStack.push(historyRedoStack.pop());
}
restoreSnapshot(historyRedoStack.pop());
updateUndoRedoButtons();
}
```
---
### 二、修復勝利紀錄並改寫結算畫面 UI
找到 `function triggerRoundEnd()`,將裡面的這段範圍進行修改,確保勝利行動在快照前先一步合併進歷史紀錄,同時刪除「導出棋譜」按鈕並精簡第二局結算畫面:
```javascript
function triggerRoundEnd() {
gameplayActive = false;
// 【修復】將最後獲勝那一回合的行動陣列合併並推入歷史紀錄
if (currentTurnCoords.length > 0) {
globalMoveHistory.push(currentTurnCoords.join(''));
currentTurnCoords = [];
}
let lastSnap = historyUndoStack[historyUndoStack.length - 1];
let isAlreadySaved = lastSnap && lastSnap.gameplayActive === false && lastSnap.cubes.some(c => c.score >= 6);
if (!isAlreadySaved) {
pushAction();
}
resetAIState();
clearMarkers();
const blueFinal = cubes.find(q => q.userData.color === 'blue').userData.score;
const orangeFinal = cubes.find(q => q.userData.color === 'green').userData.score;
blueTotalScore += blueFinal;
orangeTotalScore += orangeFinal;
let roundWinner = blueFinal > orangeFinal ? '藍方' : orangeFinal > 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}`;
const title = document.createElement('h2');
title.style.fontSize = '36px';
title.innerText = titleText;
const btnMin = document.createElement('div');
btnMin.style.cssText = 'position:absolute; top:20px; right:20px; cursor:pointer; color:#fff;';
btnMin.innerHTML =
'<svg viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M6 19h12v2H6v-2z"/></svg>';
btnMin.onclick = () => minimizeOverlay(overlay, titleText);
overlay.appendChild(btnMin);
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);
});
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 {
// 【修改】第二局結束直接顯示總分與重新開始按鈕
let ultimateWinner = blueTotalScore > orangeTotalScore ? '藍方' : orangeTotalScore > blueTotalScore ? '綠方' : '平手';
if (window.currentLang === 'en') {
ultimateWinner = ultimateWinner === '藍方' ? 'Blue' : ultimateWinner === '綠方' ? 'Green' : 'Draw';
}
const totalScoreInfo = document.createElement('p');
totalScoreInfo.style.fontSize = '26px';
totalScoreInfo.style.margin = '20px 0';
totalScoreInfo.innerText = window.currentLang === 'zh'
? `最終贏家:${ultimateWinner} | 總分 - 藍方:${blueTotalScore} 綠方:${orangeTotalScore}`
: `Ultimate Winner: ${ultimateWinner} | Total - Blue: ${blueTotalScore} Green: ${orangeTotalScore}`;
overlay.appendChild(totalScoreInfo);
const btnRestartAll = createOverlayButton(tMsg('重新開始整個遊戲'), () => {
globalMoveHistory.push('1'); // 推入終局標記
document.body.removeChild(overlay);
gameRound = 1;
blueTotalScore = 0;
orangeTotalScore = 0;
reinitNextRound(false);
});
overlay.appendChild(btnRestartAll);
}
// 已移除 overlay.appendChild(btnDownload);
document.body.appendChild(overlay);
}
```
接著,在 **`showFinalGameSummary()`** 裡面也要刪除這兩行「導出按鈕」的相關代碼:
```javascript
// 刪除以下代碼:
// const btnDownloadFinal = createOverlayButton(tMsg('導出當前棋譜'), () => {
// document.getElementById('btn-io-trigger').click();
// });
// overlay.appendChild(btnDownloadFinal);
```
---
### 三、修正語言切換問題
找到 `doMCTSAI()` 函數,將其中的 `winRateText` 賦值邏輯改為基於 `window.currentLang` 來顯示:
```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) {
// ...
```