前言:
前面幾篇炸金花的文章, 里面涉及到了一個核心問題, 就是如何實現對手的牌力提升, 以及勝率的動態調整. 這個問題是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模型的陷阱, 有利於更好的決策.
對待博彩游戲, 希望大家娛樂心態行娛樂之事, 切勿賭博, ^_^.