From db818555d5a1e221ea96cb5a2b05fde90f36ca32 Mon Sep 17 00:00:00 2001 From: abbyssoul Date: Thu, 17 Dec 2020 15:38:00 +1100 Subject: [PATCH] Wait for slow agents to initialise for up-to 3 sec. Report winner_id=None if tie --- .../dungeon/agent_driver/multiproc_driver.py | 54 +++++++++++++------ coderone/dungeon/game.py | 9 +++- coderone/dungeon/main.py | 36 +++++++++---- setup.py | 3 +- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/coderone/dungeon/agent_driver/multiproc_driver.py b/coderone/dungeon/agent_driver/multiproc_driver.py index f4ecf97..9cd03e0 100644 --- a/coderone/dungeon/agent_driver/multiproc_driver.py +++ b/coderone/dungeon/agent_driver/multiproc_driver.py @@ -21,19 +21,50 @@ def __init__(self, game=None, player=None): self.game = game self.player = player +class AgentReady: + pass + class AgentProxy(Agent): + MAX_READY_SPAM = 3 + def __init__(self, task_queue, result_queue, name:str): logger.debug("Creating multiproc agent proxy for %s", name) self.name = name self.task_queue = task_queue self.result_queue = result_queue self.silenced = False + + self.__is_ready = False + self.__last_move = None + + @property + def is_ready(self): + if not self.__is_ready: + # Pick a message: + agent_message = self.result_queue.get_nowait() if not self.result_queue.empty() else None + if isinstance(agent_message, AgentReady): + self.__is_ready = True + else: + self.__last_move = agent_message + + return self.__is_ready + + def stop(self): + # Put a poison pill to signal the stop to the agent driver self.task_queue.put(None) def next_move(self): - return self.result_queue.get_nowait() if not self.result_queue.empty() else None + for _ in range(self.MAX_READY_SPAM): # Give agent at most MAX_READY_SPAM attempts to report ready_state and start moving + agent_message = self.result_queue.get_nowait() if not self.result_queue.empty() else None + if isinstance(agent_message, AgentReady): + self.__is_ready = True + continue + + return agent_message + + return None def update(self, game_state:GameState, player_state:PlayerState): self.task_queue.put_nowait(StateUpdate(game=game_state, player=player_state)) @@ -41,18 +72,6 @@ def update(self, game_state:GameState, player_state:PlayerState): def on_game_over(self, game_state:GameState, player_state:PlayerState): self.task_queue.put_nowait(GameOver(game=game_state, player=player_state)) - # def next_move(self, game_map, game_state): - # try: - # move = self.result_queue.get_nowait() if not self.result_queue.empty() else None - # self.task_queue.put_nowait(StateUpdate(game=game_map, state=game_state)) - # return move - # except Exception as e: - # #self.agent = None # Stop existing agent untill the module is fixed - # if not self.silenced: - # # self.silenced = True - # logger.error(f"Agent '{self.name}' error: {e}", exc_info=True) - # return None - class Consumer(multiprocessing.Process): def __init__(self, task_queue, result_queue, module_name:str, watch:bool, config): @@ -90,6 +109,9 @@ def run(self): try: agent = driver.agent() + # Report agent-ready status: + self.result_queue.put(AgentReady()) + time_posted = time.time() while self.is_not_done: while not self.task_queue.empty(): @@ -122,10 +144,11 @@ def run(self): class Driver: - JOIN_TIMEOUT_SEC = 1 + JOIN_TIMEOUT_SEC = 5 def __init__(self, name:str, watch: bool = False, config={}): self.name = name + self.is_ready = False self.watch = watch self.config = config self._proxies = [] @@ -143,7 +166,7 @@ def stop(self): logger.warn(f"process for agent '{self.name}' has not finished gracefully. Terminating") w.terminate() - def agent(self): + def agent(self) -> AgentProxy: tasks_queue = multiprocessing.Queue() agent_result_queue = multiprocessing.Queue() proxy = AgentProxy(tasks_queue, agent_result_queue, self.name) @@ -153,6 +176,7 @@ def agent(self): self._workers.append(worker) self._proxies.append(proxy) + return proxy def __enter__(self): diff --git a/coderone/dungeon/game.py b/coderone/dungeon/game.py index 1a4a5d4..8c1bafc 100644 --- a/coderone/dungeon/game.py +++ b/coderone/dungeon/game.py @@ -392,6 +392,7 @@ def tick(self, dt:float): # Evaluate game termination rules if not self.is_over: over_iter_limit = True if self.max_iterations and self.tick_counter > self.max_iterations else False + # There are opponents, if there are more then 1 player still alive has_opponents = sum(p.is_alive for p in self.players.values()) > 1 # Game is over when there is at most 1 player left or @@ -400,8 +401,12 @@ def tick(self, dt:float): if self.is_over: # Picking winners: last player standing or highest scoring corps - self.winner = sorted(self.players.items(), key=lambda item: item[1].reward)[-1] if has_opponents else \ - next(((pid,p) for pid,p in self.players.items() if p.is_alive), None) + high_scores = sorted(self.players.items(), key=lambda pid_player: pid_player[1].reward) + score_range = high_scores[-1][1].reward - high_scores[0][1].reward + if has_opponents: # TODO: Tie only possible if the range of scores is 0 + self.winner = high_scores[-1] if score_range != 0 else None + else: + self.winner = next(((pid,p) for pid,p in self.players.items() if p.is_alive), None) game_state = self._serialize_state() diff --git a/coderone/dungeon/main.py b/coderone/dungeon/main.py index f9ae17f..0ababaa 100755 --- a/coderone/dungeon/main.py +++ b/coderone/dungeon/main.py @@ -7,6 +7,7 @@ import argparse import os import sys +import time import logging import jsonplus from contextlib import ExitStack @@ -15,8 +16,8 @@ from appdirs import user_config_dir from .game_recorder import FileRecorder, Recorder -# from coderone.dungeon.agent_driver.simple_driver import Driver -from .agent_driver.multiproc_driver import Driver +# from coderone.dungeon.agent_driver.simple_driver import Driver, AgentProxy +from .agent_driver.multiproc_driver import Driver, AgentProxy from .game import Game @@ -28,7 +29,8 @@ SCREEN_TITLE = "Coder One: Dungeons & Data Structures" - +AGENT_READY_WAIT_TIMEOUT = 3 +AGENT_READY_WAIT_SEC = 1 # Max numbre of sec to wait for agent to become ready. TICK_STEP = 0.1 # Number of seconds per 1 iteration of game loop ITERATION_LIMIT = 180*10 # Max number of iteration the game should go on for, None for unlimited @@ -118,7 +120,7 @@ def _prepare_import(path): return ".".join(module_name[::-1]) -def __load_agent_drivers(cntx: ExitStack, agent_modules, config:dict, watch=False): +def __load_agent_drivers(cntx: ExitStack, agent_modules, config:dict, watch=False) -> List[Driver]: agents = [] n_agents = len(agent_modules) @@ -140,6 +142,7 @@ def __load_agent_drivers(cntx: ExitStack, agent_modules, config:dict, watch=Fals class TooManyPlayers(Exception): pass + def run(agent_modules, player_names, config=None, recorder=None, watch=False): # Create a new game row_count = config.get('rows') @@ -153,25 +156,38 @@ def run(agent_modules, player_names, config=None, recorder=None, watch=False): if max_players < len(agent_modules): raise TooManyPlayers(f"Game map ({column_count}x{row_count}) supports at most {max_players} players while {len(agent_modules)} agent requested.") - # Load agent modules with ExitStack() as stack: - agents = __load_agent_drivers(stack, agent_modules, watch=watch, config=config) - if not agents: + agent_drivers = __load_agent_drivers(stack, agent_modules, watch=watch, config=config) + if not agent_drivers: return None # Exiting with an error, no contest game = Game(row_count=row_count, column_count=column_count, max_iterations=iteration_limit, recorder=recorder) # Add all agents to the game + agents: List[AgentProxy] = [] names_len = len(player_names) if player_names else 0 - for i, agent_driver in enumerate(agents): - game.add_agent(agent_driver.agent(), player_names[i] if i < names_len else agent_driver.name) + for i, agent_driver in enumerate(agent_drivers): + agent = agent_driver.agent() + agents.append(agent) + game.add_agent(agent, player_names[i] if i < names_len else agent_driver.name) # Add a player for the user if running in interactive mode or configured interactive user_pid = game.add_player("Player") if is_interactive else None - game.generate_map() + wait_time = AGENT_READY_WAIT_TIMEOUT + time.sleep(0.1) # Yeld to sub-processes a chance to start and initialise agents + agents_not_ready = [a.name for a in agents if not a.is_ready] + while agents_not_ready and wait_time > 0: + logger.info(f"Waiting for slowpoke agents [{wait_time} sec]: {agents_not_ready}") + time.sleep(AGENT_READY_WAIT_SEC) + wait_time -= AGENT_READY_WAIT_SEC + agents_not_ready = [a.name for a in agents if not a.is_ready] + + if agents_not_ready: + logger.info(f"Agents {agents_not_ready} are still not ready even after {AGENT_READY_WAIT_TIMEOUT}sec. Starting the match anyways") + tick_step = config.get('tick_step') if config.get('headless'): from .headless_client import Client diff --git a/setup.py b/setup.py index 193452d..b8406a8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='coderone-challenge-dungeon', - version='0.1.5', + version='0.1.6', description='Dungeons and data structures: Coder one AI Game Tournament', url='https://github.com/gocoderone/dungeons-and-data-structures', author='Ivan Ryabov', @@ -30,7 +30,6 @@ python_requires='>=3.6', entry_points = { 'console_scripts': [ - 'coderone=coderone.cli:main', 'coderone-dungeon=coderone.dungeon.main:main' ], },