Skip to content

Commit

Permalink
Merge pull request #13 from aiarena/feature/multithread-run-example
Browse files Browse the repository at this point in the history
Add multithread runner example
  • Loading branch information
lladdy committed Aug 4, 2023
2 parents e5099bf + ad0caef commit 8210315
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 2 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.idea/

# files created by the arena client
results
client.log
runners
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
46 changes: 46 additions & 0 deletions docker-compose-multithread-example.yml
Original file line number Diff line number Diff line change
@@ -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
130 changes: 130 additions & 0 deletions multithread-example.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 8210315

Please sign in to comment.