想學蒙地卡洛樹搜尋
发表于 : 2026年 1月 11日 23:34
代码: 全选
Conversation with Gemini
有一款遊戲,雙方輪流行動。我用js實現了它的邏輯。
boardArr是個數組,每一項是當前回合的局面情況,以及行動路徑。
當到某一方時,首先有函式winOrLose()判斷輸贏,如果未分勝負,則他將需要走至少一步。
函式whereCanGo()根據當前局面給出當前位置能去到的位置。
函式hereWeGo(n)選擇一個能去的位置並更新當前局面和行動路徑。
函式ifCanStop()判斷當前位置是否可以停下。
函式ifStopWouldLose()判斷如果在此位置停下是否將會立即輸掉。
函式hereWeStop()選擇在此停下,同時將輪到對方行動。
用以上函式做出一個蒙地卡洛樹搜尋的遊戲AI。語言為js。所有注釋使用英文和中文雙語,中文注釋開頭要有「zh:」。
這是一個非常有趣的挑戰。你的遊戲邏輯比較特殊,因為它包含了一個「多步移動」的機制(移動 -> 判斷是否停下 -> 移動... -> 停下並換人)。
為了實現蒙地卡洛樹搜尋(MCTS),我們必須將你的函式封裝在能夠**模擬(Simulate)和複製(Clone)**的狀態物件中。MCTS 最大的重點在於它不能汙染真實的遊戲局面,所以我們需要大量的「深拷貝(Deep Copy)」操作。
以下是完整的 JavaScript 實作代碼。
1. 核心概念 (Core Concepts)
在代碼中,我假設你的函式(如 whereCanGo)依賴於全域變數或當前上下文。為了讓 AI 運作,我將這些函式改寫為接受 boardArr 作為參數的形式,或者假設我們可以在模擬時切換 boardArr。
2. MCTS 實作代碼 (MCTS Implementation)
JavaScript
/**
* Helper function to deep clone the game state (boardArr).
* zh: 深拷貝遊戲狀態(boardArr)的輔助函式。
* MCTS needs to simulate future moves without changing the real game.
* zh: MCTS 需要在不改變真實遊戲的情況下模擬未來的步數。
*/
function cloneState(boardArr) {
return JSON.parse(JSON.stringify(boardArr));
}
/**
* Node class for the Monte Carlo Tree.
* zh: 蒙地卡洛樹的節點類別。
*/
class MCTSNode {
constructor(state, parent = null, action = null) {
this.state = state; // The game board at this node // zh: 此節點的遊戲局面
this.parent = parent; // Parent node // zh: 父節點
this.action = action; // The action that led to this state (e.g., move to X or Stop) // zh: 導致此局面的行動(例如移動到X或停下)
this.children = []; // Child nodes // zh: 子節點列表
this.wins = 0; // Number of wins simulating from this node // zh: 從此節點模擬獲勝的次數
this.visits = 0; // Number of times this node was visited // zh: 此節點被訪問的次數
this.untriedActions = this.getLegalActions(state); // Actions not yet expanded // zh: 尚未擴展的合法行動
}
/**
* distinct "Move" vs "Stop" actions based on user logic.
* zh: 根據使用者邏輯區分「移動」與「停下」的行動。
*/
getLegalActions(tempState) {
let actions = [];
// 1. Check where we can go (Continuous movement)
// zh: 1. 檢查可以去哪裡(連續移動)
// Assuming whereCanGo() returns an array of positions/indices
// zh: 假設 whereCanGo() 返回位置/索引的陣列
let possibleMoves = whereCanGo(tempState);
possibleMoves.forEach(pos => {
actions.push({ type: 'MOVE', payload: pos });
});
// 2. Check if we can stop (End turn)
// zh: 2. 檢查是否可以停下(結束回合)
if (ifCanStop(tempState)) {
// Optimization: Don't stop if it guarantees a loss (heuristic)
// zh: 優化:如果停下會導致必輸,則不將「停下」列為選項(啟發式)
if (!ifStopWouldLose(tempState)) {
actions.push({ type: 'STOP', payload: null });
}
}
return actions;
}
/**
* Check if the node is a leaf (no children) or terminal state.
* zh: 檢查節點是否為葉節點(無子節點)或終局狀態。
*/
isTerminal() {
return winOrLose(this.state) !== null; // Assuming returns 'Win', 'Lose', or null // zh: 假設返回 'Win', 'Lose' 或 null
}
/**
* Check if the node is fully expanded.
* zh: 檢查節點是否已完全擴展。
*/
isFullyExpanded() {
return this.untriedActions.length === 0;
}
/**
* Upper Confidence Bound (UCB1) for selection.
* zh: 用於選擇的信賴區間上界 (UCB1) 公式。
*/
getUCB1(explorationConstant = 1.41) {
if (this.visits === 0) return Infinity;
return (this.wins / this.visits) +
explorationConstant * Math.sqrt(Math.log(this.parent.visits) / this.visits);
}
}
/**
* Main MCTS Class.
* zh: MCTS 主類別。
*/
class MCTS {
constructor(iterations = 1000) {
this.iterations = iterations; // Number of simulations per move // zh: 每次行動的模擬次數
}
/**
* Execute the search and return the best action.
* zh: 執行搜尋並返回最佳行動。
*/
search(initialState) {
// Create root node
// zh: 建立根節點
let root = new MCTSNode(cloneState(initialState));
for (let i = 0; i < this.iterations; i++) {
let node = root;
// 1. Selection
// zh: 1. 選擇階段
while (node.isFullyExpanded() && !node.isTerminal()) {
node = this.selectBestChild(node);
}
// 2. Expansion
// zh: 2. 擴展階段
if (!node.isTerminal()) {
node = this.expand(node);
}
// 3. Simulation (Rollout)
// zh: 3. 模擬階段 (Rollout)
let result = this.simulate(node.state);
// 4. Backpropagation
// zh: 4. 反向傳播階段
this.backpropagate(node, result);
}
// Return the action of the most visited child
// zh: 返回被訪問次數最多的子節點的行動
let bestChild = root.children.reduce((a, b) => (a.visits > b.visits ? a : b));
return bestChild.action;
}
/**
* Select child with highest UCB1 score.
* zh: 選擇 UCB1 分數最高的子節點。
*/
selectBestChild(node) {
let bestScore = -Infinity;
let bestNodes = [];
for (let child of node.children) {
let score = child.getUCB1();
if (score > bestScore) {
bestScore = score;
bestNodes = [child];
} else if (score === bestScore) {
bestNodes.push(child);
}
}
// Randomly pick one if ties exist
// zh: 如果分數相同則隨機選擇一個
return bestNodes[Math.floor(Math.random() * bestNodes.length)];
}
/**
* Expand a new child node from untried actions.
* zh: 從未嘗試的行動中擴展一個新的子節點。
*/
expand(node) {
let action = node.untriedActions.pop();
let nextState = cloneState(node.state);
// Apply the action to the state
// zh: 將行動應用到狀態上
this.applyAction(nextState, action);
let childNode = new MCTSNode(nextState, node, action);
node.children.push(childNode);
return childNode;
}
/**
* Apply your game logic functions to a state.
* zh: 將你的遊戲邏輯函式應用到狀態上。
* Note: We pass 'state' to your functions effectively.
* zh: 注意:我們將 'state' 有效地傳遞給你的函式。
*/
applyAction(state, action) {
if (action.type === 'MOVE') {
// Call your function: hereWeGo(n)
// zh: 呼叫你的函式:hereWeGo(n)
hereWeGo(action.payload, state);
} else if (action.type === 'STOP') {
// Call your function: hereWeStop()
// zh: 呼叫你的函式:hereWeStop()
hereWeStop(state);
}
}
/**
* Simulate a random game from the current state to the end.
* zh: 從當前狀態隨機模擬遊戲直到結束。
*/
simulate(startState) {
let currentState = cloneState(startState);
let depth = 0;
// Loop until win or lose
// zh: 循環直到分出勝負
while (winOrLose(currentState) === null && depth < 100) { // Safety depth limit // zh: 安全深度限制
let legalMoves = [];
// Get moves exactly like in MCTSNode
// zh: 像在 MCTSNode 中一樣獲取移動
let moves = whereCanGo(currentState);
moves.forEach(m => legalMoves.push({ type: 'MOVE', payload: m }));
if (ifCanStop(currentState)) {
// Heuristic: Avoid stopping if it means losing immediately
// zh: 啟發式:如果停下意味著立即輸掉,則避免停下
if (!ifStopWouldLose(currentState)) {
legalMoves.push({ type: 'STOP', payload: null });
}
}
// If no moves possible (stuck), treat as loss or specific rule
// zh: 如果沒有可行的移動(卡住),視為失敗或特定規則
if (legalMoves.length === 0) break;
// Pick random move
// zh: 選擇隨機移動
let randomAction = legalMoves[Math.floor(Math.random() * legalMoves.length)];
this.applyAction(currentState, randomAction);
depth++;
}
return winOrLose(currentState);
}
/**
* Propagate the result up the tree.
* zh: 將結果向上傳播至樹中。
*/
backpropagate(node, result) {
while (node !== null) {
node.visits++;
// Check if this node's player benefited from the result
// zh: 檢查此節點的玩家是否從結果中受益
// Note: You need to define who 'won' based on your winOrLose return value
// zh: 注意:你需要根據 winOrLose 的返回值定義誰「贏了」
if (this.isWinForCurrentNode(node, result)) {
node.wins++;
}
node = node.parent;
}
}
/**
* Helper to determine if the result is positive for the node's perspective.
* zh: 輔助函式,用於判斷結果對該節點的視角是否為正面。
*/
isWinForCurrentNode(node, result) {
// Logic depends on whose turn it was at this node vs the result
// zh: 邏輯取決於此節點是誰的回合以及結果
// Usually in MCTS: Parent chooses move -> Child state results.
// If Child state is good for Player A, Parent (Player A) gets points.
// zh: 通常在 MCTS 中:父節點選擇移動 -> 產生子節點狀態。如果子節點狀態對玩家 A 有利,父節點(玩家 A)得分。
// Simple implementation: assume result returns 1 for AI win, -1 for enemy win
// zh: 簡單實作:假設 result 返回 1 代表 AI 贏,-1 代表敵人贏
return result === 1; // Simplify based on your game's return values
}
}
// --- Usage Example (使用範例) ---
// 1. Initialize AI
// zh: 1. 初始化 AI
const ai = new MCTS(2000); // 2000 iterations
// 2. Current global board array
// zh: 2. 當前的全域 boardArr
let currentBoard = boardArr;
// 3. Get best move
// zh: 3. 獲取最佳行動
let bestAction = ai.search(currentBoard);
console.log("AI recommends:", bestAction);
// zh: AI 推薦:
// 4. Apply it to real game
// zh: 4. 將其應用到真實遊戲中
if (bestAction.type === 'MOVE') {
hereWeGo(bestAction.payload); // Assumes your function updates global state
} else {
hereWeStop();
}
3. 重要注意事項 (Important Notes)
函式參數改造 (Function Parameters Refactoring): 你的原始描述中,函式似乎直接操作全域變數。為了讓 MCTS 運作,hereWeGo、whereCanGo 等函式必須支持傳入一個 臨時狀態 (tempState)。
例如:whereCanGo(state) 而不僅僅是 whereCanGo()。
如果無法修改原函式,你需要寫一個 Wrapper(包裝器),在呼叫原函式前將全域變數 boardArr 替換為模擬的 state,呼叫後再換回來(這會比較慢且危險)。最好是修改函式讓它們接受參數。
行動邏輯 (Action Logic): 因為你的遊戲是「移動、移動、移動... 停下」,這在 MCTS 樹中會形成很深的層次。
Action TYPE 'MOVE': 代表 hereWeGo(n)。這步走完後,仍然是當前玩家的回合。
Action TYPE 'STOP': 代表 hereWeStop()。這步走完後,換對方行動。 這種結構 MCTS 完全可以處理,不需要特別修改,只要 applyAction 正確更新狀態即可。
輸贏判斷 (Win/Lose Evaluation): 代碼中的 isWinForCurrentNode 需要你根據 winOrLose() 的回傳值來調整。
如果 winOrLose 回傳 "Player1", "Player2"。
你需要判斷該節點的父節點是誰在做決策,如果結果是對該決策者有利,則 wins++。
下一步 (Next Step)
Would you like me to refactor the hereWeGo and whereCanGo function signatures to accept a state parameter, or create a specific wrapper to handle the global variable state swapping? (你希望我幫你重構 hereWeGo 和 whereCanGo 的函式簽名以接受 state 參數,還是建立一個專門的包裝器來處理全域變數的狀態切換?)
如果蒙地卡洛樹搜尋,沒有搜尋到最終局面,那它依據什麼來判斷他當下走哪一步是最優的?
這是一個非常切中要害的問題。在蒙地卡洛樹搜尋(MCTS)中,「沒有搜尋到最終局面」通常有兩種情況,這兩種情況下 AI 做決策的依據略有不同:
情況一:樹沒有長滿,但「模擬」跑到了終局 (標準 MCTS)
這是 MCTS 最運作的標準方式。雖然搜尋樹(Tree)只展開了前面幾步,但在模擬階段(Simulation / Rollout),AI 會用隨機的方式把棋「下到分出勝負為止」。
判斷依據: 統計機率(勝率 & 訪問次數)。
原理: 雖然 AI 不知道具體是哪一條路徑導致勝利,但它知道:「當我第一步走 A 時,隨機模擬了 1000 次,贏了 800 次;走 B 時模擬 1000 次,只贏了 200 次。」
決策: 即使沒算透,AI 會選擇勝率最高或被驗證(訪問)次數最多的那一步。
情況二:遊戲太長,「模擬」也被迫中斷 (截斷式 MCTS)
這就是你擔心的情況:如果遊戲很複雜,隨機走到底可能要幾千步,太慢了,我們設定了 depth < 100 強制停止模擬。此時尚未分勝負。
判斷依據: 啟發式評估函數 (Heuristic Evaluation Function)。
原理: 當模擬在半路停下時,我們不能返回「贏 (+1)」或「輸 (-1)」,而是要返回一個 分數(例如 -1 到 1 之間的小數)。這就像人類下棋看一眼棋盤說:「雖然還沒贏,但我現在優勢很大。」
決策: 這個分數會像勝負結果一樣被**反向傳播(Backpropagate)**回去。
代碼改進:加入評估函數
為了處理「模擬未到終局就停止」的情況,我們需要修改 simulate 函式,加入一個 evaluateState 來給當前局面打分。
JavaScript
/**
* Heuristic Evaluation Function
* zh: 啟發式評估函式
* Give a score between -1 (Lose) and 1 (Win) for an unfinished state.
* zh: 為未完成的局面給出一個 -1 (輸) 到 1 (贏) 之間的分數。
*/
function evaluateState(state) {
// This logic depends entirely on your game rules!
// zh: 這個邏輯完全取決於你的遊戲規則!
// Example logic:
// 1. Calculate advantage based on piece count, position, etc.
// zh: 1. 根據棋子數量、位置等計算優勢。
// Let's assume we have a function getMyScore(state) and getEnemyScore(state)
// zh: 假設我們有 getMyScore(state) 和 getEnemyScore(state) 函式
// Simple example: Higher score is better
// zh: 簡單範例:分數越高越好
// let scoreDiff = getMyScore(state) - getEnemyScore(state);
// Sigmoid function to map score difference to (-1, 1) range
// zh: 使用 Sigmoid 函式將分數差映射到 (-1, 1) 範圍
// return scoreDiff / (Math.abs(scoreDiff) + 10);
// If you have no heuristic, simply return 0 (Draw)
// zh: 如果你沒有啟發式邏輯,就簡單地返回 0 (平局)
return 0;
}
// --- Modified Simulate Function inside MCTS Class ---
// --- 修改後的 MCTS 類別中的模擬函式 ---
simulate(startState) {
let currentState = cloneState(startState);
let depth = 0;
const MAX_DEPTH = 100; // Limit simulation depth // zh: 限制模擬深度
// Loop until win/lose OR max depth reached
// zh: 循環直到分出勝負 或 達到最大深度
while (winOrLose(currentState) === null && depth < MAX_DEPTH) {
let legalMoves = [];
let possiblePos = whereCanGo(currentState);
possiblePos.forEach(p => legalMoves.push({ type: 'MOVE', payload: p }));
if (ifCanStop(currentState) && !ifStopWouldLose(currentState)) {
legalMoves.push({ type: 'STOP', payload: null });
}
if (legalMoves.length === 0) break; // Stuck/Lose // zh: 卡住/輸掉
// Random move
let randomAction = legalMoves[Math.floor(Math.random() * legalMoves.length)];
this.applyAction(currentState, randomAction);
depth++;
}
// 1. If game ended naturally
// zh: 1. 如果遊戲自然結束
let result = winOrLose(currentState);
if (result !== null) {
// Convert 'Win'/'Lose' string to number (1 or -1)
// zh: 將 'Win'/'Lose' 字串轉換為數字 (1 或 -1)
return result === 'Win' ? 1 : -1; // Adjust based on who moved last
}
// 2. If simulation was cut off (Depth limit)
// zh: 2. 如果模擬被截斷 (達到深度限制)
// WE NEED TO ESTIMATE THE VALUE HERE
// zh: 我們需要在這裡估算價值
return evaluateState(currentState);
}
總結 MCTS 的決策鏈
當你問 AI 「現在該走哪一步?」時,它的思考過程是:
選擇 (Selection): 根據過去的經驗(UCB1公式),挑選最有潛力的一條路往下走。
模擬 (Simulation): 如果走到盡頭還沒分勝負,就用 evaluateState() 打個分數(比如 0.3,代表小優)。
反向傳播 (Backpropagation): 把這個 0.3 加到這條路徑上所有節點的累計分數裡。
最終決定: 根節點看哪個孩子的訪問次數(Visits)最多,就選哪一步。
為什麼看「訪問次數」而不是「平均勝率」? 因為 MCTS 的特性是「好路徑會被多走」。如果一個節點被訪問了 10,000 次,另一個只被訪問了 10 次,即使後者勝率 100%(可能是運氣好只贏了一次),前者依然是更穩健的「最優解」。
接下來我可以為你做什麼?
你的遊戲是否有明顯的「優勢特徵」(例如:佔領地盤多寡、手牌數量、距離終點遠近)?如果是,我可以幫你寫出針對你遊戲邏輯的 evaluateState 評估函式。
Gemini isn’t human. It can make mistakes, so double-check it