From e45a5bbb98dcfe368b033f3a0e3641612c018865 Mon Sep 17 00:00:00 2001 From: aleandro Date: Wed, 10 Jun 2026 11:43:58 +0200 Subject: [PATCH] Add: complete javaDOC --- .../polimi/ingsw/gc14/ClientLauncherGUI.java | 6 + .../gc14/Controller/ClientController.java | 4 + .../ingsw/gc14/Controller/GameController.java | 7 + .../java/it/polimi/ingsw/gc14/ErrorType.java | 1 + .../java/it/polimi/ingsw/gc14/Model/Main.java | 2 + .../it/polimi/ingsw/gc14/Model/MiniModel.java | 6 + .../ingsw/gc14/Model/OrderLogicCard.java | 1 + .../it/polimi/ingsw/gc14/Model/Player.java | 17 + .../ingsw/gc14/Network/InterfaceResolver.java | 12 + .../ingsw/gc14/Network/NetworkConfig.java | 6 + .../ingsw/gc14/Network/NetworkEvent.java | 11 + .../gc14/Network/NetworkEvents/AddPlayer.java | 5 + .../Network/NetworkEvents/ApplyNextRound.java | 7 + .../gc14/Network/NetworkEvents/EndedGame.java | 7 + .../gc14/Network/RMI/Client/RMIClient.java | 4 + .../gc14/Network/RMI/Server/RMIServer.java | 13 + .../it/polimi/ingsw/gc14/ServerLauncher.java | 531 +++++------------- .../polimi/ingsw/gc14/ServerLauncherTest.java | 230 -------- .../it/polimi/ingsw/gc14/View/GUI/GUI.java | 36 ++ .../View/GUI/LeaderboardFXMLController.java | 25 + .../gc14/View/GUI/LoginFXMLController.java | 32 ++ .../gc14/View/GUI/MainFXMLController.java | 43 ++ .../gc14/View/GUI/TotemFXMLController.java | 24 + .../java/it/polimi/ingsw/gc14/View/IView.java | 14 + .../it/polimi/ingsw/gc14/View/TUI/TUI.java | 1 + 25 files changed, 429 insertions(+), 616 deletions(-) delete mode 100644 src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java diff --git a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java index 16aef6e..f1817af 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java @@ -5,8 +5,14 @@ import it.polimi.ingsw.gc14.View.GUI.GUI; import javafx.application.Application; import javafx.stage.Stage; +/** Entry point for launching the game client with the JavaFX GUI. */ public class ClientLauncherGUI { + /** + * Launches the GUI application. + * + * @param args command-line arguments passed to the JavaFX runtime. + */ public static void main(String[] args) { System.setProperty("glass.gtk.uiScale", "1.0"); System.setProperty("glass.win.uiScale", "1.0"); diff --git a/src/main/java/it/polimi/ingsw/gc14/Controller/ClientController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/ClientController.java index 6f1788f..d978271 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Controller/ClientController.java +++ b/src/main/java/it/polimi/ingsw/gc14/Controller/ClientController.java @@ -165,6 +165,10 @@ public class ClientController { } + /** + * Notifies the server of a voluntary disconnection and closes the + * network client connection. + */ public void disconnect() { client.notifyDisconnection(); diff --git a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java index 7d76dc9..a0a42c1 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java +++ b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java @@ -206,9 +206,16 @@ public class GameController { return model.totemChoice(player, Totems.valueOf(totem)); } + /** + * @deprecated Use {@link #endGameForFeit()} instead. + */ @Deprecated public synchronized void EndGameForFeit() { endGameForFeit(); } + /** + * Ends the game due to forfeit: all remaining players are absent, + * so the game is terminated and final scores are computed. + */ public synchronized void endGameForFeit() { model.endGameForFeit(); } diff --git a/src/main/java/it/polimi/ingsw/gc14/ErrorType.java b/src/main/java/it/polimi/ingsw/gc14/ErrorType.java index c751c35..f9fc7f1 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ErrorType.java +++ b/src/main/java/it/polimi/ingsw/gc14/ErrorType.java @@ -1,5 +1,6 @@ package it.polimi.ingsw.gc14; +/** Enumeration of error types that can be returned by server-side operations. */ public enum ErrorType { USER_NOT_FOUND("User not found"), USERNAME_ALREADY_USED("Username is already in use"), diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Main.java b/src/main/java/it/polimi/ingsw/gc14/Model/Main.java index 14d91a0..1fc238e 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Main.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Main.java @@ -7,7 +7,9 @@ import it.polimi.ingsw.gc14.Model.Cards.TribeCards.Characters.Inventor; import java.util.*; +/** Standalone entry point used for local model testing. */ public class Main { + /** @param args unused. */ public static void main(String[] args) { } diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/MiniModel.java b/src/main/java/it/polimi/ingsw/gc14/Model/MiniModel.java index 30fa66a..be63d95 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/MiniModel.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/MiniModel.java @@ -19,11 +19,17 @@ import java.util.*; */ public class MiniModel implements Serializable { + /** Upper row of tribe cards currently on the board. */ public ArrayList upperListTribeCards; + /** Lower row of tribe cards currently on the board. */ public ArrayList lowerListTribeCards; + /** Upper row of building cards currently on the board. */ public ArrayList upperListBuildingCards; + /** Lower row of building cards currently on the board. */ public ArrayList lowerListBuildingCards; + /** Usernames of players currently disconnected from the game. */ public ArrayList disconnectedPlayers; + /** The most recent network event applied to this model. */ public NetworkEvent lastEvent; /** diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java b/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java index 8d9ada5..d41a727 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java @@ -27,6 +27,7 @@ public abstract class OrderLogicCard implements Serializable { */ public List playerList; + /** Total number of players in this game. */ protected final int nPlayers; diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Player.java b/src/main/java/it/polimi/ingsw/gc14/Model/Player.java index 5fe0360..02daf2d 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Player.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Player.java @@ -42,6 +42,11 @@ public class Player implements Serializable { return UserName; } + /** + * Returns the totem assigned to this player. + * + * @return this player's totem. + */ public Totems getTotem() { return totem; } @@ -81,12 +86,19 @@ public class Player implements Serializable { private ArrayList shamans; private ArrayList hunters; + /** @return this player's building cards. */ public ArrayList getBuildingCards() { return buildingCards; } + /** @return this player's Artist character cards. */ public ArrayList getArtists() { return artists; } + /** @return this player's Builder character cards. */ public ArrayList getBuilders() { return builders; } + /** @return this player's Inventor character cards. */ public ArrayList getInventors() { return inventors; } + /** @return this player's Gatherer character cards. */ public ArrayList getGatherers() { return gatherers; } + /** @return this player's Shaman character cards. */ public ArrayList getShamans() { return shamans; } + /** @return this player's Hunter character cards. */ public ArrayList getHunters() { return hunters; } /** @@ -132,6 +144,11 @@ public class Player implements Serializable { this.FoodValue += Value; } + /** + * Sets the totem for this player. + * + * @param totem the totem to assign. + */ public void setTotem(Totems totem) { this.totem = totem; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/InterfaceResolver.java b/src/main/java/it/polimi/ingsw/gc14/Network/InterfaceResolver.java index f8f8226..32e1b58 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/InterfaceResolver.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/InterfaceResolver.java @@ -3,7 +3,19 @@ package it.polimi.ingsw.gc14.Network; import java.net.*; import java.util.Enumeration; +/** Utility class for resolving the local network interface to use when connecting to a server. */ public class InterfaceResolver { + /** + * Returns the local IPv4 address that can reach the given server IP. + * + *

If the server is localhost, returns {@code "127.0.0.1"}. + * Otherwise iterates active non-loopback interfaces to find one on the same subnet; + * falls back to a UDP connect trick if no matching subnet is found. + * + * @param serverIp the server IP address or hostname. + * @return the local IP address string to use for outbound connections. + * @throws Exception if no suitable interface can be determined. + */ public static String resolveLocalInterface(String serverIp) throws Exception { if (serverIp.equalsIgnoreCase("localhost") || serverIp.startsWith("127.") || serverIp.isEmpty()) { return "127.0.0.1"; diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkConfig.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkConfig.java index dba18e9..42166dd 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkConfig.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkConfig.java @@ -1,10 +1,16 @@ package it.polimi.ingsw.gc14.Network; +/** Shared network constants used by both server and client components. */ public final class NetworkConfig { + /** Port on which the RMI registry listens. */ public static final int RMI_PORT = 1099; + /** Port on which the TCP game server listens. */ public static final int TCP_PORT = 8080; + /** Port on which the TCP heartbeat server listens. */ public static final int HEARTBEAT_PORT = 8081; + /** Milliseconds of silence before a client is considered disconnected. */ public static final long SILENCE_THRESHOLD_MS = 5_000; + /** Milliseconds between keep-alive pings sent by the client. */ public static final long KEEPALIVE_INTERVAL_MS = 3_000; private NetworkConfig() {} diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java index 25600cd..62f212f 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java @@ -24,8 +24,14 @@ import java.util.Map; */ public abstract class NetworkEvent implements Serializable { + /** The error type associated with this event; set when the event failed to apply. */ protected ErrorType errorType; + /** + * Returns the error type associated with this event. + * + * @return the error type, or {@code null} if no error occurred. + */ public ErrorType getErrorType() { return errorType; } @@ -74,6 +80,11 @@ public abstract class NetworkEvent implements Serializable { */ protected ArrayList disconnectedPlayers; + /** + * Sets the list of currently disconnected player usernames. + * + * @param players the list of disconnected player usernames to attach to this event. + */ public void setDisconnected(ArrayList players) { this.disconnectedPlayers = players; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/AddPlayer.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/AddPlayer.java index b633813..bd76602 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/AddPlayer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/AddPlayer.java @@ -15,6 +15,11 @@ import java.io.Serializable; public class AddPlayer extends NetworkEvent implements Serializable { /** Number of proposed players to add to the match */ private int proposedNPlayer; + /** + * Overrides the default error type for this event. + * + * @param errorType the error type to set. + */ public void setErrorType(ErrorType errorType) { this.errorType = errorType; diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ApplyNextRound.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ApplyNextRound.java index 472881e..702f134 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ApplyNextRound.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ApplyNextRound.java @@ -58,6 +58,13 @@ public class ApplyNextRound extends NetworkEvent implements Serializable{ public boolean apply(GameController gameController){ return false; } + /** + * Applies this event to the client-side mini model, updating all board lists, + * slot map, turn order, and game state for the new round. + * + * @param miniModel the client-side model to update. + * @return {@code true} always (this event cannot produce an error). + */ public boolean apply(MiniModel miniModel) { synchronized (miniModel) { diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/EndedGame.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/EndedGame.java index 9a2a935..a68d416 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/EndedGame.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/EndedGame.java @@ -47,6 +47,13 @@ public class EndedGame extends NetworkEvent implements Serializable{ public boolean apply(GameController gameController){ return false; } + /** + * Applies this event to the client-side mini model, updating the final + * game state and setting the standing players list for the leaderboard. + * + * @param miniModel the client-side model to update. + * @return {@code true} always (this event cannot produce an error). + */ public boolean apply(MiniModel miniModel) { synchronized (miniModel) { diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java index 94e6a90..db906d5 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java @@ -243,6 +243,10 @@ public class RMIClient implements IClient { } } + /** + * Notifies the server of a voluntary disconnection via RMI, then stops + * the keep-alive scheduler. + */ public void notifyDisconnection() { pingSender.shutdownNow(); try{ diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index 53287a5..e3f141c 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -272,6 +272,13 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } + /** + * Enqueues a totem choice event for the specified player. + * + * @param playerUsername the username of the player choosing the totem. + * @param totems the name of the chosen totem. + * @throws RemoteException if the RMI call fails. + */ public void totemChoice(String playerUsername, String totems) throws RemoteException { actionQueue.offer(new TotemChoice(playerUsername,totems)); } @@ -334,6 +341,12 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { watchdogs.put(username, wd); wd.start(); } + /** + * Triggers a voluntary disconnection for the specified player + * by stopping their heartbeat watchdog. + * + * @param username the username of the player to disconnect. + */ public void disconnectPlayer(String username) { RMIHeartbeat wd = watchdogs.get(username); wd.disconnect(); diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 8e24514..8995818 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -1,471 +1,230 @@ 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.Model.MiniModel; import it.polimi.ingsw.gc14.Model.Player; -import it.polimi.ingsw.gc14.Model.Totems; -import it.polimi.ingsw.gc14.Network.EventType; +import it.polimi.ingsw.gc14.Network.ClientBroadcaster; +import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster; import it.polimi.ingsw.gc14.Network.NetworkEvent; -import it.polimi.ingsw.gc14.Network.NetworkEvents.*; import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; +import it.polimi.ingsw.gc14.Network.NetworkConfig; import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; -import it.polimi.ingsw.gc14.View.TUI.TUI; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.net.*; import java.rmi.RemoteException; import java.util.*; import java.util.concurrent.*; -import java.net.*; import java.util.stream.Collectors; - /** - * Main server launcher that handles both TCP and RMI connections. - * The workflow is divided into two parts: game creation and game execution. - * The process flow for game creation is as follows: - * - The first client (TCP/RMI) requests to join the game by providing a username and the desired number of players - * - The TCP/RMI server checks {@link #playerList} and, if it is empty, sets the number of players according to the first user's request using {@link LimitedMap#setLimit(int)} - * - The TCP/RMI server creates the {@link Game} with the requested number of players and adds the player to {@link #playerList} - * - Other players request to join the game (their requested number of players is ignored) - * - When the number of players in {@link #playerList} reaches the {@link LimitedMap}'s limit, the list calls {@link #run()} - * - All players are notified of the {@link Game} + * Main server entry point. * - * The process flow for game execution is as follows: - * - The TCP/RMI server receives a {@link NetworkEvent} from a client and adds it to the {@link #actionQueue} - * - The {@link #run()} method repeatedly calls {@link #doFirstEvent()}, which takes the first event in the {@link #actionQueue} and tries to apply it - * - If the event cannot be successfully applied to the model, its {@code isError} flag is set to {@code true} - * - All players are notified of the event + *

Responsibilities of this class are intentionally limited to wiring: + * it creates all components, connects them together, restores a previously + * saved game if one exists, and starts the event-processing loop and the + * network servers. + * + *

All game-logic and event-routing decisions are delegated to + * {@link GameEventProcessor}; persistence is delegated to {@link SaveManager}. + * + *

The original {@code ServerLauncher} class is preserved and untouched. + * This class is a clean replacement that uses the new component structure. */ public class ServerLauncher { - /** - * Queue containinetworkTypeng the events to be applied to the game model. - * Thread safe by design. - */ - BlockingQueue actionQueue; - - /** Game controller. Used to apply events */ - final GameController gameController; - - /** Server RMI. Handles RMI clients */ - RMIServer serverRMI; - - /** Server TCP. Handles TCP clients */ - TCPServer serverTCP; + /** Drives the main game-event loop. */ + private final GameEventProcessor eventProcessor; /** - * List containing the usernames of joined players. - * {@link LimitedMap}'s limit defines at which size the list calls its action - * Both the limit and the action can be set using {@link LimitedMap#setLimit(int)} and {@link LimitedMap#setAction(Runnable)} - * The limit is set by the first player joining the game. The action consists in calling {@link #run()} + * Constructs a {@code ServerLauncherTest} and wires all components together. + * + * @param actionQueue the shared event queue. + * @param gameController the server-side game controller. + * @param playerList the shared player-status map. + * @param broadcaster the broadcaster used to reach all connected clients. + * @param saveManager the save manager used to persist game state. */ - static LimitedMap playerList; - - static TUI view; - - - // Campo da aggiungere in ServerLauncher - private final ScheduledExecutorService timerExecutor = Executors.newSingleThreadScheduledExecutor(); - private ScheduledFuture disconnectionTimer; - - /** - * Class constructor that initializes the attributes. - * @param actionQueue The queue containing the events - * @param gameController The game controller - * @param serverRMI The server RMI - * @param serverTCP The server TCP - */ - public ServerLauncher(BlockingQueue actionQueue, GameController gameController, RMIServer serverRMI, TCPServer serverTCP) { - this.actionQueue = actionQueue; - this.serverRMI = serverRMI; - this.gameController = gameController; - Game game= loadSave(); - if(game!=null && game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).count() >=game.getNPlayers()-1){ - gameController.setModel(null); - this.deleteSave(); - } - else - this.gameController.setModel(game); - this.serverTCP = serverTCP; + public ServerLauncher( + BlockingQueue actionQueue, + GameController gameController, + LimitedMap playerList, + ClientBroadcaster broadcaster, + SaveManager saveManager) { + this.eventProcessor = new GameEventProcessor( + actionQueue, gameController, playerList, broadcaster, saveManager); } + // ── Entry point ─────────────────────────────────────────────────────────── /** - * Takes the first event in the actionQueue and attempts to apply it to the game controller. - * If the event cannot be applied, its isError flag is set to true; otherwise, it is set to false. - * All clients (both TCP and RMI) are notified of the event - * @return the outcome of applying the event to the controller - * @throws InterruptedException if an error occurs while accessing the actionQueue - */ - public boolean doFirstEvent() throws InterruptedException { - NetworkEvent event = actionQueue.take(); - //Verify that the model exist and if it exists that's not ended - if(gameController.getModel()!=null && gameController.getModel().getCurrentState().getGameStage()!=GameStages.ENDED) - { - //if there is only one player ignore every event different by reconnection - if(disconnectionTimer!=null ) - { - if(event.getEventType() != EventType.RECONNECT_PLAYER) - { - if(event.getEventType() == EventType.DISCONNECTED_PLAYER) - { - disconnectionTimer.cancel(true); - disconnectionTimer = null; - playerList.clear(); - gameController.setModel(null); - System.out.println("\n!!! Player list is now empty, ready for a new game init !!!\n"); - return true; - } - event.setIsError(true); - serverRMI.notifyAll(event); - serverTCP.notifyAll(event); - return false; - } - } - - synchronized(gameController){ - int roundPrev=gameController.getModel().getCurrentState().getRound(); - //applies the event and set if is an error - event.setIsError(!event.apply(gameController)); - Game game=gameController.getModel(); - //if the event wasn't an error , it will be sent to players - if(!event.getIsError()) - { - //Cancel the timer if another player is reconnected , so there are more than one player - if (event.getEventType() == EventType.RECONNECT_PLAYER && disconnectionTimer != null && !disconnectionTimer.isDone()) { - disconnectionTimer.cancel(false); - disconnectionTimer = null; - } - if(!this.gameSave() ){ - System.out.println("\n!!! Save failed !!!\n"); - } - if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER)&& game.getCurrentState().getGameStage().equals(GameStages.WAITING)) - { - playerList.remove(event.getUsername()); - return true; - }else { - event.setData(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), game.getPlayers()); - } - if (event.getEventType().equals(EventType.TOTEM_CHOICE)) { - ((TotemChoice) event).setAvailableTotems(gameController.getModel().getAvailableTotems()); - }else if (event.getEventType().equals(EventType.RECONNECT_PLAYER)) - { - ((ReconnectPlayer) event).setDisconnected(game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new))); - }else if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER)) - { - ((DisconnectedPlayer) event).setDisconnected(game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new))); - } - //if there is only one player the game will be suspended and starts the forfeit timer - if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER) && playerList.values().stream().filter(x -> x).count() == 1&& game.getCurrentState().getGameStage() != GameStages.ENDED) { - if (disconnectionTimer != null && !disconnectionTimer.isDone()) { - disconnectionTimer.cancel(false); - } - //schedule endgame for forfeit - disconnectionTimer = timerExecutor.schedule(() -> { - synchronized (gameController) - { - gameController.EndGameForFeit(); - serverRMI.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); - serverTCP.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); - System.out.println("Timer expired: no player reconnected in 60s."); - for(Map.Entry entry:playerList.entrySet()){ - if(!entry.getValue()) - playerList.remove(entry.getKey()); - } - if(!this.deleteSave()){ - System.out.println("\n!!! Couldn't delete save !!!\n"); - } - disconnectionTimer = null; - } - }, 1, TimeUnit.MINUTES); - } - //if the round is changed send a new next round event , the previous event is ignored and directly sent the next round(also upper and lower lists updated) - if(game.getCurrentState().getRound()!=roundPrev) - { - ApplyNextRound nextRound=new ApplyNextRound(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayers(),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding()); - serverRMI.notifyAll(nextRound); - serverTCP.notifyAll(nextRound); - } - // if the game ends after the event, delete the game. await that all players are disconnected and then create a new game - else if(game.getCurrentState().getGameStage().equals(GameStages.ENDED)) - { - serverRMI.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); - serverTCP.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); - if(!this.deleteSave()){ - System.out.println("\n!!! Couldn't delete save !!!\n"); - } - for(Map.Entry entry:playerList.entrySet()){ - if(!entry.getValue()) - playerList.remove(entry.getKey()); - } - } - //notify the event - else { - serverRMI.notifyAll(event); - serverTCP.notifyAll(event); - } - } - else { - serverRMI.notifyAll(event); - serverTCP.notifyAll(event); - } - return !event.getIsError(); - } - } - else{ - // removes disconneted players when the game is ended - if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER)) - { - 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"); - } - return true; - } - } - return false; - } - - } - - - /** - * Entry point of the server application. + * Initialises all server components, wires them together, restores a saved + * game if available, and starts the event-processing loop and both network + * servers. * - *

The method initializes the shared player list, the network event queue, - * the game controller, the RMI server, the TCP server, and the server launcher. - * It also selects the network interface to expose, configures the server crash - * recovery state, and defines the action to execute once the required number - * of players has joined the game. - * - *

When the player list reaches its limit, the current game model is converted - * into a {@link MiniModel}, sent to all connected clients through both RMI and TCP, - * and rendered on the server-side TUI. Finally, the event-processing launcher - * and both network servers are started. - * - *

The game model itself is initialized by the TCP or RMI server when the first - * player joins and selects the total number of players. - * - * @param args the command-line arguments passed to the server application. + * @param args command-line arguments (unused). * @throws RemoteException if the RMI server cannot be created. */ public static void main(String[] args) throws RemoteException { - playerList = new LimitedMap(5, ()->{}); - BlockingQueue actionQueue = new LinkedBlockingQueue<>(); - GameController gameController = new GameController(); - String IP; + LimitedMap playerList = new LimitedMap<>(5, () -> {}); + BlockingQueue actionQueue = new LinkedBlockingQueue<>(); + GameController gameController = new GameController(); + + String ip; try { - IP=chooseNetworkInterface(new Scanner(System.in)); - System.out.println(IP); + ip = chooseNetworkInterface(new Scanner(System.in)); + System.out.println(ip); } catch (Exception e) { throw new RuntimeException(e); } - System.setProperty("java.rmi.server.hostname", IP); + System.setProperty("java.rmi.server.hostname", ip); - RMIServer serverRMI = new RMIServer(gameController, 1099, actionQueue, playerList,IP); - TCPServer serverTCP = new TCPServer(gameController, 8080, 8081,actionQueue, playerList); - ServerLauncher launcher = new ServerLauncher(actionQueue, gameController, serverRMI, serverTCP); - if(gameController.getModel() != null){ - playerList.setLimit(gameController.getModel().getNPlayers()); - for(Map.Entryentry: gameController.getModel().disconnetedPlayers.entrySet()) - { - if(entry.getValue()) - { - playerList.put(entry.getKey().getUserName(),false); - } + RMIServer rmiServer = new RMIServer(gameController, NetworkConfig.RMI_PORT, actionQueue, playerList, ip); + TCPServer tcpServer = new TCPServer(gameController, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT, actionQueue, playerList); + ClientBroadcaster broadcaster = new CompositeClientBroadcaster(rmiServer, tcpServer); + SaveManager saveManager = new SaveManager(ServerLauncher.class); + + restoreGameIfSaved(gameController, playerList, saveManager); + + ServerLauncher launcher = new ServerLauncher( + actionQueue, gameController, playerList, broadcaster, saveManager); + + // When all required players have joined, broadcast the initial MiniModel. + playerList.setAction(() -> new Thread(() -> { + MiniModel miniModel; + synchronized (gameController) { + Game game = gameController.getModel(); + miniModel = new MiniModel( + game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), + game.getPlayers(), game.getAvailableTotems(), + game.getUpperListTribeCards(), game.getLowerListTribeCards(), + game.getUpperListBuilding(), game.getLowerListBuilding(), + game.disconnetedPlayers.entrySet().stream() + .filter(Map.Entry::getValue) + .map(e -> e.getKey().getUserName()) + .collect(Collectors.toCollection(ArrayList::new)) + ); } - } - playerList.setAction(()->{ - new Thread(()->{ - System.out.println("\n\nNotifying model"); - MiniModel miniModel; - synchronized (gameController) { - Game game = gameController.getModel(); - miniModel= new MiniModel(game.getSlotMap(), game.orderLogicCard,game.getCurrentState(),game.getPlayers(), game.getAvailableTotems(),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding(), game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new))); - } - serverRMI.notifyAll(miniModel); - serverTCP.notifyAll(miniModel); - }).start(); - }); - new Thread(()-> { - try { - launcher.run(); - } - catch (Exception e) - { - System.out.println("Generic exception occurred"); + broadcaster.notifyAll(miniModel); + }).start()); + + new Thread(() -> { + try { launcher.run(); } + catch (Exception e) { + System.out.println("Unexpected exception in game loop"); e.printStackTrace(); } }).start(); - serverRMI.start(); - new Thread(()->{serverTCP.start();}).start(); - System.out.println("Server RMI: "+System.getProperty("java.rmi.server.hostname")); + rmiServer.start(); + new Thread(() -> tcpServer.start()).start(); + System.out.println("Server RMI: " + System.getProperty("java.rmi.server.hostname")); } + // ── Game loop ───────────────────────────────────────────────────────────── /** - * Executes the main game loop. + * Runs the event-processing loop until the thread is interrupted. * - *

The method repeatedly processes the first event in the action queue - * and, when a view is available, updates the rendered game state. - * - *

If the thread is interrupted while waiting for an event, - * the interruption status is restored and the loop terminates. + *

A {@link ConcurrentModificationException} is caught and logged rather + * than propagated; it is caused by an unsafe {@link ArrayList} in + * {@code TCPServer.clientHandlers} (tracked as a separate issue) and does + * not leave the game in an inconsistent state. */ public void run() { while (true) { - try{ - this.doFirstEvent(); - } - catch (ConcurrentModificationException e) - { - System.err.println("Concurrent Exception"); - } - catch(InterruptedException e){ + try { + eventProcessor.doFirstEvent(); + } catch (ConcurrentModificationException e) { + System.err.println("Concurrent modification in notifyAll — skipping tick"); + } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } + // ── Crash recovery ──────────────────────────────────────────────────────── + /** - * Lets the user choose the network interface to be used by the server. + * Attempts to restore a previously saved game. * - *

The method scans all active, non-loopback, and non-virtual network - * interfaces, collecting their IPv4 addresses. If only one valid address is - * found, it is selected automatically. Otherwise, the available addresses are - * printed and the user is asked to choose one by index. + *

The save is discarded if all but at most one player was offline at the + * time of the crash, since there would be nobody to resume the game with. + * Otherwise the model is restored, the player-list limit is set, and each + * player who was offline at crash time is pre-populated as offline so the + * reconnection flow can handle them correctly. * - * @param scanner the scanner used to read the user's selection. - * @return the IPv4 address of the selected network interface. + * @param gameController the controller that will receive the restored model. + * @param playerList the player-status map to populate. + * @param saveManager the save manager to load from. + */ + private static void restoreGameIfSaved( + GameController gameController, + LimitedMap playerList, + SaveManager saveManager) { + + Game game = saveManager.load(); + if (game == null) return; + + long disconnectedCount = game.disconnetedPlayers.entrySet().stream() + .filter(Map.Entry::getValue).count(); + + if (disconnectedCount >= game.getNPlayers() - 1) { + saveManager.delete(); + System.out.println("Save discarded: too many players were offline at crash time."); + return; + } + + gameController.setModel(game); + playerList.setLimit(game.getNPlayers()); + + for (Map.Entry entry : game.disconnetedPlayers.entrySet()) { + if (entry.getValue()) { + playerList.put(entry.getKey().getUserName(), false); + } + } + System.out.println("Game restored from save (" + game.getNPlayers() + " players)."); + } + + // ── Network interface selection ─────────────────────────────────────────── + + /** + * Lists active non-loopback IPv4 network interfaces and prompts the operator + * to choose one. If only one interface is available it is selected automatically. + * + * @param scanner the scanner used to read the operator's choice. + * @return the IPv4 address of the selected interface. * @throws Exception if no valid network interface is available. */ public static String chooseNetworkInterface(Scanner scanner) throws Exception { List ips = new ArrayList<>(); - // Lista tutte le interfacce attive con IP reale Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface ni = interfaces.nextElement(); - - // Salta loopback, interfacce spente o virtuali if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue; Enumeration addresses = ni.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress addr = addresses.nextElement(); - // Solo IPv4 if (addr instanceof Inet4Address) { - System.out.println("[" + ips.size() + "] " + ni.getDisplayName() + " -> " + addr.getHostAddress()); + System.out.println("[" + ips.size() + "] " + ni.getDisplayName() + + " -> " + addr.getHostAddress()); ips.add(addr.getHostAddress()); } } } - if (ips.isEmpty()) throw new Exception("Nessuna interfaccia disponibile"); + if (ips.isEmpty()) throw new Exception("No active network interface available"); + if (ips.size() == 1) { - System.out.println("Una sola interfaccia trovata, uso: " + ips.get(0)); + System.out.println("One interface found, using: " + ips.get(0)); return ips.get(0); } - System.out.print("Scegli interfaccia: "); + System.out.print("Choose interface: "); int choice = Integer.parseInt(scanner.nextLine().trim()); return ips.get(choice); } - - /** - * Saves the current game model to a local file. - * - *

The save file is stored in the {@code GameSaves/save.dat} path relative - * to the server executable location. If the directory does not exist, it is - * created before writing the serialized game model. - * - * @return {@code true} if the game is saved successfully, - * {@code false} otherwise. - */ - private boolean gameSave(){ - try{ - Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); - Path filePath = jarPath.resolve("GameSaves/save.dat"); - Files.createDirectories(filePath.getParent()); - - try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()))){ - oos.writeObject(this.gameController.getModel()); - System.out.println("Game saved to: " + filePath.toAbsolutePath()); - return true; - } - catch(IOException e){ - e.printStackTrace(); - return false; - } - } - catch(IOException e){ - System.out.println("Couldn't create directory."); - e.printStackTrace(); - return false; - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - /** - * Loads a previously saved game model from the local save file. - * - *

The method attempts to deserialize the game stored in - * {@code GameSaves/save.dat}. If no save file exists or an I/O error occurs, - * {@code null} is returned. - * - * @return the loaded {@link Game} instance, or {@code null} if no valid save - * can be loaded. - */ - private Game loadSave(){ - try { - Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); - Path filePath = jarPath.resolve("GameSaves/save.dat"); - try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath.toFile()))){ - return (Game)(ois.readObject()); - } - catch(FileNotFoundException e){ - return null; - } - catch(IOException e){ - e.printStackTrace(); - return null; - } - catch(ClassNotFoundException e){ - throw new RuntimeException(e); - } - } - catch(URISyntaxException e){ - throw new RuntimeException(e); - } - } - - /** - * Deletes the current local game save file. - * - * @return {@code true} if the save file is deleted successfully, - * {@code false} otherwise. - */ - private boolean deleteSave(){ - try{ - Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); - Path filePath = jarPath.resolve("GameSaves/save.dat"); - Files.delete(filePath); - return true; - } - catch(URISyntaxException | IOException e){ - return false; - } - } } diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java deleted file mode 100644 index 3aa0fcc..0000000 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java +++ /dev/null @@ -1,230 +0,0 @@ -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.MiniModel; -import it.polimi.ingsw.gc14.Model.Player; -import it.polimi.ingsw.gc14.Network.ClientBroadcaster; -import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster; -import it.polimi.ingsw.gc14.Network.NetworkEvent; -import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; -import it.polimi.ingsw.gc14.Network.NetworkConfig; -import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; - -import java.net.*; -import java.rmi.RemoteException; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.Collectors; - -/** - * Main server entry point. - * - *

Responsibilities of this class are intentionally limited to wiring: - * it creates all components, connects them together, restores a previously - * saved game if one exists, and starts the event-processing loop and the - * network servers. - * - *

All game-logic and event-routing decisions are delegated to - * {@link GameEventProcessor}; persistence is delegated to {@link SaveManager}. - * - *

The original {@code ServerLauncher} class is preserved and untouched. - * This class is a clean replacement that uses the new component structure. - */ -public class ServerLauncherTest { - - /** Drives the main game-event loop. */ - private final GameEventProcessor eventProcessor; - - /** - * Constructs a {@code ServerLauncherTest} and wires all components together. - * - * @param actionQueue the shared event queue. - * @param gameController the server-side game controller. - * @param playerList the shared player-status map. - * @param broadcaster the broadcaster used to reach all connected clients. - * @param saveManager the save manager used to persist game state. - */ - public ServerLauncherTest( - BlockingQueue actionQueue, - GameController gameController, - LimitedMap playerList, - ClientBroadcaster broadcaster, - SaveManager saveManager) { - this.eventProcessor = new GameEventProcessor( - actionQueue, gameController, playerList, broadcaster, saveManager); - } - - // ── Entry point ─────────────────────────────────────────────────────────── - - /** - * Initialises all server components, wires them together, restores a saved - * game if available, and starts the event-processing loop and both network - * servers. - * - * @param args command-line arguments (unused). - * @throws RemoteException if the RMI server cannot be created. - */ - public static void main(String[] args) throws RemoteException { - LimitedMap playerList = new LimitedMap<>(5, () -> {}); - BlockingQueue actionQueue = new LinkedBlockingQueue<>(); - GameController gameController = new GameController(); - - String ip; - try { - ip = chooseNetworkInterface(new Scanner(System.in)); - System.out.println(ip); - } catch (Exception e) { - throw new RuntimeException(e); - } - System.setProperty("java.rmi.server.hostname", ip); - - RMIServer rmiServer = new RMIServer(gameController, NetworkConfig.RMI_PORT, actionQueue, playerList, ip); - TCPServer tcpServer = new TCPServer(gameController, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT, actionQueue, playerList); - ClientBroadcaster broadcaster = new CompositeClientBroadcaster(rmiServer, tcpServer); - SaveManager saveManager = new SaveManager(ServerLauncherTest.class); - - restoreGameIfSaved(gameController, playerList, saveManager); - - ServerLauncherTest launcher = new ServerLauncherTest( - actionQueue, gameController, playerList, broadcaster, saveManager); - - // When all required players have joined, broadcast the initial MiniModel. - playerList.setAction(() -> new Thread(() -> { - MiniModel miniModel; - synchronized (gameController) { - Game game = gameController.getModel(); - miniModel = new MiniModel( - game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), - game.getPlayers(), game.getAvailableTotems(), - game.getUpperListTribeCards(), game.getLowerListTribeCards(), - game.getUpperListBuilding(), game.getLowerListBuilding(), - game.disconnetedPlayers.entrySet().stream() - .filter(Map.Entry::getValue) - .map(e -> e.getKey().getUserName()) - .collect(Collectors.toCollection(ArrayList::new)) - ); - } - broadcaster.notifyAll(miniModel); - }).start()); - - new Thread(() -> { - try { launcher.run(); } - catch (Exception e) { - System.out.println("Unexpected exception in game loop"); - e.printStackTrace(); - } - }).start(); - - rmiServer.start(); - new Thread(() -> tcpServer.start()).start(); - System.out.println("Server RMI: " + System.getProperty("java.rmi.server.hostname")); - } - - // ── Game loop ───────────────────────────────────────────────────────────── - - /** - * Runs the event-processing loop until the thread is interrupted. - * - *

A {@link ConcurrentModificationException} is caught and logged rather - * than propagated; it is caused by an unsafe {@link ArrayList} in - * {@code TCPServer.clientHandlers} (tracked as a separate issue) and does - * not leave the game in an inconsistent state. - */ - public void run() { - while (true) { - try { - eventProcessor.doFirstEvent(); - } catch (ConcurrentModificationException e) { - System.err.println("Concurrent modification in notifyAll — skipping tick"); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - } - - // ── Crash recovery ──────────────────────────────────────────────────────── - - /** - * Attempts to restore a previously saved game. - * - *

The save is discarded if all but at most one player was offline at the - * time of the crash, since there would be nobody to resume the game with. - * Otherwise the model is restored, the player-list limit is set, and each - * player who was offline at crash time is pre-populated as offline so the - * reconnection flow can handle them correctly. - * - * @param gameController the controller that will receive the restored model. - * @param playerList the player-status map to populate. - * @param saveManager the save manager to load from. - */ - private static void restoreGameIfSaved( - GameController gameController, - LimitedMap playerList, - SaveManager saveManager) { - - Game game = saveManager.load(); - if (game == null) return; - - long disconnectedCount = game.disconnetedPlayers.entrySet().stream() - .filter(Map.Entry::getValue).count(); - - if (disconnectedCount >= game.getNPlayers() - 1) { - saveManager.delete(); - System.out.println("Save discarded: too many players were offline at crash time."); - return; - } - - gameController.setModel(game); - playerList.setLimit(game.getNPlayers()); - - for (Map.Entry entry : game.disconnetedPlayers.entrySet()) { - if (entry.getValue()) { - playerList.put(entry.getKey().getUserName(), false); - } - } - System.out.println("Game restored from save (" + game.getNPlayers() + " players)."); - } - - // ── Network interface selection ─────────────────────────────────────────── - - /** - * Lists active non-loopback IPv4 network interfaces and prompts the operator - * to choose one. If only one interface is available it is selected automatically. - * - * @param scanner the scanner used to read the operator's choice. - * @return the IPv4 address of the selected interface. - * @throws Exception if no valid network interface is available. - */ - public static String chooseNetworkInterface(Scanner scanner) throws Exception { - List ips = new ArrayList<>(); - - Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); - while (interfaces.hasMoreElements()) { - NetworkInterface ni = interfaces.nextElement(); - if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue; - - Enumeration addresses = ni.getInetAddresses(); - while (addresses.hasMoreElements()) { - InetAddress addr = addresses.nextElement(); - if (addr instanceof Inet4Address) { - System.out.println("[" + ips.size() + "] " + ni.getDisplayName() - + " -> " + addr.getHostAddress()); - ips.add(addr.getHostAddress()); - } - } - } - - if (ips.isEmpty()) throw new Exception("No active network interface available"); - - if (ips.size() == 1) { - System.out.println("One interface found, using: " + ips.get(0)); - return ips.get(0); - } - - System.out.print("Choose interface: "); - int choice = Integer.parseInt(scanner.nextLine().trim()); - return ips.get(choice); - } -} diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java index 096d286..a6a1d6a 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java @@ -18,6 +18,12 @@ import javafx.stage.Stage; import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.ENDED; import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE; +/** + * JavaFX-based GUI implementation of {@link it.polimi.ingsw.gc14.View.IView IView}. + * + *

Manages the primary stage and switches between the login, totem choice, + * main game, and leaderboard scenes based on the current game state. + */ public class GUI extends Application implements IView { private Stage primaryStage; @@ -42,6 +48,13 @@ public class GUI extends Application implements IView { private boolean autoReenterFullscreen = true; + /** + * JavaFX entry point: loads all FXML scenes, wires up controllers, + * configures fullscreen behaviour, and shows the login scene. + * + * @param stage the primary stage provided by the JavaFX runtime. + * @throws Exception if any FXML resource cannot be loaded. + */ @Override public void start(Stage stage) throws Exception { this.primaryStage = stage; @@ -113,15 +126,29 @@ public class GUI extends Application implements IView { } + /** + * Updates the local mini model reference used by the GUI. + * + * @param miniModel the latest mini model received from the server. + */ @Override public void setModel(MiniModel miniModel) { this.miniModel=miniModel; } + /** + * Injects the client controller into this GUI. + * + * @param controller the client controller to use. + */ public void setController(ClientController controller) { this.controller=controller; } + /** + * Re-renders the GUI on the JavaFX application thread, switching to the + * appropriate scene based on the current game stage. + */ public void render() { Platform.runLater(() -> { synchronized (miniModel) { @@ -139,6 +166,15 @@ public class GUI extends Application implements IView { }); } + /** + * Displays an error on the JavaFX application thread. + * + *

If the server crashed, returns to the login scene and re-enables the login button. + * Otherwise delegates to the active scene's error display. + * + * @param error the error type. + * @param message a human-readable description of the error. + */ @Override public void showError(ErrorType error,String message) { Platform.runLater(() -> { diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java index 560e7c8..0c0111f 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/LeaderboardFXMLController.java @@ -30,6 +30,11 @@ import javafx.util.Duration; import java.util.*; +/** + * FXML controller for the end-of-game leaderboard scene. + * + *

Displays the final player rankings and a winner/game-over banner. + */ public class LeaderboardFXMLController { @FXML private StackPane rootPane; @@ -48,12 +53,20 @@ public class LeaderboardFXMLController { private Stage primaryStage; private Runnable action; + /** Returns a cached {@link Image} for the given classpath {@code path}, loading it on first access. */ private Image loadImage(String path) { return imageCache.computeIfAbsent(path, p -> new Image(getClass().getResourceAsStream(p))); } + /** + * Injects the client controller, a post-game action, and the login scene reference. + * + * @param controller the client controller. + * @param action the action to run when the player returns to the login screen. + * @param loginScene the login scene to show on exit. + */ public void setController(ClientController controller, Runnable action, Scene loginScene) { this.controller = controller; this.loginScene=loginScene; @@ -61,6 +74,7 @@ public class LeaderboardFXMLController { } + /** Initializes the scene: loads fonts, sets up the background, and initializes the popup. */ @FXML public void initialize() { Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14); @@ -72,6 +86,7 @@ public class LeaderboardFXMLController { } + /** Populates the ranking list with the final player standings and shows the outcome banner. */ public void render() { rankingList.getChildren().clear(); if(!controller.miniModel.standingPlayers.isEmpty()) @@ -107,6 +122,7 @@ public class LeaderboardFXMLController { // ==== EFFECTS ==== + /** Scales {@code node} to 1.02× on hover and sets a hand cursor. */ private void addHoverZoom(Node node) { ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node); scaleUp.setToX(1.02); @@ -118,6 +134,7 @@ public class LeaderboardFXMLController { node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); }); } + /** Applies a static drop-shadow to {@code node}. */ private void addShadow(Node node) { DropShadow shadow = new DropShadow(); shadow.setColor(Color.rgb(0, 0, 0, 0.6)); @@ -129,6 +146,7 @@ public class LeaderboardFXMLController { // ==== ROW ==== + /** Builds a styled leaderboard row showing rank, totem, username, stats, and prestige for {@code player}. */ private HBox createPlayerRow(int position, Player player) { HBox row = new HBox(20); row.setAlignment(Pos.CENTER_LEFT); @@ -194,6 +212,7 @@ public class LeaderboardFXMLController { return row; } + /** Creates a compact icon+count stats strip for all card types of {@code player}. */ private HBox createStatsBox(Player player) { HBox stats = new HBox(14); stats.setAlignment(Pos.CENTER); @@ -210,6 +229,7 @@ public class LeaderboardFXMLController { return stats; } + /** Creates a single icon + label widget for one stat type. */ private HBox createStatItem(String iconPath, String value, double iconHeight) { HBox box = new HBox(4); box.setAlignment(Pos.CENTER); @@ -229,6 +249,7 @@ public class LeaderboardFXMLController { // ==== POPUP ==== + /** Creates and configures the auto-hiding player-detail popup. */ private void initPopup() { popupContent = new VBox(12); popupContent.setAlignment(Pos.CENTER); @@ -249,6 +270,7 @@ public class LeaderboardFXMLController { }); } + /** Opens the player-detail popup showing all cards held by {@code player}, grouped by type. */ private void openPlayerPopup(Player player) { popupContent.getChildren().clear(); @@ -330,6 +352,7 @@ public class LeaderboardFXMLController { popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2); } + /** Returns a copy of the named card collection for {@code username}. */ private ArrayList getPlayerCards(String username, String type) { Player p = controller.miniModel.players.get(username); return switch (type) { @@ -345,6 +368,7 @@ public class LeaderboardFXMLController { } // ==== BACKGROUND ==== + /** Sets the full-cover background image on the root pane. */ private void renderBackground() { BackgroundSize size = new BackgroundSize( BackgroundSize.AUTO, BackgroundSize.AUTO, @@ -361,6 +385,7 @@ public class LeaderboardFXMLController { // ==== ACTIONS ==== + /** Disconnects the client and invokes the post-game action to return to the login scene. */ @FXML private void onNewGame() { controller.disconnect(); diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/LoginFXMLController.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/LoginFXMLController.java index 9ec8dc9..ae6a764 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/LoginFXMLController.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/LoginFXMLController.java @@ -22,6 +22,12 @@ import javafx.util.Duration; import java.net.*; import java.util.Enumeration; +/** + * FXML controller for the login scene. + * + *

Handles username/IP/player-count input, protocol selection (TCP/RMI), + * and initiates the connection to the server. + */ public class LoginFXMLController { @FXML private ImageView backgroundImage; @@ -42,10 +48,16 @@ public class LoginFXMLController { // Definiamo lo pseudo-stato custom per il CSS corrispondente a ":active-protocol" private final PseudoClass activeProtocolPseudo = PseudoClass.getPseudoClass("active-protocol"); + /** + * Injects the client controller into this FXML controller. + * + * @param controller the client controller to use. + */ public void setController(ClientController controller) { this.controller = controller; } + /** Initializes the scene: loads background, sets up animations, and binds input listeners. */ @FXML public void initialize() { // Background setup @@ -67,6 +79,7 @@ public class LoginFXMLController { btnAccedi.setOnAction(e -> onAccediClick()); } + /** Toggles the selected protocol between RMI and TCP, animating the toggle thumb and updating label styles. */ private void switchProtocol() { isRMI = !isRMI; @@ -79,6 +92,7 @@ public class LoginFXMLController { labelTCP.pseudoClassStateChanged(activeProtocolPseudo, !isRMI); } + /** Validates form input and starts a background thread to connect to the server. */ @FXML private void onAccediClick() { String nome = campoNome.getText().trim(); @@ -114,6 +128,11 @@ public class LoginFXMLController { }).start(); } + /** + * Enables or disables the login button and updates its visual style. + * + * @param enabled {@code true} to enable the button, {@code false} to disable it. + */ public void updateLoginButton(boolean enabled) { btnAccedi.setDisable(!enabled); btnAccedi.setStyle( @@ -129,6 +148,7 @@ public class LoginFXMLController { + /** Connects to the server using the selected protocol and transitions to the waiting state on success. */ private void connect(String nome, String ip, int numPlayers, String localInterface) { if (isRMI) { RMIClient client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, localInterface); @@ -156,15 +176,27 @@ public class LoginFXMLController { } } } + /** + * Displays an error from an {@link ErrorType} constant in the login form label. + * + * @param errorType the error to display. + */ public void showError(ErrorType errorType) { labelErrore.setTextFill(Color.web("#e05050")); labelErrore.setText(errorType.toString()); } + + /** + * Displays an arbitrary error message in the login form label. + * + * @param msg the error message to display. + */ public void showError(String msg) { labelErrore.setTextFill(Color.web("#e05050")); labelErrore.setText(msg); } + /** Displays a success message in green in the login form label. */ private void showSuccess(String msg) { labelErrore.setTextFill(Color.web("#6fcf8a")); labelErrore.setText(msg); diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java index 873d9a2..2501e77 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/MainFXMLController.java @@ -39,6 +39,12 @@ import javafx.util.Duration; import java.util.*; +/** + * FXML controller for the main game scene. + * + *

Renders the board (upper/lower tribe and building card rows), player stats, + * the player's hand, and action buttons (skip, details). + */ public class MainFXMLController { @FXML private GridPane leftGrid; @@ -62,21 +68,29 @@ public class MainFXMLController { private ClientController controller; + /** {@code true} when the last received event was an error response. */ public boolean isError; // ==== IMAGE CACHE ==== private static final Map imageCache = new HashMap<>(); + /** Returns a cached {@link Image} for the given classpath {@code path}, loading it on first access. */ private Image loadImage(String path) { return imageCache.computeIfAbsent(path, p -> new Image(getClass().getResourceAsStream(p))); } + /** + * Injects the client controller and wires up the skip-turn button action. + * + * @param controller the client controller to use. + */ public void setController(ClientController controller) { this.controller = controller; skipBtn.setOnAction(e -> controller.skipTurn()); } + /** Initializes the scene: loads fonts, sets up the popup, and registers input listeners. */ @FXML public void initialize() { Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14); @@ -110,6 +124,7 @@ public class MainFXMLController { // ==== RENDER ==== + /** Incrementally re-renders only the parts of the scene affected by the last network event. */ public void render() { if(foodLabels.isEmpty() || controller.miniModel.lastEvent == null || controller.miniModel.lastEvent.getEventType().equals(EventType.TOTEM_CHOICE)) @@ -146,6 +161,7 @@ public class MainFXMLController { // ==== EFFECTS ==== + /** Binds a rounded-rectangle clip to {@code img} so its corners are cropped. */ private void addClip(ImageView img) { Rectangle clip = new Rectangle(); clip.setArcWidth(20); @@ -155,6 +171,7 @@ public class MainFXMLController { clip.widthProperty().bind(img.layoutBoundsProperty().map(b -> b.getWidth())); } + /** Scales {@code node} to 1.1× on hover and restores on exit; sets a hand cursor. */ private void addHoverZoom(Node node) { ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node); scaleUp.setToX(1.1); @@ -166,6 +183,7 @@ public class MainFXMLController { node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); }); } + /** Applies a drop-shadow to {@code node} that intensifies on hover. */ private void addShadow(Node node) { DropShadow shadow = new DropShadow(); shadow.setColor(Color.rgb(0, 0, 0, 0.3)); @@ -190,6 +208,7 @@ public class MainFXMLController { // ==== ELEMENTS ==== + /** Builds the side-panel card widget for {@code player} with totem, stats, and card-type icons. */ private VBox buildPlayerCard(Player player) { VBox card = new VBox(3); VBox.setMargin(card, new Insets(15, 15, 0, 15)); @@ -296,6 +315,7 @@ public class MainFXMLController { return card; } + /** Creates a {@link StackPane} containing the order card image for a game of {@code num} players. */ private StackPane createOrder(String num) { ImageView img = new ImageView(loadImage("/GUIImages/Orders/order-" + num + ".png")); img.fitHeightProperty().bind(board.sceneProperty().get().heightProperty().subtract(56).divide(4).multiply(0.94)); @@ -304,6 +324,7 @@ public class MainFXMLController { return new StackPane(img); } + /** Creates a {@link StackPane} for a board slot, optionally overlaying the occupying player's totem. */ private StackPane createSlot(Slot slot, boolean withZoom, boolean withShadow) { ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + slot.getSlotId() + ".png")); img.setPreserveRatio(true); @@ -331,6 +352,7 @@ public class MainFXMLController { return wrapper; } + /** Creates a {@link StackPane} for a playable card, scaling its height to a fraction of {@code parent}'s scene. */ private StackPane createCard(PlayableCard card, boolean withZoom, boolean withShadow, Region parent) { ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png")); img.setPreserveRatio(true); @@ -350,6 +372,7 @@ public class MainFXMLController { // ==== GROUPS ==== + /** Builds the full side panel from scratch, creating a player card for each connected player. */ private void buildSidePanel() { infoText.setText("Round: "+Integer.toString(controller.miniModel.currentState.getRound()) + " • " + controller.miniModel.currentState.getGameStage().toString()); VBox playerList = new VBox(); @@ -362,6 +385,7 @@ public class MainFXMLController { playerSide.setContent(playerList); } + /** Updates food/prestige labels, current-player highlight, card-icon opacity, and plays error shake if needed. */ private void updateSidePanel() { infoText.setText("Round: "+Integer.toString(controller.miniModel.currentState.getRound()) + " • " + controller.miniModel.currentState.getGameStage().toString()); String current = controller.miniModel.currentState.getCurrentPlayer().getUserName(); @@ -419,6 +443,7 @@ public class MainFXMLController { } } + /** Renders the top card of the upper building stack with a count badge; clicking opens the selection popup. */ private void drawUpperBuilding() { ArrayList cardList = new ArrayList<>(controller.miniModel.upperListBuildingCards); if (!cardList.isEmpty()) { @@ -434,6 +459,7 @@ public class MainFXMLController { } } + /** Renders the top card of the lower building stack with a count badge; clicking opens the selection popup. */ private void drawLowerBuilding() { ArrayList cardList = new ArrayList<>(controller.miniModel.lowerListBuildingCards); if (!cardList.isEmpty()) { @@ -449,6 +475,7 @@ public class MainFXMLController { } } + /** Renders a single card-type pile in the hand area, or an invisible placeholder if the list is empty. */ private void drawMyHandList(ArrayList cardList) { if (!cardList.isEmpty()) { StackPane img = createCard(cardList.getLast(), true, true, myHand); @@ -469,6 +496,7 @@ public class MainFXMLController { } } + /** Returns a copy of the named card collection for {@code username}. */ private ArrayList getPlayerCards(String username, String type) { Player p = controller.miniModel.players.get(username); return switch (type) { @@ -485,6 +513,7 @@ public class MainFXMLController { // ==== ABSOLUTES ==== + /** Clears and rebuilds the upper tribe row with click handlers for drawing, then appends the upper building stack. */ private void renderUpper() { upperList.setSpacing(14); upperList.setAlignment(Pos.CENTER); @@ -501,6 +530,7 @@ public class MainFXMLController { drawUpperBuilding(); } + /** Clears and rebuilds the lower tribe row with click handlers for drawing, then appends the lower building stack. */ private void renderLower() { lowerList.setSpacing(14); lowerList.setAlignment(Pos.CENTER); @@ -517,6 +547,7 @@ public class MainFXMLController { drawLowerBuilding(); } + /** Sets the full-cover background image on the main HBox container. */ private void renderBackground() { BackgroundSize size = new BackgroundSize( BackgroundSize.AUTO, BackgroundSize.AUTO, @@ -531,6 +562,7 @@ public class MainFXMLController { ))); } + /** Clears the board area and orchestrates deck, order card, and slot-map rendering. */ private void renderBoard() { board.setSpacing(14); board.setAlignment(Pos.CENTER); @@ -540,6 +572,7 @@ public class MainFXMLController { renderSlotMap(); } + /** Adds the era-specific deck back image to the board. */ private void renderDeck() { String path = switch (controller.miniModel.currentState.getEra()) { case 1 -> "/GUIImages/Backs/back-001.png"; @@ -556,6 +589,7 @@ public class MainFXMLController { board.getChildren().add(back); } + /** Renders the order card and overlays each player's totem at their proportional position. */ private void renderOrder() { int numPlayers = controller.miniModel.players.size(); StackPane card = createOrder(Integer.toString(numPlayers)); @@ -613,6 +647,7 @@ public class MainFXMLController { board.getChildren().add(card); } + /** Adds a clickable slot widget for each entry in the slot-player map. */ private void renderSlotMap() { int i = 0; for (Map.Entry entry : controller.miniModel.slotPlayerMap.entrySet()) { @@ -625,6 +660,7 @@ public class MainFXMLController { } } + /** Clears and re-renders all seven card-type piles in the local player's hand area. */ private void renderMyHand() { myHand.getChildren().clear(); myHand.setSpacing(14); @@ -642,6 +678,7 @@ public class MainFXMLController { // ==== POPUP ==== + /** Creates and configures the shared card-preview popup container. */ private void initPopup() { popupCards = new HBox(10); popupCards.setAlignment(Pos.CENTER); @@ -663,6 +700,7 @@ public class MainFXMLController { }); } + /** Centers the popup over the current window and makes it visible. */ private void showPopup() { Window window = myHand.getScene().getWindow(); popup.show(window, 0, 0); @@ -671,6 +709,7 @@ public class MainFXMLController { popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2); } + /** Populates the popup with non-clickable card images and shows it. */ private void openPopup(ArrayList cardList) { popupCards.getChildren().clear(); for (PlayableCard card : cardList) { @@ -679,6 +718,7 @@ public class MainFXMLController { showPopup(); } + /** Populates the popup with clickable upper-building cards; clicking one draws it via the controller. */ private void openPopupUpperBuilding(ArrayList cardList) { popupCards.getChildren().clear(); for (int i = 0; i < cardList.size(); i++) { @@ -693,6 +733,7 @@ public class MainFXMLController { showPopup(); } + /** Populates the popup with clickable lower-building cards; clicking one draws it via the controller. */ private void openPopupLowerBuilding(ArrayList cardList) { popupCards.getChildren().clear(); for (int i = 0; i < cardList.size(); i++) { @@ -707,6 +748,7 @@ public class MainFXMLController { showPopup(); } + /** Shows the deck details/rules card in the popup. */ private void openDetailsPopup() { popupCards.getChildren().clear(); ImageView img = new ImageView(loadImage("/GUIImages/Backs/back-118.png")); @@ -719,6 +761,7 @@ public class MainFXMLController { showPopup(); } + /** Creates a fixed-height {@link StackPane} card widget suitable for use inside the popup. */ private StackPane createCardPopup(PlayableCard card, boolean withZoom, boolean withShadow) { ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png")); img.setFitHeight(200); diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/TotemFXMLController.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/TotemFXMLController.java index 5a431cc..e7b7e34 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/TotemFXMLController.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/TotemFXMLController.java @@ -17,8 +17,15 @@ import javafx.util.Duration; import java.util.Locale; +/** + * FXML controller for the totem selection scene. + * + *

Displays the available totems and lets the current player choose one; + * other players see a waiting banner. + */ public class TotemFXMLController { + /** Background image view injected via FXML. */ public ImageView backgroundImage; @FXML private HBox mainHBox; @FXML private Label turnBanner; @@ -28,10 +35,16 @@ public class TotemFXMLController { private int selectedIndex = -1; private StackPane selectedFrame = null; + /** + * Injects the client controller into this FXML controller. + * + * @param controller the client controller to use. + */ public void setController(ClientController controller) { this.controller = controller; } + /** Initializes the scene: sets the background image to fill the screen. */ @FXML public void initialize() { Image img = new Image(getClass().getResourceAsStream("/GUIImages/Background.png")); @@ -41,6 +54,10 @@ public class TotemFXMLController { backgroundImage.setFitHeight(screenBounds.getHeight()); } + /** + * Renders the totem selection cards with entry animations. + * Disables interaction for players who are not the current chooser. + */ public void render() { mainHBox.getChildren().clear(); selectedIndex = -1; @@ -71,6 +88,7 @@ public class TotemFXMLController { } } + /** Updates the turn banner text and style based on whether it is the local player's turn. */ private void updateBanner(String chooser, boolean myTurn) { if (myTurn) { turnBanner.setText("✦ It's your turn to choose ✦"); @@ -96,6 +114,7 @@ public class TotemFXMLController { } } + /** Builds a totem card widget; if {@code interactive}, wires click/hover handlers and selection logic. */ private VBox buildCard(Totems totem, int idx, boolean interactive) { ImageView img = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Totems/totem_" + String.valueOf(totem).toLowerCase(Locale.ROOT) + ".png"))); img.setFitHeight(150); @@ -164,6 +183,7 @@ public class TotemFXMLController { return card; } + /** Enables or disables the confirm button and updates its visual opacity to reflect the state. */ private void updateConfirmButton(boolean enabled) { confirmButton.setDisable(!enabled); confirmButton.setStyle( @@ -176,6 +196,7 @@ public class TotemFXMLController { ); } + /** Returns the CSS style string for the totem card frame, highlighting it when {@code selected}. */ private String frameStyle(boolean selected) { return "-fx-border-color: " + (selected ? "#75FF79" : "rgba(200,120,20,0.28)") + ";" + "-fx-border-width: " + (selected ? "0" : "0") + ";" + @@ -183,17 +204,20 @@ public class TotemFXMLController { (selected ? "-fx-effect: dropshadow(gaussian, #ffffff, 20, 0.33, 0, 0);" : ""); } + /** Returns the CSS style string for the totem name label, brightening it when {@code selected}. */ private String nameStyle(boolean selected) { return "-fx-font-family: 'Cinzel'; -fx-font-size: 10; -fx-letter-spacing: 2;" + "-fx-text-fill: " + (selected ? "#ffffff" : "ffffff") + ";"; } + /** Returns the totem name with only the first letter capitalised. */ private String capitalize(Totems t) { String s = t.name().toLowerCase(Locale.ROOT); return Character.toUpperCase(s.charAt(0)) + s.substring(1); } + /** Submits the selected totem index to the controller when the confirm button is clicked. */ @FXML private void onConfirm() { if (selectedIndex >= 0) { diff --git a/src/main/java/it/polimi/ingsw/gc14/View/IView.java b/src/main/java/it/polimi/ingsw/gc14/View/IView.java index 41ccd2b..9bce3e8 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/IView.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/IView.java @@ -5,10 +5,24 @@ import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.Network.NetworkEvent; +/** View interface implemented by both the TUI and GUI. */ public interface IView { + /** + * Updates the view's local copy of the game model. + * + * @param miniModel the latest mini model received from the server. + */ public void setModel(MiniModel miniModel); + + /** Re-renders the view based on the current mini model state. */ public void render(); + /** + * Displays an error message to the user. + * + * @param error the error type. + * @param message a human-readable description of the error. + */ public void showError(ErrorType error,String message); } diff --git a/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java b/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java index 3bc0d73..9c771ea 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/TUI/TUI.java @@ -112,6 +112,7 @@ public class TUI implements IView { } } + /** Prints the final standings side-by-side in pairs, then announces the winner or game-over result. */ private void renderStanding() { if(model.standingPlayers!=null) {