0
\$\begingroup\$

This is what the function is doing: it gets two arrays of tuples with 5 items each - hand = [H1,H2,H3,H4,H5] and board = [B1,B2,B3,B4,B5]

What I need to do is check all arrays formed by 2 items from hand and 3 items from board, like combination = [Hn,Hm,Bi,Bj,Bk] (100 combinations in total)

Then I need to compare each one of the combinations against a dictionary to get the combination rank, and then return the best array (best rank) and the rank itself:

def check_hand(hand, board, dictionary_A, dictionary_B):

 best_hand = []
 first_int = True

 for h1 in range (0, 4):
  for h2 in range (h1+1, 5):
   for b1 in range (0, 3):
    for b2 in range (b1+1, 4):
     for b3 in range (b2+1, 5):
      hand_check = []
      hand_check.append(hand[m1])
      hand_check.append(hand[m2])
      hand_check.append(board[b1])
      hand_check.append(board[b2])
      hand_check.append(board[b3])
      hand_check = sort(hand_check) #Custom sort for my array of objects
      hand_ranks = "".join([str(hand_check[0].rank),str(hand_check[1].rank),str(hand_check[2].rank),str(hand_check[3].rank),str(hand_check[4].rank)])

      if (hand_check[0].suit == hand_check[1].suit and hand_check[1].suit == hand_check[2].suit and hand_check[2].suit == hand_check[3].suit and hand_check[3].suit == hand_check[4].suit):
       control = [dictionary_A[hand_ranks][0],dictionary_A[hand_ranks][1]]
      else:
       control = [dictionary_B[hand_ranks][0],dictionary_B[hand_ranks][1]]

      if first_int:
       best_hand = hand_check
       rank = control
       first_int = False
      elif (int(control[0]) > int(rank[0])):
       rank = control
       best_hand = hand_check       
      elif (int(control[0]) == int(rank[0])):
       if (int(control[1]) > int(rank[1])):       
        rank = control
        best_hand = hand_check      

 return best_hand, rank[0]

I need to run this check for 2 million different hands and interact over 1000 times for every hand (Ideally I would run it for at least 100000 times for every hand, for a more statistically accurate result). Any ideas on how to make it more efficient?

Examples: I added some "prints" on the way for better understanding:

Hand: Q♥K♥Q♠K♠3♦︎ Board: 2♣2♥J♦︎6♥4♦︎

