免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 18012 | 回复: 4
打印 上一主题 下一主题

算法笔记(一):Minimax算法 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2009-02-21 20:00 |只看该作者 |倒序浏览
这几天在用Python写Tic-Tac-Toe小游戏,顺便接触了一些简单的人机博弈算法,其实在算法方面我完全算是个新手,所以这也算是一个反复折腾学习的过程。而Tic-Tac-Toe应该算是人机博弈里最简单的应用了,最经典的算法是miniMax算法,也叫极大极小值算法,主要方法就是通过考虑双方博弈N步后,从所有可能的走法中选一步最佳的走法来走。

因为初学python,而且算法也是新学的,所以代码仍然有所瑕疵,希望和各位多多交流

先简单说说算法的基本思想:
1. 设博弈双方中一方为MAX,另一方为MIN。然后为其中的一方(计算机)找一个最佳走法。

2. 为了找到当前棋局的最优走法,需要对各个可能的走法所产生的后续棋局进行比较,同时也要考虑对方可能的走法,并对后续棋局赋予一定的权值(或者称之为分数)。也就是说,以当前棋局为根节点生成一棵博弈树,N步后的棋局作为树的叶子节点。同时从树根开始轮流给每层结点赋予Max和Min的称号

3. 用一个评估函数来分析计算各个后续棋局(即叶子节点)的权值,估算出来的分数为静态估值



4. 当端节点的估值计算出来后,再推算出父节点的得分。推算的方法是:对于处于MAX层的节点,选其子节点中一个最大的得分作为父节点的得分,这是为了使自己在可供选择的方案中选一个对自己最有利的方案;对处于MIN层的节点,选其子节点中一个最小的得分作为父节点的得分,这是为了立足于最坏的情况,这样计算出的父节点的得分为倒推值。

5.如此反推至根节点下的第一层孩子,如果其中某个孩子能获得较大的倒推值,则它就是当前棋局最好的走法。

把主要的算法写出来了,分为两个函数,一个为估值函数,即计算各个后续棋局(即叶子节点)的权值;另一个为遍历函数,生成指定遍历深度的博弈树。

先贴估值函数代码:

  1. def miniMaxEvalauate(board,player):

  2.     opponent = { Player_O : Player_X, Player_X : Player_O }

  3.     winning_rows = [[0,1,2],[3,4,5],[6,7,8], # vertical
  4.                     [0,3,6],[1,4,7],[2,5,8], # horizontal
  5.                     [0,4,8],[2,4,6]]         # diagonal

  6.     count=0

  7.     clone_pieces=[Empty]*9 # Initialize an empty chessboard

  8.     # Copy the current chessboard
  9.     for i in range(9):
  10.         clone_pieces=board.pieces

  11.     # fill it into the clone chessboard
  12.     for pos in range(9):
  13.         if clone_pieces[pos] == Empty:
  14.             clone_pieces[pos]=player

  15.     # analyse ur point value
  16.     for row in winning_rows:
  17.         if allEqual([clone_pieces for i in row]):
  18.             count+=1

  19. #==========================================

  20.     clone_pieces2=[Empty]*9

  21.     for i in range(9):
  22.         clone_pieces2=board.pieces

  23.     for pos in range(9):
  24.         if clone_pieces2[pos] == Empty:
  25.             clone_pieces2[pos]=opponent[player]
  26.     for row in winning_rows:
  27.         if allEqual([clone_pieces2 for i in row]):
  28.             count-=1

  29.     return count
复制代码

代码里的board.pieces是存储当前棋局位置的变量,这个函数首先复制指定的棋局,然后在复制的棋局将棋盘中的空格填满自己的棋子,然后统计此时自己的棋子在每行每列加上斜线有多少三联通的,每个联通count值加1;接着同样的方法统计在空格填满对方棋子的棋局里,对手的棋子有多少三联通的,每个联通count值减1,这样所得的count值作为估值返回

核心是遍历函数:

  1. def comp2(board,player):

  2.     t0 = time.time()
  3.     board.output() # print the chessboard

  4.     def miniMax(board, depth, p=player):

  5.         opponent = { Player_O : Player_X, Player_X : Player_O }
  6.         moves=[]
  7.         outcomes=[]

  8.         # return if game over
  9.         if board.gameOver():
  10.             if board.gameOver()== player:
  11.                 return +100
  12.             if board.gameOver()== opponent[player]:
  13.                 return -100
  14.             return 0

  15.         if depth!=0:

  16.             depth-=1
  17.             for move in board.getValidMoves():

  18.                 board.makeMove(move, p)
  19.                 point=miniMax(board,depth,opponent[p])
  20.                 outcomes+=[point]
  21.                 moves+=[move]
  22.                 board.undoMove(move)
  23.         else:
  24.             return miniMaxEvalauate2(board,player)

  25.         if p != player:
  26.             #return min(outcomes)
  27.             min_element = 100
  28.             n=0
  29.             for o in outcomes:
  30.                 if o == -100:
  31.                     board.bestmove=moves[n]
  32.                     return o
  33.                 min_element = min(o,min_element)
  34.                 if o==min_element:
  35.                     board.bestmove=moves[n]
  36.                 n+=1
  37.             return min_element
  38.         else:
  39.             #return max(outcomes)
  40.             max_element = -100
  41.             n=0
  42.             for o in outcomes:
  43.                 if o == +100:
  44.                     board.bestmove=moves[n]
  45.                     return o
  46.                 max_element = max(o,max_element)
  47.                 if o==max_element:
  48.                     board.bestmove=moves[n]
  49.                 n+=1
  50.             return max_element

  51.     miniMax(board, 3)

  52.     board.makeMove(board.bestmove, player)
  53.     print "computer move: %0.3f ms" % ((time.time()-t0)*1000)
