了解Minimax算法

来源:互联网 发布:统计学软件spss 编辑:程序博客网 时间:2024/06/12 23:53

Tic Tac Toe:了解Minimax算法

注意!这篇文章也被翻译成日文葡萄牙文我非常感谢与我联系的读者,并翻译了这篇文章。

我最近建立了一个无与伦比的tic tac脚趾游戏。这是一个有趣和非常谦卑的项目,教我一吨。如果你想完全接受教育,请在这里拍摄tic tac toe游戏

为了使游戏无与伦比,有必要创建一个算法,可以计算可用于计算机播放器的所有可能的动作,并使用一些度量来确定最佳的移动。经过广泛的研究,很明显Minimax算法是正确的工作。

花了一点时间真正从根本上了解算法并在我的游戏中实现。我发现了许多代码示例和解释,但没有一个真正像我一样走过一个简单的过程。我希望这篇文章能帮助你们欣赏这种算法的优雅。

描述Tic Tac Toe的完美游戏

首先,我们先来定义一个完美的tic tac脚趾游戏的意义:

如果我玩得很好,每次玩,我都会赢得比赛,否则我会画这个游戏。此外,如果我对另一个完美的球员,我会总是画游戏。

我们如何定量地描述这些情况?让我们分配一个分数到“终点游戏条件:”

  • 我赢了,饿了!我得到10分!
  • 我输了,狗屎 我输10分(因为其他玩家得10分)
  • 我画,无论如何。我得到零点,没有人得到任何积分。

所以现在我们有一个情况,我们可以确定任何游戏结束状态的可能得分。

看一个简单的例子

要应用这个,我们来看一个比赛结束的一个例子,我轮到哪里。我是X.我的目标明显是最大化我的最终比赛得分。

一个tic tac脚趾游戏的设计结束状态。

如果这个图像的顶部代表了我轮到的游戏状态,那么我有一些选择可以做,有三个地方可以玩,其中一个明显地导致我赢得了10分。如果我不这样做,O可以很容易地赢。而且我不希望O赢,所以我的目标是作为第一名球员,应该是选择最大的得分。

但是关于O呢?

我们对O有什么了解?那么我们应该假设O也在玩这个游戏,但是相对于我们来说,第一个玩家,O想要显然想要选择这个动作,这给我们带来了最差的分数,它想要选择一个最小化我们的动作终极得分。从O的角度来看待事情,从上面的其他两个游戏状态开始,我们不立即赢得:

从另一个玩家的角度来看,这是一棵树

选择是清楚的,O会选择任何得分为-10的动作。

描述Minimax

Minimax算法的关键是来自两个玩家之间的距离,其中“转弯”的玩家希望以最高分数来选取动作。反过来,每个可用移动的分数由相对的玩家决定哪个可用移动具有最小分数来确定。对手球员的分数再次由转牌球员试图最大限度地提高得分,从而一直下移到移动树到结束状态。

假设X是“转牌者”的算法描述如下:

  • 如果游戏结束,请从X的角度返回分数。
  • 否则得到一个新的游戏状态列表为每一个可能的举动
  • 创建一个分数列表
  • 对于这些状态中的每一个,将该状态的最小值结果添加到分数列表
  • 如果是X的转弯,则从得分列表中返回最大分数
  • 如果是转弯,则从得分列表中返回最小分数

你会注意到这个算法是递归的,它在玩家之间来回翻转,直到找到最后的分数。

让我们用完整的移动树来浏览算法的执行,并显示为什么在算法上,即时获胜的举动将被挑选:

全最小移动树

  • X在状态1中转弯.X生成状态2,3和4,并在这些状态下调用最小值。
  • 由于游戏处于结束状态,状态2将+10的分数推至状态1的得分列表。
  • 状态3和状态4不处于结束状态,所以3生成状态5和6,并在其上调用最小值,而状态4生成状态7和8,并在其上调用最小值。
  • 状态5将10分的成绩推到了状态3的得分列表上,而同样的状态是将状态7推到了状态4的得分列表上。
  • 状态6和8生成唯一可用的移动,它们是终端状态,因此它们都将状态3和4的移动列表中的+10分数相加。
  • 因为在状态3和状态4都是O转,O将寻求最小分数,并且在-10和+10之间进行选择,状态3和4都将产生-10。
  • 最后,状态2,3和4的得分列表分别填入+10,-10和-10,而追求最大分数的状态1将选择得分+10,状态2的获胜移动。

这当然是很多的,这就是为什么我们有一台电脑执行这个算法。

Minimax的编码版本

希望现在你对粗略的算法如何决定最佳的移动方式有一个粗略的感觉。我们来看看我的算法实现来巩固理解:

这是游戏得分的功能:

# @player is the turn taking playerdef score(game)    if game.win?(@player)        return 10    elsif game.win?(@opponent)        return -10    else        return 0    endend

