package org.drooms.impl.logic; import org.drooms.api.Action; import org.drooms.api.GameProgressListener; import org.drooms.api.Player; import org.drooms.api.Playground; import org.drooms.impl.GameController; import org.drooms.impl.logic.commands.Command; import org.drooms.impl.logic.commands.DeactivatePlayerCommand; import org.drooms.impl.util.GameProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.*; import java.util.concurrent.*; /** * Receives state changes ({@link Command}s) from the {@link GameController} and * distributes them to all the player strategies ({@link DecisionMaker} to * process them and make {@link Action} decisions on them. */ public class CommandDistributor { private static final Logger LOGGER = LoggerFactory.getLogger(CommandDistributor.class); private final Map<Player, DecisionMaker> players = new LinkedHashMap<>(); private final List<GameProgressListener> listeners = new LinkedList<>(); private final int playerTimeoutInSeconds; private final ExecutorService e = Executors.newFixedThreadPool(1); private final List<Command> commands = new LinkedList<>(); /** * Initialize the class. * * @param playground * The playground on which the game is happening. * @param players * The players taking part in the game. * @param report * The game listener. * @param properties * Configuration of the game. * @param reportFolder * Where to report to. * @param playerTimeoutInSeconds * How much time the player strategies should be given to make * move decisions. */ public CommandDistributor(final Playground playground, final Collection<Player> players, final GameProgressListener report, final GameProperties properties, final File reportFolder, final int playerTimeoutInSeconds) { players.forEach(player -> { this.players.put(player, new DecisionMaker(playground, player, properties, reportFolder)); }); this.listeners.add(report); this.playerTimeoutInSeconds = playerTimeoutInSeconds; } /** * Add another listener. * * @param listener Listener to be added. * @return True if added, false if already added. */ public boolean addListener(final GameProgressListener listener) { if (!this.listeners.contains(listener)) { this.listeners.add(listener); return true; } else { return false; } } /** * Execute the commands. * * @return Strategy decisions. */ public Map<Player, Action> execute() { // hint GC to potentially not interrupt decision making later System.gc(); CommandDistributor.LOGGER.info("Starting processing next turn."); this.listeners.forEach(listener -> listener.nextTurn()); this.commands.forEach(command -> { CommandDistributor.LOGGER.info("Will process command: {}", command); this.listeners.forEach(listener -> command.report(listener)); }); CommandDistributor.LOGGER.info("Now passing these changes to players."); final Map<Player, Action> moves = new HashMap<>(); this.players.forEach((player, decisionMaker) -> { CommandDistributor.LOGGER.debug("Processing player {}.", player.getName()); // send commands to the player's strategy this.commands.forEach(command -> command.perform(decisionMaker)); decisionMaker.commit(); // begin the time-box for a player strategy to make decisions CommandDistributor.LOGGER.debug("Starting time-box for player {}.", player.getName()); final Future<Action> move = this.e.submit(decisionMaker); try { moves.put(player, move.get(this.playerTimeoutInSeconds, TimeUnit.SECONDS)); } catch (InterruptedException | ExecutionException e) { CommandDistributor.LOGGER.warn("Player {} error during decision-making, STAY forced.", player.getName(), e); moves.put(player, Action.NOTHING); } catch (final TimeoutException e) { CommandDistributor.LOGGER.info("Player {} didn't reach a decision in time, STAY forced.", player.getName()); moves.put(player, Action.NOTHING); } finally { move.cancel(true); decisionMaker.halt(); // otherwise other players' could be slowed down } // end the time-box for a player strategy CommandDistributor.LOGGER.debug("Player {} processed.", player.getName()); }); commands.clear(); CommandDistributor.LOGGER.info("Turn processed completely."); return Collections.unmodifiableMap(moves); } public GameProgressListener getReport() { return this.listeners.get(0); } /** * Clean up when the game is over. This instance shouldn't be used anymore * after this method is called. Not calling this method after the game may * result in the JVM not terminating, since the executors will still be * active. */ public void terminate() { this.players.forEach((player, decisionMaker) -> decisionMaker.terminate()); this.e.shutdownNow(); } /** * Get the players in the game (i.e. not disqualified, nor dead). * * @return Unmodifiable collection of current players. */ public Collection<Player> getPlayers() { return Collections.unmodifiableCollection(players.keySet()); } /** * Distribute the command to players. * * @param command * Command to distribute. */ public void distributeCommand(Command command) { CommandDistributor.LOGGER.debug("Command scheduled for distribution: {}.", command); if (command instanceof DeactivatePlayerCommand) { removePlayer(((DeactivatePlayerCommand) command).getPlayer()); } commands.add(command); } private void removePlayer(Player player) { CommandDistributor.LOGGER.debug("Removing player {}.", player.getName()); final DecisionMaker dm = this.players.remove(player); dm.terminate(); } }