复制代码

depth参数是遍历深度,数值越高估值越准确因而AI也越高,这里通过递归miniMax函数三次来取得三层后的棋局,再进行估值反推,并保存最佳走步的位置,最后选择那步最佳方案走。

写完调试成功后以为大功告成了,结果在对抗中败下阵来,自己的miniMax估值算法导致了以下无语的走法(自己的算法下”X”):



可以看出自己的下法仿佛在把自己逼入死路,其实这是有限遍历造成权值相同导致的。

在有限遍历很容易出现几种权值相同的情况,即有几个最佳走法,但其中一些可能是陷阱,有时候得遍历多一步可能才能判断出来,可是这样又会耗用过多的资源和时间,这样也完全不是在优化算法。网上的资源也很少有顾及到这个问题,所以只能自己解决,昨晚折腾3点多种仍然无果,才很不痛快地睡了,早上8点起来,突然想到叶节点上两联通棋子的数量也可能是一个重要的因素,自己又尝试着把估价函数优化了一下,基本思想是:估值时看看此时棋局中的每行列斜线里,对方棋子两联通加上一空白的情形有多少,若有这种情形,则扣去比较重的权值以说明此步比较危险,具体代码如下:


  1. def miniMaxEvalauate2(board,player):

  2.     opponent = { Player_O : Player_X, Player_X : Player_O }

  3.     winning_rows = [[0,1,2],[3,4,5],[6,7,8], # vertical
  4.                     [0,3,6],[1,4,7],[2,5,8], # horizontal
  5.                     [0,4,8],[2,4,6]]         # diagonal

  6.     count=0

  7.     clone_pieces=[Empty]*9

  8.     for i in range(9):
  9.         clone_pieces=board.pieces   

  10.     for pos in range(9):
  11.         if clone_pieces[pos] == Empty:
  12.             clone_pieces[pos]=player
  13.     for row in winning_rows:
  14.         flag=0
  15.         for i in row:
  16.             if(clone_pieces==opponent[player]):
  17.                 flag+=1
  18.         if (flag==0):
  19.             count+=1
  20.         if (flag==2):
  21.             count-=4

  22.     clone_pieces2=[Empty]*9

  23.     for i in range(9):
  24.         clone_pieces2=board.pieces

  25.     for pos in range(9):
  26.         if clone_pieces2[pos] == Empty:
  27.             clone_pieces2[pos]=opponent[player]
  28.     for row in winning_rows:
  29.         flag=0
  30.         for i in row:
  31.             if(clone_pieces2==opponent[player]):
  32.                 flag+=1
  33.         if (flag==3):
  34.             count-=1

  35.     return count
复制代码


经过测试,用这种优化的估值函数所产生的棋步是无法战胜了。由此可以看出估值函数在算法中的重要程度,直接决定了棋力大小。当然觉得这个算法可能还有完善的地方,感觉它选择棋步的时候比较注重于防御,即侵略性不够强,有一次走出(也有可能是我眼花了)根本不可能取胜的棋步来,可能加强自己棋子的二联通权值可以改善。

[ 本帖最后由 kavingray 于 2009-2-21 20:11 编辑 ]

评分

参与人数 1可用积分 +5 收起 理由
xiaoyu9805119 + 5 原创内容

查看全部评分

论坛徽章:
0
2 [报告]
发表于 2009-02-21 20:40 |只看该作者
写的不错...
LZ专门研究博弈类么? TTT体现不出来太多东西(毕竟可BruteForce), 试下五子棋吧, 规则也简单

BTW, 不知道Py在搜索时性能如何?

论坛徽章:
0
3 [报告]
发表于 2009-02-21 20:48 |只看该作者
没有专门研究,就是在写个TTT时候学了下然后做出来的笔记,我还是个学生,python也是初学而已~呵呵

性能没有很专门的测试过,不过在这个算法里速度可以,每次遍历三层生成的博弈树再估价再返回最优棋布平均耗时20多ms

论坛徽章:
0
4 [报告]
发表于 2009-02-21 21:19 |只看该作者
等你不是学生之后, 可能就没太多时间研究这些嘞....
大学的话可以看看ACM的题目

Python做小游戏你可以看看PySDL

论坛徽章:
0
5 [报告]
发表于 2009-02-23 12:43 |只看该作者
thanks to share.
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP