Alley Class

Represents a bowling alley where games are played, including details about the lane conditions.

Attributes:
  • name (str) –

    The name of the alley.

  • location (str) –

    The geographic location of the alley.

  • lane_type (str) –

    The type of lane surface, such as 'Wood' or 'Synthetic'.

Source code in src/virtual_lanes/alley.py
class Alley:
    """
    Represents a bowling alley where games are played, including details about the lane conditions.

    Attributes:
        name (str): The name of the alley.
        location (str): The geographic location of the alley.
        lane_type (str): The type of lane surface, such as 'Wood' or 'Synthetic'.
    """

    VALID_LANE_TYPES = {'wood', 'synthetic'}

    def __init__(self, name: str, location: str, lane_type: str) -> None:
        """
        Initialize a new Alley instance.

        Parameters:
            name (str): The name of the alley.
            location (str): The geographic location of the alley.
            lane_type (str): The type of lane surface, indicating the material of the bowling lane.

        Raises:
            ValueError: If lane_type is not 'Wood' or 'Synthetic'.
        """
        self.name = name
        self.location = location
        if lane_type.lower() not in self.VALID_LANE_TYPES:
            raise ValueError(f"Invalid lane type '{lane_type}'. Valid types are: 'Wood', 'Synthetic'")
        self.lane_type = lane_type.capitalize()

    def __str__(self) -> str:
        """
        Return a string representation of the Alley instance, which is helpful for debugging and logging.

        Returns:
            str: A string that represents this Alley.
        """
        return f"{self.name} - {self.location} ({self.lane_type})"

__init__(name, location, lane_type)

Initialize a new Alley instance.

Parameters:
  • name (str) –

    The name of the alley.

  • location (str) –

    The geographic location of the alley.

  • lane_type (str) –

    The type of lane surface, indicating the material of the bowling lane.

Raises:
  • ValueError

    If lane_type is not 'Wood' or 'Synthetic'.

Source code in src/virtual_lanes/alley.py
def __init__(self, name: str, location: str, lane_type: str) -> None:
    """
    Initialize a new Alley instance.

    Parameters:
        name (str): The name of the alley.
        location (str): The geographic location of the alley.
        lane_type (str): The type of lane surface, indicating the material of the bowling lane.

    Raises:
        ValueError: If lane_type is not 'Wood' or 'Synthetic'.
    """
    self.name = name
    self.location = location
    if lane_type.lower() not in self.VALID_LANE_TYPES:
        raise ValueError(f"Invalid lane type '{lane_type}'. Valid types are: 'Wood', 'Synthetic'")
    self.lane_type = lane_type.capitalize()

__str__()

Return a string representation of the Alley instance, which is helpful for debugging and logging.

Returns:
  • str( str ) –

    A string that represents this Alley.

Source code in src/virtual_lanes/alley.py
def __str__(self) -> str:
    """
    Return a string representation of the Alley instance, which is helpful for debugging and logging.

    Returns:
        str: A string that represents this Alley.
    """
    return f"{self.name} - {self.location} ({self.lane_type})"

Bowler Class

Represents a bowler in a bowling simulation, including probabilities for striking and sparing, as well as personal characteristics like handedness and bowling technique.

Attributes:
  • name (str) –

    The name of the bowler.

  • strike_prob (float) –

    Probability of hitting a strike.

  • spare_prob (float) –

    Probability of hitting a spare.

  • handedness (str) –

    The preferred hand of the bowler, either 'left' or 'right'.

  • technique (str) –

    The bowling technique used, either 'single' or 'double' handed.

Source code in src/virtual_lanes/bowler.py
class Bowler:
    """
    Represents a bowler in a bowling simulation, including probabilities for striking and sparing,
    as well as personal characteristics like handedness and bowling technique.

    Attributes:
        name (str): The name of the bowler.
        strike_prob (float): Probability of hitting a strike.
        spare_prob (float): Probability of hitting a spare.
        handedness (str): The preferred hand of the bowler, either 'left' or 'right'.
        technique (str): The bowling technique used, either 'single' or 'double' handed.
    """

    def __init__(self, name: str, strike_prob: float, spare_prob: float,
                 handedness: str = 'right', technique: str = 'single') -> None:
        """
        Initializes a new instance of Bowler.

        Parameters:
            name (str): The name of the bowler.
            strike_prob (float): Probability of hitting a strike, between 0 and 1.
            spare_prob (float): Probability of hitting a spare, between 0 and 1.
            handedness (str): The preferred hand of the bowler, either 'left' or 'right' (default 'right').
            technique (str): The bowling technique used by the bowler, either 'single' or 'double' handed (default 'single').

        Raises:
            ValueError: If strike_prob or spare_prob is greater than 1.
        """
        if not (0 <= strike_prob <= 1):
            raise ValueError(f"Invalid strike probability {strike_prob}. Must be between 0 and 1.")
        if not (0 <= spare_prob <= 1):
            raise ValueError(f"Invalid spare probability {spare_prob}. Must be between 0 and 1.")
        if handedness not in ['left', 'right']:
            raise ValueError(f"Invalid handedness '{handedness}'. Must be 'left' or 'right'.")
        if technique not in ['single', 'double']:
            raise ValueError(f"Invalid technique '{technique}'. Must be 'single' or 'double'.")

        self.name = name
        self.strike_prob = strike_prob
        self.spare_prob = spare_prob
        self.handedness = handedness
        self.technique = technique

    def __str__(self) -> str:
        """
        Returns a string representation of the Bowler instance.

        Returns:
            str: A string that includes all the attributes of the Bowler.
        """
        return (f"Bowler(Name: {self.name}, Strike Probability: {self.strike_prob}, "
                f"Spare Probability: {self.spare_prob}, Handedness: {self.handedness}, "
                f"Technique: {self.technique})")

__init__(name, strike_prob, spare_prob, handedness='right', technique='single')

Initializes a new instance of Bowler.

Parameters:
  • name (str) –

    The name of the bowler.

  • strike_prob (float) –

    Probability of hitting a strike, between 0 and 1.

  • spare_prob (float) –

    Probability of hitting a spare, between 0 and 1.

  • handedness (str, default: 'right' ) –

    The preferred hand of the bowler, either 'left' or 'right' (default 'right').

  • technique (str, default: 'single' ) –

    The bowling technique used by the bowler, either 'single' or 'double' handed (default 'single').

Raises:
  • ValueError

    If strike_prob or spare_prob is greater than 1.

