From ad0caefbdc62d88987461f7c4f0dbcf824a336b6 Mon Sep 17 00:00:00 2001 From: lladdy Date: Fri, 4 Aug 2023 16:38:25 +0930 Subject: [PATCH] feat: add multithread runner example --- .gitignore | 3 +- README.md | 7 ++ docker-compose-multithread-example.yml | 46 +++++++++ multithread-example.py | 130 +++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 docker-compose-multithread-example.yml create mode 100644 multithread-example.py diff --git a/.gitignore b/.gitignore index 2df0c2d..00d1111 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .idea/ -# files created by the arena client results -client.log \ No newline at end of file +runners \ No newline at end of file diff --git a/README.md b/README.md index 66004f7..48b2677 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,13 @@ You can run the test match by executing `docker-compose up` in the base folder o 3. Run `docker compose up` to run the matches. 4. View results in the `results.json` file and replays in the `replays` folder. +### Multi-threaded matches + +Refer to [multithread-example.py](./multithread-example.py) for an example of how to run multiple matches in parallel. + +Not that there are aspects of bot games that would need more work to be thread safe, +such as bots which save data to their data folder. + ## Troubleshooting All container and bot logs can be found in the `logs` folder. diff --git a/docker-compose-multithread-example.yml b/docker-compose-multithread-example.yml new file mode 100644 index 0000000..f78c30e --- /dev/null +++ b/docker-compose-multithread-example.yml @@ -0,0 +1,46 @@ +version: "3.9" +services: + sc2_controller: + image: aiarena/arenaclient-sc2:v0.4.3 + environment: + - "ACSC2_PORT=8083" + - "ACSC2_PROXY_HOST=proxy_controller" + volumes: + - "./runners/${COMPOSE_PROJECT_NAME}/logs:/logs" # a sc2_controller folder will be created in the logs folder + # SC2 Maps Path + # Set this as "- PATH_TO_YOUR_MAPS_FOLDER:/root/StarCraftII/maps" +# - C:\Program Files (x86)\StarCraft II\Maps:/root/StarCraftII/maps # Standard windows SC2 maps path + - ./maps:/root/StarCraftII/maps # Local maps folder +# - ~/StarCraftII/maps:/root/StarCraftII/maps # Relatively standard linux SC2 maps path + + bot_controller1: + image: aiarena/arenaclient-bot:v0.4.3 + volumes: + - "./bots:/bots" + - "./runners/${COMPOSE_PROJECT_NAME}/logs/bot_controller1:/logs" + environment: + - "ACBOT_PORT=8081" + - "ACBOT_PROXY_HOST=proxy_controller" + + bot_controller2: + image: aiarena/arenaclient-bot:v0.4.3 + volumes: + - "./bots:/bots" + - "./runners/${COMPOSE_PROJECT_NAME}/logs/bot_controller2:/logs" + environment: + - "ACBOT_PORT=8082" + - "ACBOT_PROXY_HOST=proxy_controller" + + proxy_controller: + image: aiarena/arenaclient-proxy:v0.4.3 + environment: + - "ACPROXY_PORT=8080" + - "ACPROXY_BOT_CONT_1_HOST=bot_controller1" + - "ACPROXY_BOT_CONT_2_HOST=bot_controller2" + - "ACPROXY_SC2_CONT_HOST=sc2_controller" + volumes: + - "./runners/${COMPOSE_PROJECT_NAME}/matches:/app/matches" + - "./config.toml:/app/config.toml" + - "./results.json:/app/results.json" + - "./runners/${COMPOSE_PROJECT_NAME}/replays:/replays" + - "./runners/${COMPOSE_PROJECT_NAME}/logs:/logs" # a proxy_controller folder will be created in the logs folder diff --git a/multithread-example.py b/multithread-example.py new file mode 100644 index 0000000..9ee38c3 --- /dev/null +++ b/multithread-example.py @@ -0,0 +1,130 @@ +import os +import random +import shutil +import subprocess +from multiprocessing.dummy import Pool as ThreadPool + +from loguru import logger + +################### +# RUNNER SETTINGS # +################### + +# all the runner files will be inside this path +root_runners_path = f"./runners/" + +# if True, delete all the runner files before starting +clean_run = True + +# In case you want to use a different docker-compose file +docker_compose_file = "./docker-compose-multithread-example.yml" + +# run this many matches at a time +num_runners = 3 + +######################### +# MATCH GENERATION CODE # +######################### + +# This is an example of how to generate matches + +bot = ["basic_bot", "T", "python"] +opponents = [ + ["loser_bot", "T", "python"], + ["loser_bot", "T", "python"], + ["loser_bot", "T", "python"], +] +map_list = ["BerlingradAIE"] +num_games = len(opponents) + + +def get_matches_to_play(): + """ + Returns a list of matches to play + Edit this function to generate matches your preferred way. + """ + matches = [] + for x in range(num_games): + # we add 1 to x because we want the runner id to start at 1 + runner_id = x + 1 + map = random.choice(map_list) + opponent = opponents[x % len(opponents)] + matches.append((runner_id, bot, opponent, map)) + return matches + + +######################################################## +# Hopefully you shouldn't need to edit below this line # +######################################################## + +def play_game(match): + try: + # prepare the match runner + runner_id = match[0] + logger.info(f"[{runner_id}] {match[1][0]}vs{match[2][0]} on {match[3]} starting") + runner_dir = prepare_runner_dir(runner_id) + prepare_matches_and_results_files(match, runner_dir) + + # start the match running + command = f'docker compose -p {runner_id} -f {docker_compose_file} up' + subprocess.Popen( + command, + shell=True, + ).communicate() + + except Exception as error: + logger.error("[ERROR] {0}".format(str(error))) + + +def prepare_matches_and_results_files(match, runner_dir): + file = open(f"{runner_dir}/matches", "w") + # match[1][0] and match[2][1] are twice, because we re-use the bot name as the bot id + file.write( + f"{match[1][0]},{match[1][0]},{match[1][1]},{match[1][2]}," # bot1 + f"{match[2][0]},{match[2][0]},{match[2][1]},{match[2][2]}," # bot2 + f"{match[3]}") # map + file.close() + + # touch results.json + file = open(f"{runner_dir}/results.json", "w") + file.close() + + +def handleRemoveReadonly(func, path, exc): + import stat + if not os.access(path, os.W_OK): + # Is the error an access error ? + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + +def prepare_runner_dir(dir_name) -> str: + runner_dir = f"{root_runners_path}/{dir_name}" + if not os.path.exists(runner_dir): + os.makedirs(runner_dir) + return runner_dir + + +def prepare_root_dir(): + if os.path.exists(root_runners_path) and clean_run: + shutil.rmtree(root_runners_path, onerror=handleRemoveReadonly) + if not os.path.exists(root_runners_path): + os.makedirs(root_runners_path) + + +def main(): + pool = ThreadPool(num_runners) + + prepare_root_dir() + + matches = get_matches_to_play() + + pool.map(play_game, matches) + pool.close() + pool.join() + + +if __name__ == "__main__": + main()