hex

hex.git
git clone git://git.lenczewski.org/hex.git
Log | Files | Refs

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