Example #1
0
    def decide_on_players(
            bot_ids: Iterable[BotID], rank_sys: RankingSystem,
            ticket_sys: TicketSystem) -> Tuple[List[BotID], List[BotID]]:
        """
        Find two balanced teams. The TicketSystem and the RankingSystem to find
        a fair match up between some bots that haven't played for a while.
        """
        limit = 200

        tries_left = limit
        while tries_left > 0:
            tries_left -= 1

            # Pick some bots that haven't played for a while
            picked = ticket_sys.pick_bots(bot_ids)
            shuffle(picked)
            ratings = [rank_sys.get(bot) for bot in picked]

            blue = tuple(ratings[0:3])
            orange = tuple(ratings[3:6])

            # Is this a fair match?
            required_fairness = min(tries_left / limit, MIN_REQ_FAIRNESS)
            if trueskill.quality([blue, orange]) >= required_fairness:
                print(
                    f"Match: {picked[0:3]} vs {picked[3:6]}\nMatch quality: {trueskill.quality([blue, orange])}"
                )
                ticket_sys.choose(picked, bot_ids)
                return picked[0:3], picked[3:6]

        raise Exception("Failed to find a fair match")
Example #2
0
def make_overlay(ld: LeagueDir, match: MatchDetails, bots: Mapping[BotID, BotConfigBundle]):
    """
    Make a `current_match.json` file which contains the details about the current
    match and its participants.
    """

    retired = load_retired_bots(ld)
    rankings = RankingSystem.load(ld).ensure_all(list(bots.keys()))
    rank_list = rankings.as_sorted_list(exclude=retired)

    def bot_data(bot_id):
        config = bots[bot_id]
        rank, mmr = [(i + 1, mrr) for i, (id, mrr, sigma) in enumerate(rank_list) if id == bot_id][0]
        return {
            "name": config.name,
            "config_path": str(config.config_path),
            "logo_path": try_copy_logo(config),
            "developer": config.base_agent_config.get("Details", "developer"),
            "description": config.base_agent_config.get("Details", "description"),
            "fun_fact": config.base_agent_config.get("Details", "fun_fact"),
            "github": config.base_agent_config.get("Details", "github"),
            "language": config.base_agent_config.get("Details", "language"),
            "rank": rank,
            "mmr": mmr,
        }

    overlay = {
        "blue": [bot_data(bot_id) for bot_id in match.blue],
        "orange": [bot_data(bot_id) for bot_id in match.orange],
        "map": match.map
    }

    with open(PackageFiles.overlay_current_match, 'w') as f:
        json.dump(overlay, f, indent=4)
Example #3
0
def create_bot_summary(ld: LeagueDir):
    """
    Create a json file with all information about the bots. Useful for casters.
    """

    bots = load_all_bots(ld)
    rankings = RankingSystem.load(ld).ensure_all(list(bots.keys()))
    rank_list = rankings.as_sorted_list()

    def bot_data(bot_id):
        config = bots[bot_id]
        rank, mmr = [(i + 1, mrr)
                     for i, (id, mrr, sigma) in enumerate(rank_list)
                     if id == bot_id][0]
        return {
            "name": config.name,
            "developer": config.base_agent_config.get("Details", "developer"),
            "description":
            config.base_agent_config.get("Details", "description"),
            "fun_fact": config.base_agent_config.get("Details", "fun_fact"),
            "github": config.base_agent_config.get("Details", "github"),
            "language": config.base_agent_config.get("Details", "language"),
            "rank": rank,
            "mmr": mmr,
        }

    bot_summary = {
        defmt_bot_name(bot_id): bot_data(bot_id)
        for bot_id in bots.keys()
    }

    with open(ld.bot_summary, 'w') as f:
        json.dump(bot_summary, f, indent=4)
