Esempio n. 1
0
    def __init__(self, division: int, blue_bot: VersionedBot, orange_bot: VersionedBot, ladder: Ladder, versioned_map,
                 old_match_result: MatchResult, rr_bots: List[str], rr_results: List[MatchResult], message: str=''):
        self.division = division
        self.blue_config_path = blue_bot.bot_config.config_path if blue_bot is not None else None
        self.orange_config_path = orange_bot.bot_config.config_path if orange_bot is not None else None
        self.ladder = ladder.bots
        self.division_names = Ladder.DIVISION_NAMES[:ladder.division_count()]
        self.old_match_result = {
            'winner': old_match_result.winner,
            'loser': old_match_result.loser,
            'blue_goals': old_match_result.blue_goals,
            'orange_goals': old_match_result.orange_goals} if old_match_result is not None else None
        self.division_bots = ladder.round_robin_participants(division)
        self.rr_bots = rr_bots
        self.rr_results = [convert_match_result(mr) for mr in rr_results]
        self.blue_name = blue_bot.bot_config.name if blue_bot is not None else ''
        self.orange_name = orange_bot.bot_config.name if orange_bot is not None else ''
        self.message = message

        self.bot_map = {}
        for bot in ladder.bots:
            self.bot_map[bot] = {
                'name': versioned_map[bot].bot_config.name,
                'updated_date': (versioned_map[bot].updated_date + timedelta(seconds=0)).timestamp(),
            }
Esempio n. 2
0
def list_matches(working_dir: WorkingDir, odd_week: bool, show_results: bool):
    """
    Prints all the matches that will be run this week.
    """

    ladder = Ladder.read(working_dir.ladder)
    playing_division_indices = get_playing_division_indices(ladder, odd_week)

    print(f'Matches to play:')

    # The divisions play in reverse order, but we don't print them that way.
    for div_index in playing_division_indices:
        print(f'--- {Ladder.DIVISION_NAMES[div_index]} division ---')

        rr_bots = ladder.round_robin_participants(div_index)
        rr_matches = generate_round_robin_matches(rr_bots)

        for match_participants in rr_matches:

            # Find result if show_results==True
            result_str = ''
            if show_results:
                result_path = working_dir.get_match_result(
                    div_index, match_participants[0], match_participants[1])
                if result_path.exists():
                    result = MatchResult.read(result_path)
                    result_str = f'  (result: {result.blue_goals}-{result.orange_goals})'

            print(
                f'{match_participants[0]} vs {match_participants[1]}{result_str}'
            )
Esempio n. 3
0
def run_bubble_sort(working_dir: WorkingDir, team_size: int, replay_preference: ReplayPreference):

    # Ladder is a list of name.lower()
    ladder = Ladder.read(working_dir.ladder)

    sorter = BubbleSorter(ladder, working_dir, team_size, replay_preference)
    sorter.begin()
    print('Bubble sort is complete!')
    time.sleep(10)  # Leave some time to display the overlay.
Esempio n. 4
0
 def __init__(self, working_dir: WorkingDir, team_size: int,
              replay_preference: ReplayPreference):
     self.ladder = Ladder.read(working_dir.ladder)
     self.working_dir = working_dir
     self.team_size = team_size
     self.replay_preference = replay_preference
     self.bundle_map = {}
     self.versioned_bots_by_name = {}
     self.num_already_played_during_iteration = 0
