讀古今文學網 > 父與子的編程之旅:與小卡特一起學Python > 23.4 Crazy Eights >

23.4 Crazy Eights

你可能聽說過一個叫做 Crazy Eights 1的紙牌遊戲,可能還玩過。

1這就類似於「變色龍」紙牌遊戲。——譯者注

計算機上的紙牌遊戲都存在一個問題:這些遊戲很難有多個玩家。這是因為,在大多數紙牌遊戲中,都不希望你看到其他玩家的牌。如果每個人都在看同一台計算機,那麼每個人都會看到所有其他人的牌。所以在計算機上玩紙牌遊戲時,最好只有兩個玩家,也就是你和計算機。Crazy Eights 就是這種適合兩個玩家的遊戲,下面就來建立一個 Crazy Eights 遊戲,用戶可以和計算機玩這個遊戲。

這裡給出這個程序的規則。這是一個有兩個玩家參與的遊戲。每個玩家有 5 張牌,其他的牌都面朝下扣著。翻開一張牌,開始出牌。這個遊戲的目標是要在另一個人之前而且在取完一副牌之前出光所有牌。

1. 每一輪,玩家必須做下面的操作之一:

  • 出一張牌,要與翻開的牌花色相同;

  • 出一張牌,要與翻開的牌點數相同;

  • 出一張 8。

2. 如果玩家出了一張 8,他可以「叫花色」,這說明他可以選擇花色,下一個玩家要根據這個花色出牌。

3. 如果玩家無法出牌,必須從這副牌中選擇一張牌,增加到自己手中。

4. 如果玩家出光了手中的所有牌,他就贏了,根據另一個玩家手中剩餘的牌計算得分:

  • 每個 8 得 50 分;

  • 每個花牌(J、Q 和 K)得 10 分;

  • 每個其他的牌按分值得分;

  • 每個 A 得 1 分。

5. 如果一副牌發光時仍沒有人獲勝,遊戲結束。在這種情況下,每個玩家會根據對方剩餘的牌計算得分。

6. 可以一直玩到達到某個總分,或者直到你累了,得分最高的獲勝。

首先要對我們的紙牌對像稍做點修改。Crazy Eights 中的分值與前面基本上一樣,只是 8 除外,它的分值是 50 分而不是 8 分。可以修改 Card 類中的 __init__ 方法,讓 8 值 50 分,不過這會影響可能用到 cards 模塊的所有其他遊戲。最好在主程序中做這個修改,而類定義不變。我們可以這樣做:

deck = for suit in range(1, 5):    for rank in range(1, 14):new_card = Card(suit, rank)if new_card.rank == 8:    new_card.value = 50deck.append(new_card)  

在這裡,將新牌增加到一副牌之前,要檢查它是不是一個 8。如果是,就把它的 分值設置為 50。

現在已經做好準備,可以具體建立遊戲了。程序需要做以下工作。

  • 跟蹤面朝上的牌。

  • 得到玩家的下一步選擇(出牌還是抽牌)。

  • 如果玩家想出牌,要確保出牌是合法的:

    • 這張牌必須是合法的牌;

    • 這張牌必須在玩家的手裡;

    • 這張牌要與面朝上的牌花色或點數一致,或者是一個 8。

  • 如果玩家出一張 8,叫一個新的花色(並確保選擇的是一個合法的花色)。

  • 輪到計算機選擇(稍後介紹)。

  • 確定遊戲何時結束。

  • 統計得分。

在本章後面,我們會逐條地完成上面的各項工作。其中一些工作只需一行或兩行代碼就可以完成,有些可能稍長一些。對這些稍長的代碼,我們會創建函數,以便從主循環調用。

主循環

介紹具體細節之前,首先要明白程序的主循環。基本說來,玩家和計算機必須輪流選擇(出牌或抽牌),直到有人獲勝或者雙方都無法繼續。如代碼清單 23-6 所示。

代碼清單 23-6 Crazy Eights 的主循環

主循環部分要確定遊戲何時結束。可能在玩家或計算機出完手上的所有牌時結束,也可能雙方手上都還有牌但是都無法繼續(也就是說雙方都不能合法地出牌),此時遊戲也會結束。輪到玩家出牌時,如果玩家無法繼續,會在相應代碼中設置 blocked 變量,輪到計算機出牌時,如果計算機無法繼續,同樣會在相應代碼中設置 blocked 變量。我們會一直等到 blocked = 2,確保玩家和計算機都無法繼續。

