利用Python破解斗地主残局详解


Posted in Python onJune 30, 2017

前言

相信大家都玩过斗地主,规则就不再介绍了。

直接上一张朋友圈看到的残局图:

利用Python破解斗地主残局详解

这道题我刚看到时,曾尝试用手工来破解,每次都以为找到了农民的必胜策略时,最后都发现其实农民跑不掉。由于手工破解无法穷尽所有可能性,所以这道题究竟农民有没有妙手跑掉呢,只能通过代码来帮助我们运算了。

本文将简要讲述怎么通过代码来求解此类问题,在最后会公布残局的最后结果,并开源代码以供大家吐槽。

minimax

代码的核心思想是minimax。minimax可以拆解为两部分,mini和max,分别是最小和最大的意思。

直观的理解是什么呢?就有点像A、B两个人下棋。A现在可以在N个点走棋,假设A在某个点走棋了,使得A的这一步的盘面评估分数最高;但是轮到B下的时候,就一定会朝着让A最不利的方向走,使得A的下一步必然按照B设定的轨迹来,而没法达到A在第一步时估算到这一步的最高盘面评分。

在牌局中是一样的,如果农民的一手牌,让地主无论如何应对都不能赢的话,那么可以说农民有必胜策略;否则,农民必输。

核心逻辑

我们可以用一个函数hand_out来模拟一个人的出牌过程。在现实生活中,一个人想要出牌的话,必然需要知道自己手上的所有牌:me_pokers,也需要知道上一手的出的牌:last_hand。如果我们要用这个函数来模拟两个人的出牌,则还需要知道对手当前的所有牌:enemy_pokers。

这个函数的返回值,是轮到我me_pokers出牌时,是否能够必赢牌。如果能赢则返回真,否则返回假。

def hand_out(me_pokers, enemy_pokers, last_hand)

假设轮到我出牌时,如果我手上的牌都出完了,那么我将立刻知道我赢了;反之如果对手的牌都出完了,而我没有,则我失败了。

if not me_pokers:
 return True
if not enemy_pokers:
 return False

因为现在轮到我出牌,所以我首先需要知道我现在能出的所有手牌组合。注意:这个组合中,包括过牌(即不出牌)的策略。

all_hands = get_all_hands(me_pokers)

现在我们要对所有可能的手牌组合进行遍历。

首先我需要知道,上一手对方出的牌是什么。

  • 如果对方上一手选择过牌,或者没有上一手牌,那么我这一轮必须不能过牌,但是我可以出任意的牌
  • 如果对手上一手出了牌,则我必须要出一个比它更大的牌或者选择这一轮直接过牌(不出牌)

关键点来了,在出完我的牌或选择过牌后,我们需要用一个递归调用来模拟对手下一步的行为。如果对手的下一次出牌不能获胜的话,则我这一次的出牌必胜;否则,对于我的每一个出牌选择,对手都能获胜的话,则我必败。

全部代码如下:

def hand_out(me_pokers, enemy_pokers, last_hand, cache):
 if not me_pokers:
  # 我全部过牌,直接获胜
  return True
 if not enemy_pokers:
  # 对手全部过牌,我失败
  return False
 # 获取我当前可以出的所有手牌组合,包括过牌
 all_hands = get_all_hands(me_pokers)
 # 遍历我的所有出牌组合,进行模拟出牌
 for hand in all_hands:
  # 如果上一轮对手出了牌,则这一轮我必须要出比对手更大的牌 或者 对手上一轮选择过牌,那么我只需出任意牌,但是不能过牌
  if (last_hand and can_comb2_beat_comb1(last_hand, hand)) or (not last_hand and hand['type'] != COMB_TYPE.PASS):
   # 模拟对手出牌,如果对手不能取胜,则我必胜
   if not hand_out(enemy_pokers, make_hand(me_pokers, hand), hand, cache):
    return True
  # 如果上一轮对手出了牌,但我这一轮选择过牌
  elif last_hand and hand['type'] == COMB_TYPE.PASS:
   # 模拟对手出牌,如果对手不能取胜,则我必胜
   if not hand_out(enemy_pokers, me_pokers, None, cache):
    return True
 # 如果之前的所有出牌组合均不能必胜,则我必败
 return False

构建

以上核心逻辑理清楚后,构建破解器将变得十分简单。

首先,我们要用数字来表示牌的大小,这里我们用3表示3,11来表示J,12表示Q,依次类推……

其次,我们需要求出一个手牌的所有出牌组合,这里需要get_all_hands函数,具体实现比较繁琐但是很简单,就不在此赘述。

然后,我们还需要一个牌力判断函数can_comb2_beat_comb1(comb1, comb2) ,这个函数用于比较两组手牌的牌力,看是否comb2可以击败comb1。唯一需要注意的一点,在斗地主的规则中,除了炸弹外,其他所有牌力均等,只有牌型一样时才能去比较。

最后,我们需要一个模拟出牌函数make_hand(pokers, hand) ,用于求出在手牌为pokers的情况下打出一手牌hand后,剩下的手牌,实现也非常简单,只需简单的移除掉那些打出的牌即可。

效率