Example #4
0
    def decide_on_players_2(bot_ids: Iterable[BotID], rank_sys: RankingSystem,
                            ticket_sys: TicketSystem) -> Tuple[List[BotID], List[BotID]]:
        """
        Find two balanced teams. The TicketSystem and the RankingSystem to find
        a fair match up between some bots that haven't played for a while.
        """

        # Composing a team of the best player + the worst two players will likely yield a balanced match (0, 4, 5).
        # These represent a few arrangements like that which seem reasonable to try, they will be checked against
        # the trueskill system.
        likely_balances = [(0, 4, 5), (0, 3, 5), (0, 2, 5), (0, 3, 4)]

        # Experimental average quality based on limit:
        # 1000: 0.4615
        # 400:  0.460
        # 100:  0.457
        # 10:   0.448
        num_bot_groups_to_test = 400

        # How much we value the tightness of rating distribution in a given match.
        # A higher number will yield matches with similarly skilled bots, but potentially lower probability of a draw.
        tightness_weight = 1.0

        tries_left = num_bot_groups_to_test
        best_quality_found = 0
        best_score_found = 0
        best_match = None
        chosen_balance = None

        while tries_left > 0:
            tries_left -= 1

            # Pick some bots that haven't played for a while
            picked = ticket_sys.pick_bots(bot_ids)
            candidates = [Candidate(bot, rank_sys.get(bot)) for bot in picked]
            candidates.sort(key=lambda c: float(c.rating), reverse=True)
            tightness = 1 / (numpy.std([float(c.rating) for c in candidates]) + 1)

            for balance in likely_balances:
                blue_candidates = candidates[balance[0]], candidates[balance[1]], candidates[balance[2]]
                orange_candidates = [c for c in candidates if c not in blue_candidates]
                quality = trueskill.quality([[c.rating for c in blue_candidates], [c.rating for c in orange_candidates]])
                score = quality + tightness * tightness_weight
                if score > best_score_found:
                    best_score_found = score
                    best_quality_found = quality
                    best_match = (blue_candidates, orange_candidates)
                    chosen_balance = balance

        blue_ids = [c.bot_id for c in best_match[0]]
        orange_ids = [c.bot_id for c in best_match[1]]
        tickets_consumed = sum([ticket_sys.get_ensured(b) for b in blue_ids + orange_ids])
        print(f"Match: {blue_ids} vs {orange_ids}\nMatch quality: {best_quality_found}  score: {best_score_found}  "
              f"Rank pattern: {chosen_balance}")
        ticket_sys.choose(blue_ids + orange_ids, bot_ids)
        return blue_ids, orange_ids
Example #5
0
 def test_gamecount_equity(self):
     rank_sys = RankingSystem.read(RESOURCES_FOLDER /
                                   '20210925212802_rankings.json')
     ticket_sys = TicketSystem.read(
         RESOURCES_FOLDER / '20210925212802_tickets.json', LeagueSettings())
     bot_ids = rank_sys.ratings.keys()
     for i in range(19):
         MatchMaker.decide_on_players_2(bot_ids, rank_sys, ticket_sys)
     game_counts = list(ticket_sys.session_game_counts.values())
     print(ticket_sys.session_game_counts)
     print(
         f'Game count std dev: {numpy.std(game_counts)}, max: {max(game_counts)} min: {min(game_counts)} avg: {numpy.mean(game_counts)}'
     )
     for i in range(5):
         print(f'Num with {i}: {game_counts.count(i)}')
Example #6
0
def parse_subcommand_rank(args: List[str]):
    assert args[0] == "rank"
    help_msg = """Usage:
        autoleague rank list                Print list of the current leaderboard"""

    ld = require_league_dir()

    if len(args) == 1 or args[1] == "help":
        print(help_msg)

    elif args[1] == "list" and len(args) == 2:

        bots = load_all_bots(ld)

        rank_sys = RankingSystem.load(ld)
        rank_sys.ensure_all(list(bots.keys()))
        rank_sys.print_ranks_and_mmr()

    else:
        print(help_msg)
Example #7
0
def parse_subcommand_rank(args: List[str]):
    assert args[0] == "rank"
    help_msg = """Usage:
        autoleague rank list [showRetired]  Print list of the current leaderboard"""

    ld = require_league_dir()

    if len(args) == 1 or args[1] == "help":
        print(help_msg)

    elif args[1] == "list" and (len(args) == 2 or len(args) == 3):

        show_retired = len(args) == 3 and bool(args[2])
        exclude = [] if show_retired else load_retired_bots(ld)

        bots = load_all_bots(ld)

        rank_sys = RankingSystem.load(ld)
        rank_sys.ensure_all(list(bots.keys()))
        rank_sys.print_ranks_and_mmr(exclude)

    else:
        print(help_msg)
