《Design of Computer Programs》学习笔记(1 - 1)Winning Poker Hands - Lesson 1

Winning Poker Hands

Lesson 1

视频链接:
Lesson 1 - Udacity

1. Welcome

2. About the class

3. Outlining the problem

一个一般设计过程的例子

从一个模糊的理解开始,提炼我们的理解,形成对问题规范的描述,然后变换成可以代码化的东西,最后我们就得到了可运行的代码了。

一句话:
从模糊的理解,到problem,到spec,到code。

扑克牌的游戏规则

一手牌的概念

举例:
一手牌 a hand 【JJ225】
一张牌 a card【红桃J】【黑桃J】
一张牌有大小(rank)和花色(suit,例如红桃、黑桃)
方片五,大小是5,花色是方片。
the 5 of diamonds,the rank is 5 and the suit is diamonds。
待定义的主程序,我们称之为“poker”。输入一系列的手牌,输出最大的手牌。

规则:
【JJ225】有两个对(2 pair),一对J、一对2和一张5。

手牌大小的概念

一样大小(数字)的牌,称为“条”(kind)。
n张一样大小的牌称为两条、三条 或 四条。
n of a kind:2 of a kind、3 of a kind、4 of a kind。n - kind
【JJ225】中,有2个两条
【56789】是顺子(straight)。(5张连续的牌)。
同花(flush):例如【方片2457十】

补充材料:

List of poker hands - 维基百科

4. 练习:Representing Hands

以下哪几项可以合理地表示一手牌:

■ ['JS', 'JD', '2S', '2C', '7H']
■ [(11, 'S'), (11, 'D'), (2, 'S'), (7,'H')]
□ set(['JS', '5D', '2S', '2C', '7H'])
□ "JS JD 2S 2C 7H"

解释:

关于第3个选项:一般的一副牌52张,可以用集合表示。因为一副牌52张中,没有重复的牌。而集合这种数据结构,里面没有重复的元素。但是,如果有2副牌,可能一手牌中,会有重复的元素,所以不能用集合这种数据结构。

关于第4个选项:
字符串包含了所有的元素,但某些情况下,你要把它分拆成5张牌,而一开始时就分开会更好,就不需要多余的操作了。

5. 练习:Poker Function

def poker(hands):
        "Return the best hand: poker([hand, ...]) => hand"
        return max
        "max函数把列表作为输入,返回最大值。"

6. 练习:Understanding Max

def poker(hands):
        "Return the best hand: poker([hand, ...]) => hand"
        return max

print max([3, 4, 5, 0]), max([3, 4, -5, 0], key=abs)

7. 练习:Using Max

def poker(hands):
        "Return the best hand: poker([hand, ...]) => hand"
        return max(hands, key=hand_rank)
        "根据输入的几手牌hands,按照hand_rank函数的排序规则,返回其中的最大值。"

def hand_rank():
        "现在假设我们定义了一个叫hand_rank的函数,把一手牌作为输入,返回某种排名"
        return None # we will be changing this later.

如果能够清楚正确地给出hand_rank函数,那就八九不离十了。

8. 练习:Testing

但在定义复杂的hand_rank函数之前,我们来考虑下这个函数怎么用,并写一些测试用例。

def poker(hands):
        "Return the best hand: poker([hand, ...]) => hand"
        return max(hands, key=hand_rank)

def test():
        "Test cases for the functions in poker program."
        sf = "6C 7C 8C 9C TC".split()
        # 这里的T应该是10,6、7、8、9、T是顺子 straight;
        # 全为C,则花色相同 flush;
        # 总而言之,是同花顺。
        # sf 应该是 straight flush (同花顺)的简写。
        fk = "9D 9H 9S 9C 7D".split()
        # fk:four of a kind,4张大小一样的牌
        # 4个9
        fh = "TD TC TH 7C 7D".split()
        # fh:full house,3带2
        # 3个(条,kind)10,2个7。
        assert poker([sf, fk, fh]) == sf
        # 上一行必须为真,如果不为真,程序就会停止。
        assert poker([fk, fh]) == fk
        assert poker([fh, fh]) == fh
        # Add 2 new assert statements here. The first 
        # should check that when fk plays fh, fk 
        # is the winner. The second should confirm that
        # fh playing against fh returns fh.
        return "tests pass"

补充:
手牌排序类别

详情请查看:
维基百科 - List of poker hands - Hand-ranking categories

9. 练习:Extreme Values

测试的一个重要原则

测试极限值。例如,assert poker([fh, fh]) == fh,是极限值的一种。
考虑传递给poker(hands)中的hands,只有1项、0项或者100项。
poker通常不是单人游戏(solitaire)。所以只有1项,通常不会有太大的意义。但是说明书里面,没有把这个规则定出来。所以增加一个测试,检查当1手牌时,1手牌是胜者。
0手牌时。说明书没有说,但是,poker(hands)函数应该返回1手牌。如果我们传递一个空的列表给poker(hands),就会没有手牌(hand)用于返回。所以最好澄清这个问题,那就是,当我们传递0手牌,这是一个错误(error)。当然,也可以返回None.
100个人同时打牌。如果hands是100种不同的hands的列表。我们想允许那种情况。我们会需要很多卡,500张卡。所以,写测试用例,测试1手牌和100手牌。

# -----------
# User Instructions
# 
# Modify the test() function to include two new test cases:
# 1) A single hand.
# 2) 100 hands.
#
# Since the program is still incomplete, clicking RUN won't do 
# anything, but clicking SUBMIT will let you know if you
# have gotten the problem right. 

def poker(hands):
    "Return the best hand: poker([hand,...]) => hand"
    return max(hands, key=hand_rank)

def test():
    "Test cases for the functions in poker program"
    sf = "6C 7C 8C 9C TC".split() 
    fk = "9D 9H 9S 9C 7D".split() 
    fh = "TD TC TH 7C 7D".split()
    assert poker([sf, fk, fh]) == sf
    assert poker([fk, fh]) == fk
    assert poker([fh, fh]) == fh
    # Add 2 new assert statements here. The first 
    # should assert that when poker is called with a
    # single hand, it returns that hand. The second 
    # should check for the case of 100 hands.
    assert poker([sf]) == sf
    # 当玩单人游戏时,它就赢了。
    assert poker([sf] + [fk] * 99)    
    return 'tests pass'

