Golang算法实战之斗地主<一>

逢年过节,回到老家,玩的最多的就是打麻将、斗地主。今天要说的,就是这个经典游戏——斗地主。

一、斗地主牌面分析

斗地主需要多少张牌?大部分人都知道需要一副完整的牌即可,也就是54张牌。

  1. 2-10 黑桃、红桃、梅花、方片各4张。
  2. J、Q、K、A 黑桃、红桃、梅花、方片各4张。
  3. 大小王各1张。

在斗地主中,牌的花色不影响。所以,在牌面比对时,不需要单独比对花色。而单张牌面值的大小顺序为: 大王>小王>2>A>K>Q>J>10……3
鉴于此,牌面的表达可以用以下方式来规定:
A:黑桃 B:红桃 C:梅花 D:方片

扑克原始值 映射值
3-10 3-10数字
J 11
Q 12
K 13
A 14
2 15
小王 Q88
大王 K99
例如:
A14----->黑桃A
C9----->梅花9

二、如何开始游戏

先来看一张图


斗地主初始化.png

游戏初始化拆分成3大块

  1. 构造一副牌
  2. 洗牌
  3. 发牌
1、构造一副牌

构造一副牌就是根据牌面分析中规定的牌面表达方法构造一副完整的54张扑克牌。
代码如下:

func CreateNew() []string {
    numbers := make([]string, 54) //构造一个大小为54的数组
    start := 0  //造牌游标
    for i := 3; i <= 16; i++ {
        if i == 16 { //i为16说明已经到大小王
            numbers[start] = "Q88"
            numbers[start+1] = "K99" //直接构造大小王
        } else {
            numbers[start] = "A" + strconv.Itoa(i)
            numbers[start+1] = "B" + strconv.Itoa(i)
            numbers[start+2] = "C" + strconv.Itoa(i)
            numbers[start+3] = "D" + strconv.Itoa(i)
            start += 4 //每造一套单值牌,游标移4位
        }
    }
    return numbers
}

验证一下:

func main() {
    initValues := card.CreateNew()
    fmt.Println(initValues)
}

打印:
[A3 B3 C3 D3 A4 B4 C4 D4 A5 B5 C5 D5 A6 B6 C6 D6 A7 B7 C7 D7 A8 B8 C8 D8 A9 B9 C9 D9 A10 B10 C10 D10 A11 B11 C11 D11 A12 B12 C12 D12 A13 B13 C13 D13 A14 B14 C14 D14 A15 B15 C15 D15 Q88 K99]

2、洗牌

洗牌就是将牌原有的顺序打乱,形成新的顺序的牌。主要利用随机数来处理。

func Shuffle(vals []string) {
    r := rand.New(rand.NewSource(time.Now().Unix()))  //根据系统时间戳初始化Random
    for len(vals) > 0 {//根据牌面数组长度遍历
        n := len(vals)//数组长度
        randIndex := r.Intn(n)//得到随机index
        vals[n-1], vals[randIndex] = vals[randIndex], vals[n-1]//最后一张牌和第randIndex张牌互换
        vals = vals[:n-1] 
    }
}

这是一种抽牌插底的洗牌算法,时间复杂度为O(n),当然还有效率更高的洗牌算法,具体可以另做研究。
验证一下:

func main() {
    initValues := card.CreateNew()
    fmt.Println("洗牌前: " , initValues)
    card.Shuffle(initValues)
    fmt.Println("洗牌后", initValues)
}

打印:
洗牌前:  [A3 B3 C3 D3 A4 B4 C4 D4 A5 B5 C5 D5 A6 B6 C6 D6 A7 B7 C7 D7 A8 B8 C8 D8 A9 B9 C9 D9 A10 B10 C10 D10 A11 B11 C11 D11 A12 B12 C12 D12 A13 B13 C13 D13 A14 B14 C14 D14 A15 B15 C15 D15 Q88 K99]
洗牌后 [A4 D15 C12 D13 A10 D4 A9 Q88 A7 A6 D6 D14 D10 A14 B4 B15 C8 B13 C14 C13 B11 C4 A12 D11 A3 C5 C10 A13 B5 D8 B6 D9 B10 D7 A5 B7 B3 B14 B12 C3 B8 C7 C15 C6 D3 D5 A8 A15 C11 B9 K99 C9 D12 A11]

可见洗牌达到了预期。

3、发牌

发牌可以说是斗地主开始前的最后一个环节(不包含叫地主抢地主),发牌是要将牌先均分给3个玩家(保留3张底牌),并从玩家中随机抽取一位玩家为地主。
首先,将牌分成4部分:
玩家一:17张牌
玩家二:17张牌
玩家三:17张牌
底牌:3张

/**
*发牌
*order==0 玩家1次序
*order==1 玩家2次序
*order==2 玩家3次序
*order==3 底牌次序
 */
