text: """
  Encapsulates the worker which evaluates newly-trained models and picks the best one
  """
  
  import os
  import shutil
  from concurrent.futures import ProcessPoolExecutor, as_completed
  from logging import getLogger
  from multiprocessing import Manager
  from time import sleep
  
  from chess_zero.agent.model_chess import ChessModel
  from chess_zero.agent.player_chess import ChessPlayer
  from chess_zero.config import Config
  from chess_zero.env.chess_env import ChessEnv, Winner
  from chess_zero.lib.data_helper import get_next_generation_model_dirs, pretty_print
  from chess_zero.lib.model_helper import save_as_best_model, load_best_model_weight
  
  logger = getLogger(__name__)
  
  
  def start(config: Config):
      return EvaluateWorker(config).start()
  
  class EvaluateWorker:
      """
      Worker which evaluates trained models and keeps track of the best one
  
      Attributes:
          :ivar Config config: config to use for evaluation
          :ivar PlayConfig config: PlayConfig to use to determine how to play, taken from config.eval.play_config
          :ivar ChessModel current_model: currently chosen best model
          :ivar Manager m: multiprocessing manager
          :ivar list(Connection) cur_pipes: pipes on which the current best ChessModel is listening which will be used to
              make predictions while playing a game.
      """
      def __init__(self, config: Config):
          """
          :param config: Config to use to control how evaluation should work
          """
          self.config = config
          self.play_config = config.eval.play_config
          self.current_model = self.load_current_model()
          self.m = Manager()
          self.cur_pipes = self.m.list([self.current_model.get_pipes(self.play_config.search_threads) for _ in range(self.play_config.max_processes)])
  
      def start(self):
          """
          Start evaluation, endlessly loading the latest models from the directory which stores them and
          checking if they do better than the current model, saving the result in self.current_model
          """
          self.ensure_folder_exists()
  
          while True:
              ng_model, model_dir = self.load_next_generation_model()
              logger.debug(f"start evaluate model {model_dir}")
              ng_is_great = self.evaluate_model(ng_model)
              if ng_is_great:
                  logger.debug(f"New Model become best model: {model_dir}")
                  save_as_best_model(ng_model)
                  self.current_model = ng_model
  
              model_name = model_dir.rpartition('/')[2]
              self.log_eval_result(model_name, ng_is_great)
              self.move_model(model_dir, model_name, ng_is_great)
  
      def evaluate_model(self, ng_model):
          """
          Given a model, evaluates it by playing a bunch of games against the current model.
  
          :param ChessModel ng_model: model to evaluate
          :return: true if this model is better than the current_model
          """
          ng_pipes = self.m.list([ng_model.get_pipes(self.play_config.search_threads) for _ in range(self.play_config.max_processes)])
  
          futures = []
          with ProcessPoolExecutor(max_workers=self.play_config.max_processes) as executor:
              for game_idx in range(self.config.eval.game_num):
                  fut = executor.submit(play_game, self.config, cur=self.cur_pipes, ng=ng_pipes, current_white=(game_idx % 2 == 0))
                  futures.append(fut)
  
              results = []
              for fut in as_completed(futures):
                  # ng_score := if ng_model win -> 1, lose -> 0, draw -> 0.5
                  ng_score, env, current_white = fut.result()
                  results.append(ng_score)
                  win_rate = sum(results) / len(results)
                  game_idx = len(results)
                  logger.debug(f"game {game_idx:3}: ng_score={ng_score:.1f} as {'black' if current_white else 'white'} "
                               f"{'by resign ' if env.resigned else '          '}"
                               f"win_rate={win_rate*100:5.1f}% "
                               f"{env.board.fen().split(' ')[0]}")
  
                  colors = ("current_model", "ng_model")
                  if not current_white:
                      colors = reversed(colors)
                  pretty_print(env, colors)
  
          win_rate = sum(results) / len(results)
          logger.debug(f"winning rate {win_rate*100:.1f}%")
          return win_rate >= self.config.eval.replace_rate
  
      def ensure_folder_and_files_exists(self):
          better_dir = os.path.join(rc.model_dir, "better")
          if not os.path.isdir(copy_dir):
              os.makedirs(copy_dir)
  
          worse_dir = os.path.join(rc.model_dir, "worse")
          if not os.path.isdir(copy_dir):
              os.makedirs(copy_dir)
  
          eval_results = os.path.join(rc.model_dir, "results.txt")
          if not os.path.isfile(eval_results):
              f = open(eval_results, "w+")
              f.close
  
      def log_eval_result(self, model_name, model_is_better):
          rc = self.config.resource
  
          eval_results_file = os.path.join(rc.model_dir, "results.txt")
          with open(eval_results_file, "a+") as f:
              if model_is_better:
                  f.write("{0:<30} {1}".format(model_name, "better"))
              else:
                  f.write("{0:<30} {1}".format(model_name, "worse"))
  
  
  
      def move_model(self, model_dir, model_name, model_is_better):
          rc = self.config.resource
  
          dir_to_move = ''
          if model_is_better:
              dir_to_move = os.path.join(rc.model_dir, "better")
          else:
              dir_to_move = os.path.join(rc.model_dir, "worse")
  
          new_dir = os.path.join(dir_to_move, model_name)
          self.move_tree(model_dir, new_dir)
  
      def move_tree(self, src, dst):
          if not os.path.exists(dst):
              os.makedirs(dst)
  
          files = os.listdir(src)
          for file in files:
              new_path = shutil.move(f"{src}/{file}", f"{dst}/{file}")
          shutil.rmtree(src)
  
      def load_current_model(self):
          """
          Loads the best model from the standard directory.
          :return ChessModel: the model
          """
          model = ChessModel(self.config)
          load_best_model_weight(model)
          return model
  
      def load_next_generation_model(self):
          """
          Loads the next generation model from the standard directory
          :return (ChessModel, file): the model and the directory that it was in
          """
          rc = self.config.resource
          while True:
              dirs = get_next_generation_model_dirs(self.config.resource)
              if dirs:
                  break
              logger.info("There is no next generation model to evaluate")
              sleep(60)
          model_dir = dirs[-1] if self.config.eval.evaluate_latest_first else dirs[0]
          config_path = os.path.join(model_dir, rc.next_generation_model_config_filename)
          weight_path = os.path.join(model_dir, rc.next_generation_model_weight_filename)
          model = ChessModel(self.config)
          model.load(config_path, weight_path)
          return model, model_dir
  
  
  def play_game(config, cur, ng, current_white: bool) -> (float, ChessEnv, bool):
      """
      Plays a game against models cur and ng and reports the results.
  
      :param Config config: config for how to play the game
      :param ChessModel cur: should be the current model
      :param ChessModel ng: should be the next generation model
      :param bool current_white: whether cur should play white or black
      :return (float, ChessEnv, bool): the score for the ng model
          (0 for loss, .5 for draw, 1 for win), the env after the game is finished, and a bool
          which is true iff cur played as white in that game.
      """
      cur_pipes = cur.pop()
      ng_pipes = ng.pop()
      env = ChessEnv().reset()
  
      current_player = ChessPlayer(config, pipes=cur_pipes, play_config=config.eval.play_config)
      ng_player = ChessPlayer(config, pipes=ng_pipes, play_config=config.eval.play_config)
      if current_white:
          white, black = current_player, ng_player
      else:
          white, black = ng_player, current_player
  
      while not env.done:
          if env.white_to_move:
              action = white.action(env)
          else:
              action = black.action(env)
          env.step(action)
          if env.num_halfmoves >= config.eval.max_game_length:
              env.adjudicate()
  
      if env.winner == Winner.draw:
          ng_score = 0.5
      elif env.white_won == current_white:
          ng_score = 0
      else:
          ng_score = 1
      cur.append(cur_pipes)
      ng.append(ng_pipes)
      return ng_score, env, current_white
  