Example #8
0
    def _test_decide(self, decide_name, decide_function):
        rank_sys = RankingSystem.read(RESOURCES_FOLDER /
                                      '20210925212802_rankings.json')
        ticket_sys = TicketSystem.read(
            RESOURCES_FOLDER / '20210925212802_tickets.json', LeagueSettings())
        bot_ids = sorted(rank_sys.ratings.keys(),
                         key=lambda id: rank_sys.ratings[id].mu)
        qualities = []
        max_mmr_diffs = []
        matches_played = {bot_id: 0 for bot_id in bot_ids}
        for i in range(NUM_ITERATIONS):
            players = decide_function(bot_ids, rank_sys, ticket_sys)
            quality = get_trueskill_quality(players, rank_sys)
            qualities.append(quality)
            max_mmr_diff = get_max_mmr_diff(players, rank_sys)
            max_mmr_diffs.append(max_mmr_diff)
            for bot_id in itertools.chain.from_iterable(players):
                matches_played[bot_id] += 1

        average = sum(qualities) / NUM_ITERATIONS
        print(f'{decide_name} average quality: {average}')
        plt.hist(qualities, bins=20, range=(0, 1))
        plt.ylabel('matches')
        plt.xlabel('quality')
        plt.title(f'{decide_name} - Matches played by quality')
        plt.show()
        plt.hist(max_mmr_diffs, bins=10, range=(0, 100))
        plt.ylabel('matches')
        plt.xlabel('max MMR difference')
        plt.title(f'{decide_name} - Matches played by MMR difference')
        plt.show()
        matches = matches_played.values()
        plt.bar(range(len(matches)), matches)
        plt.ylabel('matches')
        plt.xlabel('bot (sorted by MMR)')
        plt.title(f'{decide_name} - Matches played by bot')
        plt.show()
Example #9
0
def parse_subcommand_bot(args: List[str]):
    assert args[0] == "bot"
    help_msg = """Usage:
    autoleague bot list                       Print list of all known bots
    autoleague bot test <bot_id>              Run test match using a specific bot
    autoleague bot details <bot_id>           Print details about the given bot
    autoleague bot unzip                      Unzip all bots in the bot directory
    autoleague bot summary                    Create json file with bot descriptions"""

    ld = require_league_dir()

    if len(args) == 1 or args[1] == "help":
        print(help_msg)

    elif args[1] == "list" and len(args) == 2:

        bot_configs = load_all_bots(ld)
        rank_sys = RankingSystem.load(ld)
        ticket_sys = TicketSystem.load(ld)

        bot_ids = list(
            set(bot_configs.keys()).union(set(rank_sys.ratings.keys())).union(
                set(ticket_sys.tickets.keys())))

        print(f"{'': <22} c r t")
        for bot in sorted(bot_ids):
            c = "x" if bot in bot_configs else " "
            r = "x" if bot in rank_sys.ratings else " "
            t = "x" if bot in ticket_sys.tickets else " "
            print(f"{bot + ' ':.<22} {c} {r} {t}")

    elif args[1] == "test" and len(args) == 3:

        # Load
        bots = load_all_bots(ld)
        bot = args[2]
        if bot not in bots:
            print(f"Could not find the config file of '{bot}'")
            return

        # Run
        match = MatchMaker.make_test_match(bot)
        run_match(ld, match, bots, ReplayPreference.NONE)
        print(f"Test of '{bot}' complete")

    elif args[1] == "details" and len(args) == 3:

        bots = load_all_bots(ld)
        bot = args[2]

        if bot not in bots:
            print(f"Could not find the config file of '{bot}'")
            return

        print_details(bots[bot])

    elif args[1] == "unzip" and len(args) == 2:

        print("Unzipping all bots:")
        unzip_all_bots(ld)

    elif args[1] == "summary" and len(args) == 2:

        create_bot_summary(ld)
        print("Bot summary created")

    else:
        print(help_msg)
Example #10
0
def get_max_mmr_diff(players: Tuple[List[BotID], List[BotID]],
                     rank_sys: RankingSystem) -> float:
    mus = [rank_sys.get(bot).mu for bot in players[0] + players[1]]
    return max(mus) - min(mus)
Example #11
0
def get_trueskill_quality(players: Tuple[List[BotID], List[BotID]],
                          rank_sys: RankingSystem) -> float:
    blue_ratings = [rank_sys.get(bot) for bot in players[0]]
    orange_ratings = [rank_sys.get(bot) for bot in players[1]]
    return trueskill.quality([blue_ratings, orange_ratings])
import numpy as np
import matplotlib.pylab as plt
from pathlib import Path

