对抗博弈搜索——吃豆人

  • 介绍
  • 项目解决方案
  • question2:Minimax算法
  • question3:Alpha-Beta 剪枝
  • question4:Expectimax
  • question5:优化评估函数
  • 总结


介绍

项目解决方案

question2:Minimax算法

Minimax算法又名极小化极大算法,是一种找出失败的最大可能性中的最小值的算法。Minimax算法常用于棋类等由两方较量的游戏和程序,这类程序由两个游戏者轮流,每次执行一个步骤。我们众所周知的五子棋、象棋等都属于这类程序,所以说Minimax算法是基于搜索的博弈算法的基础。该算法是一种零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,而另一方则选择令对手优势最小化的方法。

  • Minimax是一种悲观算法,即假设对手每一步都会将我方引入从当前看理论上价值最小的格局方向,即对手具有完美决策能力。因此我方的策略应该是选择那些对方所能达到的让我方最差情况中最好的,也就是让对方在完美决策下所对我造成的损失最小。
  • Minimax不找理论最优解,因为理论最优解往往依赖于对手是否足够愚蠢,Minimax中我方完全掌握主动,如果对方每一步决策都是完美的,则我方可以达到预计的最小损失格局,如果对方没有走出完美决策,则我方可能达到比预计的最悲观情况更好的结局。总之我方就是要在最坏情况中选择最好的。1

在本问题中,我们需要实现经典的Minimax算法,我们可以选择构建一个mini函数和一个max函数进行互相调用,也可以选择使用一个函数来进行递归实现(我选择了后者)。
要实现一个函数,就要先知道我们要用它实现什么功能,它有什么参数,需要返回什么值。
下面是项目注释中提供的一些 可能用的到的函数

Here are some method calls that might be useful when implementing minimax.

        gameState.getLegalActions(agentIndex):
        Returns a list of legal actions for an agent
        agentIndex=0 means Pacman, ghosts are >= 1

        gameState.getNextState(agentIndex, action):
        Returns the child game state after an agent takes an action

        gameState.getNumAgents():
        Returns the total number of agents in the game

        gameState.isWin():
        Returns whether or not the game state is a winning state

        gameState.isLose():
        Returns whether or not the game state is a losing state
  • 实现功能:通过Minimax函数获取Pacman可以获取最高分数的策略,并通过getAction函数返回下一步动作
  • 参数:Minimax(self, agentIndex, gameState, Depth)
    agentIndex == 0 表示吃豆人,>= 1 表示幽灵,gameState 表示当前状态,depth表示深度
  • 返回值:在mini或者max层获取的最小或最大分数

我们先实现getAction函数

def getAction(self, gameState):
	bestAction = "stop"
	#由于是为吃豆人作决策,故将该层作为根结点(第0层),并设立最大值
	maxVal = float('-inf')
   		# 从吃豆人所有合法动作中选则一个最佳的动作
   		for nextAction in gameState.getLegalActions(0):
       		successor = gameState.getNextState(0,nextAction)
       		val = self.minimax(agentIndex=1,gameState=successor,Depth=1)
       		# 假如评估值比目前最大值大,则此动作为最佳动作
       		if val is not None and maxVal < val:
            	maxVal = val
            	bestAction = nextAction
   		return bestAction

再实现minimax算法,(关键核心算法)

def minimax(self, agentIndex, gameState, Depth):
   	# 如果超过了限制的深度,或者已经输了或者赢了,就可以直接返回评估值
  	# Depth达到depth*2时就可以停止了,对于解决本问题足够了
    if Depth >= self.depth * 2 or gameState.isWin() or gameState.isLose():
        return self.evaluationFunction(gameState)  # minimax() 函数返回的是评估值

    if agentIndex == 0:  # 吃豆人
        MAX = float("-inf")  # 初始化MAX为负无穷
        pacmanActions = gameState.getLegalActions(agentIndex)  # 获取吃豆人所有的合法动作
        for nextAction in pacmanActions:
           	successor = gameState.getNextState(agentIndex, each_action)  # 对每一个合法的动作生成对应的状态
            value = self.minimax(1, successor, Depth + 1)  # 下一层是到幽灵开始行动
            MAX = max(MAX, value)
        return MAX

    else:  # 幽灵即agentIndex >= 1
        values = []	# 由于可能存在多个幽灵,故设立一个values数组进行存放
        MIN = float("inf")  # 初始化MIN为正无穷
        ghostActions = gameState.getLegalActions(agentIndex)	# 获取幽灵所有的合法动作
        for nextAction in ghostActions:
           	successor = gameState.getNextState(agentIndex, each_action)
            if agentIndex >= gameState.getNumAgents() - 1:  # 遍历完幽灵后再进行下一轮的吃豆人
                values.append(self.minimax(0, successor, Depth + 1))
            else:  # 注意:>= 1代表幽灵,因为可能不止一只幽灵
                values.append(self.minimax(agentIndex + 1, successor, Depth))  # 这里得注意代入递归函数的Depth不加1,因为还有幽灵没有运动
            value = min(values)
            MIN = min(MIN, value)
        return MIN