Source code in src/virtual_lanes/bowler.py
def __init__(self, name: str, strike_prob: float, spare_prob: float,
             handedness: str = 'right', technique: str = 'single') -> None:
    """
    Initializes a new instance of Bowler.

    Parameters:
        name (str): The name of the bowler.
        strike_prob (float): Probability of hitting a strike, between 0 and 1.
        spare_prob (float): Probability of hitting a spare, between 0 and 1.
        handedness (str): The preferred hand of the bowler, either 'left' or 'right' (default 'right').
        technique (str): The bowling technique used by the bowler, either 'single' or 'double' handed (default 'single').

    Raises:
        ValueError: If strike_prob or spare_prob is greater than 1.
    """
    if not (0 <= strike_prob <= 1):
        raise ValueError(f"Invalid strike probability {strike_prob}. Must be between 0 and 1.")
    if not (0 <= spare_prob <= 1):
        raise ValueError(f"Invalid spare probability {spare_prob}. Must be between 0 and 1.")
    if handedness not in ['left', 'right']:
        raise ValueError(f"Invalid handedness '{handedness}'. Must be 'left' or 'right'.")
    if technique not in ['single', 'double']:
        raise ValueError(f"Invalid technique '{technique}'. Must be 'single' or 'double'.")

    self.name = name
    self.strike_prob = strike_prob
    self.spare_prob = spare_prob
    self.handedness = handedness
    self.technique = technique

__str__()

Returns a string representation of the Bowler instance.

Returns:
  • str( str ) –

    A string that includes all the attributes of the Bowler.

Source code in src/virtual_lanes/bowler.py
def __str__(self) -> str:
    """
    Returns a string representation of the Bowler instance.

    Returns:
        str: A string that includes all the attributes of the Bowler.
    """
    return (f"Bowler(Name: {self.name}, Strike Probability: {self.strike_prob}, "
            f"Spare Probability: {self.spare_prob}, Handedness: {self.handedness}, "
            f"Technique: {self.technique})")

Scoring Class

Source code in src/virtual_lanes/scoring.py
class Scoring:
    @staticmethod
    def traditional(frames: list[tuple[int, ...]]) -> int:
        """
        Calculate the traditional bowling score from a list of frames.

        Each frame should be represented by a tuple indicating the number of pins knocked down in each roll.

        Parameters:
            frames (list): A list of tuples representing the game frames.

        Returns:
            int: The total score calculated based on traditional bowling rules.
        """
        score = 0

        for i in range(10):
            frame = frames[i]

            # Strike
            if frame[0] == 10:
                score += 10
                if i < 9:
                    next_frame = frames[i + 1]
                    if next_frame[0] == 10:
                        score += 10
                        if i + 1 < 9:
                            score += frames[i + 2][0]
                        else:
                            score += next_frame[1]
                    else:
                        score += next_frame[0] + next_frame[1]
                else:
                    score += frame[1] + frame[2]

            # Spare
            elif sum(frame) == 10:
                score += 10
                if i < 9:
                    score += frames[i + 1][0]
                else:
                    score += frame[2]

            # Open frame
            else:
                score += sum(frame)

        return score

    @staticmethod
    def current_frame(frames: list[tuple[int, ...]]) -> int:
        """
        Calculate the score using current frame scoring rules, also known as World Bowling scoring.

        Parameters:
            frames (list): A list of tuples representing the game frames.

        Returns:
            int: The total score calculated based on current frame (World Bowling) rules.
        """
        score = 0
        for frame in frames:
            if frame[0] == 10:  # Strike
                score += 30
            elif sum(frame) == 10:  # Spare
                score += 10 + frame[0]
            else:
                score += sum(frame)
        return score

    @staticmethod
    def nine_pin_no_tap(frames: list[tuple[int, ...]]) -> int:
        """
        Calculate the score for a 9-pin no-tap game, where knocking down 9 pins counts as a strike.

        Parameters:
            frames (list): A list of tuples representing the game frames.

        Returns:
            int: The total score calculated based on 9-pin no-tap rules.
        """
        score = 0
        for i, frame in enumerate(frames):
            first_roll = frame[0]
            if first_roll == 9 or first_roll == 10:
                if i < 9:  # Not the last frame
                    next_frame = frames[i + 1]
                    score += 10 + next_frame[0] + (next_frame[1] if len(next_frame) > 1 else 0)
                else:  # Last frame
                    score += 10 + frame[1] + frame[2]
            elif sum(frame[:2]) == 10:  # Spare
                score += 10 + (frames[i + 1][0] if i < 9 else frame[2])
            else:
                score += sum(frame)
        return score

current_frame(frames) staticmethod

Calculate the score using current frame scoring rules, also known as World Bowling scoring.

Parameters:
  • frames (list) –

    A list of tuples representing the game frames.

Returns:
  • int( int ) –

    The total score calculated based on current frame (World Bowling) rules.

Source code in src/virtual_lanes/scoring.py
@staticmethod
def current_frame(frames: list[tuple[int, ...]]) -> int:
    """
    Calculate the score using current frame scoring rules, also known as World Bowling scoring.

    Parameters:
        frames (list): A list of tuples representing the game frames.

    Returns:
        int: The total score calculated based on current frame (World Bowling) rules.
    """
    score = 0
    for frame in frames:
        if frame[0] == 10:  # Strike
            score += 30
        elif sum(frame) == 10:  # Spare
            score += 10 + frame[0]
        else:
            score += sum(frame)
    return score

nine_pin_no_tap(frames) staticmethod

Calculate the score for a 9-pin no-tap game, where knocking down 9 pins counts as a strike.

Parameters:
  • frames (list) –

    A list of tuples representing the game frames.

Returns:
  • int( int ) –

    The total score calculated based on 9-pin no-tap rules.

Source code in src/virtual_lanes/scoring.py
@staticmethod
def nine_pin_no_tap(frames: list[tuple[int, ...]]) -> int:
    """
    Calculate the score for a 9-pin no-tap game, where knocking down 9 pins counts as a strike.

    Parameters:
        frames (list): A list of tuples representing the game frames.

    Returns:
        int: The total score calculated based on 9-pin no-tap rules.
    """
    score = 0
    for i, frame in enumerate(frames):
        first_roll = frame[0]
        if first_roll == 9 or first_roll == 10:
            if i < 9:  # Not the last frame
                next_frame = frames[i + 1]
                score += 10 + next_frame[0] + (next_frame[1] if len(next_frame) > 1 else 0)
            else:  # Last frame
                score += 10 + frame[1] + frame[2]
        elif sum(frame[:2]) == 10:  # Spare
            score += 10 + (frames[i + 1][0] if i < 9 else frame[2])
        else:
            score += sum(frame)
    return score

