3
\$\begingroup\$

I saw this task in the 101 documentation for the Raku programming language and wanted to implement it in Python. The code is quite long, but I tried to dot the i's and cross the t's with regards to method separation, validation, documentation, and type hinting.

Suppose that you host a table tennis tournament. The referees tell you the results of each game in the format Player1 Player2 | 3:2, which means that Player1 won against Player2 by 3 to 2 sets. You need a script that sums up how many matches and sets each player has won to determine the overall winner.

from dataclasses import dataclass
from collections import defaultdict
from typing import Tuple

score_text = """Beth Ana Charlie Dave
Ana Dave | 3:0
Charlie Beth | 3:1
Ana Beth | 2:3
Dave Charlie | 3:0
Ana Charlie | 3:1
Beth Dave | 0:3
"""

@dataclass(order=True)
class ScoreboardEntry():
  """
  Represents a player's tournament performance
  
  Attributes:
    matches (int): Number of matches won
    sets (int): Total sets won
  """
  
  matches: int = 0
  sets: int = 0
  
  def __iter__(self):
    """ Allow tuple unpacking of the ScoreboardEntry data class"""
    yield self.matches
    yield self.sets
  
  
medals = ["🥇", "🥈", "🥉"]
  
def parse_match(match_data: str) -> Tuple[str, str, int, int]:
  """
  Parse match data into its components
  
  Args:
    match_data (str): A string in the format "Player1 Player2 | score1:score2"
    
  Returns:
    Tuple[str, str, int, int]: Player1, Player2, score1, score2
  """
  
  try:
    players, scores = match_data.split("|")
    p1, p2 = players.split()
    s1, s2 = map(int, scores.split(":"))
  except ValueError:
    raise ValueError(f"Invalid match data: {match_data}. Must be \"str str | int:int\"")
  
  return p1, p2, s1, s2

  
def update_scoreboard(
  scoreboard: dict[str, ScoreboardEntry], 
  player1: str, 
  player2: str, 
  sets_player1: int, 
  sets_player2: int) -> None:
  """
  Update the scoreboard based on the results of a match
  
  Args:
    scoreboard (dict[str, ScoreboardEntry]): The current tournament scoreboard
    player1 (str): Name of the first player
    player2 (str): Name of the second player
    sets_player1 (int): Sets won by the first player
    sets_player2 (int): Sets won by the second player
  
  """
  
  winner = player1 if sets_player1 > sets_player2 else player2
  
  scoreboard[player1].sets += sets_player1
  scoreboard[player2].sets += sets_player2
  scoreboard[winner].matches += 1


def print_scoreboard(scoreboard: dict[str, ScoreboardEntry]) -> None:
  """
  Print the ranked scoreboard of a tournament
  
  Args:
    scoreboard (dict[str, ScoreboardEntry]): The scoreboard to print
  """
  
  sorted_scoreboard = sorted(scoreboard.items(), key=lambda i: i[1], reverse=True)

  for index, (player, entry) in enumerate(sorted_scoreboard):
    medal = medals[index] if index < len(medals) else ""
    matches, sets = entry
    
    print(f"{index + 1}. {medal + ' ' if medal else ''}{player}. Matches won: {matches}, sets won: {sets}")

  
def determine_winner(scoreboard: dict[str, ScoreboardEntry]) -> str:
  """Determine the overall winner of a tournament based on the scoreboard
  
  Args:
    scoreboard (dict[str, ScoreboardEntry]): The current tournament scoreboard
  
  Returns:
    str: The name of the tournament winner
  """
  
  return max(scoreboard, key=scoreboard.get)


def main(data: str) -> None:
  """
  Process tournament data to determine and announce winner
  
  Args:
    data (str): tournament data as a multiline string
  """
  
  scoreboard = defaultdict(ScoreboardEntry)
  
  lines = data.splitlines()
  
  for line in lines[1:]: # Skip first line, we don't need the explicit names
    try:
      p1, p2, s1, s2 = parse_match(line)
      update_scoreboard(scoreboard, p1, p2, s1, s2)
    except ValueError as err:
      print(err)
    
  print_scoreboard(scoreboard)
  
  print()
  
  tournament_winner = determine_winner(scoreboard)
  print(f"{tournament_winner} won the tournament🏆. Congratulations🎉!")


if __name__ == "__main__":
  main(score_text)
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

It looks quite good. A few small things:

Since in parse_match you don't plan to handle the ValueError except by raising a more detailed exception, I'd probably put the return within the try block rather than after it.

def parse_match(match_data: str) -> Tuple[str, str, int, int]:
  """
  Parse match data into its components
  
  Args:
    match_data (str): A string in the format "Player1 Player2 | score1:score2"
    
  Returns:
    Tuple[str, str, int, int]: Player1, Player2, score1, score2
  """
  
  try:
    players, scores = match_data.split("|")
    p1, p2 = players.split()
    s1, s2 = map(int, scores.split(":"))
    return p1, p2, s1, s2
  except ValueError:
    raise ValueError(f"Invalid match data: {match_data}. Must be \"str str | int:int\"")

More consistent with your other key arguments, there's also the opportunity to replace lambda i: i[1] with operator.itemgetter(1).

\$\endgroup\$
2
\$\begingroup\$

Input validation

This assumes sets_player1 can not be equal to sets_player2:

winner = player1 if sets_player1 > sets_player2 else player2

It is not clear to me if your code checks for equality. If it does the check, I suggest adding a comment somewhere to make it more obvious. Otherwise, I suggest adding a check.

Documentation

It is great that your functions have helpful docstrings, but since your code uses type hints, I think you can omit the variable types from the docs. For example, in the following, I see no strong need to mention (int):

  matches (int): Number of matches won
  sets (int): Total sets won
"""

matches: int = 0
sets: int = 0

The docstring would just be:

matches : Number of matches won
sets    : Total sets won

Indentation

Your indentation is well done and consistent. I recommend 4 spaces per level instead of 2 because I think it makes the code a little easier to read.

\$\endgroup\$
2
  • \$\begingroup\$ Interesting suggestion to skip the types in the docstrings, due to type annotations. I didn't think of that. Do they not cater to a bit different audiences, though? Annotations more for automated checks, docstrings for human readers? \$\endgroup\$
    – TomG
    Commented Nov 28 at 17:00
  • 2
    \$\begingroup\$ @TomG: It was just a suggestion. It seems redundant to me, but if you have a documentation policy that recommends the types, that's fine too. \$\endgroup\$
    – toolic
    Commented Nov 28 at 17:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.