Search code examples
pythonpython-3.xenumspython-3.7playing-cards

How to do high performance comparisons within a fixed set of options in Python 3.7?


I am struggling with the proper way to represent a fixed set of options (playing card characteristics) for the most efficient comparisons later in development.

In the playing card example you have rank (2 to 10, Jack, King, Queen, Ace) and suit (Heart, Diamond, Club, Spade). In other areas of my code I may have similar options such as preset shuffle modes (No Shuffle, Fisher-Yates, etc). Being that the application is actually a simulation these comparisons will be frequent and need to be as efficient as possible. For Example, checking if a card is a specific suit.

What is the best way to do this in Python 3.7 (or later)? I can always use plain strings, but that might be slower. I could just use integers everywhere, but that makes code harder to read (remembering 3 is Clubs, or 0 is No Shuffle). I could use Enums but my research revealed some bad performance characteristics.

In C# I had used Enums (Rank / Suit / Shuffle Mode) combined with a struct (Rank=Ace, Suit=Spade) to represent a card. Comparisons were efficient and code was readable since it is actually integer comparison.

What is the best way to do this in modern Python versions?

TL;DR - If you had a fixed set of options (say a magic spell element in a video game or something) that you had to use in a comparison super frequently (every time you do damage), how would you represent those options for optimal performance?


Solution

  • Representing cards as strings is backwards thinking. Strings are for people; computers use numbers. It's easy and quick to look up a table of strings by number when you need to communicate with a human; it's much slower and harder to look up numbers by string, although Python hides that difficulty from you by having that built into the language, giving you the illusion that it's easy.

    Using a card object with a separate integer for rank and suit is OK, but my favorite way to represent cards is as just plain integers, say, 0 to 51 (or even 8 to 59), starting with the four deuces, then the four treys, etc., so 8 = deuce of clubs, 9 = deuce of diamonds, 10 = deuce of hearts, 11 = deuce of spades, 12 = trey of clubs, . . . to 59 being the ace of spades.

    With this numbering, the rank of a card is just (c >> 2), and the suit is (c & 3). And sometimes you don't even need to separate the rank and suit to do comparisons. For example, if a blackjack hand is an array of these integers, here's the function to calculate its value, and whether it is hard or soft:

    def value(hand):
      total = 0
      found_ace = False
    
      for card in hand:
        if card >= 56:
          found_ace = True
          total += 1
        elif card >= 40:
          total += 10
        else:
          total += (card >> 2)
    
      if total < 12 and found_ace:
        return total + 10, True
      return total, False
    
    print(value([9, 22, 59])) # deuce, five, ace
    

    In this code, the total is calculated with a few comparisons, additions, and shifts, all of which combined probably take less time than just looking up that "Seven" has a value of 7. If you do need the name a card, that's simple too:

    rank_names = [ "?", "?", "Deuce", "Trey", "Four", "Five", "Six", "Seven",
      "Eight", "Nine", "Ten", "Jack", "Queen", "King", "Ace" ]
    suit_names = [ "Club", "Diamond", "Heart", "Spade" ];
    
    def name(card):
      return rank_names[card >> 2] + " of " + suit_names[card & 3] + "s"