Mancala Avalanche 版本的 Minimax:我的代码返回了错误的举动

问题描述 投票:0回答:1

我一直在使用极小极大算法制作一个曼卡拉机器人。它适用于第一位,但通常只是在空白处提供输出。有人知道如何解决这个问题,使其发挥良好吗?

这是我的代码:

let final;
function increment(board, ltif, player, re) {
    let a = 0;
    for(let i = 0; i < board[ltif]; i++) {
        if((player && (ltif+i+a+1)%14 == 13) || (!player && (ltif+i+a+1)%14 == 6)) {
            a += 1;
        }
        board[(ltif + i + a + 1)%14] += 1;
    }
    const bltif = board[ltif];
    board[ltif] = 0;
    let ans;
    board[(bltif + ltif + a)%14] == 1 || (bltif + ltif + a)%14 == 6 || (bltif + ltif + a)%14 == 13 ? ans = board : ans = increment(board, (bltif + ltif + a)%14, player);
    if(((bltif + ltif + a)%14 == 6 || (bltif + ltif + a)%14 == 13) && !re) {
        ans = 2;;
        }
    if(board[(bltif + ltif + a)%14] == 1 && !re) {
        ans = 3;
    }
    return ans;
}
function minimax(board, depth, player) {
    if(board[6] > 24) {
        return 15;
    }else if(board[13] > 24) {
        return -15;
    }else if(board[6] == 24 && board[13] == 24) {
        return 0;
    }else if(depth === 0) {
        return Math.floor((board[6]-board[13])/2);
    }
    let avail = board.map((element, index) => (element !== 0 && ((index < 6 && player)|| (index < 13 && index > 6 && !player)) ? index : -1)).filter(element => element !== -1);
    if(player) {
        let maxEval = [-Infinity];
        for(let i = 0; i < avail.length; i++) {
            let tboard = increment(board.slice(), avail[i], player, false);
            let Eval;
            if(tboard == 2) {
                Eval = 13;
                tboard = increment(board.slice(), avail[i], player, true);
            }else if(tboard == 3) {
                Eval = -13;
                tboard = increment(board.slice(), avail[i], player, true);
            }else{
                Eval = minimax(tboard, depth - 1, false);
            }
            maxEval = [Math.max(Eval, maxEval[0]),avail[i],tboard];
        }
        final = [maxEval[1], maxEval[2]];
        return maxEval[0];
    }else{
        let minEval = +Infinity;
        for(let i = 0; i < avail.length; i++) {
            let tboard = increment(board.slice(), avail[i], player, false);
            let Eval;
            if(tboard == 2) {
                Eval = 13;
                tboard = increment(board.slice(), avail[i], player, true);
            }else if(tboard == 3) {
                Eval = -13;
                tboard = increment(board.slice(), avail[i], player, true);
            }else{
                Eval = minimax(tboard, depth - 1, false);
            }
            minEval = Math.min(Eval, minEval);
        }
    return minEval;
    }
}
minimax([
    5, 0, 5, 5, 5, 0,
    3, 5, 5, 0, 5, 5,
    5, 0
  ], 9, true);
console.log(final);

它用完了基于文本的编辑器,所以这就是为什么输出进入控制台并且它只检查一个板,然后你必须输入另一个板。另外澄清一下,这是曼卡拉的雪崩版本。

我没有太多使用极小极大算法的经验,所以如果有人对这个问题有任何见解,那将会非常有帮助。

其中一个例子是,当给定棋盘状态时:

[5, 0, 5, 5, 5, 0, 3, 5, 5, 0, 5, 5, 5, 0] 

...它告诉我将数组的第 4 个位置上的那个移动到右侧 5 个位置,从而给出以下输出:

[5, 0, 5, 5, 0, 1, 4, 6, 6, 1, 5, 5, 5, 0]

还有更好的可能移动(如索引 2),我不知道为什么算法选择这个。另外,我使用的极小极大算法没有使用 alpha-beta 剪枝,这是有意的,而不仅仅是代码中的错误。

javascript minimax
1个回答
0
投票

您的实施中存在以下问题:

  • 请注意,在
    minimax
    中,
    increment
    的第一次调用只能返回2或3。这是因为只要停止条件不成立,
    increment
    就会不断进行递归调用。当停止真时,它返回2或3(如果
    re
    为假)。因此,
    minimax
    永远不会使用
    depth-1
    进行递归调用!我建议按如下方式解决这个问题。当
    tboard == 3
    时不要停止递归。这是当回合“正常”结束时的情况,即无权获得额外回合。在这种情况下,没有理由停止递归。因此,在这种情况下进行递归调用,而不是将
    Eval
    设置为 -13。
  • if(board[(bltif + ltif + a)%14] == 1 && !re)
    是一个错误。这应该是一个
    else
    ,因为你不想用 3 覆盖
    ans
    ,因为它已经设置为 2,但玩家的商店中恰好有 1 个石头。而在
    else
    的情况下,你可以确定这个条件是真的,所以不需要评估它。或者,您可以在
    return ans
    块中使用
    if
    ,这样就不存在用 3.
     覆盖 
    ans
  • 的风险。
  • maxEval = [Math.max(Eval, maxEval[0]),avail[i],tboard];
    是错误的,因为即使
    maxEval
    更差
    ,它也会更新 Eval 的第二个和第三个条目。这样你就失去了已经与最好成绩相关的动作。仅当
    maxEval[1]
    优于
    maxEval[2]
    时,您才应更新
    Eval
    maxEval[0]
    。 (我还建议使用普通对象而不是数组。使用普通对象,我们可以为三个组件提供有意义的名称)。
  • final = [maxEval[1], maxEval[2]];
    是错误的,因为它也在递归树的更深层次上执行,覆盖了已经注册在递归树顶部的结果,这才是你真正感兴趣的结果。这里更深层次的问题是
    final
    不应该是
    minimax
    作为“副作用”而改变的全局变量。这是不好的做法。相反,请
    minimax
    返回该信息。
  • 对于最大化和最小化用户,如果他们采取了良好的行动(因为他们实现了额外的回合),那么您的代码会给出 13 分。这不可能是正确的。对于最小化用户来说,该值应该是 -13,否则它实际上会成为一个bad的举动!

