Golang算法實(shí)戰(zhàn)之斗地主<一>

逢年過(guò)節(jié),回到老家,玩的最多的就是打麻將、斗地主。今天要說(shuō)的,就是這個(gè)經(jīng)典游戲——斗地主。

一、斗地主牌面分析

斗地主需要多少?gòu)埮??大部分人都知道需要一副完整的牌即可,也就?4張牌。

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

在斗地主中,牌的花色不影響。所以,在牌面比對(duì)時(shí),不需要單獨(dú)比對(duì)花色。而單張牌面值的大小順序?yàn)? 大王>小王>2>A>K>Q>J>10……3
鑒于此,牌面的表達(dá)可以用以下方式來(lái)規(guī)定:
A:黑桃 B:紅桃 C:梅花 D:方片

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

二、如何開始游戲

先來(lái)看一張圖


斗地主初始化.png

游戲初始化拆分成3大塊

  1. 構(gòu)造一副牌
  2. 洗牌
  3. 發(fā)牌
1、構(gòu)造一副牌

構(gòu)造一副牌就是根據(jù)牌面分析中規(guī)定的牌面表達(dá)方法構(gòu)造一副完整的54張撲克牌。
代碼如下:

func CreateNew() []string {
    numbers := make([]string, 54) //構(gòu)造一個(gè)大小為54的數(shù)組
    start := 0  //造牌游標(biāo)
    for i := 3; i <= 16; i++ {
        if i == 16 { //i為16說(shuō)明已經(jīng)到大小王
            numbers[start] = "Q88"
            numbers[start+1] = "K99" //直接構(gòu)造大小王
        } 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 //每造一套單值牌,游標(biāo)移4位
        }
    }
    return numbers
}

驗(yàn)證一下:

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、洗牌

洗牌就是將牌原有的順序打亂,形成新的順序的牌。主要利用隨機(jī)數(shù)來(lái)處理。

func Shuffle(vals []string) {
    r := rand.New(rand.NewSource(time.Now().Unix()))  //根據(jù)系統(tǒng)時(shí)間戳初始化Random
    for len(vals) > 0 {//根據(jù)牌面數(shù)組長(zhǎng)度遍歷
        n := len(vals)//數(shù)組長(zhǎng)度
        randIndex := r.Intn(n)//得到隨機(jī)index
        vals[n-1], vals[randIndex] = vals[randIndex], vals[n-1]//最后一張牌和第randIndex張牌互換
        vals = vals[:n-1] 
    }
}

這是一種抽牌插底的洗牌算法,時(shí)間復(fù)雜度為O(n),當(dāng)然還有效率更高的洗牌算法,具體可以另做研究。
驗(yà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]

可見洗牌達(dá)到了預(yù)期。

3、發(fā)牌

發(fā)牌可以說(shuō)是斗地主開始前的最后一個(gè)環(huán)節(jié)(不包含叫地主搶地主),發(fā)牌是要將牌先均分給3個(gè)玩家(保留3張底牌),并從玩家中隨機(jī)抽取一位玩家為地主。
首先,將牌分成4部分:
玩家一:17張牌
玩家二:17張牌
玩家三:17張牌
底牌:3張

