code: package tracks.multiPlayer.advanced.sampleRHEA;
  
  import core.game.StateObservationMulti;
  import core.player.AbstractMultiPlayer;
  import ontology.Types;
  import tools.ElapsedCpuTimer;
  import tracks.multiPlayer.tools.heuristics.StateHeuristicMulti;
  import tracks.multiPlayer.tools.heuristics.WinScoreHeuristic;
  
  import java.util.*;
  
  @SuppressWarnings("FieldCanBeLocal")
  public class Agent extends AbstractMultiPlayer {
  
      // Parameters
      private int POPULATION_SIZE = 10;
      private int SIMULATION_DEPTH = 10;
      private int CROSSOVER_TYPE = UNIFORM_CROSS;
      private boolean REEVALUATE = false;
      private int MUTATION = 1;
      private int TOURNAMENT_SIZE = 2;
      private int ELITISM = 1;
      private StateHeuristicMulti heuristic;
  
      // Constants
      private final long BREAK_MS = 10;
      public static final double epsilon = 1e-6;
      static final int POINT1_CROSS = 0;
      static final int UNIFORM_CROSS = 1;
  
      // Class vars
      private Individual[] population, nextPop;
      private int NUM_INDIVIDUALS;
      private int[] N_ACTIONS;
      private HashMap<Integer, Types.ACTIONS>[] action_mapping;
      private Random randomGenerator;
  
      // Budget
      private ElapsedCpuTimer timer;
      private double acumTimeTakenEval = 0,avgTimeTakenEval = 0, avgTimeTaken = 0, acumTimeTaken = 0;
      private int numEvals = 0, numIters = 0;
      private boolean keepIterating = true;
      private long remaining;
  
      //Multiplayer game vars
      private int playerID, noPlayers;
  
      /**
       * Public constructor with state observation and time due.
       *
       * @param stateObs     state observation of the current game.
       * @param elapsedTimer Timer for the controller creation.
       */
      public Agent(StateObservationMulti stateObs, ElapsedCpuTimer elapsedTimer, int playerID) {
          randomGenerator = new Random();
          heuristic = new WinScoreHeuristic(stateObs);
          this.timer = elapsedTimer;
  
          // Get multiplayer game parameters
          this.playerID = playerID;
          noPlayers = stateObs.getNoPlayers();
      }
  
      @Override
      public Types.ACTIONS act(StateObservationMulti stateObs, ElapsedCpuTimer elapsedTimer) {
          this.timer = elapsedTimer;
          avgTimeTaken = 0;
          acumTimeTaken = 0;
          numEvals = 0;
          acumTimeTakenEval = 0;
          numIters = 0;
          remaining = timer.remainingTimeMillis();
          NUM_INDIVIDUALS = 0;
          keepIterating = true;
  
          // INITIALISE POPULATION
          init_pop(stateObs);
  
  
          // RUN EVOLUTION
          remaining = timer.remainingTimeMillis();
          while (remaining > avgTimeTaken && remaining > BREAK_MS && keepIterating) {
              runIteration(stateObs);
              remaining = timer.remainingTimeMillis();
          }
  
          // RETURN ACTION
          return get_best_action(population);
      }
  
      /**
       * Run evolutionary process for one generation
       * @param stateObs - current game state
       */
      private void runIteration(StateObservationMulti stateObs) {
          ElapsedCpuTimer elapsedTimerIteration = new ElapsedCpuTimer();
  
          if (REEVALUATE) {
              for (int i = 0; i < ELITISM; i++) {
                  if (remaining > 2*avgTimeTakenEval && remaining > BREAK_MS) { // if enough time to evaluate one more individual
                      evaluate(population[i], heuristic, stateObs);
                  } else {keepIterating = false;}
              }
          }
  
          if (NUM_INDIVIDUALS > 1) {
              for (int i = ELITISM; i < NUM_INDIVIDUALS; i++) {
                  if (remaining > 2*avgTimeTakenEval && remaining > BREAK_MS) { // if enough time to evaluate one more individual
                      Individual newind;
  
                      newind = crossover();
                      newind = newind.mutate(MUTATION);
  
                      // evaluate new individual, insert into population
                      add_individual(newind, nextPop, i, stateObs);
  
                      remaining = timer.remainingTimeMillis();
  
                  } else {keepIterating = false; break;}
              }
              Arrays.sort(nextPop, (o1, o2) -> {
                  if (o1 == null && o2 == null) {
                      return 0;
                  }
                  if (o1 == null) {
                      return 1;
                  }
                  if (o2 == null) {
                      return -1;
                  }
                  return o1.compareTo(o2);
              });
          } else if (NUM_INDIVIDUALS == 1){
              Individual newind = new Individual(SIMULATION_DEPTH, N_ACTIONS[playerID], randomGenerator).mutate(MUTATION);
              evaluate(newind, heuristic, stateObs);
              if (newind.value > population[0].value)
                  nextPop[0] = newind;
          }
  
          population = nextPop.clone();
  
          numIters++;
          acumTimeTaken += (elapsedTimerIteration.elapsedMillis());
          avgTimeTaken = acumTimeTaken / numIters;
      }
  
      /**
       * Evaluates an individual by rolling the current state with the actions in the individual
       * and returning the value of the resulting state; random action chosen for the opponent
       * @param individual - individual to be valued
       * @param heuristic - heuristic to be used for state evaluation
       * @param state - current state, root of rollouts
       * @return - value of last state reached
       */
      private double evaluate(Individual individual, StateHeuristicMulti heuristic, StateObservationMulti state) {
  
          ElapsedCpuTimer elapsedTimerIterationEval = new ElapsedCpuTimer();
  
          StateObservationMulti st = state.copy();
          int i;
          for (i = 0; i < SIMULATION_DEPTH; i++) {
              double acum = 0, avg;
              if (! st.isGameOver()) {
                  ElapsedCpuTimer elapsedTimerIteration = new ElapsedCpuTimer();
  
                  // Multi player advance method
                  Types.ACTIONS[] advanceActs = new Types.ACTIONS[noPlayers];
                  for (int k = 0; k < noPlayers; k++) {
                      if (k == playerID)
                          advanceActs[k] = action_mapping[k].get(individual.actions[i]);
                      else advanceActs[k] = action_mapping[k].get(randomGenerator.nextInt(N_ACTIONS[k]));
                  }
                  st.advance(advanceActs);
  
                  acum += elapsedTimerIteration.elapsedMillis();
                  avg = acum / (i+1);
                  remaining = timer.remainingTimeMillis();
                  if (remaining < 2*avg || remaining < BREAK_MS) break;
              } else {
                  break;
              }
          }
  
          individual.value = heuristic.evaluateState(st, playerID);
  
          numEvals++;
          acumTimeTakenEval += (elapsedTimerIterationEval.elapsedMillis());
          avgTimeTakenEval = acumTimeTakenEval / numEvals;
          remaining = timer.remainingTimeMillis();
  
          return individual.value;
      }
  
      /**
       * @return - the individual resulting from crossover applied to the specified population
       */
      private Individual crossover() {
          Individual newind = null;
          if (NUM_INDIVIDUALS > 1) {
              newind = new Individual(SIMULATION_DEPTH, N_ACTIONS[playerID], randomGenerator);
              Individual[] tournament = new Individual[TOURNAMENT_SIZE];
              ArrayList<Individual> list = new ArrayList<>(Arrays.asList(population));
  
              //Select a number of random distinct individuals for tournament and sort them based on value
              for (int i = 0; i < TOURNAMENT_SIZE; i++) {
                  int index = randomGenerator.nextInt(list.size());
                  tournament[i] = list.get(index);
                  list.remove(index);
              }
              Arrays.sort(tournament);
  
              //get best individuals in tournament as parents
              if (TOURNAMENT_SIZE >= 2) {
                  newind.crossover(tournament[0], tournament[1], CROSSOVER_TYPE);
              } else {
                  System.out.println("WARNING: Number of parents must be LESS than tournament size.");
              }
          }
          return newind;
      }
  
      /**
       * Insert a new individual into the population at the specified position by replacing the old one.
       * @param newind - individual to be inserted into population
       * @param pop - population
       * @param idx - position where individual should be inserted
       * @param stateObs - current game state
       */
      private void add_individual(Individual newind, Individual[] pop, int idx, StateObservationMulti stateObs) {
          evaluate(newind, heuristic, stateObs);
          pop[idx] = newind.copy();
      }
  
      /**
       * Initialize population
       * @param stateObs - current game state
       */
      @SuppressWarnings("unchecked")
      private void init_pop(StateObservationMulti stateObs) {
  
          double remaining = timer.remainingTimeMillis();
  
          N_ACTIONS = new int[noPlayers];
          action_mapping = new HashMap[noPlayers];
          for (int i = 0; i < noPlayers; i++) {
              ArrayList<Types.ACTIONS> actions = stateObs.getAvailableActions(i);
              N_ACTIONS[i] = actions.size() + 1;
              action_mapping[i] = new HashMap<>();
              int k = 0;
              for (Types.ACTIONS action : actions) {
                  action_mapping[i].put(k, action);
                  k++;
              }
              action_mapping[i].put(k, Types.ACTIONS.ACTION_NIL);
          }
  
          population = new Individual[POPULATION_SIZE];
          nextPop = new Individual[POPULATION_SIZE];
          for (int i = 0; i < POPULATION_SIZE; i++) {
              if (i == 0 || remaining > avgTimeTakenEval && remaining > BREAK_MS) {
                  population[i] = new Individual(SIMULATION_DEPTH, N_ACTIONS[playerID], randomGenerator);
                  evaluate(population[i], heuristic, stateObs);
                  remaining = timer.remainingTimeMillis();
                  NUM_INDIVIDUALS = i+1;
              } else {break;}
          }
  
          if (NUM_INDIVIDUALS > 1)
              Arrays.sort(population, (o1, o2) -> {
                  if (o1 == null && o2 == null) {
                      return 0;
                  }
                  if (o1 == null) {
                      return 1;
                  }
                  if (o2 == null) {
                      return -1;
                  }
                  return o1.compareTo(o2);
              });
          for (int i = 0; i < NUM_INDIVIDUALS; i++) {
              if (population[i] != null)
                  nextPop[i] = population[i].copy();
          }
  
      }
  
      /**
       * @param pop - last population obtained after evolution
       * @return - first action of best individual in the population (found at index 0)
       */
      private Types.ACTIONS get_best_action(Individual[] pop) {
          int bestAction = pop[0].actions[0];
          return action_mapping[playerID].get(bestAction);
      }
  
  }
  