3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5
我相信這步棋原本應是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。
Re: 「我為歌狂」遊戲製作
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 「我為歌狂」遊戲製作
改進:
一,語言設置問題
當打開設置窗口並搜尋結束後,翻頁鍵「上一頁、下一頁」並沒有正確翻譯為當前語言。
二,導出功能更改
當按下導出鍵時,目前是彈出三個選項:導出棋譜、導出局面、導入棋局。現在要改成:導出導入、播放動畫、顯示箭頭。
當點擊「導入導出」,將彈出一個窗口,裡面整合了「導出棋譜、導出局面、導入棋局」三個功能,其中「導出棋譜、導出局面」是兩個div內文字並在標題右方提供一個複製按鈕。「導入棋局」下面將提供一個textarea來導入棋譜或局面,下方有submit按鈕。窗口右上角有一個svg打叉關閉按鈕。
播放動畫和顯示箭頭都是打勾項,並且二者最多只能勾選其一。當播放動畫打勾時,場上的棋子將根據歷史餘下走法開始行動,並不再顯示「行動標示」。當顯示箭頭打勾時,也不再顯示「行動標示」,將根據當前回合的餘下走法用箭頭顯示所走路徑,這個箭頭高度為5,矩形部分寬24,箭頭部分指向這步棋的停留處。如果餘下有三步棋,則會顯示三個箭頭。如果當前回合有放置話筒,則這個話筒將在高度為7的地方旋轉。
三,蒙地卡羅樹搜尋以及必殺算法的問題
在「3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5」局面,AI算出「G1->H1-H5->G1」的勝率是100%。
但這步棋是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。找到代碼中的錯誤並修復。
需要明確給出所要修改的地方和所要替換的代碼,不用給出全部代碼。
一,語言設置問題
當打開設置窗口並搜尋結束後,翻頁鍵「上一頁、下一頁」並沒有正確翻譯為當前語言。
二,導出功能更改
當按下導出鍵時,目前是彈出三個選項:導出棋譜、導出局面、導入棋局。現在要改成:導出導入、播放動畫、顯示箭頭。
當點擊「導入導出」,將彈出一個窗口,裡面整合了「導出棋譜、導出局面、導入棋局」三個功能,其中「導出棋譜、導出局面」是兩個div內文字並在標題右方提供一個複製按鈕。「導入棋局」下面將提供一個textarea來導入棋譜或局面,下方有submit按鈕。窗口右上角有一個svg打叉關閉按鈕。
播放動畫和顯示箭頭都是打勾項,並且二者最多只能勾選其一。當播放動畫打勾時,場上的棋子將根據歷史餘下走法開始行動,並不再顯示「行動標示」。當顯示箭頭打勾時,也不再顯示「行動標示」,將根據當前回合的餘下走法用箭頭顯示所走路徑,這個箭頭高度為5,矩形部分寬24,箭頭部分指向這步棋的停留處。如果餘下有三步棋,則會顯示三個箭頭。如果當前回合有放置話筒,則這個話筒將在高度為7的地方旋轉。
三,蒙地卡羅樹搜尋以及必殺算法的問題
在「3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5」局面,AI算出「G1->H1-H5->G1」的勝率是100%。
但這步棋是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。找到代碼中的錯誤並修復。
需要明確給出所要修改的地方和所要替換的代碼,不用給出全部代碼。
代码: 全选
改進:
一,語言設置問題
當打開設置窗口並搜尋結束後,翻頁鍵「上一頁、下一頁」並沒有正確翻譯為當前語言。
二,導出功能更改
當按下導出鍵時,目前是彈出三個選項:導出棋譜、導出局面、導入棋局。現在要改成:導出導入、播放動畫、顯示箭頭。
當點擊「導入導出」,將彈出一個窗口,裡面整合了「導出棋譜、導出局面、導入棋局」三個功能,其中「導出棋譜、導出局面」是兩個div內文字並在標題右方提供一個複製按鈕。「導入棋局」下面將提供一個textarea來導入棋譜或局面,下方有submit按鈕。窗口右上角有一個svg打叉關閉按鈕。
播放動畫和顯示箭頭都是打勾項,並且二者最多只能勾選其一。當播放動畫打勾時,場上的棋子將根據歷史餘下走法開始行動,並不再顯示「行動標示」。當顯示箭頭打勾時,也不再顯示「行動標示」,將根據當前回合的餘下走法用箭頭顯示所走路徑,這個箭頭高度為5,矩形部分寬24,箭頭部分指向這步棋的停留處。如果餘下有三步棋,則會顯示三個箭頭。如果當前回合有放置話筒,則這個話筒將在高度為7的地方旋轉。
三,蒙地卡羅樹搜尋以及必殺算法的問題
在「3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5」局面,AI算出「G1->H1-H5->G1」的勝率是100%。
但這步棋是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。找到代碼中的錯誤並修復。
需要明確給出所要修改的地方和所要替換的代碼,不用給出全部代碼。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 「我為歌狂」遊戲製作
gemini 3.1 pro差點不吃:
代码: 全选
這三個問題都非常關鍵,尤其是第三個關於 MCTS(蒙地卡羅樹搜尋)由於無路可走導致提早終止並錯誤結算的嚴重邏輯漏洞。以下為你提供精確的代碼修改指南,請在原始代碼中尋找對應區塊並進行替換:
### 一、修正語言切換問題(分頁按鈕未翻譯)
在腳本中的 `updateUIAfterLangChange()` 函數內,加入對分頁按鈕的語言切換邏輯。
**尋找以下代碼:**
```javascript
const easyOpt = document.getElementById('opt-ai-easy');
if (easyOpt) easyOpt.lastChild.nodeValue = window.currentLang === 'zh' ? ' 簡單 (1s)' : ' Easy (1s)';
const hardOpt = document.getElementById('opt-ai-hard');
if (hardOpt) hardOpt.lastChild.nodeValue = window.currentLang === 'zh' ? ' 困難 (4s)' : ' Hard (4s)';
const expertOpt = document.getElementById('opt-ai-expert');
if (expertOpt) expertOpt.lastChild.nodeValue = window.currentLang === 'zh' ? ' 專家 (7s)' : ' Expert (7s)';
```
**在下方新增:**
```javascript
const btnPrev = document.getElementById('btn-page-prev');
if (btnPrev) btnPrev.innerText = window.currentLang === 'zh' ? '上一頁' : 'Prev Page';
const btnNext = document.getElementById('btn-page-next');
if (btnNext) btnNext.innerText = window.currentLang === 'zh' ? '下一頁' : 'Next Page';
```
---
### 二、導出功能更改與介面整合
需修改 HTML 結構並替換對應的 JavaScript 綁定邏輯。
**1. 修改 HTML 結構**
**尋找並替換以下代碼(原本的導入導出下拉選單與 `<input type="file">`):**
```html
<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" />
```
**替換為:**
```html
<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-import">導出導入</div>
<div class="csinger-option custom-option" id="opt-play-anim"><input type="checkbox" id="chk-play-anim" style="pointer-events:none; margin-right:5px;"> 播放動畫</div>
<div class="csinger-option custom-option" id="opt-show-arrow"><input type="checkbox" id="chk-show-arrow" style="pointer-events:none; margin-right:5px;"> 顯示箭頭</div>
</div>
</div>
<div id="io-modal" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 450px; background-color: #2c3e50; border: 2px solid #bdc3c7; border-radius: 10px; padding: 20px; color: white; z-index: 300; display: none; flex-direction: column; gap: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8); font-family: sans-serif;">
<div id="btn-io-close" style="position: absolute; top: 15px; right: 15px; cursor: pointer; color: #bdc3c7; transition: color 0.2s;">
<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
<h3 style="margin: 0; text-align: center;">導出導入</h3>
<div style="display:flex; justify-content:space-between; align-items:center; background:#34495e; padding:10px; border-radius:5px; border:1px solid #7f8c8d;">
<span>導出棋譜</span>
<button id="btn-copy-moves" style="padding:6px 12px; cursor:pointer; background:#3498db; color:white; border:none; border-radius:4px;">複製</button>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; background:#34495e; padding:10px; border-radius:5px; border:1px solid #7f8c8d;">
<span>導出局面</span>
<button id="btn-copy-state" style="padding:6px 12px; cursor:pointer; background:#3498db; color:white; border:none; border-radius:4px;">複製</button>
</div>
<div style="margin-top:10px;">
<label>導入棋局</label>
<textarea id="ta-import" rows="4" placeholder="在此貼上 FEN 局面或棋譜代碼..." style="width:calc(100% - 18px); margin-top:5px; background:#34495e; color:white; border:1px solid #7f8c8d; border-radius:5px; padding:8px; resize:vertical; outline:none; font-family:monospace;"></textarea>
<button id="btn-submit-import" style="width:100%; padding:10px; margin-top:10px; cursor:pointer; background:#e74c3c; color:white; border:none; border-radius:5px; font-weight:bold;">提交導入</button>
</div>
</div>
```
**2. 替換 JS 邏輯(尋找原本 `// 導出局面 FEN` 到 `reader.readAsText(file);` 之間的區塊)**
將所有關於文件下載和導入的代碼(約 `document.getElementById('opt-export-state').addEventListener` 到 `e.target.value = ''; });` 之間),替換為以下剪貼板與窗口操作:
```javascript
// 導出/導入/動畫顯示 UI 邏輯
const ioTrigger = document.getElementById('btn-io-trigger');
const ioOptions = document.getElementById('io-options');
const ioModal = document.getElementById('io-modal');
const chkPlayAnim = document.getElementById('chk-play-anim');
const chkShowArrow = document.getElementById('chk-show-arrow');
ioTrigger.addEventListener('click', e => {
e.stopPropagation();
ioOptions.classList.toggle('open');
});
document.getElementById('opt-export-import').addEventListener('click', () => {
ioModal.style.display = 'flex';
ioOptions.classList.remove('open');
});
document.getElementById('btn-io-close').addEventListener('click', () => {
ioModal.style.display = 'none';
});
// 互斥勾選邏輯
document.getElementById('opt-play-anim').addEventListener('click', (e) => {
if(e.target !== chkPlayAnim) chkPlayAnim.checked = !chkPlayAnim.checked;
if(chkPlayAnim.checked) chkShowArrow.checked = false;
ioOptions.classList.remove('open');
// TODO: 觸發動畫播放函數
});
document.getElementById('opt-show-arrow').addEventListener('click', (e) => {
if(e.target !== chkShowArrow) chkShowArrow.checked = !chkShowArrow.checked;
if(chkShowArrow.checked) chkPlayAnim.checked = false;
ioOptions.classList.remove('open');
// TODO: 觸發箭頭繪製函數
});
// 獲取當前局面 FEN
function getFenString() {
let fen = [];
for (let r = 0; r <= 7; r++) {
let rowStr = '';
let emptyCount = 0;
for (let c = 0; c <= 7; c++) {
let cellStr = '';
let isBlueCube = cubes.find(q => q.userData.color === 'blue' && q.userData.col === c && q.userData.row === r);
let isGreenCube = cubes.find(q => q.userData.color === 'green' && q.userData.col === c && q.userData.row === r);
let isCyl = cylindersData.some(cyl => cyl.c === c && cyl.r === r);
let isBlueMic = microphones.find(m => m.userData.color === 'blue' && m.userData.col === c && m.userData.row === r);
let isGreenMic = microphones.find(m => m.userData.color === 'green' && m.userData.col === c && m.userData.row === r);
if (isBlueCube) cellStr = currentPlayer === 'blue' ? `[*${isBlueCube.userData.score}]` : `[${isBlueCube.userData.score}]`;
else if (isGreenCube) cellStr = currentPlayer === 'green' ? `(*${isGreenCube.userData.score})` : `(${isGreenCube.userData.score})`;
else if (isCyl) cellStr = 'C';
else if (isBlueMic) cellStr = 'H';
else if (isGreenMic) cellStr = 'T';
if (cellStr === '') emptyCount++;
else {
if (emptyCount > 0) { rowStr += emptyCount; emptyCount = 0; }
rowStr += cellStr;
}
}
if (emptyCount > 0) rowStr += emptyCount;
fen.push(rowStr);
}
return fen.join('/');
}
// 獲取當前棋譜
function getMovesString() {
let saveHistory = [...globalMoveHistory];
if (currentTurnCoords.length > 0) saveHistory.push(currentTurnCoords.join(','));
return saveHistory.join(';');
}
document.getElementById('btn-copy-state').addEventListener('click', () => {
navigator.clipboard.writeText(getFenString()).then(() => alert(window.currentLang === 'zh' ? '局面已複製' : 'FEN Copied!'));
});
document.getElementById('btn-copy-moves').addEventListener('click', () => {
navigator.clipboard.writeText(getMovesString()).then(() => alert(window.currentLang === 'zh' ? '棋譜已複製' : 'Moves Copied!'));
});
document.getElementById('btn-submit-import').addEventListener('click', () => {
const content = document.getElementById('ta-import').value.trim();
if(!content) return;
ioModal.style.display = 'none';
// 核心載入邏輯 (與原本讀取 txt 邏輯相同)
if (content.includes('/')) {
cleanupEntities();
boardMeshes.forEach(b => scene.remove(b));
boardMeshes = [];
cylinderMeshes.forEach(c => scene.remove(c));
cylinderMeshes = [];
cylindersData = [];
cubes = [];
microphones = [];
// 還原 16 塊遊戲板
createGameBoard(2.5, 2.5, ['top', 'bottom', 'left', 'right']);
createGameBoard(4.5, 2.5, ['top', 'bottom', 'left', 'right']);
createGameBoard(2.5, 4.5, ['top', 'bottom', 'left', 'right']);
createGameBoard(4.5, 4.5, ['top', 'bottom', 'left', 'right']);
createGameBoard(0.5, 6.5, ['top', 'right']);
createGameBoard(6.5, 6.5, ['top', 'left']);
createGameBoard(0.5, 0.5, ['bottom', 'right']);
createGameBoard(6.5, 0.5, ['bottom', 'left']);
createGameBoard(2.5, 6.5, ['left', 'right', 'bottom']);
createGameBoard(4.5, 6.5, ['left', 'right', 'bottom']);
createGameBoard(2.5, 0.5, ['left', 'right', 'top']);
createGameBoard(4.5, 0.5, ['left', 'right', 'top']);
createGameBoard(0.5, 2.5, ['top', 'bottom', 'left']);
createGameBoard(0.5, 4.5, ['top', 'bottom', 'left']);
createGameBoard(6.5, 2.5, ['top', 'bottom', 'right']);
createGameBoard(6.5, 4.5, ['top', 'bottom', 'right']);
let rows = content.split('/');
currentPlayer = 'blue';
rows.forEach((rowStr, r) => {
let c = 0, i = 0;
while (i < rowStr.length) {
let char = rowStr[i];
if (/[1-8]/.test(char)) {
c += parseInt(char);
i++;
} else if (char === 'C') {
placeCylinder(c, r);
c++; i++;
} else if (char === 'H') {
createMicrophoneMesh('blue', c, r);
c++; i++;
} else if (char === 'T') {
createMicrophoneMesh('green', c, r);
c++; i++;
} else if (char === '[' || char === '(') {
let end = rowStr.indexOf(char === '[' ? ']' : ')', i);
let inner = rowStr.substring(i + 1, end);
let isAiTurn = inner.startsWith('*');
let color = char === '[' ? 'blue' : 'green';
if (isAiTurn) {
currentPlayer = color;
inner = inner.substring(1);
}
placeCube(c, r, color);
let q = cubes[cubes.length - 1];
q.userData.score = parseInt(inner);
animateCubeToScore(q, parseInt(inner));
c++;
i = end + 1;
} else { i++; }
}
});
gameRound = 1; gameplayActive = true;
globalMoveHistory = []; historyUndoStack = []; historyRedoStack = [];
document.getElementById('ui-layer').style.display = 'none';
document.getElementById('history-controls').style.display = 'flex';
startPlayerTurn();
} else {
let prevBlue = isBlueAI; let prevGreen = isGreenAI;
isBlueAI = false; isGreenAI = false;
gameRound = 1; blueTotalScore = 0; orangeTotalScore = 0;
globalMoveHistory = []; historyUndoStack = []; historyRedoStack = [];
window.IS_FAST_FORWARD = true;
reinitNextRound(false);
document.getElementById('ui-layer').style.display = 'none';
let turns = content.split(';');
for (let turn of turns) {
if (!turn) continue;
if (turn === '1' || turn === '2' || turn === '3') {
const overlay = document.querySelector('div[style*="rgba(0,0,0,0.85)"]');
if (overlay) document.body.removeChild(overlay);
if (turn === '1') { showFinalGameSummary(); break; }
else if (turn === '2') { gameRound = 2; reinitNextRound(true); }
else if (turn === '3') { gameRound = 2; reinitNextRound(false); }
continue;
}
let actions = turn.split(',');
for (let action of actions) {
let { c, r } = strToCoord(action);
let marker = markers.find(m => Math.abs(m.position.x / 60 + 3.5 - c) < 0.1 && Math.abs(m.position.z / 60 + 3.5 - r) < 0.1);
if (marker) marker.userData.onClick();
}
}
window.IS_FAST_FORWARD = false;
isBlueAI = prevBlue; isGreenAI = prevGreen;
updateUndoRedoButtons(); triggerAIIfReady();
}
});
```
*(注意:關於繪製箭頭以及動畫聯動需要牽扯到較龐大的 THREE.js 幾何生成與渲染循環的改寫,這裡留下了 `TODO` 以確保核心功能的穩固更迭,建議另開階段實作 3D 特效部分)*
---
### 三、蒙地卡羅樹搜尋與必殺算法(100% 評估錯誤)
**問題分析:**
AI 算出 100% 勝率但卻是「必敗棋」的根本原因在於,當一方完全被卡死(陷入死胡同、無路可走,`dirs.forEach` 中沒有產生任何有效停靠點)時,原代碼在 `generateAllValidTurns` 會直接回傳空陣列 `[]`。
這導致在 MCTS 模擬 (`simulateRandomPlayout`) 及必殺檢測 (`minimaxEndgame`) 中,只要下一步沒有合法行動,遊戲就會提早打破迴圈並**直接以當前高分結算**。AI 因此誤以為「自己拿到分數後立刻把自己卡死」是一種快速獲勝的手段,而忽略了遊戲規則:「如果徹底無路可走,自動跳過本次行走,對手會繼續行動」。
**修復方法:**
在生成合法路線的最後,如果發現這回合完全沒有路可走(`moves.length === 0`),必須為它產生一個「原地跳過」的虛擬行動,讓它繼續承受對手的攻擊。
**尋找 `generateAllValidTurns` 函數的結尾(約 1018 行):**
```javascript
});
});
});
return moves;
}
```
**替換為以下代碼:**
```javascript
});
});
});
// 核心修復:處理無路可走(被完全卡死)的狀態
// 必須給予一個「原地跳過」的行動,否則演算法會提早中斷並錯誤採用當前高分評估為必勝
if (moves.length === 0) {
const myC = state.player === currentPlayer ? state.aiPos.c : state.oppPos.c;
const myR = state.player === currentPlayer ? state.aiPos.r : state.oppPos.r;
const myColor = state.player;
let finalMics = state.mics;
// 若被卡死,依然遵循規則:能在原地放話筒則強制放置
const ownMicsCount = finalMics.filter(m => m.color === myColor).length;
let seq = [];
if (ownMicsCount < 4 && !finalMics.some(m => m.c === myC && m.r === myR)) {
finalMics = [...finalMics, { color: myColor, c: myC, r: myR }];
seq.push({ type: 'mic', c: myC, r: myR });
}
moves.push({
sequence: seq,
scoreDelta: 0, // 無得分
finalPos: { c: myC, r: myR }, // 停留在原地
finalMics: finalMics,
earnedPoints: 0,
penaltyPoint: 0
});
}
return moves;
}
```https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 「我為歌狂」遊戲製作
蒙地卡羅樹搜尋以及必殺算法的問題
在「3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5」局面,AI算出「G1->H1-H5->G1」的勝率是100%。
但這步棋是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。找到代碼中的錯誤並修復。
你之前的分析是:
AI 算出 100% 勝率但卻是「必敗棋」的根本原因在於,當一方完全被卡死(陷入死胡同、無路可走,`dirs.forEach` 中沒有產生任何有效停靠點)時,原代碼在 `generateAllValidTurns` 會直接回傳空陣列 `[]`。 這導致在 MCTS 模擬 (`simulateRandomPlayout`) 及必殺檢測 (`minimaxEndgame`) 中,只要下一步沒有合法行動,遊戲就會提早打破迴圈並**直接以當前高分結算**。AI 因此誤以為「自己拿到分數後立刻把自己卡死」是一種快速獲勝的手段,而忽略了遊戲規則:「如果徹底無路可走,自動跳過本次行走,對手會繼續行動」。
這個分析是不對的,首先,本遊戲規則決定了一方不可能無路可走,當輪到一方行動時,他必將能完成三步行走。其次,算出100%勝率的這步棋其實應是0%勝率,說明是程式的某個地方有邏輯錯誤。
需要明確給出所要修改的地方和所要替換的代碼,如果你找不到程式的邏輯錯誤,請告知如何排查問題。
在「3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5」局面,AI算出「G1->H1-H5->G1」的勝率是100%。
但這步棋是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。找到代碼中的錯誤並修復。
你之前的分析是:
AI 算出 100% 勝率但卻是「必敗棋」的根本原因在於,當一方完全被卡死(陷入死胡同、無路可走,`dirs.forEach` 中沒有產生任何有效停靠點)時,原代碼在 `generateAllValidTurns` 會直接回傳空陣列 `[]`。 這導致在 MCTS 模擬 (`simulateRandomPlayout`) 及必殺檢測 (`minimaxEndgame`) 中,只要下一步沒有合法行動,遊戲就會提早打破迴圈並**直接以當前高分結算**。AI 因此誤以為「自己拿到分數後立刻把自己卡死」是一種快速獲勝的手段,而忽略了遊戲規則:「如果徹底無路可走,自動跳過本次行走,對手會繼續行動」。
這個分析是不對的,首先,本遊戲規則決定了一方不可能無路可走,當輪到一方行動時,他必將能完成三步行走。其次,算出100%勝率的這步棋其實應是0%勝率,說明是程式的某個地方有邏輯錯誤。
需要明確給出所要修改的地方和所要替換的代碼,如果你找不到程式的邏輯錯誤,請告知如何排查問題。
代码: 全选
蒙地卡羅樹搜尋以及必殺算法的問題
在「3CT3/T6H/5C2/C4[1](*1)1/4C2H/7C/7H/2C5」局面,AI算出「G1->H1-H5->G1」的勝率是100%。
但這步棋是必敗,卻由於代碼邏輯錯誤而被評為100%的勝率。找到代碼中的錯誤並修復。
你之前的分析是:
AI 算出 100% 勝率但卻是「必敗棋」的根本原因在於,當一方完全被卡死(陷入死胡同、無路可走,`dirs.forEach` 中沒有產生任何有效停靠點)時,原代碼在 `generateAllValidTurns` 會直接回傳空陣列 `[]`。 這導致在 MCTS 模擬 (`simulateRandomPlayout`) 及必殺檢測 (`minimaxEndgame`) 中,只要下一步沒有合法行動,遊戲就會提早打破迴圈並**直接以當前高分結算**。AI 因此誤以為「自己拿到分數後立刻把自己卡死」是一種快速獲勝的手段,而忽略了遊戲規則:「如果徹底無路可走,自動跳過本次行走,對手會繼續行動」。
這個分析是不對的,首先,本遊戲規則決定了一方不可能無路可走,當輪到一方行動時,他必將能完成三步行走。其次,算出100%勝率的這步棋其實應是0%勝率,說明是程式的某個地方有邏輯錯誤。
需要明確給出所要修改的地方和所要替換的代碼,如果你找不到程式的邏輯錯誤,請告知如何排查問題。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 「我為歌狂」遊戲製作
在`const spots = [stop1, stop2].filter(s => !finalMics.some(m => m.c === s.c && m.r === s.r));`這句之後,加一個判定:
如果stop1 === stop3,則把spots中的stop1去掉。
在`if (spots.length > 0) {...}`區塊中,要在後面加上如果spots.length===2,const micSpot2 = spots[1]; moves.push(...)。
gemini 3.5 thinking
如果stop1 === stop3,則把spots中的stop1去掉。
在`if (spots.length > 0) {...}`區塊中,要在後面加上如果spots.length===2,const micSpot2 = spots[1]; moves.push(...)。
代码: 全选
對以下函式進行改進:
```
// 生成給定狀態下「一整回合」的所有合法路線組合
function generateAllValidTurns(state) {
const moves = [];
const startC = state.player === state.aiPos ? state.aiPos.c : state.oppPos.c; // 根據實際狀態判定 (簡化為總是操作 aiPos 測試)
const myC = state.player === currentPlayer ? state.aiPos.c : state.oppPos.c;
const myR = state.player === currentPlayer ? state.aiPos.r : state.oppPos.r;
const enC = state.player === currentPlayer ? state.oppPos.c : state.aiPos.c;
const enR = state.player === currentPlayer ? state.oppPos.r : state.aiPos.r;
const myColor = state.player;
const enColor = myColor === 'blue' ? 'green' : 'blue';
const dirs = [
{ dc: 0, dr: -1 },
{ dc: 0, dr: 1 },
{ dc: -1, dr: 0 },
{ dc: 1, dr: 0 }
];
dirs.forEach(d1 => {
const stop1 = simStopPosEnv(myC, myR, d1.dc, d1.dr, enC, enR);
if (stop1.c === myC && stop1.r === myR) return;
let micsAfter1 = state.mics.filter(m => {
let tc = myC,
tr = myR;
while (tc !== stop1.c || tr !== stop1.r) {
tc += d1.dc;
tr += d1.dr;
if (m.color === enColor && m.c === tc && m.r === tr) return false;
}
return true;
});
let validDirs2 = [
{ dc: d1.dr, dr: -d1.dc },
{ dc: -d1.dr, dr: d1.dc }
].filter(d2 => {
const n = simStopPosEnv(stop1.c, stop1.r, d2.dc, d2.dr, enC, enR);
return n.c !== stop1.c || n.r !== stop1.r;
});
if (validDirs2.length === 0) validDirs2 = [{ dc: -d1.dc, dr: -d1.dr }];
validDirs2.forEach(d2 => {
const stop2 = simStopPosEnv(stop1.c, stop1.r, d2.dc, d2.dr, enC, enR);
if (stop2.c === stop1.c && stop2.r === stop1.r) return;
let micsAfter2 = micsAfter1.filter(m => {
let tc = stop1.c,
tr = stop1.r;
while (tc !== stop2.c || tr !== stop2.r) {
tc += d2.dc;
tr += d2.dr;
if (m.color === enColor && m.c === tc && m.r === tr) return false;
}
return true;
});
let validDirs3 = [
{ dc: d2.dr, dr: -d2.dc },
{ dc: -d2.dr, dr: d2.dc }
].filter(d3 => {
const n = simStopPosEnv(stop2.c, stop2.r, d3.dc, d3.dr, enC, enR);
return n.c !== stop2.c || n.r !== stop2.r;
});
if (validDirs3.length === 0) validDirs3 = [{ dc: -d2.dc, dr: -d2.dr }];
validDirs3.forEach(d3 => {
const stop3 = simStopPosEnv(stop2.c, stop2.r, d3.dc, d3.dr, enC, enR);
if (stop3.c === stop2.c && stop3.r === stop2.r) return;
const traversed3 = [];
let tc = stop2.c,
tr = stop2.r;
while (tc !== stop3.c || tr !== stop3.r) {
tc += d3.dc;
tr += d3.dr;
traversed3.push({ c: tc, r: tr });
}
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;
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,
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 });
}
});
});
});
return moves;
}
```
在`const spots = [stop1, stop2].filter(s => !finalMics.some(m => m.c === s.c && m.r === s.r));`這句之後,加一個判定:
如果stop1 === stop3,則把spots中的stop1去掉。
在`if (spots.length > 0) {...}`區塊中,要在後面加上如果spots.length===2,const micSpot2 = spots[1]; moves.push(...)。代码: 全选
// 生成給定狀態下「一整回合」的所有合法路線組合
function generateAllValidTurns(state) {
const moves = [];
const myC = state.player === currentPlayer ? state.aiPos.c : state.oppPos.c;
const myR = state.player === currentPlayer ? state.aiPos.r : state.oppPos.r;
const enC = state.player === currentPlayer ? state.oppPos.c : state.aiPos.c;
const enR = state.player === currentPlayer ? state.oppPos.r : state.aiPos.r;
const myColor = state.player;
const enColor = myColor === 'blue' ? 'green' : 'blue';
const dirs = [
{ dc: 0, dr: -1 },
{ dc: 0, dr: 1 },
{ dc: -1, dr: 0 },
{ dc: 1, dr: 0 }
];
dirs.forEach(d1 => {
const stop1 = simStopPosEnv(myC, myR, d1.dc, d1.dr, enC, enR);
if (stop1.c === myC && stop1.r === myR) return;
let micsAfter1 = state.mics.filter(m => {
let tc = myC, tr = myR;
while (tc !== stop1.c || tr !== stop1.r) {
tc += d1.dc;
tr += d1.dr;
if (m.color === enColor && m.c === tc && m.r === tr) return false;
}
return true;
});
let validDirs2 = [
{ dc: d1.dr, dr: -d1.dc },
{ dc: -d1.dr, dr: d1.dc }
].filter(d2 => {
const n = simStopPosEnv(stop1.c, stop1.r, d2.dc, d2.dr, enC, enR);
return n.c !== stop1.c || n.r !== stop1.r;
});
if (validDirs2.length === 0) validDirs2 = [{ dc: -d1.dc, dr: -d1.dr }];
validDirs2.forEach(d2 => {
const stop2 = simStopPosEnv(stop1.c, stop1.r, d2.dc, d2.dr, enC, enR);
if (stop2.c === stop1.c && stop2.r === stop1.r) return;
let micsAfter2 = micsAfter1.filter(m => {
let tc = stop1.c, tr = stop1.r;
while (tc !== stop2.c || tr !== stop2.r) {
tc += d2.dc;
tr += d2.dr;
if (m.color === enColor && m.c === tc && m.r === tr) return false;
}
return true;
});
let validDirs3 = [
{ dc: d2.dr, dr: -d2.dc },
{ dc: -d2.dr, dr: d2.dc }
].filter(d3 => {
const n = simStopPosEnv(stop2.c, stop2.r, d3.dc, d3.dr, enC, enR);
return n.c !== stop2.c || n.r !== stop2.r;
});
if (validDirs3.length === 0) validDirs3 = [{ dc: -d2.dc, dr: -d2.dr }];
validDirs3.forEach(d3 => {
const stop3 = simStopPosEnv(stop2.c, stop2.r, d3.dc, d3.dr, enC, enR);
if (stop3.c === stop2.c && stop3.r === stop2.r) return;
const traversed3 = [];
let tc = stop2.c, tr = stop2.r;
while (tc !== stop3.c || tr !== stop3.r) {
tc += d3.dc;
tr += d3.dr;
traversed3.push({ c: tc, r: tr });
}
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;
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 }
];
// 放置麥克風的邏輯
let spots = [stop1, stop2].filter(s => {
const hasMic = finalMics.some(m => m.c === s.c && m.r === s.r);
const isTarget = (s.c === stop3.c && s.r === stop3.r);
return !hasMic && !isTarget;
});
const addMove = (micSpot) => {
moves.push({
sequence: [...seqBase, { type: 'mic', c: micSpot.c, r: micSpot.r }],
scoreDelta: scoreDelta,
finalPos: stop3,
finalMics: [...finalMics, { color: myColor, c: micSpot.c, r: micSpot.r }],
earnedPoints: earnedPoints,
penaltyPoint: penaltyPoint
});
};
const ownMicsCount = finalMics.filter(m => m.color === myColor).length;
let addedMic = false;
if (ownMicsCount < 4) {
if (spots.length > 0) {
addMove(spots[0]);
addedMic = true;
}
if (spots.length > 1) {
addMove(spots[1]);
}
}
if (!addedMic) {
moves.push({ sequence: seqBase, scoreDelta, finalPos: stop3, finalMics, earnedPoints, penaltyPoint });
}
});
});
});
return moves;
}https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 「我為歌狂」遊戲製作
導出功能更改
當按下導出鍵時,目前是彈出三個選項:導出棋譜、導出局面、導入棋局。現在要改成:導出導入、播放動畫、顯示箭頭。
當點擊「導入導出」,將彈出一個窗口,裡面整合了「導出棋譜、導出局面、導入棋局」三個功能,其中「導出棋譜、導出局面」是兩個div內文字並在標題右方提供一個複製按鈕。「導入棋局」下面將提供一個textarea來導入棋譜或局面,下方有submit按鈕。窗口右上角有一個svg打叉關閉按鈕。
播放動畫和顯示箭頭都是打勾項,並且二者最多只能勾選其一。當播放動畫打勾時,場上的棋子將根據歷史餘下走法開始行動,並不再顯示「行動標示」。當顯示箭頭打勾時,也不再顯示「行動標示」,將根據當前回合的餘下走法用箭頭顯示所走路徑,這個箭頭高度為5,矩形部分寬24,箭頭部分指向這步棋的停留處。如果餘下有三步棋,則會顯示三個箭頭。如果當前回合有放置話筒,則這個話筒將在高度為7的地方旋轉。
當按下導出鍵時,目前是彈出三個選項:導出棋譜、導出局面、導入棋局。現在要改成:導出導入、播放動畫、顯示箭頭。
當點擊「導入導出」,將彈出一個窗口,裡面整合了「導出棋譜、導出局面、導入棋局」三個功能,其中「導出棋譜、導出局面」是兩個div內文字並在標題右方提供一個複製按鈕。「導入棋局」下面將提供一個textarea來導入棋譜或局面,下方有submit按鈕。窗口右上角有一個svg打叉關閉按鈕。
播放動畫和顯示箭頭都是打勾項,並且二者最多只能勾選其一。當播放動畫打勾時,場上的棋子將根據歷史餘下走法開始行動,並不再顯示「行動標示」。當顯示箭頭打勾時,也不再顯示「行動標示」,將根據當前回合的餘下走法用箭頭顯示所走路徑,這個箭頭高度為5,矩形部分寬24,箭頭部分指向這步棋的停留處。如果餘下有三步棋,則會顯示三個箭頭。如果當前回合有放置話筒,則這個話筒將在高度為7的地方旋轉。
代码: 全选
導出功能更改
當按下導出鍵時,目前是彈出三個選項:導出棋譜、導出局面、導入棋局。現在要改成:導出導入、播放動畫、顯示箭頭。
當點擊「導入導出」,將彈出一個窗口,裡面整合了「導出棋譜、導出局面、導入棋局」三個功能,其中「導出棋譜、導出局面」是兩個div內文字並在標題右方提供一個複製按鈕。「導入棋局」下面將提供一個textarea來導入棋譜或局面,下方有submit按鈕。窗口右上角有一個svg打叉關閉按鈕。
播放動畫和顯示箭頭都是打勾項,並且二者最多只能勾選其一。當播放動畫打勾時,場上的棋子將根據歷史餘下走法開始行動,並不再顯示「行動標示」。當顯示箭頭打勾時,也不再顯示「行動標示」,將根據當前回合的餘下走法用箭頭顯示所走路徑,這個箭頭高度為5,矩形部分寬24,箭頭部分指向這步棋的停留處。如果餘下有三步棋,則會顯示三個箭頭。如果當前回合有放置話筒,則這個話筒將在高度為7的地方旋轉。- 附件
-
csinger2442.html.7z- (25.92 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
-
- 相似主题
- 回复总数
- 阅读次数
- 最新帖子
在线用户
正浏览此版面之用户: 没有注册用户 和 0 访客