测试结果:

Pacman died! Score: 84
Average Score: 84.0
Scores:        84.0
Win Rate:      0/1 (0.00)
Record:        Loss
*** Finished running MinimaxAgent on smallClassic after 1 seconds.
*** Won 0 out of 1 games. Average score: 84.000000 ***
*** PASS: test_cases\q2\8-pacman-game.test

### Question q2: 5/5 ###

tips:
在更大的棋盘上,比如openClassicmediumClassic(默认),你会发现吃豆子擅长不死,但在获胜方面却很糟糕。他经常在没有进步的情况下四处奔波。他甚至可能会在一个点旁边打滚而不吃它,因为他不知道吃了那个点后他会去哪里。如果您看到这种行为,请不要担心,问题 5 将解决所有这些问题。

question3:Alpha-Beta 剪枝

Minimax算法往往会生成巨大的分支,尤其是当遇到复杂问题的时候,这时候就需要用到Alpha-Beta剪枝算法来对Minimax算法进行优化。

定义极大层的下界为alpha,极小层的上界为beta,alpha-beta剪枝规则描述如下:
(1)alpha剪枝。若任一极小值层结点的beta值不大于它任一前驱极大值层结点的alpha值,即alpha(前驱层) >= beta(后继层),则可终止该极小值层中这个MIN结点以下的搜索过程。这个MIN结点最终的倒推值就确定为这个beta值。
(2)beta剪枝。若任一极大值层结点的alpha值不小于它任一前驱极小值层结点的beta值,即alpha(后继层) >= beta(前驱层),则可以终止该极大值层中这个MAX结点以下的搜索过程,这个MAX结点最终倒推值就确定为这个alpha值。
2

在该问题中,我们要做的是在question2的基础上添加剪枝操作,话不多说,直接上代码:

def getAction(self, gameState):
    maxvalue = float('-inf')
    bestaction = []
    # alpha < 结点值 < beta
    alpha = float('-inf')
    beta = float('inf')
    for each_action in gameState.getLegalActions(0):
        successor = gameState.getNextState(0, each_action)
        # 求出后继状态的评估值,并和maxvalue比较,求出最大值
        value = self.new_minimax(agentIndex=1, gameState=successor, Depth=1, alpha=alpha, beta=beta)
        # 如果当前的value比maxvalue要大,则更新maxvalue,并记下bestaction
        if maxvalue < value:
            bestaction = []
            maxvalue = value
            bestaction.append(each_action)
        elif maxvalue == value:
            bestaction.append(each_action)
        if maxvalue > alpha:  # 按照alpha-beta剪枝算法,需要更新alpha值
            alpha = maxvalue

    return random.choice(bestaction)  # 随机选择最优的合法动作
