text: from collections import defaultdict
  import itertools
  import more_itertools
  from collections import Counter
  
  card_names = {1: "Ace", 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: "Jack", 12: "Queen", 13: "King"}
  
  
  def cut_calc_score(cut_card, players, dealer):
      scores = {}.fromkeys(players, 0)
      msg = ""
  
      if cut_card[0] == 11:
          scores[dealer] = 2
          msg = "Cut card is a Jack, Dealer scores 2pts. "
  
      return scores, msg
  
  
  def play_calc_score_set(played_cards, players):
      """Calculate the the scores made so far in the play.
  
      E.g.:
      scores, count, playlog = play_calc_score_set([(1, "S"), (3, "H"), (4, "S"), (4, "H"), (7, "S"), (6, "D"), (0, "GO"), (0, "GO"), (6, "C")],
       ["Abi", "Bob", "Charles"])
       
      returns 3 items: 
  
      scores = {"Abi": 2, "Bob": 0, "Charles": 4},
  
      count = 31,
  
      play_log = "Abi gets pair, Bob gets 0, and Charles gets 2nd pair and 31"
  
  
      Args:
          played_cards (list): The cards used in 'the play' part of the game, so far.
  
          players (list): A list of the players.
  
      Returns:
          dict, int, str: A dict of the scores, the total count so far, text log of the points played so far.
      """
  
      dupe_check(played_cards)
  
      scores = {}.fromkeys(players, 0)
  
      players_in_play = players.copy()
      play_log = []
      count = 0
  
      for index in range(1, len(played_cards)+1):
          cards = played_cards[0:index]
          go_played_by = None
  
          if cards[-1] == (0, "GO"):
              go_played_by = last_player(cards, players)
              players_in_play.remove(go_played_by)
  
          cards = remove_goes(cards)
  
          count, score, msg = play_score_ongoing(cards, index == len(played_cards))
  
          point_winner = last_player(cards, players_in_play)
          scores[point_winner] += score
  
          if score > 0:
              play_log.append(f"Count: {count}, {point_winner}: {msg}, score so far: {scores[point_winner]} ")
          else:
              play_log.append(f"Count: {count}, No Points scored, {go_played_by} said Go. ")
  
      return dict(scores), count, play_log
  
  
  def remove_goes(played_cards):
      cards = played_cards.copy()
      while (0, "GO") in cards:
          cards.remove((0, "GO"))
      return cards
  
  
  def play_score_ongoing(played_cards, last_card=False):
      
      dupe_check(played_cards)
  
      scores = []
      count = count_cards(played_cards)
      card_nums = get_card_numbers(played_cards)
  
      scores.append(play_score_15(count))
      scores.append(play_score_last_card(count, last_card))
      scores.append(play_score_runs(card_nums))
      scores.append(play_score_multiples(card_nums))
      return count, \
          sum([score[0] for score in scores]), \
          '|'.join([score_msg[1] for score_msg in scores if score_msg != (0, "")])
  
  
  def play_score_last_card(count, last_card):
      if count == 31:
          return 2, "31 (2pts)"
      elif last_card:
          return 1, "Last card (1pt)"
      else:
          return 0, ""
  
  
  def play_score_15(count):
  
      if count == 15:
          score = 2
          msg = f"{count} for 2pts"
      else:
          score = 0
          msg = ""
  
      return score, msg
  
  
  def play_score_runs(card_nums):
      for run_length in range(13, 2, -1):
          if len(card_nums) >= run_length:
              if are_consecutive(card_nums[-run_length: len(card_nums)]):
                  return run_length, f"Run of {run_length}, ({run_length}pts)"
      return 0, ""
  
  
  def play_score_multiples(card_nums):
      for run_length in range(4, 1, -1):
          if len(card_nums) >= 4:
              if card_nums[-1] == card_nums[-2] == card_nums[-3] == card_nums[-4]:
                  return 12, "4 of a kind (12pts)"
          if len(card_nums) >= 3:
              if card_nums[-1] == card_nums[-2] == card_nums[-3]:
                  return 6, "3 of a kind (6pts)"
          if len(card_nums) >= 2:
              if card_nums[-1] == card_nums[-2]:
                  return 2, "2 of a kind (2pts)"
          return 0, ""
  
  
  def are_consecutive(unordered_cards):
      return sorted(unordered_cards) == list(range(min(unordered_cards), max(unordered_cards) + 1))
  
  
  def count_cards(played_cards):
      card_nums = list(map(lambda x: x[0], played_cards))
      face_values = list(map(face_valuer, card_nums))
      return sum(face_values)
  
  
  def dupe_check(hand, starter=None):
      if starter is None:
          all_cards = hand
      else:
          all_cards = hand + [starter]
  
      all_cards = list(filter((0, "GO").__ne__ , all_cards))
  
      set_of_all_cards = set(all_cards)
      counter = Counter(all_cards)
  
      duplicates = []
      if len(all_cards) != len(set_of_all_cards):
          for card in set_of_all_cards:
              if counter[card] > 1:
                  duplicates.append(str(card[0]) + card[1])
  
      if len(duplicates) > 0:
          raise ValueError("Duplicate cards found: " + str(duplicates))
          
  
  def last_player(played_cards, players):
      """
      Return person who played the last card.
      
      E.g.:    
      last_player([(1, "S"), (2, "S")], ["Abi", "Bob"])
      
      returns: "Bob"
  
      Args:
          played_cards (list):
          players (list):
      
      Returns: 
          return (str): The players name
      """
      if len(played_cards) == 0:
          return None
      return players[(len(played_cards) - 1) % len(players)]
  
  
  def show_calc_score(starter, hand, crib=False):
      """
      Calculates the Score from the Show, for a given hand.
      
      E.g.:    
      cribbage_scorer.show_calc_score((6, "D"), [(6, "S"), (5, "S"), (4, "S"), (3, "S")])
      
      returns: "Bob"
  
      Args:
          starter (tuple):
          hand (list):
      
      Returns: 
          return (list): [score, description]
      """
      dupe_check(hand, starter)
  
      scores = []
  
      card_nums = sorted(get_card_numbers(hand, starter))
      hand_suits = get_card_suits(hand)
      starter_suit = get_card_suits([], starter)
  
      scores.append(score_multiples(card_nums))
      scores.append(score_fifteens(hand, starter))
      scores.append(score_runs(card_nums))
      scores.append(score_flushes(hand_suits, starter_suit, crib))
      scores.append(score_his_nobs(hand, starter))
  
      return sum([score[0] for score in scores]), '|'.join(
          [score_msg[1] for score_msg in scores if score_msg != (0, "")])
  
  
  def get_card_numbers(cards, starter=None):
      if starter is not None:
          all_cards = cards + [starter]
      else:
          all_cards = cards
      card_nums = [card[0] for card in all_cards]
      return card_nums.copy()
  
  
  def get_card_suits(hand, starter=None):
      if starter is None:
          suits = [card[1] for card in hand]
      else:
          suits = [card[1] for card in hand + [starter]]
      suits.sort()
      return suits.copy()
  
  
  def score_flushes(hand_suits, starter, crib):
      running_score = []
      running_msg = []
  
      multiples_list = count_multiples(hand_suits)
      if len(set(multiples_list)) == 1:
          if len(multiples_list) == 4 and starter[0] == hand_suits[0]:
              running_score.append(5)
              running_msg.append("Five card flush. (5pts)")
          elif len(multiples_list) == 4 and not crib:
              running_score.append(4)
              running_msg.append("Four card flush (4pts)")
      else:
          return 0, ''
  
      return sum(running_score), '|'.join(running_msg)
  
  
  def count_multiples(card_suit_or_rank):
      """Return a list of multiples.
      A multiple is 2 or more of any item in card_suit_or_rank.
  
      E.g.:
      ['C','H','S','S']
      returns:
      ['S','S']
  
      E.g.:
      ['H','H','S','S']
      returns:
      ['H','H','S','S']
  
      Args:
          card_suit_or_rank (list): card suits or ranks
  
      Returns:
          multiples_list (list): The items that occur only once have been remove.
  
      """
      multiples_list = []
      for key, group in itertools.groupby(card_suit_or_rank):
          group_list = list(group)
          if len(group_list) > 1:
              multiples_list += group_list
  
      return multiples_list
  
  
  def score_his_nobs(hand, starter):
      starter_suit = starter[1]
      for card in hand:
          if card == (11, starter_suit):
              return 1, "One for his nobs (1pt)"
  
      return 0, ""
  
  
  def score_runs(card_nums):
      running_score = []
      running_msg = []
      card_set = sorted(set(card_nums))
      for group in more_itertools.consecutive_groups(list(card_set)):
          group_list = list(group)
          group_length = len(group_list)
          if group_length >= 3:
              multiples_length = len(count_multiples(card_nums))
              if multiples_length > 0:
                  running_msg.append(f"Multi-run: {card_nameify(card_nums)} ({group_length * multiples_length}pts)")
                  running_score.append((group_length * multiples_length))
              else:
                  running_score.append(group_length)
                  running_msg.append(f"Run of {group_length} cards ({group_length}pts)")
  
      return sum(running_score), "|".join(running_msg)
  
  
  def face_valuer(raw_card_value):
      if raw_card_value <= 10:
          return raw_card_value
      else:
          return 10
  
  
  def card_nameify(card_nums):
      return list(map(lambda x: card_names[x] if x in card_names else x, card_nums))
  
  
  def score_fifteens(hand, starter):
      all_cards = hand + [starter]
  
      running_score = []
      score_msgs = []
  
      combinations_2_cards = itertools.combinations(all_cards, 2)
      for two_card_seq in combinations_2_cards:
          if face_valuer(two_card_seq[0][0]) + face_valuer(two_card_seq[1][0]) == 15:
              running_score.append(2)
              score_msgs.append(f"Made 15 from {card_nameify(two_card_seq)} (2pts)")
  
      combinations_3_cards = itertools.combinations(all_cards, 3)
      for three_card_seq in combinations_3_cards:
          if face_valuer(three_card_seq[0][0]) + \
                  face_valuer(three_card_seq[1][0]) + \
                  face_valuer(three_card_seq[2][0]) == 15:
              running_score.append(2)
              score_msgs.append(f"Made 15 from {card_nameify(three_card_seq)} (2pts)")
  
      combinations_4_cards = itertools.combinations(all_cards, 4)
      for four_card_seq in combinations_4_cards:
          if face_valuer(four_card_seq[0][0]) + \
                  face_valuer(four_card_seq[1][0]) + \
                  face_valuer(four_card_seq[2][0]) + \
                  face_valuer(four_card_seq[3][0]) == 15:
              running_score.append(2)
              score_msgs.append(f"Made 15 from {card_nameify(four_card_seq)} (2pts)")
  
      combinations_5_cards = itertools.combinations(all_cards, 5)
      for five_card_seq in combinations_5_cards:
          if face_valuer(five_card_seq[0][0]) + \
                  face_valuer(five_card_seq[1][0]) + \
                  face_valuer(five_card_seq[2][0]) + \
                  face_valuer(five_card_seq[3][0]) + \
                  face_valuer(five_card_seq[4][0]) == 15:
              running_score.append(2)
              score_msgs.append(f"Made 15 from {card_nameify(five_card_seq)} (2pts)")
  
      return sum(running_score), "|".join(score_msgs)
  
  
  def score_multiples(card_nums):
      multiples_counter = defaultdict(lambda: 0)
      for card in card_nums:
          multiples_counter[card] += 1
  
      multiples_found = {}
      for card_name, count in multiples_counter.items():
          if count > 1:
              multiples_found[card_name] = count
  
      score_total = []
      score_msgs = []
      for card, count in multiples_found.items():
          if count == 2:
              score_total.append(2)
              score_msgs.append(f"Double {card_nameify([card])[0]}s (2pts)")
          elif count == 3:
              score_total.append(6)
              score_msgs.append(f"Three of a kind: {card_nameify([card])[0]}s (6pts)")
          elif count == 4:
              score_total.append(12)
              score_msgs.append(f"Four of a kind: {card_nameify([card])[0]}s (12pts)")
          else:
              raise Exception(f"An illegal multiple  of {count} was found in hand: {card_nums}")
  
      return sum(score_total), "|".join(score_msgs)
  