炸金花遊戲(5)--動態收斂預期勝率的一種思路

 

前言:
  前面幾篇炸金花的文章, 裏面涉及到了一個核心問題, 就是如何實現對手的牌力提高, 以及勝率的動態調整. 這個問題是EV模型, 以及基準AI裏最重要的核心概念之一.
  本文將嘗試實現一個版本, 望拋磚引玉, 共同提升.html

 

相關文章:
  德州撲克AI--Programming Poker AI(譯).
  系列文章說來慚愧, 以前一直叫嚷着寫德州AI, 不過惋惜懶癌晚期, 一直沒去實踐, T_T. 相比而言, 炸金花簡單不少, 也更偏重於運氣和所謂的心理對抗.
  系列文章:
  1. 炸金花遊戲的模型設計和牌力評估
  2. 炸金花遊戲的勝率預估
  3. 基於EV(指望收益)的簡單AI模型
  4. 炸金花AI基準測試評估
  5. 動態收斂預期勝率的一種思路python

 

有趣的數學:
  在講動態勝率以前, 咱們先了解一下炸金花背後的一些數學概念.
  炸金花背後的各種票型分佈:算法

牌型 高牌 對子 順金 豹子
組合數 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. 構建牌型組合(初始化)app

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. 改造勝率算法
  以前的勝率算法是考慮去重的, 爲了簡化咱們不考慮手牌重複的問題, 若是二者的勝率接近, 能夠認爲等價.dom

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. 提高牌力規則
  牌力提高, 能夠根據幾個因素來斷定.ui

對手在看牌(see)以後, 每check一次, strength += delta
對手在看牌(see)以後, 每raise一次, strength += 2 * delta
對手在PK中, 主動PK獲勝, 則strength += delta
對手在PK中, 被動PK獲勝, 則strength += 2 * delta

  各個參數, 是須要調整修改的, 對於增量delta, 在前幾輪能夠大一點, 後面能夠小點, 不見得非要常數.
  這樣就實現了, AI勝率動態調整評估, 其勝率衰減和自身手牌相關, 從而避免線性衰減, 致使強牌價值不足, 弱牌損失慘重的問題.lua

 

完成的代碼:

# !/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模型的陷阱, 有利於更好的決策.   對待博彩遊戲, 但願你們娛樂心態行娛樂之事, 切勿賭博, ^_^.

相關文章
相關標籤/搜索