traditional(frames) staticmethod

Calculate the traditional bowling score from a list of frames.

Each frame should be represented by a tuple indicating the number of pins knocked down in each roll.

Parameters:
  • frames (list) –

    A list of tuples representing the game frames.

Returns:
  • int( int ) –

    The total score calculated based on traditional bowling rules.

Source code in src/virtual_lanes/scoring.py
@staticmethod
def traditional(frames: list[tuple[int, ...]]) -> int:
    """
    Calculate the traditional bowling score from a list of frames.

    Each frame should be represented by a tuple indicating the number of pins knocked down in each roll.

    Parameters:
        frames (list): A list of tuples representing the game frames.

    Returns:
        int: The total score calculated based on traditional bowling rules.
    """
    score = 0

    for i in range(10):
        frame = frames[i]

        # Strike
        if frame[0] == 10:
            score += 10
            if i < 9:
                next_frame = frames[i + 1]
                if next_frame[0] == 10:
                    score += 10
                    if i + 1 < 9:
                        score += frames[i + 2][0]
                    else:
                        score += next_frame[1]
                else:
                    score += next_frame[0] + next_frame[1]
            else:
                score += frame[1] + frame[2]

        # Spare
        elif sum(frame) == 10:
            score += 10
            if i < 9:
                score += frames[i + 1][0]
            else:
                score += frame[2]

        # Open frame
        else:
            score += sum(frame)

    return score

Game Class

Manages the simulation of a bowling game, providing detailed frame-by-frame results for each bowler.

This class supports simulations on specified alleys with distinct characteristics, influencing the gameplay of the bowlers.

Attributes:
  • bowlers (List[Bowler]) –

    A list of Bowler objects participating in the game.

  • alley (Alley) –

    The Alley object specifying the lane type and oil pattern where the game is played.

  • random_seed (Optional[int]) –

    Seed for the random number generator to ensure reproducibility, if provided.

Source code in src/virtual_lanes/game.py
class Game:
    """
    Manages the simulation of a bowling game, providing detailed frame-by-frame results for each bowler.

    This class supports simulations on specified alleys with distinct characteristics, influencing the gameplay of the bowlers.

    Attributes:
        bowlers (List[Bowler]): A list of `Bowler` objects participating in the game.
        alley (Alley): The `Alley` object specifying the lane type and oil pattern where the game is played.
        random_seed (Optional[int]): Seed for the random number generator to ensure reproducibility, if provided.
    """
    def __init__(self, bowlers: list[Bowler], alley: Alley, random_seed: int | None = None) -> None:
        """
        Initialises a game with a list of bowlers and the alley where the game is played.

        Parameters:
            bowlers (list[Bowler]): List of Bowler objects participating in the game.
            alley (Alley): The Alley object specifying the lane type and oil pattern.
            random_seed (int, optional): Random seed for reproducibility of the simulation.
        """
        self.bowlers = bowlers
        self.alley = alley
        self.random_seed = random_seed
        # A dedicated generator keeps simulations reproducible and isolated from
        # the global NumPy RNG (which is shared process-wide and not thread-safe).
        self.rng = np.random.default_rng(random_seed)

    def simulate_frame(self, bowler: Bowler, frame_number: int) -> tuple[int, ...]:
        """
        Simulates a single frame for a given bowler based on the frame number.

        Parameters:
            bowler (Bowler): The Bowler object for whom the frame is simulated.
            frame_number (int): The frame number (0-indexed, 0-9).

        Returns:
            Tuple[int, ...]: A tuple representing the result of the frame (pins knocked down in each roll).
        """
        if frame_number < 9:
            return self.simulate_regular_frame(bowler)
        else:
            return self.simulate_last_frame(bowler)

    def simulate_regular_frame(self, bowler: Bowler) -> tuple[int, int]:
        """
        Simulates a regular frame (not the last one), accounting for strikes, spares and open frames.

        The first ball is a strike with probability ``bowler.strike_prob``. Otherwise, if pins
        remain the bowler converts the spare with probability ``bowler.spare_prob``; failing that
        the second ball leaves an open frame.

        Parameters:
            bowler (Bowler): The Bowler object for whom the frame is simulated.

        Returns:
            tuple[int, int]: A tuple of two integers representing the pins knocked down in each roll.
        """
        if self.rng.random() < bowler.strike_prob:
            return (10, 0)
        first_roll = int(self.rng.integers(0, 10))  # 0-9; a 10 would be a strike
        remaining = 10 - first_roll
        # Convert the spare with probability spare_prob, otherwise leave an open frame.
        converts_spare = self.rng.random() < bowler.spare_prob
        second_roll = remaining if converts_spare else int(self.rng.integers(0, remaining))
        return (first_roll, second_roll)

    def simulate_last_frame(self, bowler: Bowler) -> tuple[int, ...]:
        """
        Simulates the 10th frame, which may include up to three rolls depending on the bowler's performance.

        Parameters:
            bowler (Bowler): The Bowler object for whom the last frame is simulated.

        Returns:
            tuple[int, ...]: A tuple of up to three integers representing the pins knocked down in each roll.
        """
        rolls: list[int] = []

        # First roll
        if self.rng.random() < bowler.strike_prob:
            rolls.append(10)
        else:
            rolls.append(int(self.rng.integers(0, 10)))

        # Second roll
        if rolls[0] == 10:  # First roll was a strike: fresh rack
            if self.rng.random() < bowler.strike_prob:
                rolls.append(10)
            else:
                rolls.append(int(self.rng.integers(0, 11)))
        else:  # Pins remain: attempt the spare
            remaining = 10 - rolls[0]
            if self.rng.random() < bowler.spare_prob:
                rolls.append(remaining)
            else:
                rolls.append(int(self.rng.integers(0, remaining)))

        # Third roll only earned by a strike or spare in the first two rolls (fresh rack)
        if sum(rolls[:2]) >= 10:
            if self.rng.random() < bowler.strike_prob:
                rolls.append(10)
            else:
                rolls.append(int(self.rng.integers(0, 11)))

        return tuple(rolls[:3])  # Ensure only up to three rolls are returned

    def frame_by_frame_generator(self) -> Iterator[dict[str, tuple[int, ...]]]:
        """
        A generator to simulate the game frame-by-frame, yielding results for each frame for all bowlers.

        Yields:
            Iterator[dict[str, tuple[int, ...]]]: An iterator that yields a dictionary representing the frame results of each bowler.
        """
        for frame_number in range(10):
            frame_results = {}
            for bowler in self.bowlers:
                frame_results[bowler.name] = self.simulate_frame(bowler, frame_number)
            yield frame_results

    def simulate_game(self) -> dict[str, list[tuple[int, ...]]]:
        """
        Simulates a complete game for all bowlers, returning the frame-by-frame results.

        Returns:
            dict[str, list[tuple[int, ...]]]: A dictionary where keys are bowler names and values are lists of tuples, each tuple representing a frame.
        """
        results: dict[str, list[tuple[int, ...]]] = {bowler.name: [] for bowler in self.bowlers}
        for frame_results in self.frame_by_frame_generator():
            for name, frame in frame_results.items():
                results[name].append(frame)
        return results