from matplotlib.colors import ListedColormap

from match import MatchDetails
from paths import LeagueDir
from ranking_system import RankingSystem
from settings import PersistentSettings

settings = PersistentSettings.load()
ld = LeagueDir(Path(settings.league_dir_raw))

ranks = RankingSystem.latest(ld, 1)[0]
bots = sorted(ranks.ratings.keys(), key=lambda bot: -ranks.get_mmr(bot))
N = len(bots)
matches = MatchDetails.all(ld)

win_rate = np.full((N, N), -0.01)
for i in range(N):
    for j in range(N):
        if i < j:
            wins_i = 0
            wins_j = 0
            for match in matches:
                if bots[i] in match.blue and bots[j] in match.orange:
                    if match.result.blue_goals > match.result.orange_goals:
                        wins_i += 1
                    else:
                        wins_j += 1
Example #13
0
def make_summary(ld: LeagueDir, count: int):
    """
    Make a summary of the N latest matches and the resulting ranks and tickets.
    If N is 0 the summary will just contain the current ratings.
    """
    summary = {}

    tickets = TicketSystem.load(ld)

    # ========== Matches ==========

    matches = []
    bot_wins = defaultdict(list)  # Maps bots to list of booleans, where true=win and false=loss

    if count > 0:
        latest_matches = MatchDetails.latest(ld, count)
        for i, match in enumerate(latest_matches):
            matches.append({
                "index": i,
                "blue_names": [defmt_bot_name(bot_id) for bot_id in match.blue],
                "orange_names": [defmt_bot_name(bot_id) for bot_id in match.orange],
                "blue_goals": match.result.blue_goals,
                "orange_goals": match.result.orange_goals,
            })
            for bot in match.blue:
                bot_wins[bot].append(match.result.blue_goals > match.result.orange_goals)
            for bot in match.orange:
                bot_wins[bot].append(match.result.blue_goals < match.result.orange_goals)

    summary["matches"] = matches

    # ========= Ranks/Ratings =========

    bots = load_all_unretired_bots(ld)
    retired = load_retired_bots(ld)
    bots_by_rank = []

    if count <= 0:
        # Old rankings and current rankings is the same, but make sure all bots have a rank currently
        old_rankings = RankingSystem.load(ld).as_sorted_list(exclude=retired)
        cur_rankings = RankingSystem.load(ld).ensure_all(list(bots.keys())).as_sorted_list(exclude=retired)
    else:
        # Determine current rank and their to N matches ago
        n_rankings = RankingSystem.latest(ld, count + 1)
        old_rankings = n_rankings[0].as_sorted_list(exclude=retired)
        cur_rankings = n_rankings[-1].ensure_all(list(bots.keys())).as_sorted_list(exclude=retired)

    for i, (bot, mrr, sigma) in enumerate(cur_rankings):
        cur_rank = i + 1
        old_rank = None
        old_mmr = None
        for j, (other_bot, other_mrr, _) in enumerate(old_rankings):
            if bot == other_bot:
                old_rank = j + 1
                old_mmr = other_mrr
                break
        bots_by_rank.append({
            "bot_id": defmt_bot_name(bot),
            "mmr": mrr,
            "old_mmr": old_mmr,
            "sigma": sigma,
            "cur_rank": cur_rank,
            "old_rank": old_rank,
            "tickets": tickets.get(bot) or tickets.new_bot_ticket_count,
            "wins": bot_wins[bot],
        })

    summary["bots_by_rank"] = bots_by_rank

    # =========== Write =============

    with open(PackageFiles.overlay_summary, 'w') as f:
        json.dump(summary, f, indent=4)

    league_settings = LeagueSettings.load(ld)
    league_settings.last_summary = count
    league_settings.save(ld)
