3
\$\begingroup\$

I have tried solving Project Euler #54 in Python. Please see description here -> Description:

The file, poker.txt, contains one-thousand random hands dealt to two players. Each line of the file contains ten cards (separated by a single space): the first five are Player 1's cards and the last five are Player 2's cards. You can assume that all hands are valid (no invalid characters or repeated cards), each player's hand is in no specific order, and in each hand there is a clear winner.

How many hands does Player 1 win?

#Euler problem 54
import collections

def numeric_value(num):
    values = {
        "T": 10,
        "J": 11,
        "Q": 12,
        "K": 13,
        "A": 14
    }
    return values[num] if num in values else int(num)

#sort according to descending numeric value
#useful for determining tiebreak for flushes and straights
def sort_by_numeric_value(hand, return_only_number=None):
    result = sorted(hand, key=lambda x: numeric_value(x[0]), reverse=True)

    if return_only_number:
        return [numeric_value(n[0]) for n in result]
    else:
        return result

#sort and places pairs/3 of a kind/four of a kind in front
#note that this returns a list of the numeric part of a card (i.e. T, J, 8)
def sort_according_to_pairs(hand):
    nums = [c[0] for c in hand]
    counter = collections.Counter(nums)
    temp = [c for c in counter.most_common(5)]

    temp = sorted(temp, key=lambda x: numeric_value(x[0]), reverse=True)
    temp = sorted(temp, key=lambda x: x[1], reverse=True)

    result = []
    #n stands for the number, f stands for the number of times it appears
    for n, f in temp:
        result += [numeric_value(n)] * int(f)
    return result

#returns True if it is a straight, where n is the number of the largest card; else False
def isstraight(hand):
    hand = sort_by_numeric_value(hand)
    nums = [numeric_value(c[0]) for c in hand]

    #hand is a straight if each card is one less than the preceding card
    for index, n in enumerate(nums):
        if index == 0:
            continue

        if n != nums[index - 1] - 1:
            return False
    return True

#returns True if all cards in hand contains the same suit
def isflush(hand):
    #tests if all the cards have the same suit as the first card
    return all([c[-1] == hand[0][-1] for c in hand])

#returns a list containing duplicates (only the number is returned)
def getduplicates(hand):
    nums = [c[0] for c in hand]
    counter = collections.Counter(nums)
    return [c[1] for c in counter.most_common(5) if c[1] > 1]

def ispair(duplicates):
    return len(duplicates) == 1 and duplicates[0] == 2

def istwopair(duplicates):
    return len(duplicates) == 2 and duplicates[0] == 2 and duplicates[1] == 2

def isthreofakind(duplicates):
    return len(duplicates) == 1 and duplicates[0] == 3

def isfullhouse(duplicates):
    return len(duplicates) == 2 and duplicates[0] == 3 and duplicates[1] == 2

def isfourofakind(duplicates):
    return len(duplicates) == 1 and duplicates[0] == 4

def getranking(hand):
    hand = sort_by_numeric_value(hand)
    straight = isstraight(hand)
    flush = isflush(hand)

    duplicates = getduplicates(hand)
    fourofakind = isfourofakind(duplicates)
    fullhouse = isfullhouse(duplicates)
    threeofakind = isthreofakind(duplicates)
    twopair = istwopair(duplicates)
    pair = ispair(duplicates)

    if straight and flush:
        return 10 if hand[0][0] == "A" else 9
    elif fourofakind:
        return 8
    elif fullhouse:
        return 7
    elif flush:
        return 6
    elif straight:
        return 5
    elif threeofakind:
        return 4
    elif twopair:
        return 3
    elif pair:
        return 2
    else:
        return 1

#to break ties, we compare highest card for both hands and move to next card in event of a tie
#for hands with flushes, straights and high card, we should sort according to numeric_value
#for pairs, etc, we should place the pairs in front
def break_tie(hand1, hand2):
    ranking = getranking(hand1)

    if ranking in [10, 9, 6, 5, 1]:
        hand1 = sort_by_numeric_value(hand1, True)
        hand2 = sort_by_numeric_value(hand2, True)
    else:
        hand1 = sort_according_to_pairs(hand1)
        hand2 = sort_according_to_pairs(hand2)

    for index, _ in enumerate(hand1):
        if hand1[index] == hand2[index]:
            continue
        else:
            return hand1[index] > hand2[index]