__init__(bowlers, alley, random_seed=None)

Initialises a game with a list of bowlers and the alley where the game is played.

Parameters:
  • bowlers (list[Bowler]) –

    List of Bowler objects participating in the game.

  • alley (Alley) –

    The Alley object specifying the lane type and oil pattern.

  • random_seed (int, default: None ) –

    Random seed for reproducibility of the simulation.

Source code in src/virtual_lanes/game.py
def __init__(self, bowlers: list[Bowler], alley: Alley, random_seed: int | None = None) -> None:
    """
    Initialises a game with a list of bowlers and the alley where the game is played.

    Parameters:
        bowlers (list[Bowler]): List of Bowler objects participating in the game.
        alley (Alley): The Alley object specifying the lane type and oil pattern.
        random_seed (int, optional): Random seed for reproducibility of the simulation.
    """
    self.bowlers = bowlers
    self.alley = alley
    self.random_seed = random_seed
    # A dedicated generator keeps simulations reproducible and isolated from
    # the global NumPy RNG (which is shared process-wide and not thread-safe).
    self.rng = np.random.default_rng(random_seed)

frame_by_frame_generator()

A generator to simulate the game frame-by-frame, yielding results for each frame for all bowlers.

Yields:
  • dict[str, tuple[int, ...]]

    Iterator[dict[str, tuple[int, ...]]]: An iterator that yields a dictionary representing the frame results of each bowler.

Source code in src/virtual_lanes/game.py
def frame_by_frame_generator(self) -> Iterator[dict[str, tuple[int, ...]]]:
    """
    A generator to simulate the game frame-by-frame, yielding results for each frame for all bowlers.

    Yields:
        Iterator[dict[str, tuple[int, ...]]]: An iterator that yields a dictionary representing the frame results of each bowler.
    """
    for frame_number in range(10):
        frame_results = {}
        for bowler in self.bowlers:
            frame_results[bowler.name] = self.simulate_frame(bowler, frame_number)
        yield frame_results

simulate_frame(bowler, frame_number)

Simulates a single frame for a given bowler based on the frame number.

Parameters:
  • bowler (Bowler) –

    The Bowler object for whom the frame is simulated.

  • frame_number (int) –

    The frame number (0-indexed, 0-9).

Returns:
  • tuple[int, ...]

    Tuple[int, ...]: A tuple representing the result of the frame (pins knocked down in each roll).

Source code in src/virtual_lanes/game.py
def simulate_frame(self, bowler: Bowler, frame_number: int) -> tuple[int, ...]:
    """
    Simulates a single frame for a given bowler based on the frame number.

    Parameters:
        bowler (Bowler): The Bowler object for whom the frame is simulated.
        frame_number (int): The frame number (0-indexed, 0-9).

    Returns:
        Tuple[int, ...]: A tuple representing the result of the frame (pins knocked down in each roll).
    """
    if frame_number < 9:
        return self.simulate_regular_frame(bowler)
    else:
        return self.simulate_last_frame(bowler)

simulate_game()

Simulates a complete game for all bowlers, returning the frame-by-frame results.

Returns:
  • dict[str, list[tuple[int, ...]]]

    dict[str, list[tuple[int, ...]]]: A dictionary where keys are bowler names and values are lists of tuples, each tuple representing a frame.

Source code in src/virtual_lanes/game.py
def simulate_game(self) -> dict[str, list[tuple[int, ...]]]:
    """
    Simulates a complete game for all bowlers, returning the frame-by-frame results.

    Returns:
        dict[str, list[tuple[int, ...]]]: A dictionary where keys are bowler names and values are lists of tuples, each tuple representing a frame.
    """
    results: dict[str, list[tuple[int, ...]]] = {bowler.name: [] for bowler in self.bowlers}
    for frame_results in self.frame_by_frame_generator():
        for name, frame in frame_results.items():
            results[name].append(frame)
    return results

simulate_last_frame(bowler)

Simulates the 10th frame, which may include up to three rolls depending on the bowler's performance.

Parameters:
  • bowler (Bowler) –

    The Bowler object for whom the last frame is simulated.

Returns:
  • tuple[int, ...]

    tuple[int, ...]: A tuple of up to three integers representing the pins knocked down in each roll.

Source code in src/virtual_lanes/game.py
def simulate_last_frame(self, bowler: Bowler) -> tuple[int, ...]:
    """
    Simulates the 10th frame, which may include up to three rolls depending on the bowler's performance.

    Parameters:
        bowler (Bowler): The Bowler object for whom the last frame is simulated.

    Returns:
        tuple[int, ...]: A tuple of up to three integers representing the pins knocked down in each roll.
    """
    rolls: list[int] = []

    # First roll
    if self.rng.random() < bowler.strike_prob:
        rolls.append(10)
    else:
        rolls.append(int(self.rng.integers(0, 10)))

    # Second roll
    if rolls[0] == 10:  # First roll was a strike: fresh rack
        if self.rng.random() < bowler.strike_prob:
            rolls.append(10)
        else:
            rolls.append(int(self.rng.integers(0, 11)))
    else:  # Pins remain: attempt the spare
        remaining = 10 - rolls[0]
        if self.rng.random() < bowler.spare_prob:
            rolls.append(remaining)
        else:
            rolls.append(int(self.rng.integers(0, remaining)))

    # Third roll only earned by a strike or spare in the first two rolls (fresh rack)
    if sum(rolls[:2]) >= 10:
        if self.rng.random() < bowler.strike_prob:
            rolls.append(10)
        else:
            rolls.append(int(self.rng.integers(0, 11)))

    return tuple(rolls[:3])  # Ensure only up to three rolls are returned

