Golang多執行緒簡單鬥地主

多執行緒,通道,讀寫鎖(單寫多讀),隨機(洗牌),是本文涉及的主要知識點。

先看一下做出來的效果,因為是實驗程式,跟真實的鬥地主還是有差距,理解萬歲!

[發牌員]:洗牌咯。
刷刷刷...
[發牌員]:牌洗好了。
[發牌員]:開始發牌。
[發牌員]:每個人17張牌。
[發牌員]:搶地主。
[fang]:哈哈,我是地主!
fang的牌是[♣9 ♦9 ♥A ♠9 ♣6 ♣5 ♦3 ♣10 ♥5 ♣8 ♠Q ♠A ♠8 ♦4 ♥4 ♦K ♥7 ♣A ♠K ♥3],共20張。
dong的牌是[大王 ♦8 ♠5 小王 ♠6 ♣Q ♠10 ♣7 ♠3 ♦A ♦Q ♥J ♣K ♥6 ♥9 ♥Q ♣2],共17張。
er的牌是[♣A ♠K ♥3 ♥2 ♠4 ♦2 ♦5 ♥K ♦10 ♠2 ♥8 ♦6 ♣4 ♦J ♣3 ♣J ♠7],共17張。
[fang]:我開始出牌了。
[er]:我開始出牌了。
[dong]:我開始出牌了。
贏家是er。

基本流程是洗牌->發牌->搶地主->打牌->gg。

哈哈這個程式的精髓是,由於時(lan)間(de)有(xie)限(le),打牌是哪個執行緒搶到了就出牌,直到牌出完了,就贏了。(多執行緒寫鬥地主,是我大學作業系統課程的實驗項目,當時是完整實現了鬥地主演算法的,用的是C++和MFC,可以在介面上交互打牌)

邊看程式碼變講。

主函數

func main() {
	// 洗牌
	cards := shuffle()
	// 發牌
	dealCards := deal(cards)
	// 搶地主
	fmt.Println("[發牌員]:搶地主。")
	go player(order[0], dealCards[0])
	go player(order[1], dealCards[1])
	go player(order[2], dealCards[2])
	// Winner
	winner := <-winner
	fmt.Printf("贏家是%s。\n", winner)
}

解析:

1.main裡面是打牌的步驟,洗牌,發牌,搶地主,打牌,gg。
2.用go player(),開了3個執行緒,也就是3個玩家。
3.發牌的時候,是留了3張底牌的,存在通道「bottom」裡面,搶地主的時候,3個執行緒就去取,誰先取到誰就是地主。
4.打牌打到最後,會往另外一個通道「winner」裡面寫值,誰先打完,就把自己的name存進去。
5.3個玩家在打牌的時候,main是阻塞的,等待從通道「winner」讀取值,有玩家打完了,通道「winner」有值了,就激活。

洗牌函數

func shuffle() []string {
	fmt.Println("[發牌員]:洗牌咯。")
	fmt.Println("刷刷刷...")
	cards := cards()
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(cards), func(i, j int) {
		cards[i], cards[j] = cards[j], cards[i]
	})
	fmt.Println("[發牌員]:牌洗好了。")

	return cards
}

解析:

1.rand默認是假的隨機,因為不管運行多少次都是一樣的,需要設置種子,time.Now().UnixNano(),讓每次隨機結果都不同。
2.rand.Shuffle()洗牌,隨機交換2個牌的位置。

發牌函數

func deal(cards []string) [][]string {
	fmt.Println("[發牌員]:開始發牌。")
	var dealCards [][]string
	dealCards = append(dealCards, cards[0:17])
	dealCards = append(dealCards, cards[17:34])
	dealCards = append(dealCards, cards[34:51])
	fmt.Println("[發牌員]:每個人17張牌。")

	go leaveBottom(cards[51:54])

	return dealCards
}

解析:

1.因為已經洗了牌了,直接先切3份牌出來,每份17張。
2.留了3張底牌,放到通道「bottom」中。
3.如果這裡不再開執行緒,會發生死鎖!因為main本身也是個執行緒,直接存通道的話,會把main阻塞,直到有執行緒把通道的值讀出去;而main阻塞後,是無法繼續執行後面的程式碼的,也就無法再起3個玩家執行緒來讀值了,就會發生死鎖。
4.所以leaveBottom()起了一個單獨的執行緒。

Desk牌桌

type Desk struct {
	mutex     sync.RWMutex
	playCards []string
}

func (d *Desk) write(card string) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	d.playCards = append(d.playCards, card)
}

func (d *Desk) read() []string {
	d.mutex.RLock()
	defer d.mutex.RUnlock()
	return d.playCards
}

解析:

1.定義了結構Desk,包括讀寫鎖和牌桌上打的牌。
2.定義了write()和read()2個函數,3個執行緒可以同時讀,但只能一次寫,也就是單寫多讀鎖。

player函數

func player(name string, hands []string) {
	landlord := <-bottom
	if len(landlord) > 0 {
		fmt.Printf("[%s]:哈哈,我是地主!\n", name)
		hands = append(hands, landlord...)
		desk.write(name)
	}
	fmt.Printf("%s的牌是%s,共%d張。\n", name, hands, len(hands))

	time.Sleep(time.Second)

	i := 0
	for true {
		playCards := desk.read()
		if playCards[len(playCards)-1] == name {
			if i == 1 {
				fmt.Printf("[%s]:我開始出牌了。\n", name)
			}
			desk.write(hands[i])
			desk.write(order[(getOrderID(name)+1)%3])
			i += 1
			if i == len(hands) {
				winner <- name
				break
			}
		}
	}
}

解析:

1.玩家函數,第一個參數是名字,第二個參數是手上拿的牌。
2.3個執行緒都有這樣一段程式碼。
3.首先從通道「bottom」讀取值,也就是搶地主。
4.搶到地主的玩家,會把底牌放到自己的手牌中,並且把自己的名字寫到牌桌上(根據名字來看該誰出牌),地主先出牌。
5.for true {}循環不停的出牌,從第一張到最後一張,先從牌桌上看是不是自己的名字,是自己的名字才輪到出牌。
6.牌出完了,就把自己的名字寫到通道「winner」,遊戲結束。

本文的程式只是為了實驗go的多執行緒特性,不具備可玩性,期待更多的同學請見諒。
如果需要源碼的話,請到公眾號回復「鬥地主」獲取哦。

版權申明:本文為部落客原創文章,轉載請保留原文鏈接及作者。
如果您喜歡我寫的文章,請關注公眾號支援一下,謝謝哈哈哈。

Tags: