A famous casino is suddenly faced with a sharp decline of their revenues. They decide to offer Texas hold'em also online. Can you help them by writing an algorithm that can rank poker hands? 

Task:

Create a poker hand that has a constructor that accepts a string containing 5 cards:

```python
hand = PokerHand("KS 2H 5C JD TD")
```

The characteristics of the string of cards are:

A space is used as card seperator
Each card consists of two characters
The first character is the value of the card, valid characters are:
`2, 3, 4, 5, 6, 7, 8, 9, T(en), J(ack), Q(ueen), K(ing), A(ce)`
The second character represents the suit, valid characters are: 
`S(pades), H(earts), D(iamonds), C(lubs)`



The poker hands must be sortable by rank, the highest rank first:

```python
hands = []
hands.append(PokerHand("KS 2H 5C JD TD"))
hands.append(PokerHand("2C 3C AC 4C 5C"))
hands.sort() (or sorted(hands))
```

Apply the Texas Hold'em rules for ranking the cards. 
There is no ranking for the suits.
An ace can either rank high or rank low in a straight or straight flush. Example of a straight with a low ace:
`"5C 4D 3C 2S AS"`




Note:  there are around 25000 random tests, so keep an eye on performances.

Difficulty: MEDIUM
Tags: Fundamentals

Sorting

Game theory
Skill types: Sorting

Starter code:
def __repr__(self):
	

Solutions:
from functools import total_ordering

@total_ordering
class PokerHand(object):
	CARDS = 'AKQJT987654321'
	RANKS = {card: idx for (idx, card) in enumerate(CARDS)}

	def score(self, hand):
		(values, suits) = zip(*hand.split())
		(idxs, ordered) = zip(*sorted(((self.RANKS[card], card) for card in values)))
		is_straight = ''.join(ordered) in self.CARDS
		is_flush = len(set(suits)) == 1
		return (-2 * sum((values.count(card) for card in values)) - 13 * is_straight - 15 * is_flush, idxs)

	def __init__(self, hand):
		self.hand = hand
		self.score = min(self.score(hand), self.score(hand.replace('A', '1')))

	def __repr__(self):
		return self.hand

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

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


from collections import Counter
HEX_CARDS = '23456789ABCDE'
CARD_TO_HEX = str.maketrans('TJQKA', 'ABCDE')

class PokerHand(object):

	def __init__(self, hand):
		self.hand = hand
		hand = hand.translate(CARD_TO_HEX)
		valCount = Counter((hand[n] for n in range(0, len(hand), 3)))
		colorCount = Counter((hand[n] for n in range(1, len(hand), 3)))
		minCardIdx = int(min(valCount.keys()), 16) - 2
		isStraight = list(valCount.keys()) == set(HEX_CARDS[minCardIdx:minCardIdx + 5])
		if list(valCount.keys()) == {'2', '3', '4', '5', 'E'}:
			isStraight = True
			valCount['1'] = valCount.pop('E')
		mainVal = next((i for (i, elt) in enumerate([5 in list(colorCount.values()) and isStraight, 4 in list(valCount.values()), 3 in list(valCount.values()) and 2 in list(valCount.values()), 5 in list(colorCount.values()), isStraight, 3 in list(valCount.values()), 2 in list(valCount.values()) and len(list(valCount.keys())) == 3, 2 in list(valCount.values()) and len(list(valCount.keys())) == 4, True]) if elt))
		self.handValue = int(''.join(sorted(list(valCount.keys()), key=lambda k: (valCount[k], k), reverse=True)), 16) + (8 - mainVal) * 16 ** 5

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

	def __lt__(self, other):
		return self.handValue > other.handValue

	def __gt__(self, other):
		return self.handValue < other.handValue

	def __repr__(self):
		return 'PokerHand({})'.format(self.hand)