simulate_regular_frame(bowler)

Simulates a regular frame (not the last one), accounting for strikes, spares and open frames.

The first ball is a strike with probability bowler.strike_prob. Otherwise, if pins remain the bowler converts the spare with probability bowler.spare_prob; failing that the second ball leaves an open frame.

Parameters:
  • bowler (Bowler) –

    The Bowler object for whom the frame is simulated.

Returns:
  • tuple[int, int]

    tuple[int, int]: A tuple of two integers representing the pins knocked down in each roll.

Source code in src/virtual_lanes/game.py
def simulate_regular_frame(self, bowler: Bowler) -> tuple[int, int]:
    """
    Simulates a regular frame (not the last one), accounting for strikes, spares and open frames.

    The first ball is a strike with probability ``bowler.strike_prob``. Otherwise, if pins
    remain the bowler converts the spare with probability ``bowler.spare_prob``; failing that
    the second ball leaves an open frame.

    Parameters:
        bowler (Bowler): The Bowler object for whom the frame is simulated.

    Returns:
        tuple[int, int]: A tuple of two integers representing the pins knocked down in each roll.
    """
    if self.rng.random() < bowler.strike_prob:
        return (10, 0)
    first_roll = int(self.rng.integers(0, 10))  # 0-9; a 10 would be a strike
    remaining = 10 - first_roll
    # Convert the spare with probability spare_prob, otherwise leave an open frame.
    converts_spare = self.rng.random() < bowler.spare_prob
    second_roll = remaining if converts_spare else int(self.rng.integers(0, remaining))
    return (first_roll, second_roll)

Tournament Class

Source code in src/virtual_lanes/tournament.py
class Tournament:
    def __init__(self, bowlers: list[Bowler], alley: Alley, num_games: int = 1) -> None:
        """
        Initialize a Tournament instance with bowlers, the alley where the tournament is played, and the number of games each bowler will play.

        Parameters:
            bowlers (List[Bowler]): A list of `Bowler` objects representing the participants.
            alley (Alley): The `Alley` object representing the venue of the tournament.
            num_games (int): The number of games each bowler will play in the tournament.

        Attributes:
            results (Dict[str, List[List[int]]]): A dictionary to store results for each bowler.
        """
        self.bowlers = bowlers
        self.alley = alley
        self.num_games = num_games
        self.results: dict[str, list[list[tuple[int, ...]]]] = {bowler.name: [] for bowler in bowlers}

    def run_tournament(self) -> None:
        """
        Simulate the entire tournament, running the specified number of games for each bowler.
        """
        for _ in range(self.num_games):
            game = Game(self.bowlers, self.alley)
            game_results = game.simulate_game()
            for name, scores in game_results.items():
                self.results[name].append(scores)

    def get_results(self) -> dict[str, list[int]]:
        """
        Calculate and return the total scores for each bowler over the course of the tournament.

        Returns:
            Dict[str, List[int]]: A dictionary with bowler names as keys and lists of their total scores for each game as values.
        """
        total_scores = {name: [sum(sum(frame) for frame in game) for game in games] for name, games in self.results.items()}
        return total_scores

    def get_average_scores(self) -> dict[str, float]:
        """
        Calculate and return the average scores for each bowler in the tournament.

        Returns:
            Dict[str, float]: A dictionary with bowler names as keys and their average score as values.
        """
        average_scores = {name: sum(scores) / len(scores) if scores else 0 for name, scores in self.get_results().items()}
        return average_scores

__init__(bowlers, alley, num_games=1)

Initialize a Tournament instance with bowlers, the alley where the tournament is played, and the number of games each bowler will play.

Parameters:
  • bowlers (List[Bowler]) –

    A list of Bowler objects representing the participants.

  • alley (Alley) –

    The Alley object representing the venue of the tournament.

  • num_games (int, default: 1 ) –

    The number of games each bowler will play in the tournament.

Attributes:
  • results (Dict[str, List[List[int]]]) –

    A dictionary to store results for each bowler.

Source code in src/virtual_lanes/tournament.py
def __init__(self, bowlers: list[Bowler], alley: Alley, num_games: int = 1) -> None:
    """
    Initialize a Tournament instance with bowlers, the alley where the tournament is played, and the number of games each bowler will play.

    Parameters:
        bowlers (List[Bowler]): A list of `Bowler` objects representing the participants.
        alley (Alley): The `Alley` object representing the venue of the tournament.
        num_games (int): The number of games each bowler will play in the tournament.

    Attributes:
        results (Dict[str, List[List[int]]]): A dictionary to store results for each bowler.
    """
    self.bowlers = bowlers
    self.alley = alley
    self.num_games = num_games
    self.results: dict[str, list[list[tuple[int, ...]]]] = {bowler.name: [] for bowler in bowlers}

get_average_scores()

Calculate and return the average scores for each bowler in the tournament.

Returns:
  • dict[str, float]

    Dict[str, float]: A dictionary with bowler names as keys and their average score as values.

Source code in src/virtual_lanes/tournament.py
def get_average_scores(self) -> dict[str, float]:
    """
    Calculate and return the average scores for each bowler in the tournament.

    Returns:
        Dict[str, float]: A dictionary with bowler names as keys and their average score as values.
    """
    average_scores = {name: sum(scores) / len(scores) if scores else 0 for name, scores in self.get_results().items()}
    return average_scores

get_results()

Calculate and return the total scores for each bowler over the course of the tournament.

Returns:
  • dict[str, list[int]]

    Dict[str, List[int]]: A dictionary with bowler names as keys and lists of their total scores for each game as values.

Source code in src/virtual_lanes/tournament.py
def get_results(self) -> dict[str, list[int]]:
    """
    Calculate and return the total scores for each bowler over the course of the tournament.

    Returns:
        Dict[str, List[int]]: A dictionary with bowler names as keys and lists of their total scores for each game as values.
    """
    total_scores = {name: [sum(sum(frame) for frame in game) for game in games] for name, games in self.results.items()}
    return total_scores

run_tournament()

Simulate the entire tournament, running the specified number of games for each bowler.

