text: """
  Problem: https://projecteuler.net/problem=54
  
  In the card game poker, a hand consists of five cards and are ranked,
  from lowest to highest, in the following way:
  
  High Card: Highest value card.
  One Pair: Two cards of the same value.
  Two Pairs: Two different pairs.
  Three of a Kind: Three cards of the same value.
  Straight: All cards are consecutive values.
  Flush: All cards of the same suit.
  Full House: Three of a kind and a pair.
  Four of a Kind: Four cards of the same value.
  Straight Flush: All cards are consecutive values of same suit.
  Royal Flush: Ten, Jack, Queen, King, Ace, in same suit.
  
  The cards are valued in the order:
  2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King, Ace.
  
  If two players have the same ranked hands then the rank made up of the highest
  value wins; for example, a pair of eights beats a pair of fives.
  But if two ranks tie, for example, both players have a pair of queens, then highest
  cards in each hand are compared; if the highest cards tie then the next highest
  cards are compared, and so on.
  
  The file, poker.txt, contains one-thousand random hands dealt to two players.
  Each line of the file contains ten cards (separated by a single space): the
  first five are Player 1's cards and the last five are Player 2's cards.
  You can assume that all hands are valid (no invalid characters or repeated cards),
  each player's hand is in no specific order, and in each hand there is a clear winner.
  
  How many hands does Player 1 win?
  
  Resources used:
  https://en.wikipedia.org/wiki/Texas_hold_%27em
  https://en.wikipedia.org/wiki/List_of_poker_hands
  
  Similar problem on codewars:
  https://www.codewars.com/kata/ranking-poker-hands
  https://www.codewars.com/kata/sortable-poker-hands
  """
  from __future__ import annotations
  
  import os
  
  
  class PokerHand:
      """Create an object representing a Poker Hand based on an input of a
      string which represents the best 5 card combination from the player's hand
      and board cards.
  
      Attributes: (read-only)
          hand: string representing the hand consisting of five cards
  
      Methods:
          compare_with(opponent): takes in player's hand (self) and
              opponent's hand (opponent) and compares both hands according to
              the rules of Texas Hold'em.
              Returns one of 3 strings (Win, Loss, Tie) based on whether
              player's hand is better than opponent's hand.
  
          hand_name(): Returns a string made up of two parts: hand name
              and high card.
  
      Supported operators:
          Rich comparison operators: <, >, <=, >=, ==, !=
  
      Supported builtin methods and functions:
          list.sort(), sorted()
      """
  
      _HAND_NAME = [
          "High card",
          "One pair",
          "Two pairs",
          "Three of a kind",
          "Straight",
          "Flush",
          "Full house",
          "Four of a kind",
          "Straight flush",
          "Royal flush",
      ]
  
      _CARD_NAME = [
          "",  # placeholder as lists are zero indexed
          "One",
          "Two",
          "Three",
          "Four",
          "Five",
          "Six",
          "Seven",
          "Eight",
          "Nine",
          "Ten",
          "Jack",
          "Queen",
          "King",
          "Ace",
      ]
  
      def __init__(self, hand: str) -> None:
          """
          Initialize hand.
          Hand should of type str and should contain only five cards each
          separated by a space.
  
          The cards should be of the following format:
          [card value][card suit]
  
          The first character is the value of the card:
          2, 3, 4, 5, 6, 7, 8, 9, T(en), J(ack), Q(ueen), K(ing), A(ce)
  
          The second character represents the suit:
          S(pades), H(earts), D(iamonds), C(lubs)
  
          For example: "6S 4C KC AS TH"
          """
          if not isinstance(hand, str):
              raise TypeError(f"Hand should be of type 'str': {hand!r}")
          # split removes duplicate whitespaces so no need of strip
          if len(hand.split(" ")) != 5:
              raise ValueError(f"Hand should contain only 5 cards: {hand!r}")
          self._hand = hand
          self._first_pair = 0
          self._second_pair = 0
          self._card_values, self._card_suit = self._internal_state()
          self._hand_type = self._get_hand_type()
          self._high_card = self._card_values[0]
  
      @property
      def hand(self):
          """Returns the self hand"""
          return self._hand
  
      def compare_with(self, other: "PokerHand") -> str:
          """
          Determines the outcome of comparing self hand with other hand.
          Returns the output as 'Win', 'Loss', 'Tie' according to the rules of
          Texas Hold'em.
  
          Here are some examples:
          >>> player = PokerHand("2H 3H 4H 5H 6H")  # Stright flush
          >>> opponent = PokerHand("KS AS TS QS JS")  # Royal flush
          >>> player.compare_with(opponent)
          'Loss'
  
          >>> player = PokerHand("2S AH 2H AS AC")  # Full house
          >>> opponent = PokerHand("2H 3H 5H 6H 7H")  # Flush
          >>> player.compare_with(opponent)
          'Win'
  
          >>> player = PokerHand("2S AH 4H 5S 6C")  # High card
          >>> opponent = PokerHand("AD 4C 5H 6H 2C")  # High card
          >>> player.compare_with(opponent)
          'Tie'
          """
          # Breaking the tie works on the following order of precedence:
          # 1. First pair (default 0)
          # 2. Second pair (default 0)
          # 3. Compare all cards in reverse order because they are sorted.
  
          # First pair and second pair will only be a non-zero value if the card
          # type is either from the following:
          # 21: Four of a kind
          # 20: Full house
          # 17: Three of a kind
          # 16: Two pairs
          # 15: One pair
          if self._hand_type > other._hand_type:
              return "Win"
          elif self._hand_type < other._hand_type:
              return "Loss"
          elif self._first_pair == other._first_pair:
              if self._second_pair == other._second_pair:
                  return self._compare_cards(other)
              else:
                  return "Win" if self._second_pair > other._second_pair else "Loss"
          return "Win" if self._first_pair > other._first_pair else "Loss"
  
      # This function is not part of the problem, I did it just for fun
      def hand_name(self) -> str:
          """
          Return the name of the hand in the following format:
          'hand name, high card'
  
          Here are some examples:
          >>> PokerHand("KS AS TS QS JS").hand_name()
          'Royal flush'
  
          >>> PokerHand("2D 6D 3D 4D 5D").hand_name()
          'Straight flush, Six-high'
  
          >>> PokerHand("JC 6H JS JD JH").hand_name()
          'Four of a kind, Jacks'
  
          >>> PokerHand("3D 2H 3H 2C 2D").hand_name()
          'Full house, Twos over Threes'
  
          >>> PokerHand("2H 4D 3C AS 5S").hand_name()  # Low ace
          'Straight, Five-high'
  
          Source: https://en.wikipedia.org/wiki/List_of_poker_hands
          """
          name = PokerHand._HAND_NAME[self._hand_type - 14]
          high = PokerHand._CARD_NAME[self._high_card]
          pair1 = PokerHand._CARD_NAME[self._first_pair]
          pair2 = PokerHand._CARD_NAME[self._second_pair]
          if self._hand_type in [22, 19, 18]:
              return name + f", {high}-high"
          elif self._hand_type in [21, 17, 15]:
              return name + f", {pair1}s"
          elif self._hand_type in [20, 16]:
              join = "over" if self._hand_type == 20 else "and"
              return name + f", {pair1}s {join} {pair2}s"
          elif self._hand_type == 23:
              return name
          else:
              return name + f", {high}"
  
      def _compare_cards(self, other: "PokerHand") -> str:
          # Enumerate gives us the index as well as the element of a list
          for index, card_value in enumerate(self._card_values):
              if card_value != other._card_values[index]:
                  return "Win" if card_value > other._card_values[index] else "Loss"
          return "Tie"
  
      def _get_hand_type(self) -> int:
          # Number representing the type of hand internally:
          # 23: Royal flush
          # 22: Straight flush
          # 21: Four of a kind
          # 20: Full house
          # 19: Flush
          # 18: Straight
          # 17: Three of a kind
          # 16: Two pairs
          # 15: One pair
          # 14: High card
          if self._is_flush():
              if self._is_five_high_straight() or self._is_straight():
                  return 23 if sum(self._card_values) == 60 else 22
              return 19
          elif self._is_five_high_straight() or self._is_straight():
              return 18
          return 14 + self._is_same_kind()
  
      def _is_flush(self) -> bool:
          return len(self._card_suit) == 1
  
      def _is_five_high_straight(self) -> bool:
          # If a card is a five high straight (low ace) change the location of
          # ace from the start of the list to the end. Check whether the first
          # element is ace or not. (Don't want to change again)
          # Five high straight (low ace): AH 2H 3S 4C 5D
          # Why use sorted here? One call to this function will mutate the list to
          # [5, 4, 3, 2, 14] and so for subsequent calls (which will be rare) we
          # need to compare the sorted version.
          # Refer test_multiple_calls_five_high_straight in test_poker_hand.py
          if sorted(self._card_values) == [2, 3, 4, 5, 14]:
              if self._card_values[0] == 14:
                  # Remember, our list is sorted in reverse order
                  ace_card = self._card_values.pop(0)
                  self._card_values.append(ace_card)
              return True
          return False
  
      def _is_straight(self) -> bool:
          for i in range(4):
              if self._card_values[i] - self._card_values[i + 1] != 1:
                  return False
          return True
  
      def _is_same_kind(self) -> int:
          # Kind Values for internal use:
          # 7: Four of a kind
          # 6: Full house
          # 3: Three of a kind
          # 2: Two pairs
          # 1: One pair
          # 0: False
          kind = val1 = val2 = 0
          for i in range(4):
              # Compare two cards at a time, if they are same increase 'kind',
              # add the value of the card to val1, if it is repeating again we
              # will add 2 to 'kind' as there are now 3 cards with same value.
              # If we get card of different value than val1, we will do the same
              # thing with val2
              if self._card_values[i] == self._card_values[i + 1]:
                  if not val1:
                      val1 = self._card_values[i]
                      kind += 1
                  elif val1 == self._card_values[i]:
                      kind += 2
                  elif not val2:
                      val2 = self._card_values[i]
                      kind += 1
                  elif val2 == self._card_values[i]:
                      kind += 2
          # For consistency in hand type (look at note in _get_hand_type function)
          kind = kind + 2 if kind in [4, 5] else kind
          # first meaning first pair to compare in 'compare_with'
          first = max(val1, val2)
          second = min(val1, val2)
          # If it's full house (three count pair + two count pair), make sure
          # first pair is three count and if not then switch them both.
          if kind == 6 and self._card_values.count(first) != 3:
              first, second = second, first
          self._first_pair = first
          self._second_pair = second
          return kind
  
      def _internal_state(self) -> tuple[list[int], set[str]]:
          # Internal representation of hand as a list of card values and
          # a set of card suit
          trans: dict = {"T": "10", "J": "11", "Q": "12", "K": "13", "A": "14"}
          new_hand = self._hand.translate(str.maketrans(trans)).split()
          card_values = [int(card[:-1]) for card in new_hand]
          card_suit = {card[-1] for card in new_hand}
          return sorted(card_values, reverse=True), card_suit
  
      def __repr__(self):
          return f'{self.__class__}("{self._hand}")'
  
      def __str__(self):
          return self._hand
  
      # Rich comparison operators (used in list.sort() and sorted() builtin functions)
      # Note that this is not part of the problem but another extra feature where
      # if you have a list of PokerHand objects, you can sort them just through
      # the builtin functions.
      def __eq__(self, other):
          if isinstance(other, PokerHand):
              return self.compare_with(other) == "Tie"
          return NotImplemented
  
      def __lt__(self, other):
          if isinstance(other, PokerHand):
              return self.compare_with(other) == "Loss"
          return NotImplemented
  
      def __le__(self, other):
          if isinstance(other, PokerHand):
              return self < other or self == other
          return NotImplemented
  
      def __gt__(self, other):
          if isinstance(other, PokerHand):
              return not self < other and self != other
          return NotImplemented
  
      def __ge__(self, other):
          if isinstance(other, PokerHand):
              return not self < other
          return NotImplemented
  
      def __hash__(self):
          return object.__hash__(self)
  
  
  def solution() -> int:
      # Solution for problem number 54 from Project Euler
      # Input from poker_hands.txt file
      answer = 0
      script_dir = os.path.abspath(os.path.dirname(__file__))
      poker_hands = os.path.join(script_dir, "poker_hands.txt")
      with open(poker_hands) as file_hand:
          for line in file_hand:
              player_hand = line[:14].strip()
              opponent_hand = line[15:].strip()
              player, opponent = PokerHand(player_hand), PokerHand(opponent_hand)
              output = player.compare_with(opponent)
              if output == "Win":
                  answer += 1
      return answer
  
  
  if __name__ == "__main__":
      solution()
  