2♣2♥J♦︎Q♥K♥ - 2♣2♥6♥Q♥K♥ - 2♣2♥4♦︎Q♥K♥ - 2♣6♥J♦︎Q♥K♥ - 2♣4♦︎J♦︎Q♥K♥ - 2♣4♦︎6♥Q♥K♥ - 2♥6♥J♦︎Q♥K♥ - 2♥4♦︎J♦︎Q♥K♥ - 2♥4♦︎6♥Q♥K♥ - 4♦︎6♥J♦︎Q♥K♥ - 2♣2♥J♦︎Q♥Q♠ - 2♣2♥6♥Q♥Q♠ - 2♣2♥4♦︎Q♥Q♠ - 2♣6♥J♦︎Q♥Q♠ - 2♣4♦︎J♦︎Q♥Q♠ - 2♣4♦︎6♥Q♥Q♠ - 2♥6♥J♦︎Q♥Q♠ - 2♥4♦︎J♦︎Q♥Q♠ - 2♥4♦︎6♥Q♥Q♠ - 4♦︎6♥J♦︎Q♥Q♠ - 2♣2♥J♦︎Q♥K♠ - 2♣2♥6♥Q♥K♠ - 2♣2♥4♦︎Q♥K♠ - 2♣6♥J♦︎Q♥K♠ - 2♣4♦︎J♦︎Q♥K♠ - 2♣4♦︎6♥Q♥K♠ - 2♥6♥J♦︎Q♥K♠ - 2♥4♦︎J♦︎Q♥K♠ - 2♥4♦︎6♥Q♥K♠ - 4♦︎6♥J♦︎Q♥K♠ - 2♣2♥3♦︎J♦︎Q♥ - 2♣2♥3♦︎6♥Q♥ - 2♣2♥3♦︎4♦︎Q♥ - 2♣3♦︎6♥J♦︎Q♥ - 2♣3♦︎4♦︎J♦︎Q♥ - 2♣3♦︎4♦︎6♥Q♥ - 2♥3♦︎6♥J♦︎Q♥ - 2♥3♦︎4♦︎J♦︎Q♥ - 2♥3♦︎4♦︎6♥Q♥ - 3♦︎4♦︎6♥J♦︎Q♥ - 2♣2♥J♦︎Q♠K♥ - 2♣2♥6♥Q♠K♥ - 2♣2♥4♦︎Q♠K♥ - 2♣6♥J♦︎Q♠K♥ - 2♣4♦︎J♦︎Q♠K♥ - 2♣4♦︎6♥Q♠K♥ - 2♥6♥J♦︎Q♠K♥ - 2♥4♦︎J♦︎Q♠K♥ - 2♥4♦︎6♥Q♠K♥ - 4♦︎6♥J♦︎Q♠K♥ - 2♣2♥J♦︎K♥K♠ - 2♣2♥6♥K♥K♠ - 2♣2♥4♦︎K♥K♠ - 2♣6♥J♦︎K♥K♠ - 2♣4♦︎J♦︎K♥K♠ - 2♣4♦︎6♥K♥K♠ - 2♥6♥J♦︎K♥K♠ - 2♥4♦︎J♦︎K♥K♠ - 2♥4♦︎6♥K♥K♠ - 4♦︎6♥J♦︎K♥K♠ - 2♣2♥3♦︎J♦︎K♥ - 2♣2♥3♦︎6♥K♥ - 2♣2♥3♦︎4♦︎K♥ - 2♣3♦︎6♥J♦︎K♥ - 2♣3♦︎4♦︎J♦︎K♥ - 2♣3♦︎4♦︎6♥K♥ - 2♥3♦︎6♥J♦︎K♥ - 2♥3♦︎4♦︎J♦︎K♥ - 2♥3♦︎4♦︎6♥K♥ - 3♦︎4♦︎6♥J♦︎K♥ - 2♣2♥J♦︎Q♠K♠ - 2♣2♥6♥Q♠K♠ - 2♣2♥4♦︎Q♠K♠ - 2♣6♥J♦︎Q♠K♠ - 2♣4♦︎J♦︎Q♠K♠ - 2♣4♦︎6♥Q♠K♠ - 2♥6♥J♦︎Q♠K♠ - 2♥4♦︎J♦︎Q♠K♠ - 2♥4♦︎6♥Q♠K♠ - 4♦︎6♥J♦︎Q♠K♠ - 2♣2♥3♦︎J♦︎Q♠ - 2♣2♥3♦︎6♥Q♠ - 2♣2♥3♦︎4♦︎Q♠ - 2♣3♦︎6♥J♦︎Q♠ - 2♣3♦︎4♦︎J♦︎Q♠ - 2♣3♦︎4♦︎6♥Q♠ - 2♥3♦︎6♥J♦︎Q♠ - 2♥3♦︎4♦︎J♦︎Q♠ - 2♥3♦︎4♦︎6♥Q♠ - 3♦︎4♦︎6♥J♦︎Q♠ - 2♣2♥3♦︎J♦︎K♠ - 2♣2♥3♦︎6♥K♠ - 2♣2♥3♦︎4♦︎K♠ - 2♣3♦︎6♥J♦︎K♠ - 2♣3♦︎4♦︎J♦︎K♠ - 2♣3♦︎4♦︎6♥K♠ - 2♥3♦︎6♥J♦︎K♠ - 2♥3♦︎4♦︎J♦︎K♠ - 2♥3♦︎4♦︎6♥K♠ - 3♦︎4♦︎6♥J♦︎K♠ -

Best Hand: 2♣2♥J♦︎K♥K♠ Rank: 3

Just wondering if there is a way to run the piece of code below more efficiently. I just started to get acquainted with parallel program and think this might be an answer? But I have no idea how to do it using imap or processes.

\$\endgroup\$

closed as off-topic by 200_success, Graipher, Mast, esote, Austin Hastings Apr 18 at 1:22

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Lacks concrete context: Code Review requires concrete code from a project, with sufficient context for reviewers to understand how that code is used. Pseudocode, stub code, hypothetical code, obfuscated code, and generic best practices are outside the scope of this site." – Graipher, Mast, esote, Austin Hastings
If this question can be reworded to fit the rules in the help center, please edit the question.

  • 3
    \$\begingroup\$ Welcome to Code Review. Is this real working Python code? (Introducing a comment with // is not syntactically valid.) What's mao? Also, please provide some example inputs and outputs so that we can better understand what this code accomplishes. \$\endgroup\$ – 200_success Apr 17 at 4:18
  • 1
    \$\begingroup\$ In your example, 2s5h3sTc looks like an incomplete combination. Also, you still haven't explained what mao[m1] is. \$\endgroup\$ – 200_success Apr 17 at 5:41
  • 3
    \$\begingroup\$ Python being a dynamically typed language, it is not obvious what representation your code expects for cards. A detailed runnable example would be better than a prettified one. \$\endgroup\$ – 200_success Apr 17 at 6:53
  • 4
    \$\begingroup\$ This site is for reviewing actual code. I've rolled it back to the code as it was when I reviewed it, not the hypothetical replacement. \$\endgroup\$ – Peter Taylor Apr 17 at 9:06
  • 2
    \$\begingroup\$ @Graipher, that's not directly relevant: the edit was made while I was writing my answer and before I posted it. But I consider the rollback justified because the edit wasn't improving the code but replacing it wholesale with code which didn't do anything remotely similar. \$\endgroup\$ – Peter Taylor Apr 17 at 10:03
5
\$\begingroup\$
 for h1 in range (0, 4):
  for h2 in range (h1+1, 5):
   for b1 in range (0, 3):
    for b2 in range (b1+1, 4):
     for b3 in range (b2+1, 5):
      hand_check = []
      hand_check.append(hand[m1])
      hand_check.append(hand[m2])
      hand_check.append(board[b1])
      hand_check.append(board[b2])
      hand_check.append(board[b3])

Use itertools for this kind of thing:

for h in itertools.combinations(hand, 2):
  for b in itertools.combinations(board, 3):
    hand_check = list(h) + list(b)

Also, I don't find hand_check to be a helpful name. In general I expect variable names to be nouns (unless they're Booleans, when predicates make sense), and when I parse hand_check as a noun I find that it's a check modified by hand. The smallest change would be to rename it hand_to_check; I prefer candidate_hand.


      if (hand_check[0].suit == hand_check[1].suit and hand_check[1].suit == hand_check[2].suit and hand_check[2].suit == hand_check[3].suit and hand_check[3].suit == hand_check[4].suit):

