代码: 全选
這裡為您整理了所需修改的區塊與對應的代碼替換,這將滿足您對 UI、撤銷邏輯、AI 狀態顯示以及增加「必殺計算」的需求。
### 1. 修改 CSS(增加自定義下拉選單樣式)
請在 `<style>` 區塊內,將下列代碼加入到 `#ai-status` 或 `.ai-toggle.active-green` 的下方:
```css
/* 新增的自定義下拉選單樣式 */
.custom-select-container {
position: relative;
display: flex;
align-items: center;
}
.custom-select-trigger {
display: flex;
align-items: center;
justify-content: center;
}
.custom-select-options {
position: absolute;
bottom: 110%;
left: 50%;
transform: translateX(-50%);
background-color: #34495e;
border: 2px solid #bdc3c7;
border-radius: 6px;
display: none;
flex-direction: column;
min-width: 140px;
z-index: 100;
overflow: hidden;
}
.custom-select-options.open {
display: flex;
}
.custom-option {
padding: 10px;
display: flex;
align-items: center;
gap: 10px;
color: #fff;
cursor: pointer;
transition: background-color 0.2s;
font-size: 14px;
}
.custom-option:hover {
background-color: #4e6a85;
}
.custom-option svg {
flex-shrink: 0;
}
.ai-toggle svg {
pointer-events: none;
}
```
### 2. 修改 HTML 控制區(替換 AI 按鈕及下拉選單)
請找到 `<div id="history-controls">` 區塊中,關於 AI 控制的按鈕和 `<select>`:
**原始代碼:**
```html
<div style="width: 2px; background: #7f8c8d; margin: 0 5px"></div>
<button id="btn-ai-blue" class="hist-btn ai-toggle" style="background-color: #34495e">藍方AI</button>
<button id="btn-ai-green" class="hist-btn ai-toggle" style="background-color: #34495e">綠方AI</button>
<select id="ai-strength" class="hist-btn" style="cursor: pointer; appearance: none; text-align: center">
<option value="1200">簡單</option>
<option value="3600" selected>困難</option>
<option value="7200">專家</option>
</select>
```
**替換為:**
```html
<div style="width: 2px; background: #7f8c8d; margin: 0 5px"></div>
<button id="btn-ai-blue" class="hist-btn ai-toggle" style="background-color: #34495e" title="藍方AI">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<polygon points="12,1 21.5,6.5 21.5,17.5 12,23 2.5,17.5 2.5,6.5" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M15,10 h-6 v6 h6 z M10,8 h4 M11,6 h2 v2 h-2 z" fill="currentColor"/>
</svg>
</button>
<button id="btn-ai-green" class="hist-btn ai-toggle" style="background-color: #34495e" title="綠方AI">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<polygon points="12,2 22,20 2,20" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M15,12 h-6 v5 h6 z M10,10 h4 M11,8 h2 v2 h-2 z" fill="currentColor"/>
</svg>
</button>
<div class="custom-select-container">
<button id="ai-strength-trigger" class="hist-btn custom-select-trigger" title="AI 強度">
<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="custom-option" data-value="1200">
<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="custom-option" data-value="3600">
<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="custom-option" data-value="7200">
<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>
```
### 3. 修改視角及 UI 事件榜定初始化
請找到 JS 開頭的相機與控制代碼,以及中間綁定 AI UI 事件的部分進行替換:
**原始代碼:**
```javascript
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 500, 500);
// ...
const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
```
**替換為:**
```javascript
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 1100, 150); // 改為俯視角並稍微向Z軸偏移避開下方按鈕
// ...
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 150);
controls.enablePan = false;
```
**原始代碼:**
```javascript
const btnAiBlue = document.getElementById('btn-ai-blue');
const btnAiGreen = document.getElementById('btn-ai-green');
const selectAiStrength = document.getElementById('ai-strength');
```
**替換為:**
```javascript
const btnAiBlue = document.getElementById('btn-ai-blue');
const btnAiGreen = document.getElementById('btn-ai-green');
const aiStrengthTrigger = document.getElementById('ai-strength-trigger');
const aiStrengthOptions = document.getElementById('ai-strength-options');
```
並且在腳本最下方 `animate();` 呼叫之前,加入新的下拉選單事件,**替換掉原本的 `selectAiStrength.addEventListener('change', ...)`:**
```javascript
aiStrengthTrigger.addEventListener('click', (e) => {
e.stopPropagation();
aiStrengthOptions.classList.toggle('open');
});
document.addEventListener('click', () => {
aiStrengthOptions.classList.remove('open');
});
document.querySelectorAll('.custom-option').forEach(opt => {
opt.addEventListener('click', () => {
aiStrength = parseInt(opt.getAttribute('data-value'));
aiStrengthTrigger.innerHTML = opt.querySelector('svg').outerHTML;
aiStrengthOptions.classList.remove('open');
});
});
```
### 4. 撤銷與重做邏輯改進
找到 `updateUndoRedoButtons`, `walkUndo`, `walkRedo`, `turnUndo` 這四個函式,將它們**替換為**以下代碼,確保撤銷與回合後退行為嚴格符合您的新規則:
```javascript
function updateUndoRedoButtons() {
const wUndo = document.getElementById('btn-walk-undo');
const wRedo = document.getElementById('btn-walk-redo');
const tUndo = document.getElementById('btn-turn-undo');
const tRedo = document.getElementById('btn-turn-redo');
if (!wUndo) return;
if (!gameplayActive) {
// 【開局階段】行走撤銷與重做完全無效
wUndo.disabled = true;
wRedo.disabled = true;
tUndo.disabled = historyUndoStack.length === 0;
tRedo.disabled = historyRedoStack.length === 0;
} else {
// 【對戰階段】只能在當前行動方的回合內進行行走撤銷重做
let canWalkUndo = false;
if (historyUndoStack.length > 0) {
let lastSnap = historyUndoStack[historyUndoStack.length - 1];
if (lastSnap.currentPlayer === currentPlayer && lastSnap.gameplayActive) canWalkUndo = true;
}
wUndo.disabled = !canWalkUndo;
let canWalkRedo = false;
if (historyRedoStack.length > 0) {
let nextSnap = historyRedoStack[historyRedoStack.length - 1];
if (nextSnap.currentPlayer === currentPlayer && nextSnap.gameplayActive) canWalkRedo = true;
}
wRedo.disabled = !canWalkRedo;
// 回合後退:找到上一個回合的起始狀態,或者能退回到開局階段
let hasTurnUndo = false;
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')) {
hasTurnUndo = true;
break;
}
}
tUndo.disabled = !hasTurnUndo;
let hasTurnRedo = false;
for (let i = historyRedoStack.length - 1; i >= 0; i--) {
let snap = historyRedoStack[i];
if (snap.currentPlayer !== currentPlayer && snap.walkCount === 1 && snap.subPhase === 'walk') {
hasTurnRedo = true;
break;
}
}
tRedo.disabled = !hasTurnRedo;
}
}
function walkUndo() {
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() {
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() {
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();
}
```
### 5. 確保 AI 狀態面板直到行動結束後才關閉
找到 `finishAIAction` 以及 `triggerAIIfReady`:
**原始代碼:**
```javascript
function triggerAIIfReady() {
if (markers.length > 0 && !isAIThinking) {
const shouldRun = (currentTurnColor === 'blue' && isBlueAI) || (currentTurnColor === 'green' && isGreenAI);
if (shouldRun) {
checkAndRunAI();
}
}
}
function finishAIAction(selectedMarker, timeTaken, winRateText) {
const statusDiv = document.getElementById('ai-status');
statusDiv.innerHTML = `思考結束<br>用時: ${timeTaken}s<br>勝率: ${winRateText}`;
setTimeout(() => {
statusDiv.style.display = 'none';
document.body.style.pointerEvents = 'auto';
// ...省略...
```
**替換為:**
```javascript
function triggerAIIfReady() {
const statusDiv = document.getElementById('ai-status');
if (markers.length > 0 && !isAIThinking) {
const shouldRun = (currentTurnColor === 'blue' && isBlueAI) || (currentTurnColor === 'green' && isGreenAI);
if (shouldRun) {
checkAndRunAI();
} else {
statusDiv.style.display = 'none'; // 行動結束,輪到人類時才隱藏思考數據
}
}
}
function finishAIAction(selectedMarker, timeTaken, winRateText) {
const statusDiv = document.getElementById('ai-status');
statusDiv.innerHTML = `思考結束<br>用時: ${timeTaken}s<br>勝率: ${winRateText}`;
setTimeout(() => {
// 不在此處隱藏 statusDiv
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);
}
```
### 6. AI 演算法:計分與「深度必殺檢查」
首先,修改 `generateAllValidTurns` 和 `applyTurnToState` 來攜帶真實的加分扣分:
**在 `generateAllValidTurns` 中找到原本設定 `scoreDelta` 和 `moves.push(...)` 的區段替換:**
```javascript
// 替換這段
let earnedPoints = 0;
let penaltyPoint = 0;
micsAfter2.forEach(m => {
if (m.color === enColor && traversed3.some(t => t.c === m.c && t.r === m.r)) earnedPoints += 1;
});
if (micsAfter2.some(m => m.color === myColor && m.c === stop3.c && m.r === stop3.r)) {
penaltyPoint = 1;
}
let scoreDelta = earnedPoints * 100 - penaltyPoint * 150 + (10 - (Math.abs(stop3.c - 3.5) + Math.abs(stop3.r - 3.5))) * 2;
let finalMics = micsAfter2.filter(m => {
if (m.color === enColor && traversed3.some(t => t.c === m.c && t.r === m.r)) return false;
if (m.color === myColor && m.c === stop3.c && m.r === stop3.r) return false;
return true;
});
const seqBase = [
{ type: 'walk', c: stop1.c, r: stop1.r },
{ type: 'walk', c: stop2.c, r: stop2.r },
{ type: 'walk', c: stop3.c, r: stop3.r }
];
const ownMicsCount = finalMics.filter(m => m.color === myColor).length;
let addedMic = false;
if (ownMicsCount < 4) {
const spots = [stop1, stop2].filter(s => !finalMics.some(m => m.c === s.c && m.r === s.r));
if (spots.length > 0) {
const micSpot = spots[0];
moves.push({
sequence: [...seqBase, { type: 'mic', c: micSpot.c, r: micSpot.r }],
scoreDelta: scoreDelta + (10 - (Math.abs(micSpot.c - 3.5) + Math.abs(micSpot.r - 3.5))),
finalPos: stop3,
finalMics: [...finalMics, { color: myColor, c: micSpot.c, r: micSpot.r }],
earnedPoints: earnedPoints,
penaltyPoint: penaltyPoint
});
addedMic = true;
}
}
if (!addedMic) {
moves.push({ sequence: seqBase, scoreDelta, finalPos: stop3, finalMics, earnedPoints, penaltyPoint });
}
```
**在 `applyTurnToState` 中,加入比分追蹤:**
```javascript
function applyTurnToState(state, move) {
const nextPlayer = state.player === 'blue' ? 'green' : 'blue';
const isAI = state.player === currentPlayer;
let newAiScore = state.aiScore;
let newOppScore = state.oppScore;
if (isAI) {
newAiScore += move.earnedPoints || 0;
newOppScore += move.penaltyPoint || 0;
} else {
newOppScore += move.earnedPoints || 0;
newAiScore += move.penaltyPoint || 0;
}
return {
player: nextPlayer,
aiPos: isAI ? move.finalPos : state.aiPos,
oppPos: isAI ? state.oppPos : move.finalPos,
mics: move.finalMics,
scoreDiff: state.scoreDiff + (isAI ? move.scoreDelta : -move.scoreDelta),
aiScore: newAiScore,
oppScore: newOppScore
};
}
```
**新增必殺搜尋函數並修改 `planFullTurnActionsMCTS` :**
在 `async function planFullTurnActionsMCTS(timeLimit)` 這個函式的上方加入必殺邏輯:
```javascript
let SURE_KILL_DEPTH = 2; // 必勝計算深度
function checkSureKill(state, depth, isMaximizingPlayer) {
if (state.aiScore >= 6) return true;
if (state.oppScore >= 6) return false;
if (depth <= 0) return false;
const moves = generateAllValidTurns(state);
if (moves.length === 0) return false;
if (isMaximizingPlayer) {
for (let move of moves) {
const nextState = applyTurnToState(state, move);
if (checkSureKill(nextState, depth - 1, false)) return true;
}
return false;
} else {
for (let move of moves) {
const nextState = applyTurnToState(state, move);
if (!checkSureKill(nextState, depth - 1, true)) return false;
}
return true;
}
}
async function planFullTurnActionsMCTS(timeLimit) {
const aiColor = currentPlayer;
const oppColor = aiColor === 'blue' ? 'green' : 'blue';
const activeCube = cubes.find(q => q.userData.color === aiColor);
const oppCube = cubes.find(q => q.userData.color === oppColor);
const rootState = {
player: aiColor,
aiPos: { c: activeCube.userData.col, r: activeCube.userData.row },
oppPos: { c: oppCube ? oppCube.userData.col : -1, r: oppCube ? oppCube.userData.row : -1 },
mics: microphones.map(m => ({ color: m.userData.color, c: m.userData.col, r: m.userData.row })),
scoreDiff: 0,
aiScore: activeCube.userData.score,
oppScore: oppCube ? oppCube.userData.score : 1
};
// --- 優先進行必勝步深度檢查 ---
const initMoves = generateAllValidTurns(rootState);
for (let move of initMoves) {
const nextState = applyTurnToState(rootState, move);
if (checkSureKill(nextState, SURE_KILL_DEPTH - 1, false)) {
return move.sequence; // 觸發必殺條件,直接執行
}
}
// --------------------------------
const rootNode = new MCTSNode(null, rootState, null);
// ... (原來的MCTS while迴圈邏輯保持不變) ...
```