class PokerHand(object):

	def __init__(self, hand):
		self.hand = hand
		self.hand_value = self.value(hand)

	def __repr__(self):
		return self.hand

	def __lt__(self, other):
		return self.hand_value > other.hand_value

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

	def compare_with(self, other):
		if self.hand_value > other.hand_value:
			return 'Win'
		elif self.hand_value < other.hand_value:
			return 'Loss'
		elif self.hand_value == other.hand_value:
			return 'Tie'

	@staticmethod
	def hand_to_vector(hand):
		vec_hand = [0] * 13
		for card in hand.split():
			try:
				vec_hand[int(card[0]) - 2] += 1
			except:
				switcher = {'A': 12, 'K': 11, 'Q': 10, 'J': 9, 'T': 8}
				vec_hand[switcher[card[0]]] += 1
		return vec_hand

	@staticmethod
	def combination(hand):
		vec_hand = PokerHand.hand_to_vector(hand)
		suited = False
		if 1 == len(set([x[1] for x in hand.split()])):
			suited = True
		if 4 in vec_hand:
			return 'Four of a kind'
		elif 3 in vec_hand:
			if 2 in vec_hand:
				return 'Full house'
			else:
				return 'Three of a kind'
		else:
			if 2 in vec_hand:
				if 2 == vec_hand.count(2):
					return 'Two pair'
				else:
					return 'One pair'
			if '11111' not in ''.join([str(x) for x in vec_hand[-1:] + vec_hand]):
				if suited:
					return 'Flush'
				else:
					return 'High card'
			elif not suited:
				return 'Straight'
			elif vec_hand[8] == 1 and vec_hand[12] == 1:
				return 'Royal flush'
			else:
				return 'Straight flush'

	@staticmethod
	def value(hand):
		hand_vec = PokerHand.hand_to_vector(hand)
		value_vec = ['0', '0000', '0000', '0000', '0', '0', '0000', '0000', '0000', '0000']
		combination = PokerHand.combination(hand)
		if combination == 'One pair':
			value_vec[9] = '{:04b}'.format(hand_vec.index(2))
		elif combination == 'Two pair':
			value_vec[8] = '{:04b}'.format(hand_vec.index(2))
			value_vec[7] = '{:04b}'.format(hand_vec.index(2, hand_vec.index(2) + 1))
		elif combination == 'Three of a kind':
			value_vec[6] = '{:04b}'.format(hand_vec.index(3))
		elif combination == 'Straight':
			value_vec[5] = '1'
			if hand_vec[12] == 1 and hand_vec[3] == 1:
				hand_vec[12] = 0
		elif combination == 'Flush':
			value_vec[4] = '1'
		elif combination == 'Full house':
			value_vec[3] = '{:04b}'.format(hand_vec.index(2))
			value_vec[2] = '{:04b}'.format(hand_vec.index(3))
		elif combination == 'Four of a kind':
			value_vec[1] = '{:04b}'.format(hand_vec.index(4))
		elif combination == 'Straight flush':
			value_vec[0] = '1'
			if hand_vec[12] == 1 and hand_vec[3] == 1:
				hand_vec[12] = 0
		elif combination == 'Royal flush':
			value_vec[0] = '1'
			value_vec[1] = '1111'
		return int(''.join(value_vec) + ''.join(reversed(list([str(x) if x == 1 else str(0) for x in hand_vec]))), 2)


class PokerHand(object):
	CARD = '23456789TJQKA'

	def __repr__(self):
		return str(self.hand)

	def __init__(self, hand):
		self.hand = hand
		values = ''.join(sorted(hand[::3], key=self.CARD.index))
		suits = set(hand[1::3])
		is_straight = values in self.CARD or values == '2345A'
		is_flush = len(suits) == 1
		self.score = (2 * sum((values.count(card) for card in values)) + 13 * is_straight + 15 * is_flush, [self.CARD.index(card) for card in values[::-1]])
		if values == '2345A':
			self.score = (13 * is_straight + 15 * is_flush + 10, [3, 2, 1, 0, 12])

	def __lt__(self, other):
		return self.score > other.score