/**
*發(fā)牌
*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 //默認(rèn)總長(zhǎng)度為17
        if order == 3 {
            size = 3 //次序?yàn)?(底牌次序)時(shí),總長(zhǎng)度為3
        }
        for i := 0; i < len(playCards); i++ {
            playCards = append(playCards, vals[order*17+i])//根據(jù)次序發(fā)牌
        }
    }
    return playCards
}

驗(yàn)證一下:

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]

從打印結(jié)果來(lái)看,發(fā)牌也是滿足場(chǎng)景的。

三、出牌分析

接下來(lái),就是最復(fù)雜的點(diǎn),出牌的處理。

1. 牌面分類

首先要處理的是根據(jù)所出的牌,判斷出出牌的類型。
根據(jù)以往游戲中的經(jīng)驗(yàn)來(lái)看,出牌類型總的可以分為以下幾種類型(由簡(jiǎn)單到復(fù)雜)

  1. 單根
  2. 對(duì)子
  3. 三不帶
  4. 三帶一
  5. 炸彈(4張同值牌)
  6. 四帶二
  7. 飛機(jī)
  8. 三不帶飛機(jī)
  9. 連對(duì)
  10. 順子
  11. 王炸

那么,根據(jù)以上類型,我們首先定義出出牌類型枚舉

type CardTypeStatus int

const (
    _CardTypeStatus = iota
    SINGLE          //單根
    DOUBLE          //對(duì)子
    THREE           //三不帶
    THREE_AND_ONE   //三帶一
    BOMB            //炸彈
    FOUR_TWO        //四帶二
    PLANE           //飛機(jī)
    PLANE_EMPTY     //三不帶飛機(jī)
    DOUBLE_ALONE    //連對(duì)
    SINGLE_ALONE    //順子
    KING_BOMB       //王炸
    ERROR_TYPE      //非法類型

)
2.計(jì)算推理

玩家出的牌張數(shù)不固定,那么,如何有效的判斷出玩家所出牌的類型呢。
首先從最簡(jiǎn)單的,根據(jù)牌的張數(shù)可以判斷出最簡(jiǎn)單的3種場(chǎng)景

  1. 單根
  2. 對(duì)子
  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("對(duì)子")
            }
            break
    }
}

這是最簡(jiǎn)單的判定方法,接下來(lái),張數(shù)越多,復(fù)雜度越高。
第二個(gè)方法就是根據(jù)出牌中值相同的牌的張數(shù)來(lái)判定類型。
這里首先要抽象出計(jì)算模型

type CardShow struct {
    ShowValue      []string            //牌面數(shù)組
    CardMap        map[int]int         //牌面計(jì)算結(jié)果
    MaxCount       int                 //同值牌出現(xiàn)的最大次數(shù)
    MaxValues      []int               //同值牌出現(xiàn)的次數(shù)列表
    CompareValue   int                 //用于比較大小的值
    CardTypeStatus enum.CardTypeStatus //牌面類型
}

  1. 牌面數(shù)組,表示出牌的所有牌值
  2. 牌面計(jì)算結(jié)果,表示出每個(gè)牌值出現(xiàn)的次數(shù)
  3. 同值牌出現(xiàn)的最大次數(shù)
  4. 同值牌出現(xiàn)的次數(shù)列表
  5. 用于比較大小的值
  6. 牌面類型
3.確定計(jì)算方法:

超過(guò)兩張的計(jì)算方法

  1. 根據(jù)同值牌出現(xiàn)的次數(shù)確定牌種類范圍:
    同值牌出現(xiàn)的次數(shù)均為1次---->可能為順子
    同值牌出現(xiàn)的次數(shù)均為2次---->可能為連對(duì)
    同值牌出現(xiàn)的次數(shù)均為3次---->可能為飛機(jī)或三帶一(暫時(shí)不考慮三帶二)
    同值牌出現(xiàn)的次數(shù)均為4次---->可能為炸彈或者四帶二
  2. 其中順子、連對(duì)、飛機(jī)需都要鑒別牌值的連續(xù)性
  3. 飛機(jī)需要額外鑒別非連續(xù)牌的張數(shù)是否與連續(xù)次數(shù)相等
  4. 連對(duì)組數(shù)要大于或等于3組
  5. 順子張數(shù)要大于或等于5

再根據(jù)計(jì)算方法填充計(jì)算模型

/**
* 根據(jù)牌面數(shù)量判斷牌面類型
 */
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("飛機(jī)%d", len(maxValues))
                } else {
                    cardShow.CardTypeStatus = enum.ERROR_TYPE
                    fmt.Println("非法飛機(jī)")
                }
            }
        } else if alive == 0 {
            if len(maxValues) > 1 {
                if IsContinuity(mapCard, false) {
                    cardShow.CardTypeStatus = enum.PLANE_EMPTY
                    fmt.Printf("三不帶飛機(jī)%d", len(maxValues))
                } else {
                    cardShow.CardTypeStatus = enum.ERROR_TYPE
                    fmt.Println("非法三不帶飛機(jī)")
                }

            } else {
                cardShow.CardTypeStatus = enum.THREE
                fmt.Println("三不帶")
            }
        } else {
            cardShow.CardTypeStatus = enum.ERROR_TYPE
            fmt.Println("不合法飛機(jī)或三帶一")
        }
        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連隊(duì)", len(maxValues))
                } else {
                    cardShow.CardTypeStatus = enum.ERROR_TYPE
                    fmt.Println("非法連對(duì)")
                }
            } else if len(maxValues) == 1 {
                cardShow.CardTypeStatus = enum.DOUBLE
                fmt.Printf("對(duì)%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值數(shù)組
 */
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
}

/**
* 計(jì)算牌面值是否連續(xù)
 */
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
    }
}

/**
* 計(jì)算每張牌面出現(xiàn)的次數(shù)
* mapCard 標(biāo)記結(jié)果
* MaxCount 出現(xiàn)最多的次數(shù)
* MaxValues 出現(xiàn)次數(shù)最多的所有值
 */
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 //所有的次數(shù)
    var maxCount int   //出現(xiàn)最多的次數(shù)
    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.驗(yàn)證一下
  1. 驗(yàn)證飛機(jī)
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)
}
打印:
飛機(jī)4
A玩家: 7

說(shuō)明此玩家出的是4連飛機(jī)
為了驗(yàn)證校驗(yàn)的準(zhǔn)確性,從牌中去掉一張余牌,看是否能檢驗(yàn)出合法

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)
}
打?。?不合法飛機(jī)或三帶一

A玩家: 12

然后去掉所有余牌,看校驗(yàn)的準(zhǔn)確性

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)
}

打?。?三不帶飛機(jī)4
A玩家: 8

說(shuō)明此玩家出的是4連不帶余數(shù)飛機(jī)

  1. 順子驗(yàn)證
func main() {
    cardsA := []string{"A3", "B4", "C5", "A6", "B7"}
    ashowMode := card.ParseCardsInSize(cardsA)
        fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}
打?。?5順子
A玩家: 10

去掉一張順子牌,或使其不連續(xù)

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. 炸彈驗(yàn)證
func main() {
    cardsA := []string{"A3", "B3", "C3", "D3"}
    ashowMode := card.ParseCardsInSize(cardsA)
    fmt.Println("\nA玩家:", ashowMode.CardTypeStatus)
}
打?。?炸彈
A玩家: 11

其余幾種驗(yàn)證不在此列出

四、出牌比對(duì)

出牌比對(duì)就是對(duì)同類型的出牌進(jìn)行值比對(duì),也就是用前面計(jì)算模型中的比較值進(jìn)行比較,其實(shí)也就是出現(xiàn)次數(shù)最多的最大值。

下面以 一個(gè)飛機(jī)的比對(duì)做例子

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)
}
打?。?飛機(jī)4飛機(jī)4
A玩家: 6
B玩家: 7

玩家A的比對(duì)值為6,玩家B的比對(duì)值為7,所以玩家B出的牌比玩家A出的牌大。

以上為斗地主基本算法分析完整代碼地址:github ,期待star...
下一期將會(huì)對(duì)自動(dòng)出牌簡(jiǎn)易AI算法作分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容