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