Skip to content

Commit

Permalink
Wait for slow agents to initialise for up-to 3 sec. Report winner_id=…
Browse files Browse the repository at this point in the history
…None if tie
  • Loading branch information
abbyssoul committed Dec 17, 2020
1 parent 6ed13dc commit db81855
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 29 deletions.
54 changes: 39 additions & 15 deletions coderone/dungeon/agent_driver/multiproc_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,57 @@ 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))

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):
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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 = []
Expand All @@ -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)
Expand All @@ -153,6 +176,7 @@ def agent(self):

self._workers.append(worker)
self._proxies.append(proxy)

return proxy

def __enter__(self):
Expand Down
9 changes: 7 additions & 2 deletions coderone/dungeon/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down
36 changes: 26 additions & 10 deletions coderone/dungeon/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import argparse
import os
import sys
import time
import logging
import jsonplus
from contextlib import ExitStack
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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')
Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -30,7 +30,6 @@
python_requires='>=3.6',
entry_points = {
'console_scripts': [
'coderone=coderone.cli:main',
'coderone-dungeon=coderone.dungeon.main:main'
],
},
Expand Down

0 comments on commit db81855

Please sign in to comment.