Source code in src/virtual_lanes/tournament.py
def run_tournament(self) -> None:
    """
    Simulate the entire tournament, running the specified number of games for each bowler.
    """
    for _ in range(self.num_games):
        game = Game(self.bowlers, self.alley)
        game_results = game.simulate_game()
        for name, scores in game_results.items():
            self.results[name].append(scores)

BowlingDatabase Class

Handles the storage and retrieval of bowling simulation data in an SQLite database. Provides methods to add and manage bowlers, alleys, games, and detailed game statistics.

Source code in src/virtual_lanes/bowling_database.py
class BowlingDatabase:
    """
    Handles the storage and retrieval of bowling simulation data in an SQLite database.
    Provides methods to add and manage bowlers, alleys, games, and detailed game statistics.
    """

    def __init__(self, db_name: str = 'bowling.db') -> None:
        """
        Initialises the database connection and creates tables if they do not already exist.

        Args:
            db_name (str): The filename of the database. Defaults to 'bowling.db'.
        """
        self.db_name = db_name
        self.conn = sqlite3.connect(self.db_name)
        self.create_tables()

    def create_tables(self) -> None:
        """
        Creates tables in the database if they do not exist to store bowlers, alleys, games, and game details.
        """
        c = self.conn.cursor()

        # Create tables
        c.execute('''
        CREATE TABLE IF NOT EXISTS Bowlers (
            BowlerID INTEGER PRIMARY KEY,
            Name TEXT UNIQUE,
            Handedness TEXT,
            Style TEXT
        )
        ''')

        c.execute('''
        CREATE TABLE IF NOT EXISTS Alleys (
            AlleyID INTEGER PRIMARY KEY,
            Name TEXT,
            Location TEXT,
            LaneType TEXT
        )
        ''')

        c.execute('''
        CREATE TABLE IF NOT EXISTS OilPatterns (
            PatternID INTEGER PRIMARY KEY,
            Name TEXT,
            Description TEXT
        )
        ''')

        c.execute('''
        CREATE TABLE IF NOT EXISTS Games (
            GameID INTEGER PRIMARY KEY,
            Date TEXT,
            AlleyID INTEGER,
            OilPatternID INTEGER,
            FOREIGN KEY (AlleyID) REFERENCES Alleys(AlleyID),
            FOREIGN KEY (OilPatternID) REFERENCES OilPatterns(PatternID)
        )
        ''')

        c.execute('''
        CREATE TABLE IF NOT EXISTS GameDetails (
            GameID INTEGER,
            BowlerID INTEGER,
            FrameData TEXT,
            TotalScore INTEGER,
            StrikePercentage REAL,
            SparePercentage REAL,
            FOREIGN KEY (GameID) REFERENCES Games(GameID),
            FOREIGN KEY (BowlerID) REFERENCES Bowlers(BowlerID)
        )
        ''')

        # Commit changes
        self.conn.commit()

    def add_bowler(self, bowler: Bowler) -> int:
        """
        Adds a new bowler to the database or updates an existing bowler with the same name.

        Args:
            bowler (Bowler): An instance of the Bowler class containing bowler data.

        Returns:
            int: The database ID of the added or updated bowler.
        """
        c = self.conn.cursor()
        c.execute('''
            INSERT INTO Bowlers (Name, Handedness, Style)
            VALUES (?, ?, ?) ON CONFLICT(Name) DO UPDATE SET
            Handedness=excluded.Handedness, Style=excluded.Style
        ''', (bowler.name, bowler.handedness, bowler.technique))
        self.conn.commit()
        assert c.lastrowid is not None
        return c.lastrowid

    def add_alley(self, alley: Alley) -> int:
        """
        Adds a new bowling alley to the database.

        Args:
            alley (Alley): An instance of the Alley class containing alley data.

        Returns:
            int: The database ID of the added alley.
        """
        c = self.conn.cursor()
        c.execute('INSERT INTO Alleys (Name, Location, LaneType) VALUES (?, ?, ?)',
                  (alley.name, alley.location, alley.lane_type))
        self.conn.commit()
        assert c.lastrowid is not None
        return c.lastrowid

    def add_game(self, date: str, alley_id: int, oil_pattern_id: int,
                 bowler_id: int, frames: list[tuple[int, ...]]) -> int:
        """
        Adds a new game along with detailed frame data to the database.

        Args:
            date (str): The date the game was played.
            alley_id (int): The database ID of the alley where the game was played.
            oil_pattern_id (int): The database ID of the oil pattern used in the game.
            bowler_id (int): The database ID of the bowler who played the game.
            frames (list[tuple[int, ...]]): A list of tuples representing the frames played in the game.

        Returns:
            int: The database ID of the newly created game.
        """
        c = self.conn.cursor()
        c.execute('INSERT INTO Games (Date, AlleyID, OilPatternID) VALUES (?, ?, ?)', (date, alley_id, oil_pattern_id))
        game_id = c.lastrowid
        assert game_id is not None
        total_score, strike_percentage, spare_percentage = self.calculate_stats(frames)
        c.execute('''
            INSERT INTO GameDetails (GameID, BowlerID, FrameData, TotalScore, StrikePercentage, SparePercentage)
            VALUES (?, ?, ?, ?, ?, ?)
        ''', (game_id, bowler_id, str(frames), total_score, strike_percentage, spare_percentage))
        self.conn.commit()
        return game_id

    def calculate_stats(self, frames: list[tuple[int, ...]]) -> tuple[int, float, float]:
        """
        Calculate total score, strike, and spare percentages from frame data.

        Args:
            frames (list[tuple[int, ...]]): A list of tuples representing the frames played in the game.

        Returns:
            tuple[int, float, float]: A tuple containing the total score (traditional rules),
            strike percentage, and spare percentage.
        """
        total_score = Scoring.traditional(frames)
        strikes = sum(1 for frame in frames if frame[0] == 10)
        spares = sum(1 for frame in frames if sum(frame[:2]) == 10 and frame[0] != 10)
        total_frames = len(frames)
        strike_percentage = (strikes / total_frames) * 100
        spare_percentage = (spares / total_frames) * 100
        return total_score, strike_percentage, spare_percentage

    def close(self) -> None:
        """
        Closes the database connection.
        """
        self.conn.close()

__init__(db_name='bowling.db')

Initialises the database connection and creates tables if they do not already exist.

Parameters:
  • db_name (str, default: 'bowling.db' ) –

    The filename of the database. Defaults to 'bowling.db'.

