12
\$\begingroup\$

This code finds the probabilities of the various scores in Poker. It uses a Monte Carlo approach, in which, two million (\$2 \times 10^{6}\$) hands are simulated and evaluated, not an exhaustive search.

According to Wikipedia, my results are accurate.

I strived to avoid over-engineering, where the only tricky part is poker_value as it uses a bit of metaprogramming, but it was necessary to avoid duplication.

I am interested in performance improvements only if they do not hurt readability and straightforwardness.

An example output is:

Poker probabilities:
('nothing', 50.1837)
('pair', 42.1968)
('two_pair', 4.77195)
('three_of_a_kind', 2.18145)
('straight', 0.3568)
('flush', 0.198)
('full_house', 0.08485)
('four_of_a_kind', 0.02505)
('straight_flush', 0.0014)
Time taken: 107.4311831

The code:

from __future__ import division
from collections import namedtuple, Counter
import doctest
import random
import time

CARDS_PER_HAND = 5
VALUES = 13
KINDS = 4
ACCURACY = 2 * 10 ** 6

Card = namedtuple("Card", ["value", "kind"])

def all_equal(lst):
    """
    >>> all_equal([1,1,1,1])
    True
    >>> all_equal([1,2,3])
    False
    """
    return len(set(lst)) == 1

def couples(lst):
    """
    >>> couples([1,5,7])
    [[1, 5], [5, 7]]
    """
    return [ [curr, lst[index + 1]]
                   for index, curr in enumerate(lst[:-1])]

def one_by_one_increasing(lst):
    """
    >>> one_by_one_increasing([5, 6, 7, 8])
    True
    """
    return all(nexxt == previous + 1
                   for previous, nexxt in couples(lst))

def most_common(lst):
    """
    >>> most_common([1,2,2,2,3])
    2
    """
    return Counter(lst).most_common()[0][0]

def most_common_count(lst):
    """
    >>> most_common_count([4,4,4,1,1])
    3
    >>> most_common_count([7,3,1,7,8])
    2
    """
    return lst.count(most_common(lst))

def first_true(arg, funcs):
    """
    >>> def false(n): return False
    >>> def even(n): return n % 2 == 0
    >>> first_true(14, [false, even]).__name__
    'even'
    """
    for f in funcs:
        if f(arg):
            return f

def values(cards):
    """
    >>> values([ Card(12, 3), Card(5, 2), Card(8, 3)])
    [12, 5, 8]
    """
    return [c.value for c in cards]

def kinds(cards):
    """
    >>> kinds([ Card(12, 2), Card(5, 3)])
    [2, 3]
    """
    return [c.kind for c in cards]

def is_straight_flush(hand):
    """
    >>> is_straight_flush(Card(x, 2) for x in range(4, 9))
    True
    """
    return is_flush(hand) and is_straight(hand)

def is_flush(hand):
    """
    >>> is_flush(Card(x, 2) for x in [3,6,1,10])
    True
    """
    return all_equal(kinds(hand))

def is_four_of_a_kind(hand):
    """
    >>> is_four_of_a_kind([ Card(5, 2), Card(5, 3), Card(5, 1), Card(5, 4), Card(8, 3) ])
    True
    """
    return most_common_count(values(hand)) == 4

def is_three_of_a_kind(hand):
    """
    >>> is_three_of_a_kind( [Card(12, 2)]*3 + [Card(2, 3), Card(9, 2)])
    True
    >>> is_three_of_a_kind( [Card(2, 1), Card(3, 2), Card(3, 1), Card(5, 1), Card(9, 4)] )
    False
    """
    return most_common_count(values(hand)) == 3

def is_pair(hand):
    """
    >>> is_pair( [Card(2, 1), Card(3, 2), Card(3, 1), Card(5, 1), Card(9, 4)] )
    True
    """
    return most_common_count(values(hand)) == 2