def list_matches(working_dir: WorkingDir,
                 run_strategy: RunStrategy,
                 stale_rematch_threshold: int = 0,
                 half_robin: bool = False):
    """
    Prints all the matches that will be run this week.

    :param stale_rematch_threshold: If a bot has won this number of matches in a row against a particular opponent
    and neither have had their code updated, we will consider it to be a stale rematch and skip future matches.
    If 0 is passed, we will not skip anything.
    :param half_robin: If true, we will split the division into an upper and lower round-robin, which reduces the
    number of matches required.
    """

    ladder = Ladder.read(working_dir.ladder)
    playing_division_indices = ladder.playing_division_indices(run_strategy)
    bots = load_all_bots_versioned(working_dir)

    if len(ladder.bots) < 2:
        print(f'Not enough bots on the ladder to play any matches')
        return

    print(f'Matches to play:')

    num_matches = 0
    num_skipped = 0

    # The divisions play in reverse order.
    for div_index in playing_division_indices[::-1]:
        division_name = Ladder.DIVISION_NAMES[div_index] if div_index < len(
            Ladder.DIVISION_NAMES) else div_index
        print(f'--- {division_name} division ---')

        round_robin_ranges = get_round_robin_ranges(ladder, div_index,
                                                    half_robin)

        for start_index, end_index in round_robin_ranges:
            rr_bots = ladder.bots[start_index:end_index + 1]
            rr_matches = generate_round_robin_matches(rr_bots)

            for match_participants in rr_matches:
                bot1 = bots[match_participants[0]]
                bot2 = bots[match_participants[1]]
                stale_match_result = get_stale_match_result(
                    bot1, bot2, stale_rematch_threshold, working_dir)
                if stale_match_result is not None:
                    num_skipped += 1
                    continue

                num_matches += 1
                print(f'{match_participants[0]} vs {match_participants[1]}')

    print(f'Matches to run: {num_matches}  Matches skipped: {num_skipped}')
Esempio n. 6
0
def check_bot_folder(working_dir: WorkingDir, odd_week: Optional[bool] = None):
    """
    Prints all bots missing from the bot folder.
    If odd_week is not None, it will filter for bots needed for the given type of week.
    """
    bots = load_all_bots(working_dir)
    ladder = Ladder.read(working_dir.ladder)
    needed_bots = ladder.all_playing_bots(
        odd_week) if odd_week is not None else ladder.bots
    none_missing = True
    for bot in needed_bots:
        if bot not in bots.keys():
            print(f'{bot} is missing from the bot folder.')
            none_missing = False
    if none_missing:
        print('No needed bots are missing from the bot folder.')
Esempio n. 7
0
def check_bot_folder(working_dir: WorkingDir,
                     run_strategy: Optional[RunStrategy] = None) -> bool:
    """
    Prints all bots missing from the bot folder.
    If odd_week is not None, it will filter for bots needed for the given type of week.
    Returns True if everything is okay and no bots are missing.
    """
    bots = load_all_bots(working_dir)
    ladder = Ladder.read(working_dir.ladder)
    needed_bots = ladder.all_playing_bots(
        run_strategy) if run_strategy is not None else ladder.bots
    none_missing = True
    for bot in needed_bots:
        if bot not in bots.keys():
            print(f'{bot} is missing from the bot folder.')
            none_missing = False
    if none_missing:
        print('No needed bots are missing from the bot folder.')
        return True
    return False