以上的内容,离完整的测试用例集合很远。我们真的想要更完整的测试的覆盖。但是也可以开始了。

10. 练习:Hand Rank Attempt

请注意一个前提,ranks是从高到低排列的。
现在是回到hand_rank的好时机了,这是我们将要处理的最复杂的函数。
hand_rank将一手牌(a hand)作为一个输入,但是return什么呢?

# Return a value indicating the ranking of a hand.
# 返回一个值,标明一手牌的顺序(大小)。

但是我们不确定,那个值value,是多少。value必须是可以被max比较大小的某种东西。可以试试数字(数,number)。

参照维基百科 - List of poker hands Hand-ranking categories这张图,和本博文的【手牌排序类别】图。有10种类型的手牌(hands)。(Udacity的视频里面说的是9种,这里我有点疑惑。暂时,我的猜测是,没有讨论5张一样的牌的情况,例如维基百科中的,4个A和1个王组成的5张一样的牌。)

从以上,可知,有9中不同类型的手牌。从顶部最大的同花顺(sf straight flush),到底部最小。我们可以用数字标记,从8到0。8属于同花顺straight flush。然后,我们可以得到解决办法的一个框架:

def hand_rank(hand):
    "Return a value indicating the ranking of a hand."
    # 排序大小是最重要的,我们可以先用我们的card_ranks函数,获取排序大小。
    ranks = card_ranks(hand)
    # 首先,我们想检查是否有同花顺(straight flush)
    if straight(ranks) and flush(hand):
        return 8
    # 如果有一个4条(Four of a kind)
    # 在kind()函数里面,我们只需要大小点数rank。
    elif kind(4, ranks):
        return 7
    elif ...

现在有一个问题:

这是否起作用,可以工作?

答案:

部分情况下,可以工作。例如3带一对(full house,对应6)大于顺子(straight,对应4)。
但是,并非所有情况下,都能正常工作。
例如,【1对10+3张其他的牌】【1对9+3张其他的牌】,这两个对应的value,都是1,大小相同。

11. 练习:Representing Rank

请注意一个前提,ranks是从高到低排列的。
表示不同的牌的顺序大小,以下哪几个可以正常工作?哪一个最好?

int 70905 70302
□ 小数 7.0905 7.0302
■ 元组 (7, 9, 5) (7, 3, 2)

以上全都可以正常工作,元组最好。因为元组不需要进行复杂的运算,可以直接比较大小。

12. Wild West Poker

用代码表示一手牌(the hand)

同花顺(rank 8,Staight flush)
举例:【同花顺 梅花 7/8/9/10/J】
表示为(8, 11)。解释:同花顺的排序大小最大,为8。上面这手牌中,J最大。(8, 11)完整地描述这手牌(the hand)。

4带1(rank 7,Four of a kind)
举例:【4个A+1个Q】(花色是乱的)
表示为(7, 14, 12)。解释:这手牌中,4个A是主要的rank,为7。A的数大小是14。Q是card rank number 12。

3带1对(rank 6,Full House)
举例:【3个8+1对K】(花色是乱的)
表示为(6, 8, 13)。解释:尽管K比8大,但是8的个数比K多。3个8为full house,value是6。K是13。这个3元素的元组(6, 8, 13)完整地描述了这手牌(hand)。

同花(rank 5,Flush)
举例:【同花(Flush) 方片 10+8+7+5+3】
表示为(5, [10, 8, 7, 5, 3])。解释:同花 Flush 的排序 ranked 大小是5。并且,为了更具体地表示这手牌,我们把所有的牌的数都放进来了。

顺子(rank 4,Straight)
举例:【顺子(Straight) J/10/9/8/7】(花色是乱的)
表示为(4, 11)。解释:4意思是Straight 顺子,11意思是J。

3带2张不一样的(rank 3,Three of a kind)
举例:【3个7+5+2】(花色是乱的)
表示为(3, 7, [7, 7, 7, 5, 2])。解释:通常,只用描述3个7,就可以表示1手牌,但是如果我们真的需要具体地表示(英文为break the ties),我们需要看着下一个最大的牌。

2个对子(rank 2,Two Pair)
举例:【2个J+2个3+K】(花色是乱的)
表示为(2, 11, 3, [13, 11, 11, 3, 3]):。解释:“1对J和1对3”可以描述这手牌的绝大部分,但是我们也需要比较所有的卡牌(cards)。2个对子对应的rank是2,J是11,3是3,最后列出5张card,可以完全地消除这手牌的歧义。

只有1个对子(rank 1,One pair)
举例:【2个2+J+6+3】(花色是乱的)
表示为(1, 2, [11, 6, 3, 2, 2])。解释:“1对2,J最大”,但是,还是有些部分没有描述到。1个对子的ranking是1,2代表2本身的大小,然后把所有的cards写进元组。

啥好牌都没有(rank 0,High Card)
举例:【7/5/4/3/2】(花色是乱的)
表示为(0, 7, 5, 4, 3, 2)。解释:“7/5/4/3/2”这手牌,没有对子、顺子、炸弹,rank是0。

13. Back To Hand Rank

def poker(hands):
    "Return the best hand: poker([hand,...]) => hand"
    return max(hands, key=hand_rank)

def hand_rank(hand):
    "Return a value indicating the ranking of a hand."
    ranks = card_ranks(hand)
    if straight(ranks) and flush(hand):
        return (8, max(ranks)) # 2 3 4 5 6 (8, 6) 6 7 8 9 T (8, 10)
    elif kind(4, ranks):
        return (7, kind(4, ranks), kind(1, ranks)) # 9 9 9 9 3 (7, 9, 3)
    elif ...

def test():
    ...

