def verbose_callback(game_state, action, active_player, active_idx, match_id, time_taken): if game_state.ply_count % 2 == 0 or game_state.terminal_test(): # print every other move, plus endgame summary = "\nmatch: {} | move: {} | {:.2f}s | {}({}) => {}".format( match_id, game_state.ply_count, time_taken, active_player.__class__.__name__, active_idx, DebugState.ind2xy(action) ) board = str(DebugState.from_state(game_state)) print(summary); logger.info(summary) print(board); logger.info(board)
def log_results(agents, scores, match_id, winner, start_time, args={}): if args.get('progress'): print('+' if winner == agents[0] else '-', end='', flush=True) logging = args.get('logging', 100) if (logging != 0 and match_id % logging == 0 or match_id != 0 and match_id == args.get('rounds')): total_average = 100 * sum(scores[agents[0]]) / len(scores[agents[0]]) running_average = 100 * sum( 2 * (i + 0.5) * score for i, score in enumerate(scores[agents[0]])) / len( scores[agents[0]])**2 message = " match_id: {:4d} | {:3.0f}s | {:3.0f}% -> {:3.0f}% | {} vs {}".format( match_id, time.perf_counter() - start_time, total_average, running_average, agents[0].name, agents[1].name, ) print(message) logger.info(message)
def play_sync( agents: Tuple[Agent, Agent], game_state=None, # defaults to Isolation() time_limit=TIME_LIMIT, match_id=0, debug=False, # disables the signal timeout logging=True, verbose=False, # prints an ASCII copy of the board after each turn exceptions=False, max_moves=0, # end the game early after a set number of turns callbacks: List[Callable] = None, **kwargs): gc.collect( 1 ) # reduce chance of TimeoutError in call_with_timeout_ms() | gc.collect(2) is an expensive function agents = tuple( Agent(agent, agent.__class__.name ) if not isinstance(agent, Agent) else agent for agent in agents) players = tuple(a.agent_class(player_id=i) for i, a in enumerate(agents)) game_state = game_state or Isolation() initial_state = game_state active_idx = 0 winner = None loser = None status = Status.NORMAL game_history = [] callbacks = copy(callbacks) or [] if logging: logger.info(GAME_INFO.format(initial_state, *agents)) while not game_state.terminal_test(): if max_moves and game_state.ply_count >= max_moves: break turn_start = time.perf_counter() active_idx = game_state.player() active_player = players[active_idx] winner, loser = agents[1 - active_idx], agents[ active_idx] # any problems during get_action means the active player loses action = None active_player.queue = LifoQueue() # we don't need a TimeoutQueue here try: if time_limit == 0 or debug: active_player.get_action(game_state) action = active_player.queue.get( block=False) # raises Empty if agent did not respond else: # increment timeout 2x before throwing exception - MinimaxAgent occasionally takes longer than 150ms for i in [1, 2]: exception = call_with_timeout_ms(i * time_limit, active_player.get_action, game_state) if not active_player.queue.empty(): action = active_player.queue.get( block=False ) # raises Empty if agent did not respond break # accept answer generated after minimum timeout if exceptions and action is None and exception == TimeoutError: print(active_player) raise TimeoutError except KeyboardInterrupt: raise KeyboardInterrupt except Exception as err: status = Status.EXCEPTION if exceptions: logger.error( ERR_INFO.format(err, initial_state, agents[0], agents[1], game_state, game_history)) traceback.print_exception(type(err), err, err.__traceback__) break finally: if time_limit and not debug: signal.signal(signal.SIGPROF, signal.SIG_IGN) # Unregister the timeout signal if action not in game_state.actions(): status = Status.INVALID_MOVE if exceptions: print( ERR_INFO.format('INVALID_MOVE', initial_state, agents[0], agents[1], game_state, game_history)) logger.error( ERR_INFO.format('INVALID_MOVE', initial_state, agents[0], agents[1], game_state, game_history)) break time_taken = time.perf_counter() - turn_start game_state = game_state.result(action) game_history.append(action) # Callbacks can be used to hook in additional functionality after each turn, such as verbose rendering # BUGFIX: don't modify callbacks, else the board position will be repeated multiple times per turn turn_callbacks = list(callbacks) if isinstance(callbacks, (tuple, list, set)) else [callbacks] if verbose: turn_callbacks = [verbose_callback] + callbacks for callback in turn_callbacks: if not callable(callback): continue callback(game_state=game_state, action=action, active_player=active_player, active_idx=active_idx, match_id=match_id, time_taken=time_taken) else: status = Status.GAME_OVER if game_state.utility(active_idx) > 0: winner, loser = loser, winner # swap winner/loser if active player won if logging: logger.info( RESULT_INFO.format(status, game_state, game_history, winner, loser)) return winner, game_history, match_id