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

Class Class, % Method, % Branch, % Line, %
ClientLauncherTUI 0% (0/1) 0% (0/12) 0% (0/62) 0% (0/139)


 package it.polimi.ingsw.gc14;
 
 import it.polimi.ingsw.gc14.Controller.ClientController;
 import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
 import it.polimi.ingsw.gc14.Network.IClient;
 import it.polimi.ingsw.gc14.Network.NetworkConfig;
 import it.polimi.ingsw.gc14.Network.InterfaceResolver;
 import it.polimi.ingsw.gc14.Network.RMI.Client.RMIClient;
 import it.polimi.ingsw.gc14.Network.TCP.Client.TCPClient;
 import it.polimi.ingsw.gc14.View.TUI.TUI;
 
 import org.jline.builtins.Completers.TreeCompleter;
 import org.jline.reader.*;
 import org.jline.reader.impl.completer.StringsCompleter;
 import org.jline.terminal.Terminal;
 import org.jline.terminal.TerminalBuilder;
 
 import java.io.IOException;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 
 import static org.jline.builtins.Completers.TreeCompleter.node;
 
 /**
  * Entry point for the TUI-based game client.
  * Handles login, connects to the server, and drives the JLine-powered command loop.
  */
 public class ClientLauncherTUI {
     /** TUI view shared with the client controller. */
     private TUI view;
     /** JLine terminal used for all input/output. */
     private Terminal terminal;
     /** Line reader used during active gameplay, with tab-completion wired. */
     private LineReader gameReader;
     /** Username of the currently logged-in player; empty string before login. */
     private String currentUsername = "";
 
     private static final String BUILDING_DETAILS =
         "ID  0 — Each time you complete a set of 6 different Character cards, take 5 \uD83C\uDF56. (Sets completed before acquiring don't count.)\n" +
         "ID  1 — During the Sustenance Event, pay 1 less \uD83C\uDF56 for each Artist/Inventor/Gatherer in your tribe.\n" +
         "ID  2 — During the Shamanic Ritual Event, you do not lose Prestige Points if you have fewer ⭐ than all other players.\n" +
         "ID  3 — When you move your Totem to a Food-bonus space, take 1 extra \uD83C\uDF56. If placed last, pay 1 \uD83C\uDF56 normally (no effect).\n" +
         "ID  4 — Each time you obtain a pair of identical Inventors (same icon), take 3 \uD83C\uDF56. (Pairs owned before don't count.)\n" +
         "ID  5 — During the Shamanic Ritual Event, your tribe has 3 additional ⭐ icons.\n" +
         "ID  6 — During the Shamanic Ritual Event, if you have more ⭐ than any other player, gain double Prestige. Ties still count.\n" +
         "ID  7 — During the Hunt Event, take 1 \uD83C\uDF56 and gain 1 extra Prestige Point for each Hunter in your tribe.\n" +
         "ID  8 — At end of game, gain double the Prestige Points shown on Builder cards in your tribe.\n" +
         "ID  9 — During the Cave Paintings Event, take 1 \uD83C\uDF56 for each Artist in your tribe.\n" +
         "ID 10 — At end of game, gain 6 Prestige Points for each complete set of 6 different Character cards.\n" +
         "ID 11 — At end of game, gain the indicated Prestige Points for each Character card of the indicated type.\n" +
         "ID 12 — After all Totems return to Turn Order (before End of Round), take 1 Character or 1 Building card from the top row (paying its cost).\n" +
         "ID 13 — At end of game, gain 25 Prestige Points.";
 
     private static final String EVENT_DETAILS =
         "SUSTENANCE — Pay 1 \uD83C\uDF56 per Character (Buildings don't count). If you can't feed all Characters, lose the\n" +
         "  Prestige Points shown on the card for each one you couldn't feed. Each Gatherer gives a 3\uD83C\uDF56 discount.\n" +
         "  You cannot choose to lose PP to avoid paying Food. Sustenance resolves last if multiple Events occur.\n" +
         "\n" +
         "HUNT — Take 1 \uD83C\uDF56 and gain the Prestige Points shown on the card for each Hunter in your tribe.\n" +
         "\n" +
         "SHAMANIC RITUAL — Player with the most \uD83C\uDF1F icons gains the Prestige Points on the card.\n" +
         "  Player with the fewest \uD83C\uDF1F icons loses those Prestige Points. In case of a tie, all tied players gain/lose.\n" +
         "\n" +
         "CAVE PAINTINGS — If you have fewer Artists than the top-line threshold, lose the indicated Prestige Points.\n" +
         "  If you meet the bottom-line threshold, gain the indicated Prestige Points for each Artist in your tribe.";
 
     private static final String CHARACTER_DETAILS =
         "INVENTORS — At end of game, gain PP equal to (number of Inventors) × (number of different Invention icons).\n" +
         "  There are 10 different Invention icons.\n" +
         "\n" +
         "GATHERERS — During Sustenance, each Gatherer gives a 3\uD83C\uDF56 discount on what you owe.\n" +
         "  Note: you do not take any Food from Gatherers under any circumstances.\n" +
         "\n" +
         "SHAMANS — Each Shaman shows 1–3 \uD83C\uDF1F icons. During Shamanic Ritual, majority = gain PP; minority = lose PP.\n" +
         "\n" +
         "BUILDERS — During the game, each Builder reduces the \uD83C\uDF56 cost of every Building card by the amount\n" +
         "  shown in the top-right corner. At end of game, each Builder gives the PP shown in the bottom-left corner.\n" +
         "\n" +
         "ARTISTS — During Cave Paintings, gain or lose PP based on Artist count (see 'details events').\n" +
         "  At end of game, gain 10 PP for every 2 Artists in your tribe.\n" +
         "\n" +
         "HUNTERS — Adding a Hunter without the \uD83C\uDF56 icon: nothing. Adding a Hunter WITH the \uD83C\uDF56 icon:\n" +
         "  immediately take 1 \uD83C\uDF56 for each Hunter in your tribe (with or without the icon).\n" +
         "  During Hunt Event, take \uD83C\uDF56 and gain PP based on your Hunter count (see 'details events').";
     /**
      * Launches the TUI client.
      *
      * @param args command-line arguments (unused).
      * @throws InterruptedException if the thread is interrupted during startup.
      */
     static void main(String[] args) throws InterruptedException {
         new ClientLauncherTUI().start();
     }
 
     /**
      * Starts the TUI client. Creates a JLine terminal, loops through login → game → rematch.
      *
      */
     private void start() {
         try {
             System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
             System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8));
             terminal = TerminalBuilder.builder()
                     .system(true)
                     .encoding(StandardCharsets.UTF_8)
                     .build();
         } catch (IOException e) {
             System.err.println("Could not initialise terminal: " + e.getMessage());
             return;
         }
         terminal.writer().print("\033[H\033[2J");
         terminal.writer().flush();
 
         view = new TUI(null);
         ClientController controller = new ClientController(view);
         gameReader = buildGameReader();
         view.setLineReader(gameReader);
 
         while (true) {
             if (!doLogin(controller)) continue;
 
             while (controller.getClient() != null) {
                 String prompt = buildPrompt();
                 String line;
                 try {
                     line = gameReader.readLine(prompt);
                 } catch (UserInterruptException e) {
                     safeQuit(controller);
                     return;
                 } catch (EndOfFileException e) {
                     safeQuit(controller);
                     return;
                 }
                 if (line == null || line.isBlank()) continue;
                 boolean rematch = handleCommand(line.trim(), controller);
                 if (rematch) break;
             }
         }
     }
     /**
      * Prompts for username, player count, network type, and server IP, then connects.
      *
      * @param controller the client controller to connect.
      * @return {@code true} on successful connection; {@code false} if input is invalid or connection fails.
      */
     private boolean doLogin(ClientController controller) {
         LineReader loginReader = LineReaderBuilder.builder()
                 .terminal(terminal)
                 .completer(new StringsCompleter("rmi", "tcp"))
                 .build();
         LineReader ipReader = LineReaderBuilder.builder()
                 .terminal(terminal)
                 .completer(new StringsCompleter("localhost"))
                 .build();
 
         view.setLineReader(loginReader);
         try {
             return doLoginInner(controller, loginReader, ipReader);
         } finally {
             view.setLineReader(gameReader);
         }
     }
     /**
      * Reads login fields from the terminal and connects to the server.
      *
      * @param controller  the client controller to connect.
      * @param loginReader line reader used for username, player count, and network type.
      * @param ipReader    line reader with localhost auto-complete for the server IP.
      * @return {@code true} on successful connection; {@code false} if input is invalid or connection fails.
      */
     private boolean doLoginInner(ClientController controller, LineReader loginReader, LineReader ipReader) {
         String username, networkStr, ip;
         int nPlayers;
 
         try {
             username    = loginReader.readLine("Username: ").trim();
             nPlayers    = Integer.parseInt(loginReader.readLine("Number of players [2-5]: ").trim());
             networkStr  = loginReader.readLine("Network [rmi/tcp]: ").trim().toLowerCase();
             ip          = ipReader.readLine("Server IP [localhost]: ").trim();
             if (ip.isEmpty()) ip = "localhost";
         } catch (UserInterruptException | EndOfFileException e) {
             System.exit(0);
             return false;
         } catch (NumberFormatException e) {
             loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Invalid number of players.\033[0m");
             return false;
         }
 
         currentUsername = username;
         view.setUsername(username);
         controller.setMyUsername(username);
 
         IClient client;
         if (networkStr.equals("rmi")) {
             String myIP;
             try {
                 myIP = InterfaceResolver.resolveLocalInterface(ip);
             } catch (Exception e) {
                 loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Cannot resolve local interface: " + e.getMessage() + "\033[0m");
                 return false;
             }
             System.setProperty("java.rmi.server.hostname", myIP);
             client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, myIP);
             ErrorType err = client.connect(username, nPlayers);
             if (err != null) {
                 loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: " + err + "\033[0m");
                 return false;
             }
             System.out.println("Connected via RMI, waiting for other players...");
             controller.setClient(client);
 
         } else if (networkStr.equals("tcp")) {
             client = new TCPClient(controller, ip, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT);
             ErrorType err = client.connect(username, nPlayers);
             if (err != null) {
                 loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: " + err + "\033[0m");
                 return false;
             }
             System.out.println("Connected via TCP, waiting for other players...");
             controller.setClient(client);
 
         } else {
             loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Unknown network type '" + networkStr + "'. Use rmi or tcp.\033[0m");
             return false;
         }
         return true;
     }
     /**
      * Builds the JLine {@link LineReader} used during gameplay, with tab-completion
      * for all valid commands.
      *
      * @return the configured {@link LineReader}.
      */
     private LineReader buildGameReader() {
         Completer completer = new TreeCompleter(
                 node("slot"),
                 node("draw",
                         node("upper",
                                 node("tribe"),
                                 node("building")),
                         node("lower",
                                 node("tribe"),
                                 node("building"))),
                 node("totem"),
                 node("skip"),
                 node("clear"),
                 node("details", node("buildings"), node("events"), node("characters")),
                 node("rematch"),
                 node("quit")
         );
         return LineReaderBuilder.builder()
                 .terminal(terminal)
                 .completer(completer)
                 .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
                 .build();
     }
     /**
      * Returns the command-line prompt string showing the current username.
      *
      * @return the prompt string.
      */
     private String buildPrompt() {
         return currentUsername.isEmpty() ? "> " : currentUsername + "> ";
     }
 
     /** Returns {@code true} when the caller should break out of the game loop (rematch requested). */
     private boolean handleCommand(String line, ClientController controller) {
         String[] parts = line.split("\\s+");
         String cmd = parts[0].toLowerCase();
 
         switch (cmd) {
             case "slot" -> {
                 int pos = parsePos(parts, 1);
                 controller.slotChoice(pos);
             }
             case "draw"   -> handleDraw(parts, controller);
             case "totem" -> {
                 int pos = parsePos(parts, 1);
                 controller.totemChoice(pos);
             }
             case "skip"   -> controller.skipTurn();
             case "clear" -> handleRender();
             case "details" -> {
                 if (parts.length > 1 && parts[1].equalsIgnoreCase("buildings"))
                     gameReader.printAbove(BUILDING_DETAILS);
                 else if (parts.length > 1 && parts[1].equalsIgnoreCase("events"))
                     gameReader.printAbove(EVENT_DETAILS);
                 else if (parts.length > 1 && parts[1].equalsIgnoreCase("characters"))
                     gameReader.printAbove(CHARACTER_DETAILS);
                 else
                     gameReader.printAbove("Usage: details buildings | details events | details characters");
             }
             case "rematch" -> {
                 if (controller.getMiniModel() == null
                         || !controller.getMiniModel().currentState.getGameStage().equals(GameStages.ENDED)) {
                     System.out.println("Game not ended yet. Use 'quit' to disconnect.");
                     return false;
                 }
                 controller.disconnect();
                 controller.setClient(null);
                 controller.setModel(null);
                 return true;
             }
             case "quit" -> {
                 safeQuit(controller);
                 System.exit(0);
             }
             default -> view.showError(ErrorType.GENERIC_ERROR, "Unknown command: " + cmd);
         }
         return false;
     }
     /**
      * Handles the {@code draw} command, dispatching to the appropriate controller draw method.
      *
      * @param parts      the tokenized command: {@code [draw, upper|lower, tribe|building, pos]}.
      * @param controller the client controller to invoke.
      */
     private void handleDraw(String[] parts, ClientController controller) {
         if (parts.length < 4) {
             view.showError(ErrorType.GENERIC_ERROR, "Usage: draw upper|lower tribe|building <pos>");
             return;
         }
         boolean upper = parts[1].equalsIgnoreCase("upper");
         boolean tribe = parts[2].equalsIgnoreCase("tribe");
         int pos = parsePos(parts, 3);
         if (pos < 0) return;
 
         if (upper && tribe)  controller.drawUpperTribeCard(pos);
         else if (upper)      controller.drawUpperBuildingCard(pos);
         else if (tribe)      controller.drawLowerTribeCard(pos);
         else                 controller.drawLowerBuildingCard(pos);
     }
     /**
      * Handles the {@code clear} command by triggering a full board re-render.
      *
      */
     private void handleRender() {
         view.renderBoard();
     }
     /**
      * Parses an integer position argument at {@code idx} in {@code parts}.
      *
      * @param parts the tokenized command array.
      * @param idx   index of the position argument.
      * @return the parsed integer, or {@code -1} if missing or malformed.
      */
     private int parsePos(String[] parts, int idx) {
         if (parts.length <= idx) {
             view.showError(ErrorType.GENERIC_ERROR, "Missing position argument.");
             return -1;
         }
         try {
             return Integer.parseInt(parts[idx]);
         } catch (NumberFormatException e) {
             view.showError(ErrorType.GENERIC_ERROR,
                     "Invalid position '" + parts[idx] + "' — expected integer.");
             return -1;
         }
     }
     /**
      * Disconnects the client if one is currently connected; safe to call when already disconnected.
      *
      * @param controller the client controller to disconnect.
      */
     private void safeQuit(ClientController controller) {
         if (controller.getClient() != null) controller.disconnect();
     }
 }