前言:
我也是突然心血来潮, 想写写炸金花这类游戏的AI实现. 本文算是这一系列的第二篇, 主要写炸金花的胜率预估, 主要基于蒙特卡罗的思想, 胜率是炸金花AI的核心决策数据, ^_^.
相关文章:
德州扑克AI--Programming Poker AI(译).
说来惭愧, 之前一直叫嚷着写德州AI, 不过可惜懒癌晚期, 一直没去实践, T_T. 相比而言, 炸金花简单很多, 也更偏重于运气和所谓的心理对抗.
系列文章:
1. 炸金花游戏的模型设计和牌力评估
蒙特卡罗(Monte Carlo):
该算法属于模拟统计, 通过大量的随机模拟, 来达到/接近精确解的方法, 简单有效.
它的一个最有名的例子, 就是模拟求解PI(圆周率), 在2*2的正方形中区域中, 随机生成大量的点, 最后PI满足如下公式:
圆面积/正方形面=圆内覆盖的点数/全部点=PI/4
这边不再具体阐述了, 具体可以参考博文: 蒙特卡罗(Monte Carlo)方法计算圆周率π
胜率预估:
手牌胜率预估, 我们假定一副牌(52张), 玩家数N(2~6)之间变化, 在经历足够多的模拟随机发牌后, 手牌的胜率趋于真实值.
伪代码如下(炸金花没有平局, 这里把牌力相等, 认为输):
# 假定随即模拟10000局, 其他玩家n个 sim_n = 10000 player_n = 其他玩家数 hand_cards = 玩家自己的手牌 # 玩家胜利的次数 win_n = 0 for i in range(sim_n): players <- 随机给n个玩家发牌 if 玩家的手牌 > 所有其他玩家的手牌: win_n += 1 # 这次概率值, 就接近真实的胜率 return win_n / sim_n
是不是觉得非常的简单, ^_^.
各类牌型的胜率统计:
这边选择了一些典型的牌型, 看看它在不同的对局用户数下, 胜率的变化:
牌型/几人桌 | 两人桌 | 三人桌 | 四人桌 | 五人桌 | 六人桌 |
[HK, SK, DK] 豹子 | 0.9997 | 0.9997 | 0.9995 | 0.9989 | 0.9988 |
[HA, HK, HQ] 同花顺 | 0.997 | 0.9949 | 0.9926 | 0.9894 | 0.989 |
[HA, HK, HT] 金 | 0.9951 | 0.9869 | 0.9805 | 0.976 | 0.9668 |
[HA, HK, SQ] 顺 | 0.9425 | 0.8928 | 0.8427 | 0.8006 | 0.7506 |
[H9, D9, ST] 对子 | 0.847 | 0.7113 | 0.605 | 0.5197 | 0.4335 |
[H9, DA, ST] 高牌 | 0.6644 | 0.4423 | 0.292 | 0.1901 | 0.1245 |
由此可见, 拿到顺以上的牌, 胜率相当的高, 而且随人数变化小. 拿到对子也是不错的牌, 需要根据对子本身的大小和参与人数来做一个合理的评估.
真实代码:
贴一下代码:
import random import time 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) class Shoe(object): def __init__(self, deck_num=1): """ :param deck_num: 几副牌, 默认为1副牌 """ self.deck_num = deck_num self.cards = [Card(s+c) for s in "HDSC" for c in "A23456789TJQK"] * self.deck_num self.idx = 0 def reshuffle(self): # 打散牌 self.idx = 0 random.shuffle(self.cards) def deal(self, exc_arr=[]): """ :param exc_arr: 发牌需要过滤掉的牌, 避免重复 :return: """ while self.idx < len(self.cards): card = self.cards[self.idx] self.idx = self.idx + 1 if str(card) in exc_arr: continue return card return None # 核心思路和德州一致, 把牌力映射为一个整数 # 牌力组成: 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 class ThreeCardEvaluator(object): """ 工具类 """ @staticmethod def win_prop(cards, n=2, sim_n=10000): """ 胜率计算 :param cards: :param n: 玩家数(包含玩家自己) :param sim_n: 模拟的轮数, 轮数越多越接近真实值 :return: """ random.seed(time.time()) shoe = Shoe(deck_num=1) exc_arr = [str(_) for _ in cards] owner_hand_value = ThreeCardEvaluator.evaluate(cards) # 胜利次数 win_n = 0 for _ in xrange(sim_n): # 打散牌谱 shoe.reshuffle() player_cards = [] for j in xrange(n - 1): player_cards.append([shoe.deal(exc_arr=exc_arr) for _ in range(3)]) # 统计其他玩家中最大的手牌值 max_hand_value = max([ThreeCardEvaluator.evaluate(_) for _ in player_cards]) if owner_hand_value > max_hand_value: win_n += 1 # 大量模拟后的胜率 return win_n * 1.0 / sim_n @staticmethod def evaluate(cards): """ 牌力值计算 :param cards: 三张牌构成的手牌 :return: """ 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 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 STRAIGHT_FLUSH_TYPE + (straight_val << 8) if flush_res: return FLUSH_TYPE + (flush_list[2] << 8) + (flush_list[1] << 4) + flush_list[2] if straight_res: return STRAIGHT_TYPE + (straight_val << 8) # 对子检测 pair_res, pair_list = ThreeCardEvaluator.__pairs(cards, vals) if pair_res: return PAIR_TYPE + (pair_list[0] << 8) + (pair_list[1] << 4) # 剩下的高high return 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, []
测试代码:
# !/usr/bin/env python # -*- coding:utf-8 -*- import sys reload(sys) sys.setdefaultencoding("utf-8") if __name__ == "__main__": card_cases = [ [Card('HK'), Card('SK'), Card('DK')], # 豹子 [Card('HA'), Card('HK'), Card('HQ')], # 顺金 [Card('HA'), Card('HK'), Card('HT')], # 金 [Card('HA'), Card('HK'), Card('SQ')], # 顺子 [Card('H9'), Card('D9'), Card('ST')], # 对子 [Card('H9'), Card('DA'), Card('ST')] # 高牌 ] for case in card_cases: p = ThreeCardEvaluator.win_prop(case, n=6, sim_n=10000) card = ', '.join([str(_) for _ in case]) print "[{}] = {}".format(card, p)
测试结果:
[HK, SK, DK] = 0.9988 [HA, HK, HQ] = 0.989 [HA, HK, HT] = 0.9668 [HA, HK, SQ] = 0.7506 [H9, D9, ST] = 0.4335 [H9, DA, ST] = 0.1245
总结:
本文是炸金花系列的第二篇, 后续要讲讲炸金花AI的编写, ^_^, 希望自己能坚持.