前言:
我也是突然心血來潮, 想寫寫炸金花這類游戲的AI實現. 本文算是這一系列的第二篇, 主要寫炸金花的勝率預估, 主要基於蒙特卡羅的思想, 勝率是炸金花AI的核心決策數據, ^_^.
相關文章:
德州撲克AI--Programming Poker AI(譯).
系列文章說來慚愧, 之前一直叫嚷着寫德州AI, 不過可惜懶癌晚期, 一直沒去實踐, T_T. 相比而言, 炸金花簡單很多, 也更偏重於運氣和所謂的心理對抗.
系列文章:
1. 炸金花游戲的模型設計和牌力評估
2. 炸金花游戲的勝率預估
3. 基於EV(期望收益)的簡單AI模型
4. 炸金花AI基准測試評估
5. 動態收斂預期勝率的一種思路
蒙特卡羅(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的編寫, ^_^, 希望自己能堅持.