class PokerHand(object):
	CARD = '23456789TJQKA'
	RESULT = ['Loss', 'Tie', 'Win']

	def __repr__(self):
		return self.hand

	def __lt__(self, other):
		res = self.compare_with(other)
		return res == self.RESULT[2]

	def __init__(self, hand):
		self.hand = hand
		values = ''.join(sorted(hand[::3], key=self.CARD.index))
		suits = set(hand[1::3])
		is_straight = values in self.CARD
		is_straight_bad = values in ['2JQKA', '23QKA', '234KA', '2345A']
		is_flush = len(suits) == 1
		self.score = (2 * sum((values.count(card) for card in values)) + 13 * is_straight + 12.5 * is_straight_bad + 15 * is_flush, [self.CARD.index(card) for card in values[::-1]])

	def compare_with(self, other):
		return self.RESULT[(self.score > other.score) - (self.score < other.score) + 1]


from collections import defaultdict
from typing import List

class PokerHand(object):

	def __repr__(self):
		return self.hand

	def __init__(self, hand: str):
		self.hand = hand
		self.cards = [PokerCard(card) for card in hand.split(' ')]
		self.cards.sort()
		self.hand_weight = self._calculate_hand_weight()

	def __eq__(self, other):
		if self.hand_weight[0] != other.hand_weight[0]:
			return False
		for i in range(1, len(self.hand_weight)):
			if self.hand_weight[i] != other.hand_weight[i]:
				return False
		return True

	def __lt__(self, other):
		if self.hand_weight[0] < other.hand_weight[0]:
			return True
		if self.hand_weight[0] == other.hand_weight[0]:
			for i in range(1, len(self.hand_weight)):
				if self.hand_weight[i] == other.hand_weight[i]:
					continue
				if self.hand_weight[i] < other.hand_weight[i]:
					return True
				return False
		return False

	def _calculate_hand_weight(self):
		hand = defaultdict(int)
		for card in self.cards:
			hand[card.rank] += 1
		length = len(hand)
		values = list(hand.values())
		if length == 5:
			suited = self.is_suited()
			straight_high_card = self.is_a_straight()
			if suited and straight_high_card:
				return (1, straight_high_card)
			if suited:
				return (4, *self.cards)
			if straight_high_card:
				return (5, straight_high_card)
			return (9, *self.cards)
		if length == 2:
			if 4 in values:
				i_four = values.index(4)
				i_kicker = values.index(1)
				return (2, self.cards[i_four], self.cards[i_kicker])
			i_triple = values.index(3)
			i_pair = values.index(2)
			return (3, self.cards[i_triple], self.cards[i_pair])
		if length == 3:
			if 3 in values:
				i_triple = values.index(3)
				return (6, self.cards[i_triple], *self.cards[:i_triple], *self.cards[i_triple + 1:])
			i_pair = values.index(2)
			j_pair = values.index(2, i_pair + 1)
			i_kicker = values.index(1)
			return (7, self.cards[i_pair], self.cards[j_pair], self.cards[i_kicker])
		if length == 4:
			i_pair = values.index(2)
			return (8, self.cards[i_pair], *self.cards[:i_pair], *self.cards[i_pair + 1:])

	def is_suited(self):
		for i in range(len(self.cards) - 1):
			if self.cards[i].suit != self.cards[i + 1].suit:
				return False
		return True

	def is_a_straight(self):
		starter = 0
		if self.cards[0].rank == 'A' and self.cards[1].rank == '5':
			starter = 1
		for i in range(starter, len(self.cards) - 1):
			if self.cards[i + 1] - self.cards[i] != 1:
				return False
		if starter == 1:
			return self.cards[1]
		return self.cards[0]

class PokerCard(object):

	def __repr__(self):
		return self.card_weight

	def __init__(self, card: str):
		self.card = card
		self.rank = card[0]
		self.suit = card[1]
		self.card_weight = self.get_card_weight()

	def __eq__(self, other):
		return True if self.card_weight == other.card_weight else False

	def __lt__(self, other):
		return True if self.card_weight < other.card_weight else False

	def __sub__(self, other):
		return self.card_weight - other.card_weight

	def get_card_weight(self):
		if self.rank == 'T':
			return 5
		if self.rank == 'J':
			return 4
		if self.rank == 'Q':
			return 3
		if self.rank == 'K':
			return 2
		if self.rank == 'A':
			return 1
		return 63 - ord(self.rank)