func Dispacther(order int, vals []string) []string {
    var playCards []string
    if order < 0 || order > 3 {//判断玩家次序是否正确
        return []string{}
    } else {
        size := 17 //默认总长度为17
        if order == 3 {
            size = 3 //次序为3(底牌次序)时,总长度为3
        }
        for i := 0; i < len(playCards); i++ {
            playCards = append(playCards, vals[order*17+i])//根据次序发牌
        }
    }
    return playCards
}

验证一下:

func main() {
    initValues := card.CreateNew()
    card.Shuffle(initValues)
    fmt.Println("玩家1:", card.Dispacther(0, initValues))
    fmt.Println("玩家2:", card.Dispacther(1, initValues))
    fmt.Println("玩家3:", card.Dispacther(2, initValues))
    fmt.Println("底牌:", card.Dispacther(3, initValues))
}

打印:
玩家1: [A4 C14 B14 C4 C13 C15 D6 D14 A13 B13 D11 B4 B12 C12 B9 D8 B6]
玩家2: [A9 D3 D10 A5 C5 C7 C8 A7 C6 A6 C11 B15 C9 A3 C10 A8 D13]
玩家3: [K99 D15 C3 B3 B5 A15 A11 B7 Q88 A10 D12 A12 A14 D7 B11 B8 D9]
底牌: [B10 D4 D5]

从打印结果来看,发牌也是满足场景的。

三、出牌分析

接下来,就是最复杂的点,出牌的处理。

1. 牌面分类

首先要处理的是根据所出的牌,判断出出牌的类型。
根据以往游戏中的经验来看,出牌类型总的可以分为以下几种类型(由简单到复杂)

  1. 单根
  2. 对子
  3. 三不带
  4. 三带一
  5. 炸弹(4张同值牌)
  6. 四带二
  7. 飞机
  8. 三不带飞机
  9. 连对
  10. 顺子
  11. 王炸

那么,根据以上类型,我们首先定义出出牌类型枚举

type CardTypeStatus int

const (
    _CardTypeStatus = iota
    SINGLE          //单根
    DOUBLE          //对子
    THREE           //三不带
    THREE_AND_ONE   //三带一
    BOMB            //炸弹
    FOUR_TWO        //四带二
    PLANE           //飞机
    PLANE_EMPTY     //三不带飞机
    DOUBLE_ALONE    //连对
    SINGLE_ALONE    //顺子
    KING_BOMB       //王炸
    ERROR_TYPE      //非法类型

)
2.计算推理

玩家出的牌张数不固定,那么,如何有效的判断出玩家所出牌的类型呢。
首先从最简单的,根据牌的张数可以判断出最简单的3种场景

  1. 单根
  2. 对子
  3. 王炸
func ParseCardsInSize(plays []string) {
    switch len(plays) {
        case 1:
            fmt.Println("单根")
            break
        case 2:
            if plays[0] == "Q88" && plays[1] == "K99" {
               fmt.Println("王炸")
            } else {
              fmt.Println("对子")
            }
            break
    }
}

这是最简单的判定方法,接下来,张数越多,复杂度越高。
第二个方法就是根据出牌中值相同的牌的张数来判定类型。
这里首先要抽象出计算模型

type CardShow struct {
    ShowValue      []string            //牌面数组
    CardMap        map[int]int         //牌面计算结果
    MaxCount       int                 //同值牌出现的最大次数
    MaxValues      []int               //同值牌出现的次数列表
    CompareValue   int                 //用于比较大小的值
    CardTypeStatus enum.CardTypeStatus //牌面类型
}

  1. 牌面数组,表示出牌的所有牌值
  2. 牌面计算结果,表示出每个牌值出现的次数
  3. 同值牌出现的最大次数
  4. 同值牌出现的次数列表
  5. 用于比较大小的值
  6. 牌面类型
3.确定计算方法:

超过两张的计算方法

  1. 根据同值牌出现的次数确定牌种类范围:
    同值牌出现的次数均为1次---->可能为顺子
    同值牌出现的次数均为2次---->可能为连对
    同值牌出现的次数均为3次---->可能为飞机或三带一(暂时不考虑三带二)
    同值牌出现的次数均为4次---->可能为炸弹或者四带二
  2. 其中顺子、连对、飞机需都要鉴别牌值的连续性
  3. 飞机需要额外鉴别非连续牌的张数是否与连续次数相等
  4. 连对组数要大于或等于3组
  5. 顺子张数要大于或等于5

再根据计算方法填充计算模型

/**
* 根据牌面数量判断牌面类型
 */
