做一个微信欢乐斗地主之残局解答器!

版权声明:本文为博主原创文章,可以转载但必须注明出处。 https://blog.csdn.net/nirendao/article/details/79548720
今年过年的时候,在玩微信小程序之欢乐斗地主。发现里面还含有一个小游戏叫做“残局闯关”,如下图。




这里面的题,如果不熟悉其中的套路,有个别几道还真的不好做(下文有例子)。于是,我便萌发了设计并实现一个残局解答器的想法。从过年期间就开始利用业余时间进行coding,到今天晚上,用了大约3周左右的业余时间,终于实现出了一个基本的残局解答器。目前UI也已经全部完成。可以轻松解题了!
那么,如何做一个残局解答器呢?它是由哪几个部分构成的呢?算法又是怎样的呢?下面一个一个来介绍。

一、 残局解答器的组成部分
本残局解答器由以下这几部分组成:
1. 局面表示法
2. 招法生成器
3. 招法分类器
4. 招法过滤器
5. 招法应对器
6. 计算引擎
7. UI界面(未全部完成)
8. 测试程序


以下一一介绍:

1. 局面表示法
   局面表示分为2种,一种是用户输入输出局面表示法,第二种是引擎计算时使用的局面表示法。
   对于用户输入输出,那就和自然表示一样,J就是'J', Q就是'Q',小王是'Y',大王是'Z', 2是2或'2'都可以。
   对于引擎计算时的局面表示,那就全部是数据了,如J、Q、K、A、2分别是11,12,13,14,18. 小王是20,大王是30. 
   
2. 招法生成器(MoveGener)
   给定一个list,即一手牌,能够计算出这手牌所有的可出的招法,包括pass。
   本程序中,除pass外,共分为14种招法,分别为:单牌、对子、三个、炸弹、王炸、3+1、3+2、连续单牌(至少5连张)、连对(至少3连对)、飞机(连续2个或多个相同的3张牌)、连续3+1、连续3+2、4+2、4+双对。
   
3. 招法分类器(MoveClassifier)
   给定一个招法(即一个list),能够返回该招法的类型。若是连续型招法(如连对),还能指出是几连。
   
4. 招法过滤器(MoveFilter)
   给定一系列招法,再给定一个对手招法,在这一系列招法中,找出能够克制对手招法的招法。
   比如,[[3,3], [4,4], [5,5]], 对手招法是[4,4],那么经过MoveFilter的计算,得到的返回应该是 [[5,5]]
   
5. 招法应对器(get_resp_moves)
   给定一手牌,以及一个对手招法,给出这手牌中所有能够应对该对手招法的招法。
   可见,招法应对器就是对MoveGener、MoveClassifier、和MoveFilter的一个综合使用。
   第一步,利用MoveClassifier,判断对手招法是14种招法类型的哪一种;
   第二步,利用MoveGener,生成这种类型的这手牌的所有招法;
   第三步,利用MoveFilter,过滤第二步中产生的招法,得到可以应对对手招法的招法。
   
6. 计算引擎
   这是本程序的核心部分。
   本程序一共实现了2个引擎 - 蒙特卡洛搜索引擎(分为单进程版和多进程版) 和 Min-Max搜索引擎。
   详情见下一章。
   
7. UI界面
   因为未完全实现,所以这里从略。不过即使没有UI界面,本程序也是可以用来解答残局的。
   
8. 测试程序
   上述各个组成部分都有自己对应的测试程序。保证无bug. 
   

二、计算引擎

1.蒙特卡洛之殇