from itertools import groupby
from enum import IntEnum, auto

class PokerHand:

	def __init__(self, hand):
		self.hand = hand
		cards = hand.split()
		values = tuple(sorted((cardval.get(c[0]) or int(c[0]) for c in cards)))
		groups = tuple(sorted((len(tuple(g)) for (_, g) in groupby(values)), reverse=True))
		hand_value = hv_table.get(groups)
		if hand_value is None:
			flush = len(set((c[1] for c in cards))) == 1
			straight = set((a - b for (a, b) in zip(values[1:], values[:-1]))) == {1} or values == (2, 3, 4, 5, 14)
			if straight and flush:
				hand_value = HV.royal_flush if values[0] == 10 else HV.straight_flush
			elif flush:
				hand_value = HV.flush
			elif straight:
				hand_value = HV.straight
			else:
				hand_value = HV.highcard
		self.hand_value = hand_value

	def __lt__(lhs, rhs):
		return lhs.hand_value > rhs.hand_value

	def __eq__(lhs, rhs):
		return lhs.hand_value == rhs.hand_value

	def __repr__(self):
		return self.hand
cardval = dict(T=10, J=11, Q=12, K=13, A=14)

class HV(IntEnum):
	highcard = auto()
	pair = auto()
	two_pairs = auto()
	three_ok_a_kind = auto()
	straight = auto()
	flush = auto()
	full_house = auto()
	four_of_a_kind = auto()
	straight_flush = auto()
	royal_flush = auto()
hv_table = {(2, 1, 1, 1): HV.pair, (2, 2, 1): HV.two_pairs, (3, 1, 1): HV.three_ok_a_kind, (3, 2): HV.full_house, (4, 1): HV.four_of_a_kind}


from typing import Tuple, List
from functools import total_ordering

