def main(): arguments = docopt(__doc__, version=__version__) settings = PersistentSettings.load() if arguments['setup']: working_dir = Path(arguments['<working_dir>']) working_dir.mkdir(exist_ok=True, parents=True) WorkingDir(working_dir) # Creates relevant directories and files settings.working_dir_raw = str(working_dir) settings.save() print(f'Working directory successfully set to \'{working_dir}\'') else: # Following commands require a working dir. Make sure it is set. if settings.working_dir_raw is None: print('No working directory set, use \'autoleagueplay setup <working_dir>\'') sys.exit(0) working_dir = WorkingDir(Path(settings.working_dir_raw)) if arguments['odd'] or arguments['even']: replay_preference = ReplayPreference(arguments['--replays']) if arguments['--results']: list_matches(working_dir, arguments['odd'], True) elif arguments['--list']: list_matches(working_dir, arguments['odd'], False) elif arguments['--test']: check_bot_folder(working_dir, arguments['odd']) else: run_league_play(working_dir, arguments['odd'], replay_preference) elif arguments['test']: check_bot_folder(working_dir) elif arguments['fetch']: week_num = int(arguments['<week_num>']) if week_num < 0: print(f'Week number must be a positive integer.') sys.exit(1) ladder = fetch_ladder_from_sheets(week_num) ladder.write(working_dir.ladder) print(f'Successfully fetched week {week_num} to \'{working_dir.ladder}\':') for bot in ladder.bots: print(bot) else: raise NotImplementedError()
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}' )
def load_all_bots(working_dir: WorkingDir) -> Mapping[str, BotConfigBundle]: bots = dict(working_dir.get_bots()) psyonix_allstar, psyonix_pro, psyonix_rookie = load_psyonix_bots() bots[psyonix_allstar.name] = psyonix_allstar bots[psyonix_pro.name] = psyonix_pro bots[psyonix_rookie.name] = psyonix_rookie return bots
def load_all_bots(working_dir: WorkingDir) -> Mapping[str, BotConfigBundle]: bots = dict(working_dir.get_bots()) # Psyonix bots psyonix_allstar = get_bot_config_bundle(PackageFiles.psyonix_allstar) psyonix_pro = get_bot_config_bundle(PackageFiles.psyonix_pro) psyonix_rookie = get_bot_config_bundle(PackageFiles.psyonix_rookie) bots[psyonix_allstar.name] = psyonix_allstar bots[psyonix_pro.name] = psyonix_pro bots[psyonix_rookie.name] = psyonix_rookie # Skill values for later. This way the user can rename the Psyonix bots by changing the config files, but we still # have their correct skill psyonix_bots[psyonix_allstar.name] = 1.0 psyonix_bots[psyonix_pro.name] = 0.5 psyonix_bots[psyonix_rookie.name] = 0.0 return bots
def main(): arguments = docopt(__doc__, version=__version__) if arguments['odd'] or arguments['even'] or arguments['bubble']: ladder_path = Path(arguments['<ladder>']) if not ladder_path.exists(): print(f'\'{ladder_path}\' does not exist.') sys.exit(1) working_dir = WorkingDir(ladder_path) replay_preference = ReplayPreference(arguments['--replays']) team_size = int(arguments['--teamsize']) if arguments['--results']: list_matches(working_dir, arguments['odd'], True) elif arguments['--list']: list_matches(working_dir, arguments['odd'], False) elif arguments['bubble']: run_bubble_sort(working_dir, team_size, replay_preference) else: run_league_play(working_dir, arguments['odd'], replay_preference, team_size) elif arguments['fetch']: week_num = int(arguments['<week_num>']) if week_num < 0: print(f'Week number must be a positive integer.') sys.exit(1) league_dir = Path(arguments['<league_dir>']) if not league_dir.is_dir(): league_dir = league_dir.parent league_dir.mkdir(exist_ok=True) ladder_path = league_dir / 'ladder.txt' ladder = fetch_ladder_from_sheets(week_num) ladder.write(ladder_path) print(f'Successfully fetched week {week_num} to \'{ladder_path}\'') else: raise NotImplementedError()
def get_stale_match_result(bot1: VersionedBot, bot2: VersionedBot, stale_rematch_threshold: int, working_dir: WorkingDir, print_debug: bool = False): if stale_rematch_threshold > 0: match_history = MatchHistory( working_dir.get_version_specific_match_files( bot1.get_key(), bot2.get_key())) if not match_history.is_empty(): streak = match_history.get_current_streak_length() if streak >= stale_rematch_threshold: if print_debug: print( f'Found stale rematch between {bot1.bot_config.name} and {bot2.bot_config.name}. ' f'{match_history.get_latest_result().winner} has won {streak} times in a row.' ) return match_history.get_latest_result() return None
def main(): arguments = docopt(__doc__, version=__version__) ladder_path = Path(arguments['<ladder>']) if not ladder_path.exists(): print(f'\'{ladder_path}\' does not exist.') sys.exit(1) working_dir = WorkingDir(ladder_path) replay_preference = ReplayPreference(arguments['--replays']) if arguments['odd'] or arguments['even']: if arguments['--results']: list_matches(working_dir, arguments['odd'], True) elif arguments['--list']: list_matches(working_dir, arguments['odd'], False) else: run_league_play(working_dir, arguments['odd'], replay_preference) else: raise NotImplementedError()
def parse_results_and_write_files(working_dir: WorkingDir, results_file: Path, fallback_time: datetime): """ If you have the output of the list_results function as a text document, you can use this function to parse it out into MatchResult objects, and write those results with versioned file names. If a bot cannot be found in the working_dir, we will use the fallback_time when generating the versioned file name. """ results = parse_results(working_dir._working_dir / results_file) bots = load_all_bots_versioned(working_dir) for result in results: blue_time = fallback_time if result.blue in bots: blue_time = bots[result.blue].updated_date orange_time = fallback_time if result.orange in bots: orange_time = bots[result.orange].updated_date result_path = working_dir.get_version_specific_match_result_from_times( result.blue, blue_time, result.orange, orange_time) print(f'Writing result to {result_path}') result.write(result_path)
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
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
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}\'')
def main(): arguments = docopt(__doc__, version=__version__) settings = PersistentSettings.load() if arguments['setup']: working_dir = Path(arguments['<working_dir>']) working_dir.mkdir(exist_ok=True, parents=True) WorkingDir(working_dir) # Creates relevant directories and files settings.working_dir_raw = f'{working_dir}' settings.save() print(f'Working directory successfully set to \'{working_dir}\'') else: # Following commands require a working dir. Make sure it is set. if settings.working_dir_raw is None: print('No working directory set, use \'autoleagueplay setup <working_dir>\'') sys.exit(0) working_dir = WorkingDir(Path(settings.working_dir_raw)) stale_rematch_threshold = 0 if arguments['--stale-rematch-threshold']: stale_rematch_threshold = int(arguments['--stale-rematch-threshold']) run_strategy = None if arguments['odd']: run_strategy = RunStrategy.ODD elif arguments['even']: run_strategy = RunStrategy.EVEN elif arguments['rolling']: run_strategy = RunStrategy.ROLLING half_robin = False if arguments['--half-robin']: half_robin = True if arguments['leaderboard']: if run_strategy is not None: generate_leaderboard(working_dir, run_strategy, not arguments['--top-only']) elif arguments['clip']: generate_leaderboard_clip(working_dir) elif arguments['symbols']: generate_symbols() elif arguments['legend']: generate_legend(working_dir) else: raise NotImplementedError() elif arguments['run']: replay_preference = ReplayPreference(arguments['--replays']) team_size = int(arguments['--teamsize']) shutdown_time = int(arguments['--autoshutdown']) if not arguments['--ignore-missing']: all_present = check_bot_folder(working_dir, run_strategy) if all_present: run_league_play(working_dir, run_strategy, replay_preference, team_size, shutdown_time, stale_rematch_threshold, half_robin) else: run_league_play(working_dir, run_strategy, replay_preference, team_size, shutdown_time, stale_rematch_threshold, half_robin) elif arguments['bubble']: replay_preference = ReplayPreference(arguments['--replays']) team_size = int(arguments['--teamsize']) run_bubble_sort(working_dir, team_size, replay_preference) elif arguments['list']: list_matches(working_dir, run_strategy, stale_rematch_threshold, half_robin) elif arguments['results']: list_results(working_dir, run_strategy, half_robin) elif arguments['check']: check_bot_folder(working_dir) elif arguments['test']: test_all_bots(working_dir) elif arguments['fetch']: season = int(arguments['<season_num>']) week_num = int(arguments['<week_num>']) ladder = fetch_ladder_from_sheets(season, week_num) ladder.write(working_dir.ladder) print(f'Successfully fetched season {season} week {week_num} to \'{working_dir.ladder}\':') for bot in ladder.bots: print(bot) elif arguments['results-to-version-files']: results_file = arguments['<results_file>'] parse_results_and_write_files(working_dir, working_dir._working_dir / results_file, DEFAULT_TIMESTAMP) elif arguments['unzip']: unzip_all_bots(working_dir) else: raise NotImplementedError()