前两天看到一个斗地主残局(斗地主都有残局了..), 如下图. 我便想写个程序算一下结果如何. 记录一下, 锻练一下自己说清楚事情的能力.

想法是用程序模拟两个人对局. 就每人依次出牌. 如果一个人牌打完了, 对手就会要求悔棋. 一直到悔到最后没办法悔了就输了.

每个人都先从最简单的牌开始出,最开始出单张,打到最后发现单张不行,就出对子.
比如说, 农民先出一张3, 打到最后发现没办法赢, 再尝试先出一张4. 打到最后发现单张没办法赢, 就出2张3. 依次继续.
大小顺序我们可以随便定, 我是按这样来的: 单张,一对,三张,三带一,顺子,四带2,普通炸,王炸,Pass
解释一下最后的Pass是什么意思: 竟然想不到简单的例子, 就直接拿上图举例. 农民先出A, 地主小怪. 按顺序的话, 应该是出炸,但最后会发现炸会输,按我们定义好的顺序,接下来就是Pass,也就是说,地主出小怪, 我选择不出牌.

其中几个需要仔细斟酌一下的细节:

  1. 什么时候需要悔牌?
    • 一个人的牌出完了, 对手要求悔牌
    • A悔牌之后, 发现自己上一轮是Pass, 那只能继续往前悔牌
  2. 什么时候算赢?
    • 农民第一次出牌已经需要Pass了, 说明他输了
    • 地主要求农民悔牌的时候, 农民已经是第一张牌, 说明地主输了
  3. 需要保存/传递哪些信息
    1. 按顺序记录当前出过的所有牌
    2. 当前出牌的人
    3. 当前桌面上的牌是什么 (可以放第1项里面)
    4. 如果是刚刚悔牌, 需要保存/传递刚悔的是什么牌
    5. 是不是处于悔牌状态(具体到我的代码实现中, 这个值是需要传递的. 在你的实现中,可以通过第4项直接得到). 这个关系到下一次可以出什么牌. 比如说桌面是444带J, 如果我不是悔牌, 那我的3张必须比4大,单张随意. 如果我是悔牌, 那么三张可以一样, 但单张必须比之前的大.

简单的伪代码:

while True:
    cureent_player.next
    if len(paths) == 1 and desktop is Pass:
        第一牌就只能Pass,没得打了,判cureent_player输

    if destop is Pass and previous_desktop is Pass:
        //注意这里是需要从Pass往前悔三次牌, 这种回退是一定可以的
        rollback(cureent_player,previous_player, cureent_player)
    if cureent_player.win:
        if rollback(cureent_player,previous_player) is False:
            无牌可悔,cureent_player赢

具体代码见https://github.com/childe/doudizhu

英语太差, 有几个变量名字解释一下:

  • Round 是指选手出的一次牌. 比如444带J是一个Round对象. Pass也是一个Round对象
  • minimal Two.minimal(cards)本意是指在所有cards里面, 选出最小的对子牌. 这个方法用在对手刚Pass, 而自己悔牌,需要从单张升级到对子的时候. 但可能没有对子,需要继续寻找有没有3张,3带1,顺子等.

就这么多了, 我代码水平太次, 真正实现起来,还是Debug了好久,代码可读性也不是很理想. 另外希望没有打扰到你的思路, 对程序员来说, 递归可能是一种更加自然的思路. 但用Python实现的话,有可能会抛递归层次太深的异常. 毕竟我在i5上面跑了整5分钟, 日志打印了300MB…