def new_minimax(self, agentIndex, gameState, Depth, alpha, beta):
	# 这一部分与前面minimax对应部分一模一样,无需改动
    if Depth >= self.depth * 2 or gameState.isWin() or gameState.isLose():
        return self.evaluationFunction(gameState)  # minimax() 函数返回的是评估值

    if agentIndex == 0:  # 吃豆人
        MAX = float("-inf")  # 初始化MAX为负无穷
        pacmanActions = gameState.getLegalActions(agentIndex)  # 获取吃豆人合法的动作
        for nextAction in pacmanActions:
            successor = gameState.getNextState(agentIndex, nextAction)  # 对每一个合法的动作生成对应的状态
            value = self.new_minimax(1, successor, Depth + 1,alpha,beta)  # 下一轮是到幽灵开始行动
            if value is not None:
                MAX = max(MAX, value)
            if MAX > beta:  # alpha-beta剪枝算法
                return MAX
            if MAX > alpha:  # 更新alpha值
                alpha = MAX
        return MAX

    else:  # 幽灵
        values = []
        MIN = float("inf")  # 初始化MIN为正无穷
        ghostActions = gameState.getLegalActions(agentIndex)
        for nextAction in ghostActions:
            successor = gameState.getNextState(agentIndex, each_action)
            if agentIndex >= gameState.getNumAgents() - 1:
                values.append(self.new_minimax((agentIndex+1) % gameState.getNumAgents(),successor, Depth + 1, alpha,beta))
            else:
                values.append(self.new_minimax((agentIndex+1) % gameState.getNumAgents(),successor, Depth, alpha,beta))

            if values is not None:
                value = min(values)
                MIN = min(MIN, value)
            if MIN < alpha:
                return MIN
            # 按照alpha-beta剪枝算法,这里需要更新beta的值
            if MIN < beta:
                beta = MIN
       	return MIN

测试结果:

Pacman died! Score: 84
Average Score: 84.0
Scores:        84.0
Win Rate:      0/1 (0.00)
Record:        Loss
*** Finished running AlphaBetaAgent on smallClassic after 0 seconds.
*** Won 0 out of 1 games. Average score: 84.000000 ***
*** PASS: test_cases\q3\8-pacman-game.test

### Question q3: 5/5 ###

question4:Expectimax

对于Minimax算法来说,它所考虑的对手都是选择最利他(对方尽可能得分)的动作,但是在现实情况中,并非如此。
在本问题中,我并没有基于前两个问题进行优化和改进,而是选取了新的方法:对于吃豆人和幽灵采用不同的函数,然后进行交替调取,并且深度的判断方式转为了剩余深度与0的比较。

def getAction(self, gameState):
    val = maxiExp(gameState, self.depth, gameState.getNumAgents() - 1, self.evaluationFunction)
    # print(val)
    return val[1]

由于后面的maxiExp和miniExp函数都使用了双参数的返回值,一个是得分,一个动作,故要返回动作参数。

def maxiExp(state, depth, numGhosts, func):
	# 如果超过了限制的深度,或者已经输了或者赢了,就可以直接返回评估值
    if depth == 0 or state.isLose() or state.isWin():
        score = func(state)
        return (score, Directions.STOP)
    else:
        v = float('-inf')
        # 获取吃豆人所有合法动作
        actions = state.getLegalActions(0)
        bestAction = Directions.STOP
        for action in actions:
            successor = state.getNextState(0, action)
            m = miniExp(successor, depth, 1, successor.getNumAgents() - 1, func)
            # 得分值中取大
            if m > v:
                v = m
                bestAction = action
        return (v, bestAction)
def miniExp(state, depth, ghost, numGhosts, func):
    if depth == 0 or state.isLose() or state.isWin():
        score = func(state)
        return score
    v = 0
    actions = state.getLegalActions(ghost)
    for action in actions:
        successor = state.getNextState(ghost, action)
        m = 0
        # 若幽灵还没遍历完则继续遍历幽灵
        if ghost < numGhosts:
            m = miniExp(successor, depth, ghost + 1, numGhosts, func)
        # 遍历完幽灵后进行吃豆人的回合
        else:
            m = maxiExp(successor, depth - 1, numGhosts, func)[0]
        v += m / len(actions)
    return v

测试结果:

Pacman died! Score: 84
Average Score: 84.0
Scores:        84.0
Win Rate:      0/1 (0.00)
Record:        Loss
*** Finished running ExpectimaxAgent on smallClassic after 1 seconds.
*** Won 0 out of 1 games. Average score: 84.000000 ***
*** PASS: test_cases\q4\7-pacman-game.test

### Question q4: 5/5 ###

question5:优化评估函数

在question5中,我们将解决前面遗留下来的问题:吃豆人总是无法获胜。
我们需要对评估函数进行一定的优化,我的主要思路就是在所剩的食物中挑选最近的食物进行食用。
ps:我在本问题中没有特别考虑幽灵对于吃豆人的启发值,后续可以再次进行优化。

