前言:
前面几篇炸金花的文章, 里面涉及到了一个核心问题, 就是如何实现对手的牌力提高, 以及胜率的动态调整. 这个问题是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模型的陷阱, 有利于更好的决策. 对待博彩游戏, 但愿你们娱乐心态行娱乐之事, 切勿赌博, ^_^.