elif kind(4, ranks):
    return (7, kind(4, ranks)

的解释:
这里,不够清晰,到底会返回什么。为了解决这个问题,我们可以用一个实际的值,让它返回一个值。如果有4个7,则返回(7, 7)。如果有4个10,则返回(7, 10)。

elif kind(4, ranks):
        return (7, kind(4, ranks), kind(1, ranks)) # 9 9 9 9 3 (7, 9, 3)

的解释:如果只有1副牌,那么,1手牌里面,最多只有1个4张10。但是,我们需要允许,有两副牌的情况下,也可以玩游戏。所以,我们需要搞一个决胜属性,决胜属性将会是你手里的牌里剩下的卡牌,也就是这张一条的卡牌。(the tiebreaker would be the remaining card in your hand,which is the card that you have 1 of a kind of.)例如,如果你有一手牌,【99993】,表示为(7, 9, 3)

【视频勘误】
【优达学城视频链接】
【优达学城该视频下方的纠正】【At 4:17, Peter says “…0 is false in Java” this should actually be “…0 is false in python”】

Back To Hand Rank - Design of Computer Programs - YouTube

14. 练习:Testing Hand Rank

# -----------
# User Instructions
# 
# Modify the test() function to include three new test cases.
# These should assert that hand_rank gives the appropriate
# output for the given straight flush, four of a kind, and
# full house.
#
# For example, calling hand_rank on sf should output (8, 10)
#
# Since the program is still incomplete, clicking RUN won't do 
# anything, but clicking SUBMIT will let you know if you
# have gotten the problem right. 

def poker(hands):
    "Return the best hand: poker([hand,...]) => hand"
    return max(hands, key=hand_rank)

def test():
    "Test cases for the functions in poker program"
    sf = "6C 7C 8C 9C TC".split() # Straight Flush
    fk = "9D 9H 9S 9C 7D".split() # Four of a Kind
    fh = "TD TC TH 7C 7D".split() # Full House
    assert poker([sf, fk, fh]) == sf
    assert poker([fk, fh]) == fk
    assert poker([fh, fh]) == fh
    assert poker([sf]) == sf
    assert poker([sf] + 99*[fh]) == sf
    # 
    # add 3 new assert statements here.
    #
    assert hand_rank(sf) == (8, 10)
    assert hand_rank(fk) == (7, 9, 7)
    assert hand_rank(fh) == (6, 10, 7)
print test()

15. 练习:Writing Hand Rank

请注意一个前提,ranks是从高到低排列的。

# -----------
# User Instructions
# 
# Modify the hand_rank function so that it returns the
# correct output for the remaining hand types, which are:
# full house, flush, straight, three of a kind, two pair,
# pair, and high card hands. 
# 
# Do this by completing each return statement below.
#
# You may assume the following behavior of each function:
#
# straight(ranks): returns True if the hand is a straight.
# flush(hand):     returns True if the hand is a flush.
# kind(n, ranks):  returns the first rank that the hand has
#                  exactly n of. For A hand with 4 sevens 
#                  this function would return 7.
#                  返回第一顺序大小rank。。。对于一手牌,
#                  有着4个7的,这个函数会返回7.
# two_pair(ranks): if there is a two pair, this function 
#                  returns their corresponding ranks as a 
#                  tuple. For example, a hand with 2 twos
#                  and 2 fours would cause this function
#                  to return (4, 2).
# card_ranks(hand) returns an ORDERED tuple of the ranks 
#                  in a hand (where the order goes from
#                  highest to lowest rank). 
#                  输入1手牌(a hand),返回这个排列(ranks)的
#                  一个有序的元组(顺序是从最高到最低)。
# Since we are assuming that some functions are already
# written, this code will not RUN. Clicking SUBMIT will 
# tell you if you are correct.

def poker(hands):
    "Return the best hand: poker([hand,...]) => hand"
    return max(hands, key=hand_rank)

def hand_rank(hand):
    ranks = card_ranks(hand)
    # 注意本段代码开头部分对于card_ranks()的定义。
    # 上一行的含义是:ranks存放从高到低的手里的牌,作为元组。
    if straight(ranks) and flush(hand):            # straight flush
        return (8, max(ranks))
    elif kind(4, ranks):                           # 4 of a kind
        return (7, kind(4, ranks), kind(1, ranks))
    elif kind(3, ranks) and kind(2, ranks):        # full house
        return (6, kind(3, ranks), kind(2,ranks))  # your code here
    elif flush(hand):                              # flush
        return (5, ranks)# your code here
    elif straight(ranks):                          # straight
        return (4, max(ranks))# your code here
    elif kind(3, ranks):                           # 3 of a kind
        return (3, kind(3, ranks), ranks)           # your code here
    elif two_pair(ranks):                          # 2 pair
        # return (2, max(two_pair(ranks)), min(two_pair(ranks)), hand) 
        # 上一行是我的代码,请留意本段代码的开始对于two_pair()函数的定义,发现可以简化为Peter放入代码,如下一行
        return (2, two_pair(ranks), ranks)
    elif kind(2, ranks):                           # kind
        # return (1, max(ranks), hand)               # your code here
        #上一行是我的代码,请看下一行Peter的代码
        return (1, kind(2, ranks), ranks)
    else:                                          # high card
        # return (0,) + card_ranks(hand)             # your code here
        # 上一行是我的代码,请看下一行Peter的代码
        return (0, ranks)

def test():
    "Test cases for the functions in poker program"
    sf = "6C 7C 8C 9C TC".split() # Straight Flush
    fk = "9D 9H 9S 9C 7D".split() # Four of a Kind
    fh = "TD TC TH 7C 7D".split() # Full House
    assert poker([sf, fk, fh]) == sf
    assert poker([fk, fh]) == fk
    assert poker([fh, fh]) == fh
    assert poker([sf]) == sf
    assert poker([sf] + 99*[fh]) == sf
    assert hand_rank(sf) == (8, 10)
    assert hand_rank(fk) == (7, 9, 7)
    assert hand_rank(fh) == (6, 10, 7)
    return 'tests pass'

16. 练习:Testing Card Rank

请注意一个前提,ranks是从高到低排列的。

# -----------
# User Instructions
# 
# Modify the test() function to include three new test cases.
# These should assert that card_ranks gives the appropriate
# output for the given straight flush, four of a kind, and
# full house.
#
# For example, calling card_ranks on sf should output  
# [10, 9, 8, 7, 6]
#
# Since the program is still incomplete, clicking RUN won't do 
# anything, but clicking SUBMIT will let you know if you
# have gotten the problem right. 

def test():
    "Test cases for the functions in poker program"
    sf = "6C 7C 8C 9C TC".split() # Straight Flush
    fk = "9D 9H 9S 9C 7D".split() # Four of a Kind
    fh = "TD TC TH 7C 7D".split() # Full House
    assert card_ranks(sf) == [10, 9, 8, 7, 6]
    assert card_ranks(fk) == [9, 9, 9, 9, 7]
    assert card_ranks(fh) == [10, 10, 10, 7, 7]
    assert poker([sf, fk, fh]) == sf
    assert poker([fk, fh]) == fk
    assert poker([fh, fh]) == fh
    assert poker([sf]) == sf
    assert poker([sf] + 99*[fh]) == sf
    assert hand_rank(sf) == (8, 10)
    assert hand_rank(fk) == (7, 9, 7)
    assert hand_rank(fh) == (6, 10, 7)
    return 'tests pass'

17. 练习:Fixing Card Rank

请注意一个前提,ranks是从高到低排列的。

# -----------
# User Instructions
# 
# Modify the card_ranks() function so that cards with
# rank of ten, jack, queen, king, or ace (T, J, Q, K, A)
# are handled correctly. Do this by mapping 'T' to 10, 
# 'J' to 11, etc...

def card_ranks(cards):
    "Return a list of the ranks, sorted with higher first."
    # 每一张卡牌card,有两个值:大小点数rank和花色suit。
    # 每一张card,有2个值元素:value element。
    # 这里,将2个元素分成第1个和第2个。我们说,a card由a rank 和 a suit组成。
    # 下一行代码,迭代cards中的点数rank和花色suit。
    # 我们将只收集ranks,搞成一个列表。
    ranks = [r for r,s in cards]
    # 但是,这里有一个问题,那就是,
    # !!!卡牌T(10)/J/Q/K/A,全他妈乱套了,没法正确排序!!!
    # 取而代之的是,我们想把
    # T映射到10,J映射到11,Q映射到12,K映射到13,A映射到14。
    ranks.sort(reverse=True)
    return ranks

print card_ranks(['AC', '3D', '4S', 'KH']) #should output [14, 13, 4, 3]

我的办法:
测试通过的:(但是代码不够简洁)

l = ['AC', '3D', '4S', 'KH']
l2 = []
for r,s in l:
        if r == 'T':
                l2.append(10)
        elif r == 'J':
                l2.append(11)
        elif r == 'Q':
                l2.append(12)
        elif r == 'K':
                l2.append(13)
        elif r == 'A':
                l2.append(14)
        else:
                l2.append(int(r))

print(l2)
print "---------"

我也考虑过使用列表生成式,但是测试不通过(不光测试不通过,而且代码很不简洁):

[10 for r,s in hand if r == 'T',
 11 for r,s in hand if r == 'J',
 ...罗列。。。]

考虑Peter的代码:

# -----------
# User Instructions
# 
# Modify the card_ranks() function so that cards with
# rank of ten, jack, queen, king, or ace (T, J, Q, K, A)
# are handled correctly. Do this by mapping 'T' to 10, 
# 'J' to 11, etc...

def card_ranks(cards):
    "Return a list of the ranks, sorted with higher first."
    ranks = ['--23456789TJQKA'.index(r) for r,s in cards]
    ranks.sort(reverse=True)
    return ranks

print card_ranks(['AC', '3D', '4S', 'KH']) #should output [14, 13, 4, 3]

Peter的代码中,'--23456789TJQKA'.index(r)是string.index(char)形式,index的作用是,返回前面的字符串string中,某个字符char的下标index(也称为索引)。非常巧妙,代码也最简洁。最前面的--,代表01,刚好一副牌中,没有这两个数,或者说,没有这2张牌。
上述代码的调用的示例:

>>> '--23456789TJQKA'.index('2')
2
>>> '--23456789TJQKA'.index('T')
10

18. 练习:Straight And Flush

请注意一个前提,ranks是从高到低排列的。
我的答案:

# -----------
# User Instructions
# 
# Define two functions, straight(ranks) and flush(hand).
# Keep in mind that ranks will be ordered from largest
# to smallest.

def straight(ranks):
    "Return True if the ordered ranks form a 5-card straight."
    # Your code here.
    """
    if ranks[0]-4 == ranks[1]-3 == ranks[2]-2 == ranks[3]-1 == ranks[4]:
        return True
    """
    """
    我的答案:
    return ranks[0]-4 == ranks[1]-3 == ranks[2]-2 == ranks[3]-1 == ranks[4]
    """
    # Peter的答案
    return (max(ranks)-min(ranks) == 4) and len(set(ranks) == 5)


def flush(hand):
    "Return True if all the cards have the same suit."
    # Your code here.
    """
    我的答案:
    return hand[0][1] == hand[1][1] == hand[2][1] == hand[3][1] == hand[4][1]
    """
    # Peter的答案,巧妙地利用了集合的特性:不能含有重复的元素!!!
    suits = [s for r,s in hand]
    return len(set(suits)) == 1

def test():
    "Test cases for the functions in poker program."
    sf = "6C 7C 8C 9C TC".split()
    fk = "9D 9H 9S 9C 7D".split()
    fh = "TD TC TH 7C 7D".split()
    assert straight([9, 8, 7, 6, 5]) == True
    assert straight([9, 8, 8, 6, 5]) == False
    assert flush(sf) == True
    assert flush(fk) == False
    return 'tests pass'

print test()

19. 练习:Kind Function

请注意一个前提,ranks是从高到低排列的。

# -----------
# User Instructions
# 
# Define a function, kind(n, ranks).

def kind(n, ranks):
    """Return the first rank that this hand has exactly n of.
    Return None if there is no n-of-a-kind in the hand."""
    """如果这手牌中,有n张一样的牌(或者称为n条),则返回牌的大小点数。
    如果一手牌中,没有n张一样的牌,则返回None。
    """
    # Your code here.
    # 以下是Peter的答案
    for r in ranks:
        if ranks.count(r) == n: return r
        # 如果列表ranks中的元素r的个数等于n,则返回元素r。
        # 结合前面一行的for r in ranks,可以理解,
        # 是对列表ranks中的每一个元素r,都进行统计,
        # 求出列表中每一个元素r的个数。
        # 如果是n个r,则返回r。
    return None
    # 如果没有n个相同的元素,则返回None。
    # 举例:【99997】没有3个相同的元素,则返回None。

    """
    我在自己思考时,没有考虑到这个方法aList.count(element)
    统计列表aList中的元素element的个数
    使用示例:
    >>> ranks = [9,9,9,9,7]
    >>> ranks.count(9)
    4
    >>> ranks.count(7)
    1
    """

def test():
    "Test cases for the functions in poker program."
    sf = "6C 7C 8C 9C TC".split() # Straight Flush
    fk = "9D 9H 9S 9C 7D".split() # Four of a Kind
    fh = "TD TC TH 7C 7D".split() # Full House
    # 注意Peter新增了一行一手牌two pair 【55996】
    tp = "5S 5D 9H 9C 6S".split() # Two pairs
    # fkranks是对fk调用card_ranks之后的排序的结果
    fkranks = card_ranks(fk)
    # tpranks是对tp调用card_ranks之后的排序的结果
    tpranks = card_ranks(tp)
    # fk有4张9,下面的表达式应当返回9。
    assert kind(4, fkranks) == 9
    # fk没有3张一样的牌,应当返回None。
    assert kind(3, fkranks) == None
    # fk没有2张一样的牌,应当返回None。
    assert kind(2, fkranks) == None
    # fk有1张7,应当返回7。
    assert kind(1, fkranks) == 7
    return 'tests pass'

def card_ranks(hand):
    "Return a list of the ranks, sorted with higher first."
    ranks = ['--23456789TJQKA'.index(r) for r, s in hand]
    ranks.sort(reverse = True)
    return ranks

print test()

20. 练习:Two Pair Function

请注意一个前提,ranks是从高到低排列的。

# -----------
# User Instructions
# 
# Define a function, two_pair(ranks).

def two_pair(ranks):
    """If there are two pair, return the two ranks as a
    tuple: (highest, lowest); otherwise return None."""
    # Your code here.
    """
    我的答案,很偷懒,但是居然测试通过了。。。
    if ranks[0] == ranks[1] and ranks[2] == ranks[3]:
            # return(ranks[0], ranks[2]) 改为下一行
            return max(ranks[2], ranks[0]), min(ranks[2], ranks[0])
    else:
        return None
    """
    # Peter的答案
    """
    Peter的方法很巧妙,先取得ranks中的、从左到右的、第1对牌的对应的元素大小:pair,
    (注意,ranks中未必存在1对牌,也就是说,pair可能为None。)
    然后,将ranks反转,取得ranks中的、从右到左的、第1对牌的对应的元素大小:lowpair,
    再然后,如果pair不为None,并且如果pair与lowpair不相等,
    也就是说,如果ranks中存在2对牌,
    那么,返回(pair, lowpair)
    如果不是上述的情况,那么返回None。
    """
    pair = kind(2, ranks)
    lowpair = kind(2, list(reversed(ranks)))
    if pair and lowpair != pair:
        return (pair, lowpair)
    else:
        return None

def kind(n, ranks):
    """Return the first rank that this hand has exactly n of.
    Return None if there is no n-of-a-kind in the hand."""
    for r in ranks:
        if ranks.count(r) == n: return r 
    return None

def test():
    "Test cases for the functions in poker program."
    sf = "6C 7C 8C 9C TC".split() # Straight Flush
    fk = "9D 9H 9S 9C 7D".split() # Four of a Kind
    fh = "TD TC TH 7C 7D".split() # Full House
    tp = "TD 9H TH 7C 3S".split() # Two Pair
    fkranks = card_ranks(fk)
    tpranks = card_ranks(tp)
    assert kind(4, fkranks) == 9
    assert kind(3, fkranks) == None
    assert kind(2, fkranks) == None
    assert kind(1, fkranks) == 7
    return 'tests pass'

def card_ranks(hand):
    "Return a list of the ranks, sorted with higher first."
    ranks = ['--23456789TJQKA'.index(r) for r, s in hand]
    ranks.sort(reverse = True)
    return ranks

print test()

21. 练习:Making Changes

def test():
    ...
    s1 = "AS 2S 3S 4S 5C".split() # A-5 straight
    ...

增加了一些测试,但是这里把A当做1来对待,与我们前面定义的不一样。
那么,(如果要让测试通过)我们应该怎么样对待这个问题呢?
1、不用做任何修改,因为这是个小问题;
2、不用做任何修改,无视它;
3、我们可以做得更好。

22. 练习:What To Change

□ poker
□ hand_rank
□ card_ranks
■ straight

回头去看各函数的代码,会发现,只需要对straight做一个小的修改,即可。

23. 练习:Ace Low Straight

def card_ranks(hand):
    "Return a list of the ranks, sorted with higher first."
    ranks = ['--23456789TJQKA'.index(r) for r,s in hand]
    """
    这里要解决的问题是【21. 练习:Making Changes】的问题,我的答案是,
    修改上一行代码为
    ranks = ['-A23456789TJQK'.index(r) for r,s in hand]
    """
    ranks.sort(reverse=True)
    # return ranks
    # Peter的答案:
    return [5, 4, 3, 2, 1] if (ranks == [14, 5, 4, 3, 2]) else ranks

24. 练习:Handling Ties

(解决积分相等(ties)的问题)

def poker(hands):
    """Return a list of winning hands: poker([hand,...]) => [hand,...]"""
    return max(hands, key=hand_rank)

def hand_rank(hand):
    """Return a value indicating the ranking of a hand."""
    ranks = card_ranks(hand) 
    if straight(ranks) and flush(hand):
        return (8, max(ranks))
    elif kind(4, ranks):
        return (7, kind(4, ranks), kind(1, ranks))
    elif kind(3, ranks) and kind(2, ranks):
        return (6, kind(3, ranks), kind(2, ranks))
    elif flush(hand):
        return (5, ranks)
    elif straight(ranks):
        return (4, max(ranks))
    elif kind(3, ranks):
        return (3, kind(3, ranks), ranks)
    elif two_pair(ranks):
        return (2, two_pair(ranks), ranks)
    elif kind(2, ranks):
        return (1, kind(2, ranks), ranks)
    else:
        return (0, ranks)

def card_ranks(hand):
    """Return a list of the ranks, sorted with higher first."""
    ranks = ['--23456789TJQKA'.index(r) for r, s in hand]
    ranks.sort(reverse = True)
    return ranks

例如,我有一个同花顺,TJQKA。但是,另外一个玩家,有着相同的同花顺,但是花色不同,怎么办?

修改如下的哪个个函数?修改哪个最好?

□ □ hand_rank
■ ■ poker
■ □ new function

答案:使用poker函数,或者新增一个函数,都可以解决这个问题。使用poker函数最好。

解释:
使用hand_rank无法解决这个问题,因为,如果真的积分相等,hand_rank只会返回相同的值。如果2个人都有同花顺,那么hand_rank应该返回相同的值。即使两个同花顺有不同的花色,hand_rank返回的值,仍然相同。
我们可以修改poker函数,返回积分相同时的1个列表。或者,我们可以创建一个新的函数,例如
poker_with_ties
但是,最好的方式,不是引入新的函数,因为,会有一个问题,就是到底用poker,还是poker_with_ties来玩游戏?

25. 练习:Allmax

# -----------
# User Instructions
# 
# Write a function, allmax(iterable, key=None), that returns
# a list of all items equal to the max of the iterable, 
# according to the function specified by key. 

def poker(hands):
    "Return a list of winning hands: poker([hand,...]) => [hand,...]"
    return allmax(hands, key=hand_rank)

def allmax(iterable, key=None): 
    # 注意,这里的key是一个函数,就像max(..., key=...)中的一样
    "Return a list of all items equal to the max of the iterable."
    # Your code here.我没有想出allmax怎么写,这里列出Peter的方法。
    result, maxval = [], None
    key = key or (lambda x: x)
    # 这里的lambda表达式,含义是x映射到x,我存在疑问,为何不直接写【x】?
    for x in iterable:
        xval = key(x)
        if not result or xval > maxval:
            result, maxval =[x], xval
        elif xval == maxval:
            result.append(x)
        return result
    """
    Peter方法的注释:
    开始阶段,保持对result和maxval的追踪
    result初始值的空列表[],maxval为None;
    如果参数列表中,传入了key函数,则使用key函数,
    如果没有传入可以函数,则使用(lambda x: x)函数;
    对可迭代对象iterable中的每一个元素,进行迭代(循环进行某种操作):
        对每个x,应用key函数,将key(x)的值传递给xval;
        如果xval比maxval大,则将xval的值传递给maxval(目的是为了让maxval的值最大),
        如果result为空列表[],则将单元素列表[x]的值传递给result;
        如果积分相等(平局),则将x添加到result列表的末尾。
    最后,返回result列表。
    """    

def hand_rank(hand):
    "Return a value indicating the ranking of a hand."
    ranks = card_ranks(hand) 
    if straight(ranks) and flush(hand):
        return (8, max(ranks))
    elif kind(4, ranks):
        return (7, kind(4, ranks), kind(1, ranks))
    elif kind(3, ranks) and kind(2, ranks):
        return (6, kind(3, ranks), kind(2, ranks))
    elif flush(hand):
        return (5, ranks)
    elif straight(ranks):
        return (4, max(ranks))
    elif kind(3, ranks):
        return (3, kind(3, ranks), ranks)
    elif two_pair(ranks):
        return (2, two_pair(ranks), ranks)
    elif kind(2, ranks):
        return (1, kind(2, ranks), ranks)
    else:
        return (0, ranks)

def card_ranks(hand):
    "Return a list of the ranks, sorted with higher first."
    ranks = ['--23456789TJQKA'.index(r) for r, s in hand]
    ranks.sort(reverse = True)
    return [5, 4, 3, 2, 1] if (ranks == [14, 5, 4, 3, 2]) else ranks

def flush(hand):
    "Return True if all the cards have the same suit."
    suits = [s for r,s in hand]
    return len(set(suits)) == 1

def straight(ranks):
    "Return True if the ordered ranks form a 5-card straight."
    return (max(ranks)-min(ranks) == 4) and len(set(ranks)) == 5

def kind(n, ranks):
    """Return the first rank that this hand has exactly n-of-a-kind of.
    Return None if there is no n-of-a-kind in the hand."""
    for r in ranks:
        if ranks.count(r) == n: return r
    return None

def two_pair(ranks):
    "If there are two pair here, return the two ranks of the two pairs, else None."
    pair = kind(2, ranks)
    lowpair = kind(2, list(reversed(ranks)))
    if pair and lowpair != pair:
        return (pair, lowpair)
    else:
        return None

def test():
    "Test cases for the functions in poker program."
    sf1 = "6C 7C 8C 9C TC".split() # Straight Flush
    sf2 = "6D 7D 8D 9D TD".split() # Straight Flush
    fk = "9D 9H 9S 9C 7D".split() # Four of a Kind
    fh = "TD TC TH 7C 7D".split() # Full House
    assert poker([sf1, sf2, fk, fh]) == [sf1, sf2] 
    return 'tests pass'

现在,应该给poker函数增加一些测试,代替返回1手牌的测试,目前的测试,只返回1手牌的列表。还有,也应当增加一些测试,用于一些积分相等的情况。

关于上述的allmax函数,我仍然存在疑问,后续再继续深入理解。

26. 练习:Deal

deal是发牌的意思

Spade n.(纸牌的)黑桃;
Hearts n.(纸牌的)红桃;
Diamonds n.(纸牌的)方块;
Club n.(纸牌的)梅花。

我的答案(没有测试通过):

# -----------
# User Instructions
# 
# Write a function, deal(numhands, n=5, deck), that 
# deals numhands hands with n cards each.
#

import random # this will be a useful library for shuffling

# This builds a deck of 52 cards. If you are unfamiliar
# with this notation, check out Andy's supplemental video
# on list comprehensions (you can find the link in the 
# Instructor Comments box below).

mydeck = [r+s for r in '23456789TJQKA' for s in 'SHDC'] 

def deal(numhands, n=5, deck=mydeck):
    # Your code here.
    random_deck, result = [], []
    for i in range(len(deck)):
        random_deck.append(deck.pop(int(random.random() * (52 - i))))
    for j in range(numhands):
        result.append(ran_l[0+j*5 : 5+j*5])
    result result

哼哧哼哧搞了大概个把钟头,搞出来,但是居然没有测试通过,代码应该没有问题, 我猜测是因为没有引入shuffle发牌函数吧。
大概讲解下我的代码的逻辑:
第1个for循环:根据原始的52张牌组成的一个列表deck,生成一个随机列表random_deck。
第2个for循环:取出随机列表random_deck的第0~4个、第5~9个……元素,放入空列表result的尾部。

Peter的答案:

import random

def deal(numhands, n=5, deck=[r+s for r in '23456789TJQKA' for s in 'SHDC']):
    "Shuffle the deck and deal out numhands n-card hands"
    random.shuffle(deck)
    return [deck[deck[n*i:n*(i+1)] for i in range(numhands)]]

Peter的答案,使用了shuffle函数。使用了列表生成式,简化了代码,思路与我的差不多。

27. 练习:Hand Frequencies

维基百科有一个页面提到了,所有种类的手牌(hands),也给出了每手牌的概率。
(截图来自视频中)
《Design of Computer Programs》学习笔记(1 - 1)Winning Poker Hands - Lesson 1_第1张图片

现在,我们可以做的一件事,是我们是否可以复制这种类型的表。也就是,我们测试我们的程序,确保同花顺、high card…等等等,是否是正确的次数。
所以,现在我们要实现一个程序,可以从deck中,随机地生成纸牌decks或者发牌deals。然后检查是哪种类型的(同花顺、同花、顺子…),检查我们是否可以再次生成这个概率表。
我们将要看下,随机发牌概率。首先,我们将要有个quiz。我们想要发1手随机数的牌(deal out a random number of hands)。然后,统计每种类型,我们有多少种。然后,完成一个维基百科上面的那种表。
问题是,我们应该发多少手的牌?以得到关于概率的较好的估计?那就是,我们将不会得到准确的估计。但是,我们想要得到一些估计,接近准确的概率,我们抽样越多,我们就越接近。
我们应该看52手牌、50000手牌、700,000手牌,还是52!(阶乘)手牌?我还想要你回答为什么,我们需要one per card还是1,000 per card,以便随机变量是平稳的?
或者,我们是否需要…(do we need about 10 for least common hand, least common ranking?)
or do we need one for each of the possiable permutations(排列) of everything that came out of the deck?

《Design of Computer Programs》学习笔记(1 - 1)Winning Poker Hands - Lesson 1_第2张图片

解答:
one per card 不够。任何情况下,card的数量不相干。重要的是,the number of possible hands和the number of possible results we get for each type of hand,the rankings we get for each.
所以one per card 没有意义。1,000没有多大帮助。对于最不普遍的rank,最少期待10种可能的结果。
看明白了,为了算出同花顺的概率,最少需要发大概多少次牌。估算出来需要大约700,000次。
为什么咧?因为,如果是按照出现1次同花顺估算,那么,可能是0或2,会严重影响概率估算。所以不是1次。如果按照出现1000次同花顺估算,那么需要计算的次数会太多,计算量太大。
按照10次同花顺来估算,需要的计算了不会太大、也不至于算出来的概率偏差太大。

def hand_percentages(n=700*1000): # 按照700,000手牌为分母,来计算概率。
        "Sample n random hands and print a table of percentages for type of hand."
        counts = [0] * 9 # 初始一个矩阵,用来存放700k手牌中,各种牌型出现的次数。
        for i in range(n/10): # 迭代操作n/10次
                for hand in deal(10):
                        ranking = hand_rank(hand)[0]
                        counts[ranking] += 1
                # 迭代每1局中的每1手牌hand,1局发牌给10个人
                        # 获取每手牌的ranking,我猜测这里的ranking是牌型的名称
                        # 每手牌的ranking出现1次,则count自增一次
        for i in reversed(range(9)):
                print "%14s: %6.3f %%" % (hand_names[i], 100.*counts[i]/n)

28. Dimensions Of Programming

编程的几个维度

在真实的世界中,问题倾向于不是只解决一个特定的问题,但是它们存在于一个环境中。伴随着时间,它们占用一个环境中的一个位置。
correctness 正确性
efficiency 效率 是否足够快
features 特征 确切用来做什么
elegance 优雅;简洁 a programmer said “elegance is not optional”
elegance 包含很多attributes,像clarity(清楚;明确)、simplicity(简单)、generality(一般性),都是elegance的一部分。

正确性没有必要达到100%。我们使用的程序,有时会崩溃,但是这些程序仍然对我们有用。
随着时间过去,这个程序发展,向其他方向移动。如果程序运行太慢,我们不得不把它向相率维度移动。

你可能认为,改变elegance可能没有给你增加任何其他维度的特性。但是,它给你带来的东西,不是立马显现的,而是在将来给你带来一些东西。
更优雅、简洁的程序,将来会更容易维护、修改,所以你现在正在做的事情,通过让程序更容易维护,是在为将来买时间。
“the best is the enemy of the good”如果你追求完美,你可能浪费了太多的时间。
问问自己,最需要哪个维度?

29. Refactoring

重构

现在,我想向优雅、简洁这个方向移动,而不改变其他维度,通常,这被称为“重构”。那就是,我们修改一个程序,改了却没什么不同,但是程序会更清晰、更容易维护。

考虑如下的代码片段:

elif kind(3, ranks) and kind(2, ranks): # 这是判断是否为3带1对儿,full house。
    return (6, kind(3, ranks), kind(2, ranks))

return语句中,返回了重复的内容,违反了”DRY”(Don’t Repeat Yourself)原则。
考虑是否可以重构这段程序,避免重复自己?
这样做,我想出了1手牌(for a hand,for the ranks of a hand)的一个不同的表示(representation)。

考虑这手牌:
7 10 7 9 7
这里,没有考虑花色,在目前我们的表示方法中,这没关系,我们会把上述按照顺序排列。我们有10、9和3个7,我们说这里有3条(3 of a kind)、4条(4 of a kind)、2条(2 of a kind),等等等等。
但是,现在,我想要完成一个表现形式,这个表现形式,考虑我们需要知道的、关于这手牌的所有的东西,并且,我可以使用这个函数”group”,来完成这件事。

我们对这手牌进行group操作,group将返回2个值。第一个值是每种kind的数量(the counts for each of the different kinds of card ranks),我们有3个7、1个10、1个9。因此返回(3, 1, 1)。还有,这些数量将被排序为最大的在前面。group返回的第2个值是牌的大小(card ranks for each of these),我们有3个7、1个10、1个9,因此返回(7, 10, 9)。

从group(7 10 7 9 7)到【(3, 1, 1), (7, 10, 9)】的这种表现形式,在某些方面,是一种更好的表现形式(关于how a poker hand counts)。

Peter的代码:

def hand_rank(hand):
    "Return a value indicating how high the hand ranks."
    # counts is the count of each rank; ranks lists corresponding ranks
    # E.g. '7 T 7 9 7' => count = (3, 1, 1); ranks = (7, 10 ,9)
    groups = group(['--23456789TJQKA'.index(r) for r,s in hand])
    counts, ranks = unzip(groups)
    # 上一行代码:序列解包,获得counts和ranks的值。
    if ranks == (14, 5, 4, 3, 2):
        ranks = (5, 4, 3, 2, 1)
    straight = len(ranks) == 5 and max(ranks)-min(ranks) == 4
    # 对于顺子,例如'9 8 7 6 5',count = (1, 1, 1, 1, 1),ranks = (9, 8, 7, 6, 5)
    # 这样一来,上一行代码很好理解。
    flush = len(set([s for r,s in hand])) == 1
    # 对于同花,肯定花色只有一个,集合中,不会有重复的元素,那么成一行代码也很好理解。
    return (9 if (5,) == counts) else
            8 if straight amd flush else
            7 if (4, 1) == counts else
            6 if (3, 2) == counts else
            5 if flush else
            4 if straight else
            3 if (3, 1, 1) == counts else
            2 if (2, 2, 1) == counts else
            1 if (2, 1, 1, 1) == counts else
            0), ranks

