tournament-host.py (5262B)
1 #!/usr/bin/env python3 2 3 import argparse 4 import asyncio 5 import csv 6 import itertools 7 import os 8 import pwd 9 import re 10 import sys 11 import time 12 13 14 HEX_SERVER_PROGRAM = os.path.abspath('./server/bin/hex-server') 15 16 HEX_AGENT_USERS = [ 17 ent.pw_name for ent in pwd.getpwall() if re.match('hex-agent-[0-9]+$', ent.pw_name) 18 ] 19 20 if not HEX_AGENT_USERS: 21 raise Exception('No hex agent runners found. Please run install.sh first') 22 23 HEX_AGENT_UIDS = [ 24 str(pwd.getpwnam(user).pw_uid) for user in HEX_AGENT_USERS 25 ] 26 27 28 arg_parser = argparse.ArgumentParser(prog='tournament-host', 29 description='Helper script for hosting Hex tournaments.') 30 31 arg_parser.add_argument('schedule_file', 32 type=argparse.FileType('r'), 33 help='schedule file of agent-pairs to define the tournament schedule') 34 arg_parser.add_argument('output_file', 35 type=argparse.FileType('w'), 36 help='output file to write final tournament statistics to') 37 arg_parser.add_argument('--concurrent-matches', 38 default=1, 39 choices=[1,2,3,4,5,6,7,8], 40 type=int, 41 help='number of matches that can occur concurrently') 42 arg_parser.add_argument('-d', '--dimension', 43 default=11, 44 type=int, 45 help='size of the hex board') 46 arg_parser.add_argument('-s', '--seconds', 47 default=300, 48 type=int, 49 help='per-agent game timer length (seconds)') 50 arg_parser.add_argument('-t', '--threads', 51 default=4, 52 type=int, 53 help='per-agent thread hard-limit') 54 arg_parser.add_argument('-m', '--memory', 55 default=1024, 56 type=int, 57 help='per-agent memory hard-limit (MiB)') 58 arg_parser.add_argument('-v', '--verbose', 59 default=False, 60 action='store_true', 61 help='enabling verbose logging for the server') 62 63 64 def log(string): 65 print(f'[tournament-host] {string}') 66 67 68 async def game(sem, args, agent_pair, uid_pool): 69 ''' 70 Plays a single game using the hex-server program, between the given pair 71 of user agents and taking 2 uids from the current pool. 72 ''' 73 74 await sem.acquire() # wait until we can play another (potentially concurrent) game 75 76 agent1, agent2 = agent_pair 77 agent1_uid = uid_pool.pop(0) 78 agent2_uid = uid_pool.pop(0) 79 80 log(f'Starting game between {agent1} (uid: {agent1_uid}) and {agent2} (uid: {agent2_uid}) ...') 81 82 proc = await asyncio.create_subprocess_exec( 83 HEX_SERVER_PROGRAM, 84 '-a', agent1, '-A', agent1_uid, 85 '-b', agent2, '-B', agent2_uid, 86 '-d', str(args.dimension), 87 '-s', str(args.seconds), 88 '-t', str(args.threads), 89 '-m', str(args.memory), 90 '-v' if args.verbose else '', 91 stdout=asyncio.subprocess.PIPE) 92 93 stdout, _ = await proc.communicate() 94 output = stdout.decode() 95 csv_rows = [ 96 [e.strip() for e in row.split(',') if len(e)] for row in output.split('\n') if len(row) 97 ] 98 99 uid_pool.append(agent1_uid) 100 uid_pool.append(agent2_uid) 101 102 sem.release() 103 104 return dict(zip(*csv_rows)) 105 106 107 async def tournament(args, schedule): 108 ''' 109 Play an entire tournament, using the given parsed args and the given 110 agent-pair schedule. 111 ''' 112 113 sem = asyncio.BoundedSemaphore(args.concurrent_matches) # limits concurrent matches 114 uid_pool = [uid for uid in HEX_AGENT_UIDS] 115 116 log(f'Starting tournament with {args.concurrent_matches} concurrent games...') 117 118 start = time.time() 119 120 tasks = [asyncio.create_task(game(sem, args, pair, uid_pool)) for pair in schedule] 121 results = await asyncio.gather(*tasks) 122 123 end = time.time() 124 125 log(f'Finished tournament in {end - start:.03f} seconds') 126 127 return results 128 129 130 def main(): 131 if not os.path.exists(HEX_SERVER_PROGRAM): 132 print(f'Failed to find server executable: {HEX_SERVER_PROGRAM}. Ensure it exists (run Make?) before attempting a tournament', file=sys.stderr) 133 quit(1) 134 135 args = arg_parser.parse_args() 136 137 schedule = [] 138 with args.schedule_file as f: 139 for raw_line in f.read().strip().split('\n'): 140 line = raw_line.strip() 141 if len(line) == 0: continue # skip empty lines 142 if line[0] == '#': continue # skip commented lines 143 elif ',' in line: schedule.append(line.split(',')[:2]) 144 145 results = asyncio.run(tournament(args, schedule)) 146 147 fields = [ 148 'agent_1', 'agent_1_won', 'agent_1_rounds', 'agent_1_secs', 'agent_1_err', 'agent_1_logfile', 149 'agent_2', 'agent_2_won', 'agent_2_rounds', 'agent_2_secs', 'agent_2_err', 'agent_2_logfile', 150 ] 151 152 fields_hdr = ','.join(fields) 153 154 with args.output_file as f: 155 f.write(f'game,{fields_hdr},\n') 156 for i, res in enumerate(results): 157 fields_row = ','.join(map(lambda f: str(res[f]), fields)) 158 f.write(f'{i},{fields_row},\n') 159 160 161 if __name__ == '__main__': 162 main() 163