@total_ordering
class PokerHand(object):

	def __init__(self, hand: str):
		self._hand = hand
		self._cards = self._hand_with_values()
		self._hand_value = self._total_hand_value()
		self._hand_type = self._hand_type()
		self._high_card = self._high_card()
		self._first_pair = 0
		self._second_pair = 0

	def compare_with(self, other: 'PokerHand') -> str:
		if self._hand_type > other._hand_type:
			return 'Win'
		elif self._hand_type < other._hand_type:
			return 'Loss'
		else:
			return self._tie_breaker(other, self._hand_type)

	def _tie_breaker(self, other: 'PokerHand', hand_type: int) -> str:
		if hand_type == 23:
			return 'Tie'
		elif hand_type in [22, 18]:
			return 'Win' if self._high_card > other._high_card else 'Loss' if self._high_card < other._high_card else 'Tie'
		elif hand_type in [21, 20]:
			if self._first_pair == other._first_pair:
				return 'Win' if self._hand_value > other._hand_value else 'Loss' if self._hand_value < other._hand_value else 'Tie'
			return 'Win' if self._first_pair > other._first_pair else 'Loss'
		elif hand_type in [17, 15]:
			if self._first_pair == other._first_pair:
				return self._compare_cards(other)
			return 'Win' if self._first_pair > other._first_pair else 'Loss'
		elif hand_type == 16:
			if self._first_pair == other._first_pair:
				if self._second_pair == other._second_pair:
					return 'Win' if self._hand_value > other._hand_value else 'Loss' if self._hand_value < other._hand_value else 'Tie'
				else:
					return 'Win' if self._second_pair > other._second_pair else 'Loss'
			return 'Win' if self._first_pair > other._first_pair else 'Loss'
		else:
			return self._compare_cards(other)

	def _compare_cards(self, other: 'PokerHand') -> str:
		for i in range(4, -1, -1):
			if self._cards[i][0] != other._cards[i][0]:
				return 'Win' if self._cards[i][0] > other._cards[i][0] else 'Loss'
		return 'Tie'

	def _hand_type(self) -> int:
		if self._is_flush() and (self._is_five_high_straight() or self._is_straight()):
			if self._hand_value == 60:
				return 23
			else:
				return 22
		elif self._is_flush():
			return 19
		elif self._is_five_high_straight() or self._is_straight():
			return 18
		elif self._is_same_kind():
			kind_type = self._is_same_kind()
			return 14 + kind_type
		else:
			return 14

	def _high_card(self) -> int:
		return self._cards[-1][0]

	def _total_hand_value(self) -> int:
		return sum((card[0] for card in self._cards))

	def _is_flush(self) -> bool:
		suit = self._cards[0][-1]
		return all(map(lambda card: suit in card, self._cards))

	def _is_five_high_straight(self) -> bool:
		if self._hand_value == 28:
			if self._cards[-1][0] == 14:
				ace_card = self._cards.pop()
				self._cards.insert(0, ace_card)
			return True
		return False

	def _is_straight(self) -> bool:
		for i in range(4):
			if self._cards[i + 1][0] - self._cards[i][0] != 1:
				return False
		return True

	def _is_same_kind(self) -> int:
		kind = val1 = val2 = 0
		for i in range(4):
			if self._cards[i][0] == self._cards[i + 1][0]:
				if not val1:
					val1 = self._cards[i][0]
					kind += 1
				elif val1 == self._cards[i][0]:
					kind += 2
				elif not val2:
					val2 = self._cards[i][0]
					kind += 1
				elif val2 == self._cards[i][0]:
					kind += 2
		kind = kind + 2 if kind in [4, 5] else kind
		self._first_pair = max(val1, val2)
		self._second_pair = min(val1, val2)
		return kind

	def _hand_with_values(self) -> List[Tuple[int, str]]:
		trans = {'T': '10', 'J': '11', 'Q': '12', 'K': '13', 'A': '14'}
		new_hand = self._hand.translate(str.maketrans(trans)).split()
		final_hand = [(int(card[:-1]), card[-1]) for card in new_hand]
		return sorted(final_hand)

	def __repr__(self):
		return self._hand

	def __str__(self):
		return self._hand

	def __lt__(self, other: 'PokerHand'):
		outcome = self.compare_with(other)
		return True if outcome == 'Win' else False

	def __eq__(self, other: 'PokerHand'):
		outcome = self.compare_with(other)
		return True if outcome == 'Tie' else False


from collections import Counter

class PokerHand(object):
	CARD = '123456789TJQKA'

	def __repr__(self):
		return self.hand

	def __init__(self, hand):
		self.hand = hand
		CARD = self.CARD
		values = ''.join(sorted(hand[::3], key=CARD.index))
		if all([c in values for c in 'A2345']):
			CARD = 'A' + self.CARD[1:-1]
			values = ''.join(sorted(hand[::3], key=CARD.index))
		is_flush = len(set(hand[1::3])) == 1
		counts = Counter(values)
		is_straight = values in CARD
		self.score = (2 * sum((counts[card] for card in values)) + 13 * is_straight + 15 * is_flush, sorted(((cnt, CARD.index(card)) for (card, cnt) in counts.most_common()))[::-1])

	def __lt__(self, other):
		return self.score > other.score


class PokerHand(object):
	VALUES = 'A23456789TJQKA'

	def __repr__(self):
		return self.hand

	def __init__(self, hand):
		self.hand = hand
		values = ''.join(sorted(hand[::3], key=self.VALUES[1:].index))
		suits = set(hand[1::3])
		hi_straight = values in self.VALUES
		lo_straight = values[-1] + values[:-1] in self.VALUES
		is_flush = len(suits) == 1
		self.score = (2 * sum((values.count(card) for card in values)) + 13 * (hi_straight or lo_straight) + 15 * is_flush, [self.VALUES[:-1].index(card) if lo_straight else self.VALUES[1:].index(card) for card in values[::-1]])

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