def group(items):
    pass            

上述代码的逻辑,非常清晰。
首先,判断是否是(5,) == counts,如果是,则返回(9, ranks);如果不是,
则判断是否是同花顺, 如果是,则返回(8, ranks);如果不是,
则。。。
。。。
。。。

30. Summary

接下来看看group函数:

def group(items):
    "Return a list of [(count, x)...], highest count first, then highest x first."
    groups = [(items.count(x), x) for x in set(items)]
    return sorted(groups, reverse=True)

def unzip(pairs):
    return zip(*pairs)

以一个例子,去理解上述代码:

>>> items = [7, 8, 7, 9, 7]
>>> groups = [(items.count(x), x) for x in set(items)]
>>> groups
[(1, 8), (1, 9), (3, 7)]

显然,set(items)的值是{7, 8, 9},不含重复的元素。
迭代set(items)中的元素,统计items中,元素x出现的次数,并与x组成元组,进而组成列表,返回给groups。
返回降序排列后的groups。

再次重构:

def hand_rank(hand):
    "Return a value indicating how high the hand ranks."
    # counts is the count of each rank; ranks lists correponding ranks
    # E.g. '7 t 7 9 7' => counts = (3, 1, 1); ranks = (7, 10, 9)
    groups = group(['--23456789TQKA'.index(r) for r, s in hand])
    counts, ranks = unzip(groups)
    if ranks == (14, 5, 4, 3, 2):
        ranks = (5, 4, 3, 2, 1)
    straight = len(ranks) == 5 and max(ranks)-min(ranks)==4
    flush = len(set([s for r,s in hand])) == 1
    return max(count_ranking[counts], 4 * straight + 5* flush), ranks