写这个程序之初,我很想实现一下蒙特卡洛搜索。因为所有的围棋AI,都使用了蒙特卡洛搜索,原因是传统的象棋类的Min-Max算法对它们并不适用。而其实几年前,我就用蒙特卡洛方法编程来做过概率题,效果还不错。
所谓的蒙特卡洛搜索,说白了,就是模拟所有的牌局至终局,或指定时间内模拟尽可能多的终局,然后统计每种招法对应的胜局概率。
但是在真正实现了蒙特卡洛引擎之后,终于证实了我曾经的一个想法:蒙特卡洛算法是不精确的,有致命伤。而这甚至都不需要数学证明,随便举个例子即可。
举例如下:
地主手上有牌:J, 8, 4
农民手上有牌:2, 4, 6
地主先出,地主应该出什么呢?聪明的读者,思考一下。
好了,公布答案,地主出J或8,都是必胜的,而唯独不能出4. 可是,蒙特卡洛在模拟完所有牌局后,给出的结论是:地主出4胜率最高,超过50%; 而出8或J,胜率都是刚好50%. 于是蒙特卡洛引擎给出的答案是出4. 这是错误的。
换句话说,在千万条路中,蒙特卡洛只会选择胜率最高的路,但是敌人往往只需要在这胜率最高的路的子路口中选择唯一一条绝杀的路就可以了。
现在的围棋AI,包括水平非常高的DeepZenGo和绝艺,似乎都有死活不好的问题,我感觉应该是和蒙特卡洛搜索有关。而举世闻名的AlphaGo在其第一版,也有这个问题,输给李世石的那一盘棋,与其说李世石下出了神之一手,不如说是引起了AlphaGo Lee的bug. 不过AlphaGo Master和AlphaGo Zero似乎并没有死活方面的问题,因为它们不再出战,这就是个历史之谜了。

2. 回归到Min-Max纯暴力搜索加剪枝

既然蒙特卡洛无法给出正确的结果,我还是回到了人类最原始的想法,Min-Max算法。
这个算法是国际象棋和中国象棋的人工智能程序的基石算法。虽然象棋AI种类繁多,优化方法也是千变万化,但是所有象棋AI归根结底,都是建立于Min-Max这个基石之上的。
那么,Min-Max算法是什么意思呢?
其实,意思很直观。先这么看,假设地主先出,地主一手牌就出完了,那么这种情况就是地主必胜。给地主先出之前的局面打分,就是满分100分。假设地主先出,但牌没有出完,农民接着出。不论地主出什么牌,农民手上只有2张牌,王炸,好了,那么农民必胜。这个时候,给地主出完牌而农民待出牌的局面打分,就是0分,这是农民必胜局面。再往上一层,给地主待出牌的局面打分,发现无论地主出什么牌,比如10种出法,10种返回都是0分,那么地主待出牌的这个局面自然也就是0分。
换句话说,在地主待出牌的局面下,一旦找到一种出法的返回值是100分,那么选这种打法就地主必胜啦;反之,若所有招法的局面都返回0分,那这就是地主必败局面,如果要往上一层返回,就是返回0分。
总结一下:
地主待出牌局面,一旦找到一个100分的招法,剩下招法都不用试了,直接返回100分,这就是剪枝;
同理,农民待出牌局面,一旦找到一个0分的招法,剩下的招法都不用试了,直接返回0分,这就是剪枝。

利用Min-Max算法,以及上面介绍的剪枝算法,我发现程序运行的速度有了极大的提升。其实原因就是剪枝了。

三、运行效果

最后,来看一下运行效果吧。
聪明的读者,请看一下2018年3月残局闯关的第12关吧,您能过去吗?

首先,编辑solve_python.py文件,只需修改其中2行表示地主牌和农民牌的,如下:



然后,运行程序:

没错,唯一的一招必杀是先出 10 !所有其他招法都是必败招。这就是Min-Max的威力了!

其实,一般的题用不了这么长的时间,几秒到几十秒就做出来了,但这一道题确实有点难度,所以时间也耗得久一些。地主出牌后,用户可以根据手机上农民的出牌,再把农民出的牌手动输入到以上人机交互界面里,然后电脑会继续计算地主的应招,这一次计算就很快了,基本是秒级的。

最后,公布一下源代码地址:

https://github.com/FinixLei/WeChat_LandLords


   
   

猜你喜欢

转载自blog.csdn.net/nirendao/article/details/79548720