def compare(row):
    cards = row.split(" ")
    p1 = cards[0:5]
    p2 = cards[5:]

    p1_ranking = getranking(p1)
    p2_ranking = getranking(p2)

    return break_tie(p1, p2) if p1_ranking == p2_ranking else p1_ranking > p2_ranking

if __name__ == "__main__":
    with open("poker.txt") as f:
        wins = 0

        rows = [row for row in f.read().split("\n") if len(row) > 0]
        for row in rows:
            if compare(row):
                wins += 1

        print(wins)
\$\endgroup\$
  • 1
    \$\begingroup\$ Well, at least you consistently misspelled threofakind :P \$\endgroup\$ – Sumurai8 May 8 '16 at 14:11
  • \$\begingroup\$ @Sumurai8 yikes, I completely missed that! Thanks for pointing it out \$\endgroup\$ – Dan Tang May 8 '16 at 14:35
  • 1
    \$\begingroup\$ See this answer. \$\endgroup\$ – Gareth Rees May 8 '16 at 17:00
3
\$\begingroup\$

Style

You are using both snake_case and alllowercase for function names. I would recommend choosing one, and if you were to choose between those two, to choose for snake_case. It gives me a headache to try to find the word boundaries in isfourofakind.

Unnecessary work

You are doing a lot of unnecessary work. If, for example, the hand is a straight, you are also calculating if it is a pair, a full house, etc. It will likely save a couple of 100 of function calls to just move the functions to the if-else statement.

Straight

Since you are already calculating the duplicates regardless what hand it is, you can simplify the check for a straight to this:

def isstraight(hand, duplicates):
    hand = sort_by_numeric_value(hand)
    return len(duplicates) == 0 and numeric_value(hand[0]) == numeric_value(hand[4]) + 4

Edit:

sort_according_to_pairs

The function sort_according_to_pairs is somewhat messy. Instead of temp = sorted(temp you can just use temp.sort(.

Besides this, you are using two sorts, which more or less reverses all elements twice. Then again, the list is at most 4 items long, with at most 6 comparisons. It might not be worth it to optimize. You could create a key that takes into account both values, but that would require an extra multiplication and the calculation of the maximum value of a card.

You can also pull the conversion to numeric representation to the front, putting only numbers in nums: nums = [numeric_value(c[0]) for c in hand]. This requires a few less calls to numeric_value.

Up to you to find out if it performs better than your implementation.

def sort_according_to_pairs(hand):
    nums_in_hand = [numeric_value(c[0]) for c in hand]
    counter = collections.Counter(nums_in_hand)
    max_value = max(nums_in_hand)

    temp = [c for c in counter.most_common()]
    temp.sort(key=lambda x: x[0]+max_value*x[1], reverse=True)

    result = []
    #n stands for the number, f stands for the number of times it appears
    for n, f in temp:
        result += [n] * int(f)
    return result

break_tie

In break_tie you have the following code:

for index, _ in enumerate(hand1):
    if hand1[index] == hand2[index]:
        continue
    else:
        return hand1[index] > hand2[index]

There is no real reason to use enumerate here. A cleaner way of writing it is:

for card1, card2 in zip(hand1, hand2):
    if card1 == card2:
        continue
    else:
        return card1 > card2
\$\endgroup\$
  • \$\begingroup\$ Thank you for the great advice and I have incorporate some of your suggestions in a refactored version (github.com/tangbj/toyprojects/blob/master/pokerhands.py). I experimented with testing whether a hand has straight or flush before checking for duplicates, and the run time was similar so didn't incorporate that in. I also found removing one unnecessary sort cut down computation time by about 20%. Thanks so much for all the advice! \$\endgroup\$ – Dan Tang May 9 '16 at 5:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.