count_rankings = {(5,):10, (4, 1):7, (3, 2):6, (3, 1, 1):3, (2, 2, 1):2,
                 (2, 1, 1, 1):1, (1, 1, 1, 1, 1):0}

我们学到了什么?
understanding 理解。 我们总是通过理解问题,来开始。阅读规格说明书,检查说明书是否可以理解、讲得通。如果无法理解,和别人讨论。尽力弄明白。考虑不同的方式,进而理解说明书。
define pieces 定义问题的碎片。 对于我们的问题,我们有cards、hands、ranks、suits,等等等等。弄清楚问题当中的所有事情的表现形式。
reuse 复用。 尝试复用你拥有的碎片。我们用了max函数、随机洗牌函数random shuffle function。
test 测试。 使用测试,可以知道程序做了什么。
explore 探索设计空间。 我们在设计空间由许多维度。correctness、efficiency、elegance(优雅、简单)、features(特征、用来做什么)。决定你想要到设计空间的哪个地方。保持移动到正确的方向,使用好的品味,知道何时停止。

参考文献:

  1. Design of Computer Programs - 英文介绍 - Udacity;
  2. Design of Computer Programs - 中文介绍 - 果壳;
  3. Design of Computer Programs - 视频列表 - Udacity;
  4. Design of Computer Programs - 视频列表 - YouTube。

你可能感兴趣的:(软件工程,SICP,Design,of,Computer,Programs,学习)