足够简单,如果当前玩家赢得游戏,则返回+10,如果其他玩家获胜,则返回0,平局为0。你会注意到玩家不重要。X或O是无关紧要的,只有谁转过来才是。

现在实际的最小值算法; 请注意,在此实现中,choice或者move简单地是板上的行/列地址,例如[0,2]是3x3板上的右上角。

def minimax(game)    return score(game) if game.over?    scores = [] # an array of scores    moves = []  # an array of moves    # Populate the scores array, recursing as needed    game.get_available_moves.each do |move|        possible_game = game.get_new_state(move)        scores.push minimax(possible_game)        moves.push move    end    # Do the min or the max calculation    if game.active_turn == @player        # This is the max calculation        max_score_index = scores.each_with_index.max[1]        @choice = moves[max_score_index]        return scores[max_score_index]    else        # This is the min calculation        min_score_index = scores.each_with_index.min[1]        @choice = moves[min_score_index]        return scores[min_score_index]    endend

当这个算法在PerfectPlayer类中运行时,最佳移动的最终选择存储在@choice变量中,然后用于返回当前玩家移动的新游戏状态。

完美但致命的玩家

实现上述算法可以让你达到一个点,你的tic tac脚趾游戏不能被击败。但是,在测试时发现的有趣的细微差别在于,完美的玩家必须始终是完美的。换句话说,在完美的玩家最终会失去或画出的情况下,下一步的决定是相当的宿命论。该算法基本上说:“嘿,我会失去反正,所以如果我在现在的下一个或者六个动作中输了,真的没关系。”

我发现这是通过一个明显的索引板,或一个“错误”在它的算法,并要求下一个最好的举动。我会期望完美的球员至少打架,阻止我的直接胜利。但是,它没有:

一个完美的玩家提交seppuku。

看看这里发生了什么,看看可能的移动树(注意,为了清楚起见,我已经删除了一些可能的状态):

X总是赢得的移动树。

  • 给予板状态1,两个玩家都玩得很好,而O是电脑玩家。O选择状态5中的移动,然后当X在状态9中获胜时立即失去。
  • 但是如果O阻挡了X在第三的胜利,X将明显地阻止O的潜在胜利,如状态7所示。
  • 这为X所示的X取得了一定的胜利,所以无论在状态7中选择哪个动作,X都将最终获胜。

作为这些情景的结果,以及我们正在遍历每个空白空间的事实,从左到右,从上到下,所有的移动是相等的,也就是导致O的输入,最后一个移动将被选择为在状态5中显示,因为它是状态1中可用移动的最后一个。移动的数组是:[左上角,右上角,中间左侧,中间中心]。

什么是gosh-darn,tic tac脚趾大师要做什么?

打好战斗深度

这种算法的关键改进是,无论董事会安排如何,完美的玩家都将完美地发挥其作用,就是考虑到“深度”或转弯,直到游戏结束为止。基本上,完美的玩家应该玩得很好,但是尽可能延长游戏。

为了达到这个目的,我们将从最终的比赛得分减去深度,即轮到次数或递归,得分越多,得分越高。从上面更新我们的代码,我们有一些如下所示:

def score(game, depth)    if game.win?(@player)        return 10 - depth    elsif game.win?(@opponent)        return depth - 10    else        return 0    endenddef minimax(game, depth)    return score(game) if game.over?    depth += 1    scores = [] # an array of scores    moves = []  # an array of moves    # Populate the scores array, recursing as needed    game.get_available_moves.each do |move|        possible_game = game.get_new_state(move)        scores.push minimax(possible_game, depth)        moves.push move    end    # Do the min or the max calculation    if game.active_turn == @player        # This is the max calculation        max_score_index = scores.each_with_index.max[1]        @choice = moves[max_score_index]        return scores[max_score_index]    else        # This is the min calculation        min_score_index = scores.each_with_index.min[1]        @choice = moves[min_score_index]        return scores[min_score_index]    endend

所以每当我们调用最小值时,深度会增加1,最终计算结束游戏状态时,分数会根据深度进行调整。我们来看看我们的移动树中的外观:

结束状态考虑到深度。

这次深度(左侧显示为黑色)导致每个结束状态的分数不同,并且由于最小值的0级部分将尝试最大化可用分数(因为O是转牌球员),所以-6得分将被选中,因为它大于其他状态,得分为-8。所以即使面对一定的死亡,我们的信任,完美的球员现在将选择阻止的举动,而不是承诺荣誉死亡。

结论是

我希望所有这些讨论有助于您进一步了解最小化算法,也许如何在一个tic tac脚趾游戏中占主导地位。如果您有其他问题或任何令人困惑的事情,请留下一些意见,我会尝试改进文章。我的tic tac脚趾游戏的所有源代码可以在github找到

如果您喜欢阅读本文或了解的内容,请考虑通过FacebookTwitterGoogle Plus进行分享谢谢!

原创粉丝点击