def betterEvaluationFunction(currentGameState):
	# 获取吃豆人的位置
	Position = currentGameState.getPacmanPosition()
	# 获取当前所剩食物的位置
    Food = currentGameState.getFood().asList()
    # 参数初始化
    FoodCheck = -float("inf")
    GhostCheck = float("inf")

	# 对所剩的所有食物进行判断
    for food in Food:
    	# 计算吃豆人和食物的曼哈顿距离
        MinFood = manhattanDistance(Position, food) * (-1)  
        # 如果我们在最好的情况下:食物就在我们旁边!
        if MinFood > FoodCheck:
            FoodCheck = MinFood

    if FoodCheck == -float("inf"):
        FoodCheck = 0

    return currentGameState.getScore() + FoodCheck

测试结果:

Pacman emerges victorious! Score: 1083
Pacman emerges victorious! Score: 1145
Pacman emerges victorious! Score: 1009
Pacman emerges victorious! Score: 1081
Pacman emerges victorious! Score: 994
Pacman emerges victorious! Score: 1097
Pacman emerges victorious! Score: 1088
Pacman emerges victorious! Score: 1007
Pacman emerges victorious! Score: 1112
Pacman emerges victorious! Score: 1100
Average Score: 1071.6
Scores:        1083.0, 1145.0, 1009.0, 1081.0, 994.0, 1097.0, 1088.0, 1007.0, 1112.0, 1100.0
Win Rate:      10/10 (1.00)
Record:        Win, Win, Win, Win, Win, Win, Win, Win, Win, Win
*** PASS: test_cases\q5\grade-agent.test (6 of 6 points)
***     1071.6 average score (2 of 2 points)
***         Grading scheme:
***          < 500:  0 points
***         >= 500:  1 points
***         >= 1000:  2 points
***     10 games not timed out (1 of 1 points)
***         Grading scheme:
***          < 0:  fail
***         >= 0:  0 points
***         >= 10:  1 points
***     10 wins (3 of 3 points)
***         Grading scheme:
***          < 1:  fail
***         >= 1:  1 points
***         >= 5:  2 points
***         >= 10:  3 points

### Question q5: 6/6 ###

完成question5后我们发现,我们获得的分数也仅仅在1000分左右徘徊,如果想要获得更高的分数,除了完善幽灵的启发值,其次也要考虑特效豆子的特殊效果(吃特效豆子可以让幽灵进入恐惧状态,从而吃掉幽灵后可以获得更高的分数)。为了能够高效利用特效豆子的效果,拉高特效豆子的权重,如果特效豆子的启发值和普通豆子一样,那么当幽灵来的时候,吃豆人只会去躲避幽灵,不会想到去吃特效豆子让幽灵变成恐惧状态。把特效豆子权重拉高一些,就可以抵消幽灵对于吃豆人的消极影响。
但是要注意,特效豆子是有有效时长的,吃了特效豆子一定时间后,幽灵就会恢复正常,这时候我们就要考虑幽灵距离吃豆人的距离以及吃豆人距离特效豆子的距离,可以考虑在一般时候的特效豆子分数和普通豆子相等或者略低,等幽灵靠近时,拉高特效豆子的启发值。

总结

minimax算法可以在对抗博弈搜索中取得最大分数,但是是在理想情况下(即对手都会选择最有利于它的行为),但在现实中并不是这样,所以要对minimax算法中mini层进行优化。
其次单纯的minimax树会随着游戏的复杂度而变得非常庞大,此时就需要使用alpha-beta剪枝来对minimax算法进行剪枝优化,如此一来不仅能够提高算法效率还能够减少空间复杂度。
所以按照上面的分析结合本实验来讲,这三个问题(minimax算法,alpha-beta剪枝及Expectimax)虽然能达到不同的目的,但是对于分数的提升很小甚至没有,而想要获取更高的分数则需要通过优化评估函数来实现,具体优化方式要视游戏规则而定。
总的来说,可以通过这个实验项目来加深自己对minimax,alpha-beta剪枝以及评估函数的理解,对于光理论学习来说提升太多。


  1. Minimax算法转自: ↩︎
  2. alpha-beta剪枝规则转自: ↩︎