6
\$\begingroup\$

Just as a learning exercise, I am implementing a poker game simulator. I am purposefully not searching for ready made tutorials showing how to do this, relying mostly on the python docs - that way I hope to learn in a more memorable, albeit slower, way. I'm looking for advice on how to code the comparison between cards, so that e.g. Five beats Four.

My current implementation works, but I feel it is a little hacky and very inefficient in one particular detail and there must be a better way. In the Card class init, I am setting an attribute order which is just a list of the members of the CardValue enum, in the order they are declared.

While I could just use enum.auto(), or manually set the values as integers and hence simply use those for comparisons (still with eq etc), that would lose me the ability to make use of the enum values to form the human readable card value names.

Is there a better way to go about this than the below code? I feel like the comparison operators should be in the enum itself, but so far have not worked out how to get that to work with my meaningful values for the members of the enum.

from enum import Enum
from functools import total_ordering

class CardValue(Enum):
    TWO = "2"
    THREE = "3"
    ...
    ...
    KING = "King"
    ACE = "Ace"    

@total_ordering
class Card:
    def __init__(self, value, suit):
        self.value = value
        self.suit = suit
        self.order = [en for en in CardValue] ### This seems wrong and very inefficient!

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.order.index(self.value) == self.order.index(other.value)

    def __lt__(self, other):
        if self.__class__ is other.__class__:
            return self.order.index(self.value) < self.order.index(other.value)
\$\endgroup\$
2
  • 1
    \$\begingroup\$ type(self) is type(other) =) \$\endgroup\$
    – hjpotter92
    Sep 28 '20 at 11:13
  • 1
    \$\begingroup\$ Actually, the comparison of cards varies across different card games. Therefore it might be more logical to separate cards from their ordering in different classes. \$\endgroup\$
    – GZ0
    Sep 29 '20 at 3:54
2
\$\begingroup\$

You can define an __init__ method for an Enum. It gets passed the value of each enum member. Use it to define an order attribute for each enum member. For example, the Enum member Ace = "Ace" calls __init__ with value = "Ace", and CardValue.ACE.order will be set to '__23456789TJQKA'.index(value[0]), which is 14.

Also, define __eq__ and __lt__ for comparing CardValues.

I changed the Enum to CardRank, because I found it clearer.

@total_ordering
class CardRank(Enum):
    TWO = "2"
    THREE = "3"
    KING = "King"
    ACE = "Ace"    
    
    def __init__(self, value):
        # the '__' are so "2" has an order of 2 and so on.
        self.order = '__23456789TJQKA'.index(value[0])
        
    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.order == other.order
        raise NotImplemented
        
    def __lt__(self, other):
        if self.__class__ is other.__class__:
            return self.order < other.order
        raise NotImplemented
        

@total_ordering
class Card:
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

    def __eq__(self, other):
        return self.rank == other.rank

    def __lt__(self, other):
        return self.rank < other.rank

Try to choose data types/structures/values that are going to make programming the core of your application easier. If needed, provide methods to translate internal values for the user. In this case, using an IntEnum would let you compare CardRanks without needing extra code (__lt__, total_ordering, etc). Just add a __str__() method to get the text for the rank.

class CardRank(IntEnum):
    Two = 2
    Three = 3
    Four = 4
    Five = 5
    Six = 6
    Seven = 7
    Eight = 8
    Nine = 9
    Ten = 10
    Jack = 11
    Queen = 12
    King = 13
    Ace = 14
    
    def __str__(self):
        if self.value <= 10:
            return str(self.value)
        else:
            return ['Jack', 'Queen', 'King', 'Ace'][self.value - 11]

Test it:

>>> print(CardRank.King, str(CardRank.Two), CardRank.Four < CardRank.Ten)
King 2 True
\$\endgroup\$
2
  • 1
    \$\begingroup\$ ['Jack', 'Queen', 'King', 'Ace'][self.value - 11] can be replaced with self.name. \$\endgroup\$
    – GZ0
    Sep 28 '20 at 22:54
  • \$\begingroup\$ Using IntEnum also means your enum members can be compared to completely different integer-based enums, which is almost always a bad idea. \$\endgroup\$ Sep 29 '20 at 21:16
5
\$\begingroup\$

There are a couple choices, but the best is to record the value's index on Card creation:

self.order = value._member_names_.index(value.name)

Then your comparison can use self.order directly:

return self.order < other.order

_member_names_ is a supported attribute and won't be going away.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Nice answer. To expand on the answer; if, for some reason, value is mutable then you'd want to make value a property. Putting the assignment to order in the setter instead of the __init__. \$\endgroup\$
    – Peilonrayz
    Sep 28 '20 at 13:43
  • \$\begingroup\$ Nope, sorry -- I forgot my answer was not in the Enum, and thought you were speaking about the Enum. You're first comment is perfectly correct; deleting my erroneous comment. \$\endgroup\$ Sep 30 '20 at 0:46
0
\$\begingroup\$

In the end I have decided to use IntEnum. Also, a little trick to create it I found on other posts (fine for now, I can refactor as need arises if I need something more complex later).

from enum import IntEnum
cards = ['LowAce', 'Two', 'King', 'Ace']
CardRank = IntEnum('CardRank', cards)

Check:

_ = [print(card.name, card.value) for card in CardRank]
CardRank.Two < CardRank.King

Result:

LowAce 1
Two 2
King 3
Ace 4
True
\$\endgroup\$

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.