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