炸金花游戏(5)--动态收敛预期胜率的一种思路

 

前言:
  前面几篇炸金花的文章, 里面涉及到了一个核心问题, 就是如何实现对手的牌力提升, 以及胜率的动态调整. 这个问题是EV模型, 以及基准AI里最重要的核心概念之一.
  本文将尝试实现一个版本, 望抛砖引玉, 共同提高.

 

相关文章:
  德州扑克AI--Programming Poker AI(译).
  系列文章说来惭愧, 之前一直叫嚷着写德州AI, 不过可惜懒癌晚期, 一直没去实践, T_T. 相比而言, 炸金花简单很多, 也更偏重于运气和所谓的心理对抗.
  系列文章:
  1. 炸金花游戏的模型设计和牌力评估
  2. 炸金花游戏的胜率预估
  3. 基于EV(期望收益)的简单AI模型
  4. 炸金花AI基准测试评估
  5. 动态收敛预期胜率的一种思路

 

有趣的数学:
  在讲动态胜率之前, 我们先了解一下炸金花背后的一些数学概念.
  炸金花背后的各类票型分布:

牌型 高牌 对子 顺金 豹子
组合数 16440 3744 720 1096 48 52

  52张牌, 总共22100种组合, 一手牌有74.3891%的概率是高牌, 因此在单挑局中, 带个A的高牌也是不小的牌, 不要轻易丢掉, ^_^.
  而从出现分布上来, 顺金(48) > 豹子(52) > 顺(720) > 金(1096) > 对子(3744) > 高牌(16440), 其实牌力按这个顺序其实更合理, 不过规则就是规则, 还是尊重历史吧.

 

模型思路:
  一副牌的炸金花, 共有22100种组合, 对这些组合我们按牌力大小进行排序(从小到大), 最后构建为一个牌力数组.
  每个玩家都有一个牌力值(strength), 默认为0. 玩家的牌力随机分布在牌力数组的[strength, 22100]之间.
  根据玩家的反应, 按规则提升其牌力值(strenth), 然后再利用蒙特卡洛算法重新计算其AI手牌的胜率p.
  1. 构建牌型组合(初始化)

def init_cards_combination():
    """
    炸金花手牌生成器
    :return:
    """
    arr_ranks = []
    # 生成52张牌
    cards = [Card(s + r) for s in "HDSC" for r in "A23456789TJQK"]
    card_len = len(cards)

    # 三层循环, 枚举22110种组合
    for i in range(card_len):
        for j in range(i + 1, card_len):
            for k in range(j + 1, card_len):
                hand = [cards[i], cards[j], cards[k]]
                arr_ranks.append({
                    # 牌力值计算
                    "hand_value": ThreeCardEvaluator.evaluate(hand),
                    # 手牌组合保存
                    "cards": hand
                })

    # 根据牌力值, 进行从小到大的排序
    return sorted(arr_ranks, key=lambda item: item["hand_value"])

  2. 改造胜率算法
  之前的胜率算法是考虑去重的, 为了简化我们不考虑手牌重复的问题, 如果两者的胜率接近, 可以认为等价.

class ThreeCardWinRate(object):

    # 初始化牌组合
    _g_ranks = init_cards_combination()

    @staticmethod
    def win_prop_dy(hand, players=[], sim_n=10000):
        """
        引入动态调整牌力的胜率评估函数
        :param hand: 玩家手牌
        :param players: 玩家数组
        :param sim_n:
        :return:
        """

        # 计算玩家的手牌牌力
        hand_value = ThreeCardEvaluator.evaluate(hand)
        card_len = len(ThreeCardWinRate._g_ranks)

        # 胜利次数
        win_n = 0
        for i in range(sim_n):
            t_max_hand_value = 0
            for player in players:
                strength = player["strength"]
                if strength >= card_len:
                    strength = card_len - 1

                # 随机选择在牌力范围[strength, card_len-1]的手牌
                idx = random.randint(strength, card_len - 1)
                t_hand = ThreeCardWinRate._g_ranks[idx]["cards"]

                t_hand_value = ThreeCardEvaluator.evaluate(t_hand)
                if t_hand_value > t_max_hand_value:
                    t_max_hand_value = t_hand_value

            if hand_value > t_max_hand_value:
                win_n += 1

        return win_n * 1.0 / sim_n

  我们选取几手具有代表性的手牌, 分别采用两种模式(去重, 不去重)来计算胜率, 此时玩家的strength默认为0, 即范围在[0, 22100]之间, 胜率如下:

牌型 二人桌 三人桌 四人桌 五人桌 六人桌
豹子[H2,S2,D2] 0.9975/0.9981 0.994/0.9959 0.9931/0.9928 0.9911/0.9911 0.9875/0.9881
顺金[H2,H3,H4] 0.9959/0.9963 0.9907/0.9907  0.9857/0.9887  0.9808/0.9844 0.9797/0.9794
金[H2,H3,H5] 0.9451/0.9434  0.8911/0.9006  0.8394/0.8438  0.7967/0.8064  0.7532/0.7638
顺子[H2,H3,S4] 0.9143/0.9122  0.8416/0.8363  0.7656/0.7707  0.7004/0.6979 0.633/0.6459 
对子[H2,D2,S3] 0.7388/0.7494  0.556/0.5622  0.4037/0.4114  0.2972/0.3164  0.2354/0.2249
高牌[H2,D3,S5]  0/0  0/0  0/0  0/0  0/0

  注: 前者为去重后胜率, 后者为不去重的胜率, 两者接近, 为了加速计算, 可以用不去重的版本来快速评估胜率.

  3. 提升牌力规则
  牌力提升, 可以根据几个因素来判定.

对手在看牌(see)之后, 每check一次, strength += delta
对手在看牌(see)之后, 每raise一次, strength += 2 * delta
对手在PK中, 主动PK获胜, 则strength += delta
对手在PK中, 被动PK获胜, 则strength += 2 * delta

  各个参数, 是需要调整修改的, 对于增量delta, 在前几轮可以大一点, 后面可以小点, 不见得非要常数.
  这样就实现了, AI胜率动态调整评估, 其胜率衰减和自身手牌相关, 从而避免线性衰减, 导致强牌价值不足, 弱牌损失惨重的问题.

 

完成的代码:

# !/usr/bin/env python
# -*- coding:utf-8 -*-

import random
import time

import sys
reload(sys)
sys.setdefaultencoding("utf-8")


CARD_CONST = {
    "A": 14,
    "2": 2,
    "3": 3,
    "4": 4,
    "5": 5,
    "6": 6,
    "7": 7,
    "8": 8,
    "9": 9,
    "T": 10,
    "J": 11,
    "Q": 12,
    "K": 13
}


class Card(object):
    """
        牌的花色+牌值
    """
    def __init__(self, val):
        self.suit = val[0]
        self.rank = val[1]
        self.value = CARD_CONST[val[1]]

    def __str__(self):
        return "%s%s" % (self.suit, self.rank)

    def __eq__(self, other):
        return self.suit == other.suit and self.rank == other.rank

    def __repr__(self):
        return "'{}'".format(str(self))


class ThreeCardEvaluator(object):
    """
    核心思路和德州一致, 把牌力映射为一个整数
    牌力组成: 4个半字节(4位), 第一个半字节为牌型, 后三个半字节为牌型下最大的牌值
    牌型, 0: 单张, 1: 对子, 2: 顺子, 3: 金, 4: 顺金, 5: 豹子
    """

    # 高high
    HIGH_TYPE = 0
    # 对子
    PAIR_TYPE = 1 << 12
    # 顺子
    STRAIGHT_TYPE = 2 << 12
    # 同花(金)
    FLUSH_TYPE = 3 << 12
    # 同花顺
    STRAIGHT_FLUSH_TYPE = 4 << 12
    # 豹子
    LEOPARD_TYPE = 5 << 12

    @staticmethod
    def evaluate(cards):
        if not isinstance(cards, list):
            return -1
        if len(cards) != 3:
            return -1

        vals = [card.value for card in cards]
        # 默认是从小到大排序
        vals.sort()

        # 豹子检测
        leopard_res, leopard_val = ThreeCardEvaluator.__leopard(cards, vals)
        if leopard_res:
            return ThreeCardEvaluator.LEOPARD_TYPE + (vals[0] << 8)

        # 同花检测
        flush_res, flush_list = ThreeCardEvaluator.__flush(cards, vals)
        # 顺子检测
        straight_res, straight_val = ThreeCardEvaluator.__straight(cards, vals)

        if flush_res and straight_res:
            return ThreeCardEvaluator.STRAIGHT_FLUSH_TYPE + (straight_val << 8)
        if flush_res:
            return ThreeCardEvaluator.FLUSH_TYPE + (flush_list[2] << 8) + (flush_list[1] << 4) + flush_list[2]
        if straight_res:
            return ThreeCardEvaluator.STRAIGHT_TYPE + (straight_val << 8)

        # 对子检测
        pair_res, pair_list = ThreeCardEvaluator.__pairs(cards, vals)
        if pair_res:
            return ThreeCardEvaluator.PAIR_TYPE + (pair_list[0] << 8) + (pair_list[1] << 4)

        # 剩下的高high
        return ThreeCardEvaluator.HIGH_TYPE + (vals[2] << 8) + (vals[1] << 4) + vals[2]

    @staticmethod
    def __leopard(cards, vals):
        if cards[0].rank == cards[1].rank and cards[1].rank == cards[2].rank:
            return True, cards[0].value
        return False, 0

    @staticmethod
    def __flush(cards, vals):
        if cards[0].suit == cards[1].suit and cards[1].suit == cards[2].suit:
            return True, vals
        return False, []

    @staticmethod
    def __straight(cards, vals):
        # 顺子按序递增
        if vals[0] + 1 == vals[1] and vals[1] + 1 == vals[2]:
            return True, vals[2]
        # 处理特殊的牌型, A23
        if vals[0] == 2 and vals[1] == 3 and vals[2] == 14:
            return True, 3
        return False, 0

    @staticmethod
    def __pairs(cards, vals):
        if vals[0] == vals[1]:
            return True, [vals[0], vals[2]]
        if vals[1] == vals[2]:
            return True, [vals[1], vals[0]]
        return False, []