Example #14
0
def parse_subcommand_retirement(args: List[str]):
    assert args[0] == "retirement"
    help_msg = """Usage:
        autoleague retirement list                  Print all bots in retirement
        autoleague retirement retire <bot>          Retire a bot, removing it from play and the leaderboard
        autoleague retirement unretire <bot>        Unretire a bot
        autoleague retirement retireall             Retire all bots"""

    ld = require_league_dir()

    if len(args) == 1 or args[1] == "help":
        print(help_msg)

    elif args[1] == "list" and len(args) == 2:

        retired = load_retired_bots(ld)

        if len(retired) == 0:
            print("There are no bots in retirement")
        else:
            print("Retired bots:")
            for bot_id in sorted(retired):
                print(bot_id)

    elif args[1] == "retire" and len(args) == 3:

        bot = args[2]
        retired = load_retired_bots(ld)

        retired.add(bot)
        save_retired_bots(ld, retired)

        print(f"Retired {bot}")

    elif args[1] == "unretire" and len(args) == 3:

        bot = args[2]
        retired = load_retired_bots(ld)

        try:
            retired.remove(bot)
            save_retired_bots(ld, retired)
            print(f"Unretired {bot}")
        except KeyError:
            print(f"The bot {bot} is not in retirement")

    elif args[1] == "retireall" and len(args) == 2:

        bot_configs = load_all_bots(ld)
        rank_sys = RankingSystem.load(ld)
        ticket_sys = TicketSystem.load(ld)
        retired = load_retired_bots(ld)

        all_bots = set(
            bot_configs.keys()).union(set(rank_sys.ratings.keys())).union(
                set(ticket_sys.tickets.keys())).union(retired)

        save_retired_bots(ld, all_bots)

        count = len(all_bots) - len(retired)
        print(f"Retired {count} bots")

    else:
        print(help_msg)
Example #15
0
from paths import LeagueDir
from ranking_system import RankingSystem
from settings import PersistentSettings
import seaborn as sns
import pandas as pd
import matplotlib.pylab as plt

settings = PersistentSettings.load()
ld = LeagueDir(Path(settings.league_dir_raw))

rankings = {}
for path in list(ld.rankings.iterdir()):
    time = path.name[:8]
    if time not in rankings:
        rankings[time] = {}
    ranking = RankingSystem.read(path)
    rankings[time].update(ranking.get_mmr_all())

bots = sorted(set([bot_id for time in rankings for bot_id in rankings[time]]))
times = sorted(rankings.keys())
data = {bot: [rankings[time].get(bot) or 33 for time in times] for bot in bots}
df = pd.DataFrame.from_dict(data,
                            orient='index',
                            columns=range(1,
                                          len(times) + 1)).transpose()

print(df)