def is_straight(hand):
    """
    >>> is_straight( [Card(value, random.randint(0,4)) for value in range(0,5)] )
    True
    """
    return one_by_one_increasing(sorted(values(hand)))

def is_two_pair(hand):
    """
    >>> is_two_pair([Card(1, 1), Card(3, 2), Card(3, 1), Card(9, 1), Card(9, 4)])
    True
    >>> is_two_pair( [Card(2, 1), Card(3, 2), Card(3, 1), Card(5, 1), Card(9, 4)] )
    False
    """
    return is_pair(hand) and is_pair([c for c in hand if c.value != most_common(values(hand))])

def is_full_house(hand):
    """
    >>> is_full_house([Card(3, 1), Card(3, 2), Card(3, 1), Card(9, 1), Card(9, 4)])
    True
    """
    return is_three_of_a_kind(hand) and is_pair([c for c in hand if c.value != most_common(hand).value])

def is_nothing(hand):
    """
    A hand is always at least nothing
    >>> is_nothing(Card(random.randint(0,12), random.randint(0,3)) for _ in range(CARDS_PER_HAND))
    True
    """
    return True

def poker_value(hand, possible_scores=[is_straight_flush, is_four_of_a_kind, is_full_house,
                        is_flush, is_straight, is_three_of_a_kind, is_two_pair, is_pair, is_nothing]):
    """
    >>> poker_value([ Card(5, 1), Card(7, 2), Card(9, 3), Card(10, 1), Card(10, 1) ])
    'pair'
    >>> poker_value([ Card(val, 3) for val in range(2, 7)])
    'straight_flush'
    """
    return first_true(hand, possible_scores).__name__[3:]

def poker_deck(max_value=VALUES, number_of_kinds=KINDS):
    """
    >>> len(poker_deck())
    52
    """
    return [ Card(value, kind)
                 for value in range(max_value)
                     for kind in range(number_of_kinds)]

def poker_percentages(accuracy):
    deck = poker_deck()
    occurencies = Counter( poker_value(random.sample(deck, CARDS_PER_HAND))
                        for _ in range(accuracy))
    return list(sorted(((name, round((occurencies[name] / accuracy * 100), CARDS_PER_HAND) )
                           for name in occurencies), key=lambda x: x[1], reverse=True))

def main():
    start = time.time()
    print("Poker probabilities:")
    print("\n".join(map(str, poker_percentages(ACCURACY))))
    print("Time taken: {}".format(time.time() - start))

if __name__ == "__main__":
    doctest.testmod()
    main()
\$\endgroup\$
2
  • \$\begingroup\$ Does this code recognize that (T, J, Q, K, A) and (A, 2, 3, 4, 5) are both valid straights? I.e., that for purposes of a straight, an ace can be both high and low? \$\endgroup\$
    – Curt F.
    Jul 22, 2015 at 18:05
  • \$\begingroup\$ Ace is only considered in the lower straight for simplicity of implementation. \$\endgroup\$
    – Caridorc
    Jul 22, 2015 at 18:08

1 Answer 1

4
\$\begingroup\$

Your code looks good and the logic is well separated in testable (and tested) functions.

A few details :

  • Maybe it would make sense for all_equal(lst) to return True for an empty list (just like the all builtin returns True for empty iterable). You'd just need to replace == 1 by <= 1.

  • In couples, you are using explicitely the index to iterate over consecutive arrays. You'll find various other ways to do so. I'll let you pick the one you prefer (after performing some benchmark if needed). Also, because of the way you use it, there might not be a need for returning a newly constructed list, an iterable would be enough.

  • Your implemement of most_common should pass 1 as a parameter to most_common so that it doesn't generate the whole list so that you are mostly ignoring.

  • Your implementation of most_common_count is not really efficient : the most_common() function already returns the count and you are simply ignoring it.

I think the right way to do it would be to have a most_common() function returning a tuple (item, count). Also you should probably handle the cases where different items are the most common.

I'll try to continue.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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