首页 » 父与子的编程之旅:与小卡特一起学Python » 父与子的编程之旅:与小卡特一起学Python全文在线阅读

《父与子的编程之旅:与小卡特一起学Python》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 个呢?……你发现规律了吗?