func ParseCardsInSize(plays []string) cardmodel.CardShow {
    cardShow := cardmodel.CardShow{
        ShowValue: plays,
        ShowTime:  util.GetNowTime(),
    }
    switch len(plays) {
    case 1:
        cardShow.CardTypeStatus = enum.SINGLE
        cardShow.CompareValue = GetCardValue(plays[0])
        cardShow.MaxCount = 1
        cardShow.MaxValues = []int{cardShow.CompareValue}
        fmt.Printf("根%d", GetCardValue(plays[0]))
        break
    case 2:
        if plays[0] == "Q88" && plays[1] == "K99" {
            cardShow.CardTypeStatus = enum.KING_BOMB
            cardShow.CompareValue = GetCardValue(plays[0])
            cardShow.MaxCount = 2
            cardShow.MaxValues = []int{cardShow.CompareValue}
            fmt.Println("王炸")
        } else {
            ParseCardsType(plays, &cardShow)
        }
        break
    }
    if len(plays) > 2 {
        ParseCardsType(plays, &cardShow)
    } else {
        cardShow.CardTypeStatus = enum.ERROR_TYPE
    }
    return cardShow
}

/**
* 获取牌面类型
 */
func ParseCardsType(cards []string, cardShow *cardmodel.CardShow) {
    mapCard, maxCount, maxValues := ComputerValueTimes(cards)
    cardShow.MaxCount = maxCount
    cardShow.MaxValues = maxValues
    cardShow.CardMap = mapCard
    cardShow.CompareValue = maxValues[len(maxValues)-1]
    switch maxCount {
    case 4:
        if maxCount == len(cards) {
            cardShow.CardTypeStatus = enum.KING_BOMB
            fmt.Println("炸弹")
        } else if len(cards) == 6 {
            cardShow.CardTypeStatus = enum.FOUR_TWO
            fmt.Println("四带二")
        } else {
            cardShow.CardTypeStatus = enum.ERROR_TYPE
            fmt.Println("不合法出牌")
        }
        break
    case 3:
        alive := len(cards) - len(maxValues)*maxCount
        if len(maxValues) == alive {
            if len(maxValues) == 1 {
                cardShow.CardTypeStatus = enum.THREE_AND_ONE
                fmt.Println("三带一")
            } else if len(maxValues) > 1 {
                if IsContinuity(mapCard, false) {
                    cardShow.CardTypeStatus = enum.PLANE
                    fmt.Printf("飞机%d", len(maxValues))
                } else {
                    cardShow.CardTypeStatus = enum.ERROR_TYPE
                    fmt.Println("非法飞机")
                }
            }
        } else if alive == 0 {
            if len(maxValues) > 1 {
                if IsContinuity(mapCard, false) {
                    cardShow.CardTypeStatus = enum.PLANE_EMPTY
                    fmt.Printf("三不带飞机%d", len(maxValues))
                } else {
                    cardShow.CardTypeStatus = enum.ERROR_TYPE
                    fmt.Println("非法三不带飞机")
                }

            } else {
                cardShow.CardTypeStatus = enum.THREE
                fmt.Println("三不带")
            }
        } else {
            cardShow.CardTypeStatus = enum.ERROR_TYPE
            fmt.Println("不合法飞机或三带一")
        }
        break
    case 2:
        if len(maxValues) == (len(cards) / 2) {
            if len(maxValues) > 1 {
                if IsContinuity(mapCard, false) && len(maxValues) > 2 {
                    cardShow.CardTypeStatus = enum.DOUBLE_ALONE
                    fmt.Printf("%d连队", len(maxValues))
                } else {
                    cardShow.CardTypeStatus = enum.ERROR_TYPE
                    fmt.Println("非法连对")
                }
            } else if len(maxValues) == 1 {
                cardShow.CardTypeStatus = enum.DOUBLE
                fmt.Printf("对%d", GetCardValue(cards[0]))
            }
        } else {
            cardShow.CardTypeStatus = enum.ERROR_TYPE
            fmt.Println("不合法出牌")
        }
        break
    case 1:
        if IsContinuity(mapCard, true) && len(cards) >= 5 {
            cardShow.CardTypeStatus = enum.SINGLE_ALONE
            fmt.Printf("%d顺子", len(mapCard))
        } else {
            fmt.Println("非法顺子")
        }
        break
    }
}

/**
* 获取顺序的key值数组
 */
func GetOrderKeys(cardMap map[int]int, isSingle bool) []int {
    var keys []int
    for key, value := range cardMap {
        if (!isSingle && value > 1) || isSingle {
            keys = append(keys, key)
        }
    }
    sort.Ints(keys)
    return keys
}

/**
* 计算牌面值是否连续
 */
func IsContinuity(cardMap map[int]int, isSingle bool) bool {
    keys := GetOrderKeys(cardMap, isSingle)
    lastKey := 0
    for i := 0; i < len(keys); i++ {
        if (lastKey > 0 && (keys[i]-lastKey) != 1) || keys[i] == 15 {
            return false
        }
        lastKey = keys[i]
    }
    if lastKey > 0 {
        return true
    } else {
        return false
    }
}