其他备注:

  • 使用更具描述性的变量名称。例如,
    numStones
    而不是
    bltif
  • 避免重复像
    (bltif + ltif + a)%14
    这样的相同表达。相反,有一个变量专用于表示该值。您应该只需要在代码中出现一次
    % 14
    操作。
  • 避免在同一个板上调用
    increment
    两次,只是为了从中获取两条不同的信息(基于
    re
    )。如果您在调用
    board.slice()
    之前将
    increment
    存储在变量中,那么在
    increment
    调用之后您就已经拥有了变异板。这样您就不需要将
    re
    设置为 true 进行调用,并且可以删除该参数。
  • 避免使用“幻数”。例如,返回值 2 和 3 应该被解释,并且最好用具有描述性名称的常量替换。然而,在这种特殊情况下,我们可以只使用布尔值(带有描述性名称),它指示玩家是否有权进行额外的移动。
  • 不要在较大的表达式中执行赋值(如
    ans = board
    )。将这样的表达式转换为赋值,其中右侧表达式具有必要的逻辑。
  • 使用
    camelCase
    作为变量,因此不要使用
    Eval
    作为名称。我建议
    score
    (因为
    eval
    已经用于本机函数)。
  • 要确定
    avail
    ,不要使用
    .map
    ,而是立即使用
    .filter
    。这样你就不需要虚拟的 -1 值。

更正代码

这是我在应用上述更正和建议时最终得到的代码:

// Don't use magic values, but instead define constants with meaningful names:
const STORE = { false: 13, true: 6 }; // Per player: the index of their store
const STORES = new Set(Object.values(STORE));
      // Per player: the indices of their pits 
const PITS = { false: [7, 8, 9, 10, 11, 12], true: [0, 1, 2, 3, 4, 5] };
const NUM_STONES = 48;
const GOOD_SCORE = NUM_STONES / 4 + 1; // Better score than max heuristic score
const MAX_SCORE = GOOD_SCORE + 1; // Overall best possible score

// The function increment returns a boolean that is true when the move ended with a stone 
//     in the player's store, giving them a right to an extra move.
//     The given board is mutated, so the caller can see the effect of the move in that board.
function increment(board, startPit, player) {
    const numStones = board[startPit];
    let lastPit = startPit;
    for (let i = 1; i <= board[startPit]; i++) {
        // We can use a conditional inline expression to decide to add an extra 1 or not.
        lastPit = (lastPit + 1 + (lastPit + 1 == STORE[!player])) % board.length;
        board[lastPit]++;
    }
    board[startPit] = 0;
    // There are three cases:
    if (lastPit === STORE[player]) { // Extra turn!
        return true;
    }
    if (board[lastPit] == 1) { // Normal move ended.
        return false;
    }
    // The turn is not finished yet; continue...
    return increment(board, lastPit, player);
}

function minimax(board, depth, player) {
    if (board[STORE[true]] * 2 > NUM_STONES) {
        // Maximizing player has more than half of the stones, so they won the game.
        return MAX_SCORE;
    } else if (board[STORE[false]] * 2 > NUM_STONES) {
        // Minimizing player has more than half of the stones, so they won the game.
        return -MAX_SCORE;
    } else if (board[STORE[true]] + board[STORE[false]] == NUM_STONES) {
        // All stones are in the stores, and both players have an equal amount: it's draw
        return 0;
    } else if (depth === 0) {
        // Search must stop here. Give a heuristic value:
        return Math.floor((board[STORE[true]] - board[STORE[false]]) / 2);
    }
    // Get the player's own pits that have at least one stone in it:
    const availablePits = PITS[player].filter(pit => board[pit]); 
    if (player) { // Maximizing player
        let best = { score: -Infinity };
        for (const pit of availablePits) {
            const newBoard = board.slice(); // Keep a reference to the new board
            const extraTurn = increment(newBoard, pit, player);
            const score = extraTurn ? GOOD_SCORE // We get an extra turn (good!), so we will not look deeper
                                      // Otherwise: no extra turn. Continue the recursion.
                                    : minimax(newBoard, depth - 1, false).score; // Get score component
            if (score > best.score) {
                // Only make updates when the score is better
                best = { score, pit, board: newBoard };
            }
        }
        return best; // Don't alter a global. Just return complete information: {score, pit, board}.
    } else {
        let best = { score: +Infinity };
        for (const pit of availablePits) {
            const newBoard = board.slice(); 
            const extraTurn = increment(newBoard, pit, player);
            const score = extraTurn ? -GOOD_SCORE // Minimizing user wants a negative value here!
                                    : minimax(newBoard, depth - 1, false).score;
            if (score < best.score) {
                best = { score, pit, board: newBoard };
            }
        }
        return best;
    }
}

// Minimax should *return* the information instead of setting a global variable.
const { score, pit, board } = minimax([ 
    // Layout was confusing. This seems clearer:
    5, 0, 5, 5, 5, 0,     3, 
    5, 5, 0, 5, 5, 5,     0
  ], 9, true);

console.log(`The best move is from pit ${pit}. It got score ${score}, and leads to this board: ${board}`);

算法现在返回最佳走法,坑号 2。我玩这个游戏的时间不够长,无法判断这是否真的是最佳走法。

© www.soinside.com 2019 - 2024. All rights reserved.