由于一副牌的可能手牌巨大,导致递归的分支数巨大。所以时间开销非常大,为阶乘级O(N!),根据斯特林公式,大约为O(N^N)。

由于可能会有很多重复的牌面出现,导致了很多重复的递归调用。所以加一个缓存能极大提升效率。

即对我方手牌和敌方手牌和上一轮手牌的描述(str(me_pokers)+str(enemy_pokers)+str(last_hand))为键,将求出的结果存进缓存字典中。下一次遇到相同的局面时,即可直接从缓存字典中取出,而无需再次重复计算。时间复杂度优化为指数级O(C^N)。

结果

代码运算出来的结果是,农民没有必胜策略。换言之,只要地主会玩,农民不可能赢。阶级固化已经如斯了么……

开源

代码放于Github: doudizhu_solver,或者大家可以本地下载,MIT协议,随便玩。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
PyCharm使用教程之搭建Python开发环境
Jun 07 Python
Android 兼容性问题:java.lang.UnsupportedOperationException解决办法
Mar 19 Python
对变量赋值的理解--Pyton中让两个值互换的实现方法
Nov 29 Python
Python时间和字符串转换操作实例分析
Mar 16 Python
Python面向对象程序设计之私有属性及私有方法示例
Apr 08 Python
python隐藏终端执行cmd命令的方法
Jun 24 Python
基于TensorFlow中自定义梯度的2种方式
Feb 04 Python
python 使用递归回溯完美解决八皇后的问题
Feb 26 Python
python实现图像全景拼接
Mar 27 Python
pyecharts动态轨迹图的实现示例
Apr 17 Python
python如何写try语句
Jul 14 Python
端午节将至,用Python爬取粽子数据并可视化,看看网友喜欢哪种粽子吧!
Jun 11 Python
Python实现的文本编辑器功能示例
Jun 30 #Python
Python构建XML树结构的方法示例
Jun 30 #Python
基于python的Tkinter编写登陆注册界面
Jun 30 #Python
Python使用微信SDK实现的微信支付功能示例
Jun 30 #Python
python实现的二叉树定义与遍历算法实例
Jun 30 #Python
Python使用openpyxl读写excel文件的方法
Jun 30 #Python
python中关于for循环的碎碎念
Jun 30 #Python
You might like
可快速识别放射性物质-国外大神教你diy一个开放式辐射探测器
2020/03/12 无线电
php对大文件进行读取操作的实现代码
2013/01/23 PHP
php使用指定编码导出mysql数据到csv文件的方法
2015/03/31 PHP
PHP如何使用cURL实现Get和Post请求
2020/07/11 PHP
解决 FireFox 下[使用event很麻烦] 的问题.
2006/08/22 Javascript
模拟jQuery ajax服务器端与客户端通信的代码
2011/03/28 Javascript
js中更短的 Array 类型转换
2011/10/30 Javascript
node.js中的fs.existsSync方法使用说明
2014/12/17 Javascript
angularjs 处理多个异步请求方法汇总
2015/01/06 Javascript
JavaScript事件委托技术实例分析
2015/02/06 Javascript
JS使用eval()动态创建变量的方法
2016/06/03 Javascript
AngularJS入门教程之迭代器过滤详解
2016/08/18 Javascript
从零开始学习Node.js系列教程六:EventEmitter发送和接收事件的方法示例
2017/04/13 Javascript
基于AngularJS实现的工资计算器实例
2017/06/16 Javascript
vue使用axios实现文件上传进度的实时更新详解
2017/12/20 Javascript
JS和Canvas实现图片的预览压缩和上传功能
2018/03/30 Javascript
react 应用多入口配置及实践总结
2018/10/17 Javascript
vue基础之事件简写、事件对象、冒泡、默认行为、键盘事件实例分析
2019/03/11 Javascript
vue响应式更新机制及不使用框架实现简单的数据双向绑定问题
2019/06/27 Javascript
微信小程序在线客服自动回复功能(基于node)
2019/07/03 Javascript
微信小程序点击生成朋友圈分享图(遇到的坑)
2020/06/17 Javascript
JavaScript图像放大镜效果实现方法详解
2020/06/28 Javascript
[01:32]dota2拉比克至宝(222)
2018/12/20 DOTA
Python入门_浅谈数据结构的4种基本类型
2017/05/16 Python
Django使用Celery异步任务队列的使用
2018/03/13 Python
django 使用 request 获取浏览器发送的参数示例代码
2018/06/11 Python
Python实现两个list求交集,并集,差集的方法示例
2018/08/02 Python
Python编程快速上手——strip()函数的正则表达式实现方法分析
2020/02/29 Python
设置jupyter中DataFrame的显示限制方式
2020/04/12 Python
html5+css3气泡组件的实现
2014/11/21 HTML / CSS
党员应该树立反腐倡廉的坚定意识思想汇报
2014/09/12 职场文书
2014预备党员批评与自我批评思想汇报
2014/09/20 职场文书
2015年社区精神文明工作总结
2015/05/26 职场文书
企业法人任命书
2015/09/21 职场文书
2016年社区“我们的节日·中秋节”活动总结
2016/04/05 职场文书
MySql学习笔记之事务隔离级别详解
2021/05/12 MySQL