/**
* 计算每张牌面出现的次数
* mapCard 标记结果
* MaxCount 出现最多的次数
* MaxValues 出现次数最多的所有值
 */
func ComputerValueTimes(cards []string) (mapCard map[int]int, MaxCount int, MaxValues []int) {
    newMap := make(map[int]int)
    if len(cards) == 0 {
        return newMap, 0, nil
    }
    for _, value := range cards {
        cardValue := GetCardValue(value)
        if newMap[cardValue] != 0 {
            newMap[cardValue]++
        } else {
            newMap[cardValue] = 1
        }
    }
    var allCount []int //所有的次数
    var maxCount int   //出现最多的次数
    for _, value := range newMap {
        allCount = append(allCount, value)
    }
    maxCount = allCount[0]
    for i := 0; i < len(allCount); i++ {
        if maxCount < allCount[i] {
            maxCount = allCount[i]
        }
    }
    var maxValue []int
    for key, value := range newMap {
        if value == maxCount {
            maxValue = append(maxValue, key)
        }
    }
    sort.Ints(maxValue)
    return newMap, maxCount, maxValue
}

/**
* 获取牌面值
 */
func GetCardValue(card string) int {
    stringValue := util.Substring(card, 1, len(card))
    value, err := strconv.Atoi(stringValue)
    if err == nil {
        return value
    }
    return -1
}
5.验证一下
  1. 验证飞机
func main() {
    cardsA := []string{"A3", "B3", "C3", "A4", "B4", "C4", "A5", "B5", "A5", "A6", "B6", "A6", "A11", "A7", "B12", "B7"}
    ashowMode := card.ParseCardsInSize(cardsA)
        fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}
打印:
飞机4
A玩家: 7

说明此玩家出的是4连飞机
为了验证校验的准确性,从牌中去掉一张余牌,看是否能检验出合法

func main() {
    cardsA := []string{"A3", "B3", "C3", "A4", "B4", "C4", "A5", "B5", "A5", "A6", "B6", "A6", "A11", "A7", "B12"}
    ashowMode := card.ParseCardsInSize(cardsA)
        fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}
打印:
不合法飞机或三带一

A玩家: 12

然后去掉所有余牌,看校验的准确性

func main() {
    cardsA := []string{"A3", "B3", "C3", "A4", "B4", "C4", "A5", "B5", "A5", "A6", "B6", "A6"}
    ashowMode := card.ParseCardsInSize(cardsA)
        fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}

打印:
三不带飞机4
A玩家: 8

说明此玩家出的是4连不带余数飞机

  1. 顺子验证
func main() {
    cardsA := []string{"A3", "B4", "C5", "A6", "B7"}
    ashowMode := card.ParseCardsInSize(cardsA)
        fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}
打印:
5顺子
A玩家: 10

去掉一张顺子牌,或使其不连续

func main() {
    cardsA := []string{"A3", "B4", "C5", "A6"}
    cardsB := []string{"A3", "B4", "C5", "A8"}
    ashowMode := card.ParseCardsInSize(cardsA)
    bshowMode := card.ParseCardsInSize(cardsB)
    fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
    fmt.Println("\nB玩家:", bshowMode.CardTypeStatus)
}

打印:
 非法顺子
非法顺子

A玩家: 0

B玩家: 0
  1. 炸弹验证
func main() {
    cardsA := []string{"A3", "B3", "C3", "D3"}
    ashowMode := card.ParseCardsInSize(cardsA)
    fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}
打印:
炸弹
A玩家: 11

其余几种验证不在此列出

四、出牌比对

出牌比对就是对同类型的出牌进行值比对,也就是用前面计算模型中的比较值进行比较,其实也就是出现次数最多的最大值。

下面以 一个飞机的比对做例子

func main() {
    cardsA := []string{"A3", "B3", "C3", "A4", "B4", "C4", "A5", "B5", "A5", "A6", "B6", "A6", "A11", "A7", "B12", "B7"}
    ashowMode := card.ParseCardsInSize(cardsA)
    cardsB := []string{"A4", "B4", "C4", "A5", "B5", "C5", "A6", "B6", "A6", "A7", "B7", "A7", "A11", "A10", "B12", "13"}
    bshowMode := card.ParseCardsInSize(cardsB)
    fmt.Println("\nA玩家:", ashowMode.CompareValue)
    fmt.Println("B玩家:", bshowMode.CompareValue)
}
打印:
飞机4飞机4
A玩家: 6
B玩家: 7

玩家A的比对值为6,玩家B的比对值为7,所以玩家B出的牌比玩家A出的牌大。

以上为斗地主基本算法分析完整代码地址:github ,期待star...
下一期将会对自动出牌简易AI算法作分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容