Esempio n. 8
0
def test_all_bots(working_dir: WorkingDir):
    """
    Tests if all bots work by starting a series of matches and check if the bots move
    """

    ladder = Ladder.read(working_dir.ladder)
    bot_count = len(ladder.bots)
    bots = load_all_bots(working_dir)

    # Pair each bot for a match. If there's an odd amount of bots, the last bot plays against the first bot
    pairs = [(ladder.bots[2 * i], ladder.bots[2 * i + 1])
             for i in range(bot_count // 2)]
    if bot_count % 2 == 1:
        pairs.append((ladder.bots[0], ladder.bots[-1]))

    # Run matches
    fails = []
    for match_participant_pair in pairs:
        participant_1 = bots[match_participant_pair[0]]
        participant_2 = bots[match_participant_pair[1]]
        match_config = make_match_config(participant_1, participant_2)
        grade = run_test_match(participant_1.name, participant_2.name,
                               match_config)
        if isinstance(grade, Fail):
            fails.append(grade)
        time.sleep(1)

    time.sleep(2)

    # Print summary
    print(
        f'All test matches have been played ({len(pairs)} in total). Summary:')
    if len(fails) == 0:
        print(f'All bots seem to work!')
    else:
        for fail in fails:
            print(fail)
def get_playing_division_indices(ladder: Ladder, odd_week: bool) -> List[int]:
    # Result is a list containing either even or odd indices.
    # If there is only one division always play that division (division 0, quantum).
    return range(
        ladder.division_count())[int(odd_week) %
                                 2::2] if ladder.division_count() > 1 else [0]
Esempio n. 10
0
def fetch_ladder_from_sheets(week_num: int) -> Ladder:
    values = get_values_from_sheet(get_credentials(), SHEET_ID,
                                   get_ladder_range(week_num), SHEET_NAME)
    bots = [row[0] for row in values]
    return Ladder(bots)
Esempio n. 11
0
def generate_leaderboard(working_dir: WorkingDir,
                         odd_week: bool,
                         extra: bool = False,
                         background: bool = True):
    """
    Created a leaderboard that shows differences between the old ladder and the new ladder.
    :param working_dir: The working directory
    :param odd_week: Whether odd or even divisions played this week
    :param extra: Whether to include the next 5 divisions.
    :param background: Whether to use a background for the leaderboard.
    :param make_clip: Whether to also make an mp4 clip.
    :param duration: Duration of the clip in seconds.
    :param frames_per_second: frames per second of the clip.
    """

    assert working_dir.ladder.exists(
    ), f'\'{working_dir.ladder}\' does not exist.'
    assert working_dir.new_ladder.exists(
    ), f'\'{working_dir.new_ladder}\' does not exist yet.'

    old_ladder = Ladder.read(working_dir.ladder)
    new_ladder = Ladder.read(working_dir.new_ladder)

    new_bots, moved_up, moved_down = ladder_differences(old_ladder, new_ladder)
    played = old_ladder.all_playing_bots(odd_week)

    # ---------------------------------------------------------------

    # PARAMETERS FOR DRAWING:

    # Divisions
    divisions = ('Quantum', 'Overclocked', 'Processor', 'Circuit',
                 'Transistor', 'Abacus', 'Babbage', 'Colossus', 'Dragon',
                 'ENIAC', 'Ferranti')
    '''
    Each division has the origin at the top left corner of their emblem.

    Offsets:
        title offsets determine where the division title is drawn relative to the emblem.
        bot offsets determine where bot names are drawn relative to the emblem.
        sym offsets determine where the symbol is placed relative to the bot name.

    Increments:
        div increments are how much to move the origin between each division.
        bot increment is how much to move down for each bot name.

    '''

    # Start positions for drawing.
    start_x = 0
    start_y = 0

    # Division emblem offsets from the division name position.
    title_x_offset = 350
    title_y_offset = 85

    # Bot name offsets from the division name position.
    bot_x_offset = 200
    bot_y_offset = 300

    # Offsets for the symbols from the bot name position.
    sym_x_offset = 1295
    sym_y_offset = 5

    # Incremenets for x and y.
    div_x_incr = 1790
    div_y_incr = 790
    bot_y_incr = 140

    # ---------------------------------------------------------------

    # DRAWING:

    # Opening image for drawing.
    if background:
        if extra:
            leaderboard = Image.open(LeaderboardPaths.leaderboard_extra_empty)
        else:
            leaderboard = Image.open(LeaderboardPaths.leaderboard_empty)
    else:
        if extra:
            leaderboard = Image.open(
                LeaderboardPaths.leaderboard_extra_no_background)
        else:
            leaderboard = Image.open(
                LeaderboardPaths.leaderboard_no_background)

    draw = ImageDraw.Draw(leaderboard)

    # Fonts for division titles and bot names.
    div_font = ImageFont.truetype(str(LeaderboardPaths.font_medium), 120)
    bot_font = ImageFont.truetype(str(LeaderboardPaths.font_medium), 80)

    # Bot name colour.
    bot_colour = (0, 0, 0)

    # For each divion, draw the division name, and each bot in the division.
    for i, div in enumerate(divisions):

        # Calculates position for the division.
        div_pos = (start_x + div_x_incr * (i // 5),
                   start_y + div_y_incr * (i % 5))

        # Draws the division emblem.
        try:
            # Opens the division emblem image.
            emblem = Image.open(LeaderboardPaths.emblems / f'{div}.png')
            # Pastes emblem onto image.
            leaderboard.paste(emblem, div_pos, emblem)
        except:
            # Sends warning message if it can't find the emblem.
            print(f'WARNING: Missing emblem for {div}.')

        # Draws the division title at an offset.
        title_pos = (div_pos[0] + title_x_offset, div_pos[1] + title_y_offset)
        draw.text(xy=title_pos,
                  text=div,
                  fill=Symbols.palette[div][0],
                  font=div_font)

        # Loops through the four bots in the division and draws each.
        for ii, bot in enumerate(new_ladder.division(i)):

            # Calculates position for the bot name and draws it.
            bot_pos = (div_pos[0] + bot_x_offset,
                       div_pos[1] + bot_y_offset + ii * bot_y_incr)
            draw.text(xy=bot_pos, text=bot, fill=bot_colour, font=bot_font)

            # Calculates symbol position.
            sym_pos = (bot_pos[0] + sym_x_offset, bot_pos[1] + sym_y_offset)

            # Pastes appropriate symbol
            if bot in new_bots:
                symbol = Image.open(LeaderboardPaths.symbols /
                                    f'{div}_new.png')
                leaderboard.paste(symbol, sym_pos, symbol)

            elif bot in moved_up:
                symbol = Image.open(LeaderboardPaths.symbols / f'{div}_up.png')
                leaderboard.paste(symbol, sym_pos, symbol)

            elif bot in moved_down:
                symbol = Image.open(LeaderboardPaths.symbols /
                                    f'{div}_down.png')
                leaderboard.paste(symbol, sym_pos, symbol)

            elif bot in played:
                symbol = Image.open(LeaderboardPaths.symbols /
                                    f'{div}_played.png')
                leaderboard.paste(symbol, sym_pos, symbol)

    # Saves the image.
    leaderboard.save(working_dir.leaderboard, 'PNG')

    print('Successfully generated leaderboard.')
Esempio n. 12
0
def fetch_ladder_from_sheets(season: int, week_num: int) -> Ladder:
    bots = fetch_bots_from_sheets(season, week_num)
    return Ladder(bots)
Esempio n. 13
0
def run_league_play(working_dir: WorkingDir, odd_week: bool,
                    replay_preference: ReplayPreference):
    """
    Run a league play event by running round robins for half the divisions. When done, a new ladder file is created.
    """

    bots = load_all_bots(working_dir)
    ladder = Ladder.read(working_dir.ladder)

    # We need the result of every match to create the next ladder. For each match in each round robin, if a result
    # exist already, it will be parsed, if it doesn't exist, it will be played.
    # When all results have been found, the new ladder can be completed and saved.
    new_ladder = Ladder(ladder.bots)
    event_results = []

    # playing_division_indices contains either even or odd indices.
    # If there is only one division always play that division (division 0, quantum).
    playing_division_indices = range(
        ladder.division_count())[int(odd_week) %
                                 2::2] if ladder.division_count() > 1 else [0]

    # The divisions play in reverse order, so quantum/overclocked division plays last
    for div_index in playing_division_indices[::-1]:
        print(
            f'Starting round robin for the {Ladder.DIVISION_NAMES[div_index]} division'
        )

        rr_bots = ladder.round_robin_participants(div_index)
        rr_matches = generate_round_robin_matches(rr_bots)
        rr_results = []

        for match_participants in rr_matches:

            # Check if match has already been play, i.e. the result file already exist
            result_path = working_dir.get_match_result(div_index,
                                                       match_participants[0],
                                                       match_participants[1])
            if result_path.exists():
                # Found existing result
                try:
                    print(f'Found existing result {result_path.name}')
                    result = MatchResult.read(result_path)

                    rr_results.append(result)

                except Exception as e:
                    print(
                        f'Error loading result {result_path.name}. Fix/delete the result and run script again.'
                    )
                    raise e

            else:
                assert match_participants[
                    0] in bots, f'{match_participants[0]} was not found in \'{working_dir.bots}\''
                assert match_participants[
                    1] in bots, f'{match_participants[1]} was not found in \'{working_dir.bots}\''

                # Play the match
                print(
                    f'Starting match: {match_participants[0]} vs {match_participants[1]}. Waiting for match to finish...'
                )
                match_config = make_match_config(working_dir,
                                                 bots[match_participants[0]],
                                                 bots[match_participants[1]])
                match = MatchExercise(
                    name=f'{match_participants[0]} vs {match_participants[1]}',
                    match_config=match_config,
                    grader=MatchGrader(replay_monitor=ReplayMonitor(
                        replay_preference=replay_preference), ))

                # Let overlay know which match we are about to start
                overlay_data = OverlayData(
                    div_index, bots[match_participants[0]].config_path,
                    bots[match_participants[1]].config_path)
                overlay_data.write(working_dir.overlay_interface)

                with setup_manager_context() as setup_manager:
                    # Disable rendering by replacing renderer with a renderer that does nothing
                    setup_manager.game_interface.renderer = FakeRenderer()

                    # For loop, but should only run exactly once
                    for exercise_result in run_playlist(
                        [match], setup_manager=setup_manager):

                        # Warn users if no replay was found
                        if isinstance(
                                exercise_result.grade, Fail
                        ) and exercise_result.exercise.grader.replay_monitor.replay_id == None:
                            print(
                                f'WARNING: No replay was found for the match \'{match_participants[0]} vs {match_participants[1]}\'. Is Bakkesmod injected and \'Automatically save all replays\' enabled?'
                            )

                        # Save result in file
                        result = exercise_result.exercise.grader.match_result
                        result.write(result_path)
                        print(
                            f'Match finished {result.blue_goals}-{result.orange_goals}. Saved result as {result_path}'
                        )

                        rr_results.append(result)

                        # Let the winner celebrate and the scoreboard show for a few seconds.
                        # This sleep not required.
                        time.sleep(8)

        print(f'{Ladder.DIVISION_NAMES[div_index]} division done')
        event_results.append(rr_results)

        # Find bots' overall score for the round robin
        overall_scores = [
            CombinedScore.calc_score(bot, rr_results) for bot in rr_bots
        ]
        sorted_overall_scores = sorted(overall_scores)[::-1]
        print(
            f'Bots\' overall performance in {Ladder.DIVISION_NAMES[div_index]} division:'
        )
        for score in sorted_overall_scores:
            print(
                f'> {score.bot}: goal_diff={score.goal_diff}, goals={score.goals}, shots={score.shots}, saves={score.saves}, points={score.points}'
            )

        # Rearrange bots in division on the new ladder
        first_bot_index = new_ladder.division_size * div_index
        bots_to_rearrange = len(rr_bots)
        for i in range(bots_to_rearrange):
            new_ladder.bots[first_bot_index + i] = sorted_overall_scores[i].bot

    # Save new ladder
    Ladder.write(new_ladder, working_dir.new_ladder)
    print(f'Done. Saved new ladder as {working_dir.new_ladder.name}')

    # Remove overlay interface file now that we are done
    if working_dir.overlay_interface.exists():
        working_dir.overlay_interface.unlink()

    return new_ladder
Esempio n. 14
0
def run_league_play(working_dir: WorkingDir,
                    run_strategy: RunStrategy,
                    replay_preference: ReplayPreference,
                    team_size: int,
                    shutdowntime: int,
                    stale_rematch_threshold: int = 0,
                    half_robin: bool = False):
    """
    Run a league play event by running round robins for half the divisions. When done, a new ladder file is created.

    :param stale_rematch_threshold: If a bot has won this number of matches in a row against a particular opponent
    and neither have had their code updated, we will consider it to be a stale rematch and skip future matches.
    If 0 is passed, we will not skip anything.
    :param half_robin: If true, we will split the division into an upper and lower round-robin, which reduces the
    number of matches required.
    """

    bots = load_all_bots_versioned(working_dir)
    ladder = Ladder.read(working_dir.ladder)

    latest_bots = [
        bot for bot in bots.values() if bot.bot_config.name in ladder.bots
    ]
    latest_bots.sort(key=lambda b: b.updated_date, reverse=True)
    print('Most recently updated bots:')
    for bot in latest_bots:
        print(f'{bot.updated_date.isoformat()} {bot.bot_config.name}')

    # We need the result of every match to create the next ladder. For each match in each round robin, if a result
    # exist already, it will be parsed, if it doesn't exist, it will be played.
    # When all results have been found, the new ladder can be completed and saved.
    new_ladder = Ladder(ladder.bots)
    event_results = []

    # playing_division_indices contains either even or odd indices.
    # If there is only one division always play that division (division 0, quantum).
    playing_division_indices = ladder.playing_division_indices(run_strategy)

    # The divisions play in reverse order, so quantum/overclocked division plays last
    for div_index in playing_division_indices[::-1]:
        print(
            f'Starting round robin for the {Ladder.DIVISION_NAMES[div_index]} division'
        )

        round_robin_ranges = get_round_robin_ranges(ladder, div_index,
                                                    half_robin)

        for start_index, end_index in round_robin_ranges:
            rr_bots = ladder.bots[start_index:end_index + 1]
            rr_matches = generate_round_robin_matches(rr_bots)
            rr_results = []

            for match_participants in rr_matches:

                # Check if match has already been played during THIS session. Maybe something crashed and we had to
                # restart autoleague, but we want to pick up where we left off.
                session_result_path = working_dir.get_match_result(
                    div_index, match_participants[0], match_participants[1])
                participant_1 = bots[match_participants[0]]
                participant_2 = bots[match_participants[1]]

                if session_result_path.exists():
                    print(f'Found existing result {session_result_path.name}')
                    rr_results.append(MatchResult.read(session_result_path))
                else:
                    historical_result = get_stale_match_result(
                        participant_1, participant_2, stale_rematch_threshold,
                        working_dir, True)
                    if historical_result is not None:
                        rr_results.append(historical_result)
                        # Don't write to result files at all, since this match didn't actually occur.
                        overlay_data = OverlayData(div_index, participant_1,
                                                   participant_2, new_ladder,
                                                   bots, historical_result,
                                                   rr_bots, rr_results)
                        overlay_data.write(working_dir.overlay_interface)
                        time.sleep(
                            8
                        )  # Show the overlay for a while. Not needed for any other reason.

                    else:
                        # Let overlay know which match we are about to start
                        overlay_data = OverlayData(div_index, participant_1,
                                                   participant_2, new_ladder,
                                                   bots, None, rr_bots,
                                                   rr_results)

                        overlay_data.write(working_dir.overlay_interface)

                        match_config = make_match_config(
                            participant_1.bot_config, participant_2.bot_config,
                            team_size)
                        result = run_match(participant_1.bot_config.name,
                                           participant_2.bot_config.name,
                                           match_config, replay_preference)
                        result.write(session_result_path)
                        versioned_result_path = working_dir.get_version_specific_match_result(
                            participant_1, participant_2)
                        result.write(versioned_result_path)
                        print(
                            f'Match finished {result.blue_goals}-{result.orange_goals}. Saved result as '
                            f'{session_result_path} and also {versioned_result_path}'
                        )

                        rr_results.append(result)

                        # Let the winner celebrate and the scoreboard show for a few seconds.
                        # This sleep not required.
                        time.sleep(8)

            # Find bots' overall score for the round robin
            overall_scores = [
                CombinedScore.calc_score(bot, rr_results) for bot in rr_bots
            ]
            sorted_overall_scores = sorted(overall_scores)[::-1]
            division_result_message = f'Bots\' overall round-robin performance ({Ladder.DIVISION_NAMES[div_index]} division):\n'
            for score in sorted_overall_scores:
                division_result_message += f'> {score.bot:<32}: wins={score.wins:>2}, goal_diff={score.goal_diff:>3}\n'

            print(division_result_message)
            overlay_data = OverlayData(div_index, None, None, new_ladder, bots,
                                       None, rr_bots, rr_results,
                                       division_result_message)
            overlay_data.write(working_dir.overlay_interface)

            # Rearrange bots in division on the new ladder
            first_bot_index = start_index
            bots_to_rearrange = len(rr_bots)
            for i in range(bots_to_rearrange):
                new_ladder.bots[first_bot_index +
                                i] = sorted_overall_scores[i].bot

            event_results.append(rr_results)

            time.sleep(8)  # Show the division overlay for a while.

        print(f'{Ladder.DIVISION_NAMES[div_index]} division done')

    # Save new ladder
    Ladder.write(new_ladder, working_dir.new_ladder)
    print(f'Done. Saved new ladder as {working_dir.new_ladder.name}')
    if shutdowntime != 0:
        import subprocess
        subprocess.call("shutdown.exe -s -t " + str(shutdowntime))

    # Remove overlay interface file now that we are done
    if working_dir.overlay_interface.exists():
        working_dir.overlay_interface.unlink()

    return new_ladder
Esempio n. 15
0
def generate_leaderboard(working_dir: WorkingDir,
                         run_strategy: RunStrategy,
                         allow_extra: bool = True,
                         background: bool = True):
    """
    Created a leaderboard that shows differences between the old ladder and the new ladder.
    :param working_dir: The working directory
    :param run_strategy: The strategy for running the ladder that was used this week.
    :param extra: Whether to include the next 5 divisions.
    :param background: Whether to use a background for the leaderboard.
    :param make_clip: Whether to also make an mp4 clip.
    :param duration: Duration of the clip in seconds.
    :param frames_per_second: frames per second of the clip.
    """

    assert working_dir.ladder.exists(
    ), f'\'{working_dir.ladder}\' does not exist.'

    if not working_dir.new_ladder.exists():
        print(f'The new ladder has not been determined yet.')
        return

    old_ladder = Ladder.read(working_dir.ladder)
    new_ladder = Ladder.read(working_dir.new_ladder)

    new_bots, ranks_moved = ladder_differences(old_ladder, new_ladder)
    played = old_ladder.all_playing_bots(run_strategy)

    # ---------------------------------------------------------------

    # PARAMETERS FOR DRAWING:

    # Divisions. We only have color palettes configured for a certain number of them, so enforce a limit.
    divisions = Ladder.DIVISION_NAMES[:len(Symbols.palette)]

    extra = (allow_extra and len(new_ladder.bots) > 40)
    '''
    Each division has the origin at the top left corner of their emblem.

    Offsets:
        title offsets determine where the division title is drawn relative to the emblem.
        bot offsets determine where bot names are drawn relative to the emblem.
        sym offsets determine where the symbol is placed relative to the bot name.

    Increments:
        div increments are how much to move the origin between each division.
        bot increment is how much to move down for each bot name.

    '''

    # Start positions for drawing.
    start_x = 0
    start_y = 0

    # Division emblem offsets from the division name position.
    title_x_offset = 350
    title_y_offset = 85

    # Bot name offsets from the division name position.
    bot_x_offset = 200
    bot_y_offset = 292

    # Offsets for the symbols from the bot name position.
    sym_x_offset = 1300
    sym_y_offset = 5

    # Offsets for the symbols' description from the bot name position.
    sym_desc_x_offset = 1220
    sym_desc_y_offset = 0

    # Incremenets for x and y.
    div_x_incr = 1790
    div_y_incr = 790
    bot_y_incr = 140

    # ---------------------------------------------------------------

    # DRAWING:

    # Opening image for drawing.
    if background:
        if extra:
            leaderboard = Image.open(LeaderboardPaths.leaderboard_extra_empty)
        else:
            leaderboard = Image.open(LeaderboardPaths.leaderboard_empty)
    else:
        if extra:
            leaderboard = Image.open(
                LeaderboardPaths.leaderboard_extra_no_background)
        else:
            leaderboard = Image.open(
                LeaderboardPaths.leaderboard_no_background)

    draw = ImageDraw.Draw(leaderboard)

    # Fonts for division titles and bot names.
    div_font = ImageFont.truetype(str(LeaderboardPaths.font_medium), 120)
    bot_font = ImageFont.truetype(str(LeaderboardPaths.font_medium), 80)

    # Bot name colour.
    bot_colour = (0, 0, 0)

    # For each divion, draw the division name, and each bot in the division.
    for i, div in enumerate(divisions):

        # Calculates position for the division.
        div_pos = (start_x + div_x_incr * (i // 5),
                   start_y + div_y_incr * (i % 5))

        # Draws the division emblem.
        try:
            # Opens the division emblem image.
            emblem = Image.open(LeaderboardPaths.emblems / f'{div}.png')
            # Pastes emblem onto image.
            leaderboard.paste(emblem, div_pos, emblem)
        except:
            # Sends warning message if it can't find the emblem.
            print(f'WARNING: Missing emblem for {div}.')

        # Draws the division title at an offset.
        title_pos = (div_pos[0] + title_x_offset, div_pos[1] + title_y_offset)
        draw.text(xy=title_pos,
                  text=div,
                  fill=Symbols.palette[div][0],
                  font=div_font)

        # Loops through the four bots in the division and draws each.
        for ii, bot in enumerate(new_ladder.division(i)):

            # Calculates position for the bot name and draws it.
            bot_pos = (div_pos[0] + bot_x_offset,
                       div_pos[1] + bot_y_offset + ii * bot_y_incr)
            draw.text(xy=bot_pos, text=bot, fill=bot_colour, font=bot_font)

            # Calculates symbol position.
            sym_pos = (bot_pos[0] + sym_x_offset, bot_pos[1] + sym_y_offset)
            sym_desc_pos = (bot_pos[0] + sym_desc_x_offset,
                            bot_pos[1] + sym_desc_y_offset)
            sym_div_colors = Symbols.palette[div]

            # Pastes appropriate symbol
            if bot in new_bots:
                symbol = Image.open(LeaderboardPaths.symbols /
                                    f'{div}_new.png')
                leaderboard.paste(symbol, sym_pos, symbol)

            elif bot in played:

                # Insert symbol to show rank movement
                if ranks_moved[bot] > 0:
                    symbol = Image.open(LeaderboardPaths.symbols /
                                        f'{div}_up.png')
                elif ranks_moved[bot] < 0:
                    symbol = Image.open(LeaderboardPaths.symbols /
                                        f'{div}_down.png')
                else:
                    symbol = Image.open(LeaderboardPaths.symbols /
                                        f'{div}_played.png')
                leaderboard.paste(symbol, sym_pos, symbol)

                if ranks_moved[bot] != 0:
                    move_txt = f'{abs(ranks_moved[bot])}'
                    w, h = draw.textsize(move_txt, font=bot_font)
                    color = sym_div_colors[Symbols.DARK if ranks_moved[bot] < 0
                                           else Symbols.LIGHT]
                    draw.text(xy=(sym_desc_pos[0] - w / 2, sym_desc_pos[1]),
                              text=move_txt,
                              fill=color,
                              font=bot_font)

    # Saves the image.
    leaderboard.save(working_dir.leaderboard, 'PNG')

    print('Successfully generated leaderboard.')
def list_results(working_dir: WorkingDir, run_strategy: RunStrategy,
                 half_robin: bool):
    ladder = Ladder.read(working_dir.ladder)
    playing_division_indices = ladder.playing_division_indices(run_strategy)

    if len(ladder.bots) < 2:
        print(f'Not enough bots on the ladder to play any matches')
        return

    # Write overview to file first, then print content of the file
    with open(working_dir.results_overview, 'w') as f:
        f.write(f'Matches:\n')

        if run_strategy == RunStrategy.ROLLING or half_robin:
            # The ladder was dynamic, so we can't print divisions neatly.
            # Just print everything in one blob.
            match_results = [
                MatchResult.read(path)
                for path in working_dir.match_results.glob('*')
            ]
            for result in match_results:
                result_str = f'  (result: {result.blue_goals}-{result.orange_goals})'
                f.write(f'{result.blue} vs {result.orange}{result_str}\n')

            write_overall_scores(f, ladder.bots, match_results)

        else:
            # The divisions play in reverse order, but we don't print them that way.
            for div_index in playing_division_indices:
                f.write(
                    f'--- {Ladder.DIVISION_NAMES[div_index]} division ---\n')

                rr_bots = ladder.round_robin_participants(div_index)
                rr_matches = generate_round_robin_matches(rr_bots)

                for match_participants in rr_matches:
                    result_str = ''
                    result_path = working_dir.get_match_result(
                        div_index, match_participants[0],
                        match_participants[1])
                    if result_path.exists():
                        result = MatchResult.read(result_path)
                        result_str = f'  (result: {result.blue_goals}-{result.orange_goals})'
                    f.write(
                        f'{match_participants[0]} vs {match_participants[1]}{result_str}\n'
                    )

            f.write('\n')

            # Print a table with all the combined scores
            for div_index in playing_division_indices:
                rr_bots = ladder.round_robin_participants(div_index)
                rr_matches = generate_round_robin_matches(rr_bots)
                rr_results = []
                for match_participants in rr_matches:
                    result_path = working_dir.get_match_result(
                        div_index, match_participants[0],
                        match_participants[1])
                    if result_path.exists():
                        rr_results.append(MatchResult.read(result_path))

                write_overall_scores(f, rr_bots, rr_results, div_index)

            f.write(
                f'--------------------------------+------+----------+-------+-------+-------+-------+\n'
            )

    # Results have been writen to the file, now display the content
    with open(working_dir.results_overview, 'r') as f:
        print(f.read())

    print(f'Result overview was written to \'{working_dir.results_overview}\'')