注意代碼清單 23-6 不是一個完整的程序,所以如果試圖運行這個代碼,你會得到一條錯誤消息。這只是一個主循環。我們還需要其他部分來構成一個完整的程序。

這個代碼對應一次遊戲。如果希望繼續玩多次,可以把整個代碼包在另一個外部 while 循環中:

done = Falsep_total = c_total = 0while not done:   [play a game... see listing 23.6]play_again = raw_input(\"Play again (Y/N)? \")    if play_again.lower.startswith(\'y\'):done = False    else:done = True  

這就得到了程序的主結構。下面需要增加各個部分來實現我們需要的功能。

像程序員一樣思考

前面描述的方法稱為「自頂向下」編程方法。

這種方法先從需求大綱開始,然後填入具體細節。

另一種方法叫做「自下而上」編程。採用這種方法時,首先創建各個部分,如「輪到玩家出牌」、「輪到計算機出牌」等,然後把它們放在一起,就像搭積木一樣。

這兩種方法各有優缺點。究竟選擇哪一種方法不是這本書要討論的主題。但是我想你應當知道可以採用不同的方法來構建一個程序。

明牌

最開始發牌時,要從一副牌中選一張牌翻過來面朝上,作為不要的一堆牌(棄牌堆)中的第一張牌。玩家出牌時,他出的這張牌也要面朝上放在棄牌堆中。棄牌堆中顯示的牌叫做明牌(upcard)。可以為棄牌堆建立一個列表來跟蹤明牌,具體做法與代碼清單 23-5 的測試代碼中為「一手牌」建立列表相同。不過我們並不關心棄牌堆中的所有牌。我們只關心最後增加的那張牌。所以可以使用 Card 對象的一個實例來跟蹤這張牌。

玩家或計算機出牌時,我們會這樣做:

hand.remove(chosen_card)up_card = chosen_card  

當前花色

通常,當前花色就是明牌的花色,玩家或計算機出牌時要與這個花色一致。不過,也有例外。出一張 8 時,玩家可以叫花色。所以如果玩家出了一張方塊 8,他可能會叫花色為黑桃。這意味著下一張牌必須是黑桃,儘管現在顯示的是方塊(方塊 8)。

這說明,我們需要跟蹤當前花色,因為它可能與現在顯示的花色不同。可以使用一個變量 active_suit 來做到:

active_suit = card.suit  

只要出一張牌,我們就會更新當前花色,玩家出一張 8 時,他會選擇新的當前花色。

輪到玩家選擇

輪到玩家出牌時,首先我們要得到他選擇做什麼。他可能從手中出一張牌(如果可能的話),或者從這副牌中抽一張牌。如果建立這個程序的一個 GUI 版本,我們會讓玩家點擊他想出的牌,或者點擊這副牌來抽牌。不過現在先建立這個程序的一個基於文本的版本,所以玩家必須鍵入他的選擇,然後我們要檢查他鍵入的內容,明確他想做什麼,還要檢查輸入是否合法。

玩家需要提供什麼樣的輸入呢?為了讓你對這些輸入有所認識,下面看一個示例遊戲。玩家的輸入用粗體顯示:

Crazy EightsYour hand: 4S, 7D, KC, 10D, QS    Up Card:  6CWhat would you like to do?  Type a card name or \"Draw\" to take a card:  KCYou played the KC (King of Clubs)Computer plays  8S (8 of spades) and changes suit to DiamondsYour hand:  4S, 7D, 10D, QS   Up Card:  8S    Suit:  DiamondsWhat would you like to do?  Type a card name or \"Draw\" to take a card: 10DYou played 10D (10 of Diamonds)Computer plays QD (Queen of Diamonds)Your hand:  4S, 7D QS   Up card:  QDWhat would you like to do?  Type a card name or \"Draw\" to take a card: 7DYou played 7D (7 of Diamonds)Computer plays 9D (9 of Diamonds)Your hand:  4S, QS   Up card: 9DWhat would you like to do?  Type a card name or \"Draw\" to take a card: QMThat is not a valid card.  Try again:  QDYou do not have that card in your hand.  Try again: QSThat is not a legal play. You must match suit, match rank, play an 8, or draw a cardTry again: DrawYou drew 3CComputer draws a cardYour hand:  4S, QS, 3C   Up card:  9DWhat would you like to do?  Type a card name or \"Draw\" to take a card: DrawYou drew 8CComputer plays 2DYour hand:  4S, QS, 3C, 8C   Up card:  2DWhat would you like to do?  Type a card name or \"Draw\" to take a card: 8CYou played 8C (8 of Clubs)Your hand:  4S, QS, 3C   Pick a suit: SYou picked spadesComputer draws a cardYour hand:  4S, QS, 3C   Up card: 8C   Suit:  SpadesWhat would you like to do?  Type a card name or \"Draw\" to take a card: QSYou played QS (Queen of Spades)...  

儘管這還不是一個完整的遊戲,不過你應該已經有些瞭解了。玩家必須鍵入 QSDraw 之類的文本,把他的選擇告訴程序。程序要檢查玩家鍵入的內容是合法的。這裡將要使用一些字符串方法(第 21 章中介紹的方法)來提供幫助。

顯示手中的牌

詢問玩家想要做什麼之前,我們應當為他顯示他手中有哪些牌以及明牌是什麼。下面是相關的代碼:

print \"nYour hand: \",for card in p_hand:    print card.short_name,print \"   Up card: \",    up_card.short_name  

如果出了一張 8,我們還要告訴他當前花色是什麼。所以下面再增加幾行代碼,如代碼清單 23-7 所示。

代碼清單 23-7 顯示玩家手中的牌

print \"nYour hand: \",for card in p_hand:    print card.short_name,print \"   Up card: \", up_card.short_nameif up_card.rank == \'8\':    print\"   Suit is\", active_suit  

就像代碼清單 23-6 一樣,代碼清單 23-7 也不是一個完整的程序。我們還需要構建其他部分才能建立一個完整的程序。不過運行代碼清單 23-7 中的代碼時(作為完整程序的一部分),它會給出類似下面的輸出:

Your hand:  4S, QS, 3C   Up card: 8C   Suit:  Spades  

如果想使用紙牌的長名而不是短名,輸出會像這樣:

Your hand:  4 of Spades, Queen of Spades, 3 of ClubsUp Card:  8 of Clubs    Suit:  Spades  

在我們的例子中,我們將使用短名。

得到玩家的選擇

現在我們需要詢問玩家想做什麼,並處理他的響應。他主要有兩種選擇:

  • 出一張牌

  • 抽一張牌

如果他決定出一張牌,我們需要確保這張牌是合法的。之前說過,需要檢查 3 個方面。

  • 他選擇的是一張合法的牌嗎?(他是不是想出一張「蜀葵」4 ?)

  • 這張牌在他手裡嗎?

  • 選擇的這張牌能合法出牌嗎?(是否與明牌的點數或花色一致,或者是不是一張 8 ?)

不過如果再考慮一下,可以想到:他手裡只能有合法的牌。所以如果我們檢查到這張牌確實在他手裡,就不用再考慮檢查這張牌是否合法。他手裡不可能有類似「蜀葵」4 之類的牌,因為這在一副牌中根本不存在。

下面的代碼可以得到並驗證玩家 的選擇,見代碼清單 23-8。

術語箱

驗證(validate)是指確保一樣東西是合法的,即允許的或者合理的。

代碼清單 23-8 得到玩家的選擇

(這也不是一個完整的、可運行的程序。)

在這裡,我們會得到一個合法的選擇:玩家可能抽牌,也可能出一張合法的牌。如果玩家抽牌,只要這副牌中還有剩餘的牌,就在玩家手裡增加一張牌。

如果出一張牌,需要從玩家手裡刪除這張牌,讓它成為明牌:

p_hand.remove(selected_card)up_card  = selected_cardactive_suit = up_card.suitprint \"You played\", selected_card.short_name  

如果出的牌是一張 8,玩家要告訴我們他下一步想要什麼花色。因為 player_turn 函數稍有點長,我們把得到新花色的代碼放在一個單獨的函數中,名為 get_new_suit。代碼清單 23-9 顯示了這個函數的代碼。

代碼清單 23-9 玩家出一張 8 時得到新花色

輪到玩家出牌時所要做的就是這些。下一節中,我們要讓計算機變得足夠聰明來玩這個 Crazy Eights 遊戲。

輪到計算機選擇

玩家選擇之後,就輪到計算機了,所以我們要告訴程序怎麼玩 Crazy Eights。它必須與玩家遵循同樣的規則,不過程序需要確定出哪一張牌。我們必須專門告訴它如何處理所有可能的情況:

  • 出一張 8(並挑選一個新花色);

  • 出另一張牌;

  • 抽牌。

為了簡化程序,我們要告訴計算機如果有 8 就總是出 8。這可能不是最佳的策略,不過很簡單。

如果計算機出了一張 8,它必須挑選新花色。最簡單的方法就是統計計算機手中每種花色各有多少張牌,並選擇牌數最多的花色。同樣,這也不是最完美的策略,不過這樣編寫代碼最為簡單。

如果計算機手中沒有 8,程序就必須檢查所有牌,查看哪些牌可以出。在這些牌中,它會選擇出分值最大的牌。

如果根本無法出牌,計算機會抽牌。倘若計算機想要抽牌,但這副牌中已經沒有任何牌了,計算機就無法繼續,這和人類玩家是一樣的。

代碼清單 23-10 顯示了輪到計算機選擇的相應代碼,這裡給出了一些說明來作出解釋。

代碼清單 23-10 輪到計算機選擇

這個程序已經基本上完成了,只需要增加幾點就可以了。你可能已經注意到,輪到計算機選擇定義為一個函數,而且我們在這個函數中使用了一些全局變量。其實也可以向這個函數傳入變量,不過使用全局變量也完全可以,而且與真實世界的實際情況更接近,一副牌是「全局」的——任何人都可以拿到並從中取一張牌。

輪到玩家選擇也是一個函數,不過我們還沒有顯示這個函數定義的第一部分,這部分是這樣的:

def player_turn:    global deck, p_hand, blocked, up_card, active_suit    valid_play = False    is_eight = False    print \"nYour hand: \",    for card in p_hand:print card.short_name,    print \"   Up card: \", up_card.short_name    if up_card.rank == \'8\':print\"   Suit is\", active_suit    print \"What would you like to do? \",    response = raw_input (\"Type a card to play or \'Draw\' to take a card: \" )  

現在還有一點要做。我們必須跟蹤最終誰獲勝!

記錄分數

要完成這個遊戲,還需要最後一點:這就是記錄得分。遊戲結束時,需要得到贏家的得分,這要根據輸家剩餘的牌來計算。我們要顯示這次遊戲的得分,還要顯示所有遊戲的總分。加入這些內容後,就得到了類似代碼清單 23-11 的主循環。

代碼清單 23-11 增加了得分的主循環

init_cards 函數(這裡沒有顯示)的工作只是建立一副牌並創建玩家的一手牌(5 張牌)、計算機的一手牌(5 張牌)以及第一張明牌。

代碼清單 23-11 仍然不是一個完整的程序,所以如果你運行這個代碼,就會得到一條錯誤消息。不過如果你一直按我說的做,現在你的編輯器裡應該已經有了幾乎整個程序。Crazy Eights 的完整代碼清單太長了,無法在這裡全部列出(大約 200 行代碼,還要加上空行和註釋),不過你可以在 Examples 文件夾找到這個代碼(如果你使用了本書的安裝程序),另外在網站上(www.helloworldbook2.com)也可以找到。可以使用 IDLE 來編輯和運行這個程序。

你學到了什麼

在這一章,你學到了以下內容。

  • 什麼是隨機性和隨機事件。

  • 有關概率的一點內容。

  • 如何使用 random 模塊在程序中生成隨機事件。

  • 如何模擬扔硬幣或擲骰子。

  • 如何模擬從一副洗過的牌中抽牌。

  • 如何玩 Crazy Eights(如果你以前不知道)。

測試題

1. 說明什麼是「隨機事件」。給出兩個例子。

2. 為什麼扔一個 11 面(各個面上的數為 2 ~ 12)的骰子與扔兩個 6 面的骰子(總和也是 2 ~ 12)不同?

3. 在 Python 中有哪兩種方法來模擬擲骰子?

4. 我們使用哪種 Python 變量表示一張牌?

5. 我們使用哪種 Python 變量表示一副牌?

6. 要在抽牌時從一副牌中刪除一張牌,或者出牌時從一手牌中刪除一張牌,要 使用什麼方法?

動手試一試

使用代碼清單 23-3 的程序試一試「連續 10 次正面朝上」試驗,不過可以試試不同的連續次數。多久能出現一次連續 5 個正面朝上? 6 個呢? 7 個呢? 8 個呢?……你發現規律了嗎?