def init_cards_combination():
    """
    炸金花手牌生成器
    :return:
    """
    arr_ranks = []
    # 生成52张牌
    cards = [Card(s + r) for s in "HDSC" for r in "A23456789TJQK"]
    card_len = len(cards)

    # 三层循环, 枚举22110种组合
    for i in range(card_len):
        for j in range(i + 1, card_len):
            for k in range(j + 1, card_len):
                hand = [cards[i], cards[j], cards[k]]
                arr_ranks.append({
                    # 牌力值计算
                    "hand_value": ThreeCardEvaluator.evaluate(hand),
                    # 手牌组合保存
                    "cards": hand
                })

    # 根据牌力值, 进行从小到大的排序
    return sorted(arr_ranks, key=lambda item: item["hand_value"])


class ThreeCardWinRate(object):

    # 初始化牌组合
    _g_ranks = init_cards_combination()

    @staticmethod
    def win_prop_dy(hand, players=[], sim_n=10000):
        """
        引入动态调整牌力的胜率评估函数
        :param hand: 玩家手牌
        :param players: 玩家数组
        :param sim_n:
        :return:
        """

        # 计算玩家的手牌牌力
        hand_value = ThreeCardEvaluator.evaluate(hand)
        card_len = len(ThreeCardWinRate._g_ranks)

        # 胜利次数
        win_n = 0
        for i in range(sim_n):
            t_max_hand_value = 0
            for player in players:
                strength = player["strength"]
                if strength >= card_len:
                    strength = card_len - 1

                # 随机选择在牌力范围[strength, card_len-1]的手牌
                idx = random.randint(strength, card_len - 1)
                t_hand = ThreeCardWinRate._g_ranks[idx]["cards"]

                t_hand_value = ThreeCardEvaluator.evaluate(t_hand)
                if t_hand_value > t_max_hand_value:
                    t_max_hand_value = t_hand_value

            if hand_value > t_max_hand_value:
                win_n += 1

        return win_n * 1.0 / sim_n


if __name__ == "__main__":

    random.seed(time.time())

    card_cases = [
        [Card('H2'), Card('S2'), Card('D2')],      # 豹子
        [Card('H2'), Card('H3'), Card('H4')],      # 顺金
        [Card('H2'), Card('H3'), Card('H5')],      # 金
        [Card('H2'), Card('H3'), Card('S4')],      # 顺子
        [Card('H2'), Card('D2'), Card('S3')],      # 对子
        [Card('H2'), Card('D3'), Card('S5')]       # 高牌
    ]

    for case in card_cases:
        print "{}=".format(",".join([str(c) for c in case])),
        for n in range(2, 7):
            p = ThreeCardWinRate.win_prop_dy(
                hand=case,
                players=[{"strength": 0} for _ in range(n)],
                sim_n=10000
            )
            print "{}".format(p),
        print ""

  

总结:
  总的感觉, 这个思路还是符合真实的打牌场景的. 这种动态调整胜率的做法, 也避免之前EV模型的陷阱, 有利于更好的决策.
  对待博彩游戏, 希望大家娱乐心态行娱乐之事, 切勿赌博, ^_^.

 

  

  

posted on 2019-01-24 15:59  mumuxinfei  阅读(...)  评论(...编辑  收藏

导航

统计