Source code in src/virtual_lanes/bowling_database.py
def __init__(self, db_name: str = 'bowling.db') -> None:
    """
    Initialises the database connection and creates tables if they do not already exist.

    Args:
        db_name (str): The filename of the database. Defaults to 'bowling.db'.
    """
    self.db_name = db_name
    self.conn = sqlite3.connect(self.db_name)
    self.create_tables()

add_alley(alley)

Adds a new bowling alley to the database.

Parameters:
  • alley (Alley) –

    An instance of the Alley class containing alley data.

Returns:
  • int( int ) –

    The database ID of the added alley.

Source code in src/virtual_lanes/bowling_database.py
def add_alley(self, alley: Alley) -> int:
    """
    Adds a new bowling alley to the database.

    Args:
        alley (Alley): An instance of the Alley class containing alley data.

    Returns:
        int: The database ID of the added alley.
    """
    c = self.conn.cursor()
    c.execute('INSERT INTO Alleys (Name, Location, LaneType) VALUES (?, ?, ?)',
              (alley.name, alley.location, alley.lane_type))
    self.conn.commit()
    assert c.lastrowid is not None
    return c.lastrowid

add_bowler(bowler)

Adds a new bowler to the database or updates an existing bowler with the same name.

Parameters:
  • bowler (Bowler) –

    An instance of the Bowler class containing bowler data.

Returns:
  • int( int ) –

    The database ID of the added or updated bowler.

Source code in src/virtual_lanes/bowling_database.py
def add_bowler(self, bowler: Bowler) -> int:
    """
    Adds a new bowler to the database or updates an existing bowler with the same name.

    Args:
        bowler (Bowler): An instance of the Bowler class containing bowler data.

    Returns:
        int: The database ID of the added or updated bowler.
    """
    c = self.conn.cursor()
    c.execute('''
        INSERT INTO Bowlers (Name, Handedness, Style)
        VALUES (?, ?, ?) ON CONFLICT(Name) DO UPDATE SET
        Handedness=excluded.Handedness, Style=excluded.Style
    ''', (bowler.name, bowler.handedness, bowler.technique))
    self.conn.commit()
    assert c.lastrowid is not None
    return c.lastrowid

add_game(date, alley_id, oil_pattern_id, bowler_id, frames)

Adds a new game along with detailed frame data to the database.

Parameters:
  • date (str) –

    The date the game was played.

  • alley_id (int) –

    The database ID of the alley where the game was played.

  • oil_pattern_id (int) –

    The database ID of the oil pattern used in the game.

  • bowler_id (int) –

    The database ID of the bowler who played the game.

  • frames (list[tuple[int, ...]]) –

    A list of tuples representing the frames played in the game.

Returns:
  • int( int ) –

    The database ID of the newly created game.

Source code in src/virtual_lanes/bowling_database.py
def add_game(self, date: str, alley_id: int, oil_pattern_id: int,
             bowler_id: int, frames: list[tuple[int, ...]]) -> int:
    """
    Adds a new game along with detailed frame data to the database.

    Args:
        date (str): The date the game was played.
        alley_id (int): The database ID of the alley where the game was played.
        oil_pattern_id (int): The database ID of the oil pattern used in the game.
        bowler_id (int): The database ID of the bowler who played the game.
        frames (list[tuple[int, ...]]): A list of tuples representing the frames played in the game.

    Returns:
        int: The database ID of the newly created game.
    """
    c = self.conn.cursor()
    c.execute('INSERT INTO Games (Date, AlleyID, OilPatternID) VALUES (?, ?, ?)', (date, alley_id, oil_pattern_id))
    game_id = c.lastrowid
    assert game_id is not None
    total_score, strike_percentage, spare_percentage = self.calculate_stats(frames)
    c.execute('''
        INSERT INTO GameDetails (GameID, BowlerID, FrameData, TotalScore, StrikePercentage, SparePercentage)
        VALUES (?, ?, ?, ?, ?, ?)
    ''', (game_id, bowler_id, str(frames), total_score, strike_percentage, spare_percentage))
    self.conn.commit()
    return game_id

calculate_stats(frames)

Calculate total score, strike, and spare percentages from frame data.

Parameters:
  • frames (list[tuple[int, ...]]) –

    A list of tuples representing the frames played in the game.

Returns:
  • int

    tuple[int, float, float]: A tuple containing the total score (traditional rules),

  • float

    strike percentage, and spare percentage.

Source code in src/virtual_lanes/bowling_database.py
def calculate_stats(self, frames: list[tuple[int, ...]]) -> tuple[int, float, float]:
    """
    Calculate total score, strike, and spare percentages from frame data.

    Args:
        frames (list[tuple[int, ...]]): A list of tuples representing the frames played in the game.

    Returns:
        tuple[int, float, float]: A tuple containing the total score (traditional rules),
        strike percentage, and spare percentage.
    """
    total_score = Scoring.traditional(frames)
    strikes = sum(1 for frame in frames if frame[0] == 10)
    spares = sum(1 for frame in frames if sum(frame[:2]) == 10 and frame[0] != 10)
    total_frames = len(frames)
    strike_percentage = (strikes / total_frames) * 100
    spare_percentage = (spares / total_frames) * 100
    return total_score, strike_percentage, spare_percentage

close()

Closes the database connection.

Source code in src/virtual_lanes/bowling_database.py
def close(self) -> None:
    """
    Closes the database connection.
    """
    self.conn.close()

create_tables()

Creates tables in the database if they do not exist to store bowlers, alleys, games, and game details.