Ugh. Use set.

if len(set(card.suit for card in hand_check)) == 1:

      hand_check = sort(hand_check) #Custom sort for my array of objects
      hand_ranks = "".join([str(hand_check[0].rank),str(hand_check[1].rank),str(hand_check[2].rank),str(hand_check[3].rank),str(hand_check[4].rank)])
       control = [dictionary_A[hand_ranks][0],dictionary_A[hand_ranks][1]]
      else:
       control = [dictionary_B[hand_ranks][0],dictionary_B[hand_ranks][1]]

Firstly, why the quadruple-lookup? Why not

lookup_table = dictionary_A if only_one_suit else dictionaryB
control = lookup_table[hand_ranks]

?

Secondly, although Python's string manipulation is quite fast, since you're aiming to microoptimise you should think in terms of integers rather than strings. I'd delve into the fascinating world of perfect hashes to try to get a lookup table which fits into L2 cache, or maybe even L1 with luck (there are only 6188 multisets of 5 ranks, so less than 25 kB of lookup table suffices).


      if first_int:
       best_hand = hand_check
       rank = control
       first_int = False
      elif (int(control[0]) > int(rank[0])):
       rank = control
       best_hand = hand_check       
      elif (int(control[0]) == int(rank[0])):
       if (int(control[1]) > int(rank[1])):       
        rank = control
        best_hand = hand_check      

If you initialise rank to [-1] then you should be able to eliminate first_int and simplify this to

      if (control > rank):
        rank = control
        best_hand = hand_check       

Now, I've addressed micro-optimisation above, but the first rule of optimisation is to start by looking at the algorithm.

What I need to do is check all arrays formed by 2 items from hand and 3 items from board, like combination = [Hn,Hm,Bi,Bj,Bk] (100 combinations in total)

Then I need to compare each one of the combinations against a dictionary

is wrong. What you need to do is find the set of five cards formed by 2 cards from the hand and 3 cards from the board which has the highest score.

I'd suggest that you micro-optimise and then benchmark against a major restructuring with the following outline:

for suit in the_four_suits:
  hand_mask = build_mask((card for card in hand if card.suit == suit))
  board_mask = build_mask((card for card in board is card.suit == suit))
  check_royal_flush(hand_mask, board_mask)
  check_straight_flush(hand_mask, board_mask)
  check_flush(hand_mask)

hand_counts = count_by_ranks(hand)
board_counts = count_by_ranks(board)
check_four_of_a_kind(hand_counts)
check_full_house(hand_counts)
...

The key ideas are to do everything with bit manipulation and to return as soon as you know that no further checks will produce a hand better than the best you've already found. For count_by_ranks, if you allocate three bits per rank then overflow into the next rank is impossible since there are only four suits. So e.g. to check for a four of a kind:

total_counts = hand_counts + board_counts
possible_fours = total_counts & 0x4924924924
# possible_fours indicates ranks where we have all four cards between hand and board
# We take three cards from board, so rule out ranks where all four are in the board
possible_fours &= ~board_counts
# Similarly, rule out ranks where all four are in hand
possible_fours &= ~hand_counts
# And rule out ranks where three are in hand
possible_fours &= ~((hand_counts << 1) & (hand_counts << 2))
if possible_fours != 0:
  # We have a four-of-a-kind: just need to extract it
\$\endgroup\$
  • \$\begingroup\$ list(h) + list(b) could be list(h + b) \$\endgroup\$ – Alex Hall Apr 17 at 18:23
  • \$\begingroup\$ @AlexHall It actually can't, because h and b are generators (from itertools.combinations). To concatenate two generators you need itertools.chain. It's possible that list(chain(h, b)) might be a little more performant because it creates only one list instead of 3 (but that list is twice the size). \$\endgroup\$ – Bailey Parker Apr 17 at 21:10
  • \$\begingroup\$ Thanks, peter. Take a look at the edit on the original post! \$\endgroup\$ – Vandré Dreer Bonaite Apr 17 at 22:58
  • \$\begingroup\$ No, h and b are tuples. In fact h+b might be good enough on its own without converting to a list. \$\endgroup\$ – Alex Hall Apr 18 at 5:18

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