想學蒙地卡洛樹搜尋

活到老学到老
回复
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4394
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 171 次
被圈友点赞: 200 次
联系:

想學蒙地卡洛樹搜尋

帖子 ejsoon »

代码: 全选

 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
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 0 访客