plt.figure(figsize=(10.0, 5.0))
plt.plot(df)
plt.legend(bots,
Example #16
0
def parse_subcommand_match(args: List[str]):
    assert args[0] == "match"
    help_msg = """Usage:
    autoleague match run                        Run a standard 3v3 soccer match
    autoleague match undo                       Undo the last match
    autoleague match list [n]                   Show the latest matches"""

    ld = require_league_dir()

    if len(args) == 1 or args[1] == "help":
        print(help_msg)

    elif args[1] == "run" and len(args) == 2:

        # Load
        bots = load_all_bots(ld)
        rank_sys = RankingSystem.load(ld)
        ticket_sys = TicketSystem.load(ld)

        # Run
        match = MatchMaker.make_next(bots, rank_sys, ticket_sys)
        result, replay = run_match(ld, match, bots, ReplayPreference.SAVE)
        rank_sys.update(match, result)
        match.result = result
        match.replay_id = replay.replay_id

        # Save
        match.save(ld)
        rank_sys.save(ld, match.time_stamp)
        ticket_sys.save(ld, match.time_stamp)

        # Print new ranks
        rank_sys.print_ranks_and_mmr()

        # Make summary
        league_settings = LeagueSettings.load(ld)
        make_summary(ld, league_settings.last_summary + 1)
        print(
            f"Created summary of the last {league_settings.last_summary + 1} matches."
        )

    elif args[1] == "undo" and len(args) == 2:

        # Undo latest match
        ld = require_league_dir()
        latest_matches = MatchDetails.latest(ld, 1)
        if len(latest_matches) == 0:
            print("No matches to undo")
        else:
            latest_match = latest_matches[0]

            # Prompt user
            print(f"Latest match was {latest_match.name}")
            if prompt_yes_no(
                    "Are you sure you want to undo the latest match?"):

                # Undo latest update to all systems
                RankingSystem.undo(ld)
                TicketSystem.undo(ld)
                MatchDetails.undo(ld)

                # New latest match
                new_latest_match = MatchDetails.latest(ld, 1)
                if new_latest_match:
                    print(f"Reverted to {new_latest_match[0].name}")
                else:
                    print("Reverted to beginning of league (no matches left)")

    elif args[1] == "list" and len(args) <= 3:

        count = 999999
        if len(args) == 3:
            count = int(args[2])

        # Show list of latest n matches played
        latest_matches = MatchDetails.latest(ld, count)
        if len(latest_matches) == 0:
            print("No matches have been played yet.")
        else:
            print(f"Match history (latest {len(latest_matches)} matches):")
            for match in latest_matches:
                print(
                    f"{match.time_stamp}: {', '.join(match.blue) + ' ':.<46} {match.result.blue_goals} VS {match.result.orange_goals} {' ' + ', '.join(match.orange):.>46}"
                )

    else:
        print(help_msg)
Example #17
0
def main():
    RankingSystem.setup()
    parse_args(sys.argv[1:])
    return 0
Example #18
0
    def decide_on_players_3(bot_ids: Iterable[BotID], rank_sys: RankingSystem,
                            ticket_sys: TicketSystem) -> Tuple[List[BotID], List[BotID]]:
        """
        Find two balanced teams. The TicketSystem and the RankingSystem to find
        a fair match up between some bots that haven't played for a while.
        """
        # Higher ticket strength produces a more uniform distribution of matches played, adjust by increments of 0.1
        TICKET_STRENGTH = 1
        # Higher MMR tolerance allows accurately rated bots to play in more "distant" MMR matches, adjust by increments of 1
        MMR_TOLERANCE = 4
        # Max attempts to build match of quality >= MIN_QUALITY
        MAX_ITERATIONS = 20
        MIN_QUALITY = 0.4

        rank_sys.ensure_all(bot_ids)
        ticket_sys.ensure(bot_ids)

        best_quality = 0
        best_match = None

        max_tickets = max([ticket_sys.get(bot_id) for bot_id in bot_ids])

        for i in range(MAX_ITERATIONS):
            # Get Leader Bot (choose randomly between bots with highest tickets)
            possible_leaders = [bot_id for bot_id, tickets in ticket_sys.tickets.items() if tickets == max_tickets and bot_id in bot_ids]
            leader = numpy.random.choice(possible_leaders)

            # Get MU for Leader bot, that will be the match mmr
            match_mmr = rank_sys.get(leader).mu

            # Score all bots based on probability to perform at target mmr, scaled by amount of tickets
            candidates = [Candidate(bot_id, rank_sys.get(bot_id)) for bot_id in bot_ids if bot_id != leader]
            scores = []

            for c in candidates:
                # Calculate probability to perform at desired mmr
                performance_prob = pdf(match_mmr, mu=c.rating.mu, sigma=math.sqrt(c.rating.sigma**2 + MMR_TOLERANCE**2))

                # Calculate weighting factor based on tickets
                tickets = ticket_sys.get(c.bot_id)
                tickets_weight = tickets ** TICKET_STRENGTH

                # Calculate candidate score
                scores.append(performance_prob * tickets_weight)

            # Pick 5 bots randomly based on their score
            probs = numpy.asarray(scores) / sum(scores)
            players = list(numpy.random.choice(candidates, size=5, p=probs, replace=False))
            players.append(Candidate(leader, rank_sys.get(leader)))

            # Get the highest quality match with the 6 chosen bots
            combinations = list(itertools.combinations(players, 3))
            possible_matches = len(combinations) // 2
            blue_combs = combinations[:possible_matches]
            orange_combs = combinations[:possible_matches-1:-1]

            for i in range(possible_matches):
                blue_team = blue_combs[i]
                orange_team = orange_combs[i]
                quality = trueskill.quality([[c.rating for c in blue_team], [c.rating for c in orange_team]])
                if quality > best_quality:
                    best_quality = quality
                    best_match = (blue_team, orange_team)

            if best_quality >= MIN_QUALITY:
                break

        # We sort by get_mmr() because it considers sigma
        blue_ids = sorted([c.bot_id for c in best_match[0]], key=lambda id: rank_sys.get_mmr(id), reverse=True)
        orange_ids = sorted([c.bot_id for c in best_match[1]], key=lambda id: rank_sys.get_mmr(id), reverse=True)
        
        tickets_consumed = sum([ticket_sys.get_ensured(b) for b in blue_ids + orange_ids])
        print(f"Match: {blue_ids} vs {orange_ids}\nMatch quality: {best_quality}  Tickets consumed: {tickets_consumed}")
        ticket_sys.choose(blue_ids + orange_ids, bot_ids)
        return blue_ids, orange_ids