Coverage Summary for Class: GameEventProcessor (it.polimi.ingsw.gc14)

Class Method, % Branch, % Line, %
GameEventProcessor 0% (0/19) 0% (0/48) 0% (0/110)
GameEventProcessor$1 0% (0/1) 0% (0/1)
Total 0% (0/20) 0% (0/48) 0% (0/111)


 package it.polimi.ingsw.gc14;
 
 import it.polimi.ingsw.gc14.Controller.GameController;
 import it.polimi.ingsw.gc14.Model.Game;
 import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
 import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster;
 import it.polimi.ingsw.gc14.Network.EventType;
 import it.polimi.ingsw.gc14.Network.NetworkEvent;
 import it.polimi.ingsw.gc14.Network.NetworkEvents.*;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.*;
 import java.util.stream.Collectors;
 
 /**
  * Processes game events from the action queue and routes each one
  * based on the current game state.
  *
  * <p>The three top-level states that determine routing are:
  * <ul>
  *   <li><b>Inactive</b> – no game model is present, or the game has ended.
  *       Only disconnection cleanup is performed.</li>
  *   <li><b>Suspended</b> – a forfeit timer is running because exactly one
  *       player remains online. Only reconnection events are accepted.</li>
  *   <li><b>Active</b> – normal gameplay; every event is applied, saved,
  *       and broadcast.</li>
  * </ul>
  *
  * <p>This class is not thread-safe by itself: it relies on the caller
  * (the game loop in {@code ServerLauncher}) to drive it from a single
  * thread via {@link #doFirstEvent()}.
  */
 public class GameEventProcessor {
     /** Queue from which incoming client events are consumed one at a time. */
     private final BlockingQueue<NetworkEvent> actionQueue;
     /** Server-side game controller; all model mutations go through this. */
     private final GameController gameController;
     /** Shared map tracking each player's online status ({@code true} = online). */
     private final LimitedMap<String, Boolean> playerList;
     /** Broadcaster used to push events and model snapshots to all connected clients. */
     private final CompositeClientBroadcaster broadcaster;
     /** Persists and deletes game save files. */
     private final SaveManager saveManager;
 
     /** Single-thread executor used exclusively for the forfeit timer. Daemon so it does not block JVM shutdown. */
     private final ScheduledExecutorService timerExecutor =
             Executors.newSingleThreadScheduledExecutor(r -> {
                 Thread t = new Thread(r, "forfeit-timer");
                 t.setDaemon(true);
                 return t;
             });
 
     /**
      * Handle to the running forfeit timer, or {@code null} when no timer is active.
      * A non-null value signals that the game is in the suspended state.
      */
     private ScheduledFuture<?> disconnectionTimer;
 
     /**
      * Constructs a {@code GameEventProcessor} with all required dependencies.
      *
      * @param actionQueue    the queue from which incoming events are consumed.
      * @param gameController the server-side game controller.
      * @param playerList     the shared map tracking each player's online status.
      * @param broadcaster    the broadcaster used to notify all connected clients.
      * @param saveManager    the save manager used to persist the game state.
      */
     public GameEventProcessor(
             BlockingQueue<NetworkEvent> actionQueue,
             GameController gameController,
             LimitedMap<String, Boolean> playerList,
             CompositeClientBroadcaster broadcaster,
             SaveManager saveManager) {
         this.actionQueue    = actionQueue;
         this.gameController = gameController;
         this.playerList     = playerList;
         this.broadcaster    = broadcaster;
         this.saveManager    = saveManager;
     }
 
 
     /**
      * Blocks until one event is available in the queue, then routes it to the
      * appropriate handler based on the current game state.
      *
      * @throws InterruptedException if the thread is interrupted while waiting
      *                              for the next event.
      */
     public void doFirstEvent() throws InterruptedException {
         NetworkEvent event = actionQueue.take();
 
         if (!isGameActive()) {
             handleInactiveGame(event);
         } else if (isSuspended()) {
             handleSuspendedGame(event);
         } else {
             applyAndBroadcast(event);
         }
     }
 
 
     /**
      * Returns {@code true} when there is an ongoing game that has not yet ended.
      */
     private boolean isGameActive() {
         Game model = gameController.getModel();
         return model != null
                 && model.getCurrentState().getGameStage() != GameStages.ENDED;
     }
 
     /**
      * Returns {@code true} when the forfeit timer is running, meaning only one
      * player is currently online and the game is waiting for a reconnection.
      */
     private boolean isSuspended() {
         return disconnectionTimer != null;
     }
 
 
     /**
      * Handles events that arrive when no active game exists (not yet started,
      * or already ended). Only disconnection cleanup is relevant here.
      *
      * @param event the incoming event.
      */
     private void handleInactiveGame(NetworkEvent event) {
         if (event.getEventType() != EventType.DISCONNECTED_PLAYER) return;
 
         synchronized (gameController) {
             playerList.remove(event.getUsername());
             if (playerList.isEmpty()) {
                 gameController.setModel(null);
                 System.out.println("\n!!! Player list is now empty, ready for a new game init !!!\n");
             }
         }
     }
 
 
     /**
      * Routes events while the game is suspended waiting for a reconnection.
      * <ul>
      *   <li>A second disconnection while suspended means no player remains
      *       online: the game is aborted entirely.</li>
      *   <li>Any non-reconnection event is rejected with an error.</li>
      *   <li>A reconnection event is allowed through to {@link #applyAndBroadcast}.</li>
      * </ul>
      *
      * @param event the incoming event.
      */
     private void handleSuspendedGame(NetworkEvent event) {
         switch (event.getEventType()) {
             case DISCONNECTED_PLAYER -> abortGame();
             case RECONNECT_PLAYER    -> applyAndBroadcast(event);
             default                  -> rejectEvent(event);
         }
     }
 
     /**
      * Cancels the forfeit timer, clears the player list, and resets the model.
      * Called when the last remaining player disconnects while the game is suspended.
      */
     private void abortGame() {
         disconnectionTimer.cancel(true);
         disconnectionTimer = null;
         playerList.clear();
         gameController.setModel(null);
         System.out.println("\n!!! All players disconnected — game aborted, ready for a new game init !!!\n");
     }
 
     /**
      * Marks the event as an error and broadcasts it back to the requesting
      * player. Used to reject actions that are not permitted in the current state.
      *
      * @param event the event to reject.
      */
     private void rejectEvent(NetworkEvent event) {
         event.setIsError(true);
         broadcaster.notifyAll(event);
     }
 
 
     /**
      * Applies the event to the game controller, saves the updated state,
      * and broadcasts the result to connected clients.
      *
      * <p>The entire method body is synchronized on {@code gameController} to
      * prevent concurrent modification of the game model by the network threads.
      *
      * @param event the event to apply.
      */
     private void applyAndBroadcast(NetworkEvent event) {
         synchronized (gameController) {
             int roundBefore = gameController.getModel().getCurrentState().getRound();
 
             event.setIsError(!event.apply(gameController));
             Game game = gameController.getModel();
 
             if (event.isError()) {
                 broadcaster.notifyAll(event);
                 return;
             }
 
             cancelForfeitTimerIfReconnect(event);
 
             if (!saveManager.save(game)) {
                 System.out.println("\n!!! Save failed !!!\n");
             }
             else {
                 System.out.println(event.getUsername()+": "+ event.getEventType() + " save successful.");
             }
 
             // During the lobby phase a disconnection only removes the player
             // from the list; no broadcast is needed.
             if (event.getEventType() == EventType.DISCONNECTED_PLAYER
                     && game.getCurrentState().getGameStage() == GameStages.WAITING) {
                 playerList.remove(event.getUsername());
                 return;
             }
 
             enrichEvent(event, game);
             startForfeitTimerIfNeeded(event, game);
             broadcastResult(event, game, roundBefore);
         }
     }
 
     /**
      * Cancels the forfeit timer if the event is a successful reconnection.
      *
      * @param event the event that was just successfully applied.
      */
     private void cancelForfeitTimerIfReconnect(NetworkEvent event) {
         if (event.getEventType() == EventType.RECONNECT_PLAYER
                 && disconnectionTimer != null
                 && !disconnectionTimer.isDone()) {
             disconnectionTimer.cancel(false);
             disconnectionTimer = null;
         }
     }
 
     /**
      * Populates the event with the current game state so that clients can
      * update their mini-model after receiving it.
      *
      * <p>Each event subclass overrides {@link NetworkEvent#enrichWithGameState}
      * to append any type-specific extra fields (available totems, etc.).
      *
      * @param event the event to enrich.
      * @param game  the current game model.
      */
     private void enrichEvent(NetworkEvent event, Game game) {
         event.enrichWithGameState(game, buildDisconnectedList(game));
     }
 
     /**
      * Schedules the 60-second forfeit timer if a disconnection has left
      * exactly one player online.
      *
      * <p>If a previous timer is still pending it is canceled first to avoid
      * duplicate timers.
      *
      * @param event the event that was just applied.
      * @param game  the current game model.
      */
     private void startForfeitTimerIfNeeded(NetworkEvent event, Game game) {
         if (event.getEventType() != EventType.DISCONNECTED_PLAYER) return;
         if (game.getCurrentState().getGameStage() == GameStages.ENDED) return;
         if (onlinePlayerCount() != 1) return;
 
         if (disconnectionTimer != null && !disconnectionTimer.isDone()) {
             disconnectionTimer.cancel(false);
             System.out.println("Disconnection TIMER reset");
         }
 
         disconnectionTimer = timerExecutor.schedule(
                 () -> endGameForfeit(game),
                 1, TimeUnit.MINUTES
         );
         System.out.println("Disconnection TIMER started, 60 seconds from now..." );
     }
 
     /**
      * Ends the game by forfeit when the timer expires without a reconnection.
      * Broadcasts an {@link EndedGame} event, deletes the save, and resets state.
      *
      * @param game the game model captured when the timer was scheduled.
      */
     private void endGameForfeit(Game game) {
         synchronized (gameController) {
             gameController.endGameForfeit();
             EndedGame forfeitEnd = new EndedGame(
                     game.getSlotMap(), game.getOrderLogicCard(),
                     game.getCurrentState(), game.getPlayerStanding()
             );
             broadcaster.notifyAll(forfeitEnd);
             System.out.println("Timer expired: no player reconnected in 60 s.");
             removeOfflinePlayers();
             if (!saveManager.delete()) {
                 System.out.println("\n!!! Couldn't delete save !!!\n");
             }
             disconnectionTimer = null;
         }
     }
 
     /**
      * Determines what to broadcast after a successful event application:
      * <ul>
      *   <li>If the round advanced, an {@link ApplyNextRound} event (with updated
      *       card lists) replaces the original event.</li>
      *   <li>If the game has ended, an {@link EndedGame} event is sent and the
      *       save file is deleted.</li>
      *   <li>Otherwise the original event is broadcast as-is.</li>
      * </ul>
      *
      * @param event       the event that was applied.
      * @param game        the current game model (post-apply).
      * @param roundBefore the round number before the event was applied.
      */
     private void broadcastResult(NetworkEvent event, Game game, int roundBefore) {
         broadcaster.notifyAll(event);
         if (game.getCurrentState().getRound() != roundBefore) {
             ApplyNextRound nextRound = new ApplyNextRound(
                     game.getSlotMap(), game.getOrderLogicCard(), game.getCurrentState(),
                     game.getPlayers(),
                     game.getUpperListTribeCards(), game.getLowerListTribeCards(),
                     game.getUpperListBuilding(),   game.getLowerListBuilding()
             );
             broadcaster.notifyAll(nextRound);
         } else if (game.getCurrentState().getGameStage() == GameStages.ENDED) {
             EndedGame endedGame = new EndedGame(
                     game.getSlotMap(), game.getOrderLogicCard(),
                     game.getCurrentState(), game.getPlayerStanding()
             );
             broadcaster.notifyAll(endedGame);
             if (!saveManager.delete()) {
                 System.out.println("\n!!! Couldn't delete save !!!\n");
             }
             removeOfflinePlayers();
         }
     }
 
 
     /**
      * Returns the number of players currently marked as online in the player list.
      */
     private long onlinePlayerCount() {
         return playerList.values().stream().filter(v -> v).count();
     }
 
     /**
      * Builds the list of usernames of players currently marked as disconnected
      * in the game model.
      *
      * @param game the current game model.
      * @return a new {@link ArrayList} of disconnected usernames.
      */
     private ArrayList<String> buildDisconnectedList(Game game) {
         return game.getDisconnectedPlayers().entrySet().stream()
                 .filter(Map.Entry::getValue)
                 .map(e -> e.getKey().getUserName())
                 .collect(Collectors.toCollection(ArrayList::new));
     }
 
     /**
      * Removes all offline entries (value {@code false}) from the player list.
      * Online players remain until they disconnect naturally.
      */
     private void removeOfflinePlayers() {
         List<String> toRemove = playerList.entrySet().stream()
                 .filter(e -> !e.getValue())
                 .map(Map.Entry::getKey)
                 .toList();
         toRemove.forEach(playerList::remove);
     }
 }