Source code in src/virtual_lanes/bowling_database.py
def create_tables(self) -> None:
    """
    Creates tables in the database if they do not exist to store bowlers, alleys, games, and game details.
    """
    c = self.conn.cursor()

    # Create tables
    c.execute('''
    CREATE TABLE IF NOT EXISTS Bowlers (
        BowlerID INTEGER PRIMARY KEY,
        Name TEXT UNIQUE,
        Handedness TEXT,
        Style TEXT
    )
    ''')

    c.execute('''
    CREATE TABLE IF NOT EXISTS Alleys (
        AlleyID INTEGER PRIMARY KEY,
        Name TEXT,
        Location TEXT,
        LaneType TEXT
    )
    ''')

    c.execute('''
    CREATE TABLE IF NOT EXISTS OilPatterns (
        PatternID INTEGER PRIMARY KEY,
        Name TEXT,
        Description TEXT
    )
    ''')

    c.execute('''
    CREATE TABLE IF NOT EXISTS Games (
        GameID INTEGER PRIMARY KEY,
        Date TEXT,
        AlleyID INTEGER,
        OilPatternID INTEGER,
        FOREIGN KEY (AlleyID) REFERENCES Alleys(AlleyID),
        FOREIGN KEY (OilPatternID) REFERENCES OilPatterns(PatternID)
    )
    ''')

    c.execute('''
    CREATE TABLE IF NOT EXISTS GameDetails (
        GameID INTEGER,
        BowlerID INTEGER,
        FrameData TEXT,
        TotalScore INTEGER,
        StrikePercentage REAL,
        SparePercentage REAL,
        FOREIGN KEY (GameID) REFERENCES Games(GameID),
        FOREIGN KEY (BowlerID) REFERENCES Bowlers(BowlerID)
    )
    ''')

    # Commit changes
    self.conn.commit()

League Class

Source code in src/virtual_lanes/league.py
class League:
    def __init__(self, name: str, alley: Alley, oil_pattern: str, team_size: int,
                 num_games_per_night: int, season_length: int) -> None:
        """
        Initialises a league with specified parameters, setting up the environment where the league games are played,
        the type of oil pattern used, the team size, the number of games per league night, and the duration of the season.

        Args:
            name (str): The name of the league.
            alley (Alley): An Alley object where the league games are held.
            oil_pattern (str): The oil pattern used on the lanes throughout the season.
            team_size (int): The number of bowlers in each team.
            num_games_per_night (int): The number of games played each league night.
            season_length (int): The number of weeks the league runs.

        Attributes:
            teams (List[List[Bowler]]): Stores the teams participating in the league.
        """
        self.name = name
        self.alley = alley
        self.oil_pattern = oil_pattern
        self.team_size = team_size
        self.num_games_per_night = num_games_per_night
        self.season_length = season_length
        self.teams: list[list[Bowler]] = []

    def add_team(self, team: list[Bowler]) -> None:
        """
        Adds a team to the league. Ensures the team size matches the league's required team size.

        Args:
            team (List[Bowler]): A list of Bowler objects making up the team.

        Raises:
            ValueError: If the number of bowlers in the team does not match the league's specified team size.
        """
        if len(team) != self.team_size:
            raise ValueError("Team size must match the league's specified team size")
        self.teams.append(team)

    def run_season(self) -> dict[str, list[dict[str, float]]]:
        """
        Simulates the entire season of the league, organising games per night for each team over the specified season length.

        Returns:
            dict[str, list[dict[str, float]]]: A dictionary with team names as keys and a list of their average scores per night as values.
        """
        results: dict[str, list[dict[str, float]]] = {f"Team {i+1}": [] for i in range(len(self.teams))}
        for _week in range(self.season_length):
            for i, team in enumerate(self.teams):
                tournament = Tournament(team, self.alley, self.num_games_per_night)
                tournament.run_tournament()
                results[f"Team {i+1}"].append(tournament.get_average_scores())
        return results

__init__(name, alley, oil_pattern, team_size, num_games_per_night, season_length)

Initialises a league with specified parameters, setting up the environment where the league games are played, the type of oil pattern used, the team size, the number of games per league night, and the duration of the season.

Parameters:
  • name (str) –

    The name of the league.

  • alley (Alley) –

    An Alley object where the league games are held.

  • oil_pattern (str) –

    The oil pattern used on the lanes throughout the season.

  • team_size (int) –

    The number of bowlers in each team.

  • num_games_per_night (int) –

    The number of games played each league night.

  • season_length (int) –

    The number of weeks the league runs.

Attributes:
  • teams (List[List[Bowler]]) –

    Stores the teams participating in the league.

Source code in src/virtual_lanes/league.py
def __init__(self, name: str, alley: Alley, oil_pattern: str, team_size: int,
             num_games_per_night: int, season_length: int) -> None:
    """
    Initialises a league with specified parameters, setting up the environment where the league games are played,
    the type of oil pattern used, the team size, the number of games per league night, and the duration of the season.

    Args:
        name (str): The name of the league.
        alley (Alley): An Alley object where the league games are held.
        oil_pattern (str): The oil pattern used on the lanes throughout the season.
        team_size (int): The number of bowlers in each team.
        num_games_per_night (int): The number of games played each league night.
        season_length (int): The number of weeks the league runs.

    Attributes:
        teams (List[List[Bowler]]): Stores the teams participating in the league.
    """
    self.name = name
    self.alley = alley
    self.oil_pattern = oil_pattern
    self.team_size = team_size
    self.num_games_per_night = num_games_per_night
    self.season_length = season_length
    self.teams: list[list[Bowler]] = []

add_team(team)

Adds a team to the league. Ensures the team size matches the league's required team size.

Parameters:
  • team (List[Bowler]) –

    A list of Bowler objects making up the team.

Raises:
  • ValueError

    If the number of bowlers in the team does not match the league's specified team size.

Source code in src/virtual_lanes/league.py
def add_team(self, team: list[Bowler]) -> None:
    """
    Adds a team to the league. Ensures the team size matches the league's required team size.

    Args:
        team (List[Bowler]): A list of Bowler objects making up the team.

    Raises:
        ValueError: If the number of bowlers in the team does not match the league's specified team size.
    """
    if len(team) != self.team_size:
        raise ValueError("Team size must match the league's specified team size")
    self.teams.append(team)

run_season()

Simulates the entire season of the league, organising games per night for each team over the specified season length.

Returns:
  • dict[str, list[dict[str, float]]]

    dict[str, list[dict[str, float]]]: A dictionary with team names as keys and a list of their average scores per night as values.

Source code in src/virtual_lanes/league.py
def run_season(self) -> dict[str, list[dict[str, float]]]:
    """
    Simulates the entire season of the league, organising games per night for each team over the specified season length.

    Returns:
        dict[str, list[dict[str, float]]]: A dictionary with team names as keys and a list of their average scores per night as values.
    """
    results: dict[str, list[dict[str, float]]] = {f"Team {i+1}": [] for i in range(len(self.teams))}
    for _week in range(self.season_length):
        for i, team in enumerate(self.teams):
            tournament = Tournament(team, self.alley, self.num_games_per_night)
            tournament.run_tournament()
            results[f"Team {i+1}"].append(tournament.get_average_scores())
    return results