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

Class Class, % Method, % Branch, % Line, %
TUI 0% (0/1) 0% (0/16) 0% (0/74) 0% (0/131)


 package it.polimi.ingsw.gc14.View.TUI;
 import it.polimi.ingsw.gc14.ErrorType;
 import it.polimi.ingsw.gc14.Model.*;
 import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
 import it.polimi.ingsw.gc14.View.IView;
 
 import org.jline.reader.LineReader;
 import org.jline.terminal.Terminal;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
 /**
  * Text-based User Interface (TUI) implementation of {@link IView}.
  *
  * <p>Single render view: board + menu on top, all players' hands in a
  * responsive grid below. Hands wrap to a new row when their combined width
  * would exceed the terminal width.
  *
  * <p>All output goes through {@link #display(String)}, which uses
  * {@link LineReader#printAbove} when a JLine reader is set so that the
  * readline prompt is correctly redrawn after background-thread renders.
  */
 public class TUI implements IView {
 
     private MiniModel model;
     private String username;
 
     /**
      * JLine reader — when non-null, all output uses {@code printAbove} so the
      * readline prompt is preserved after background-thread renders.
      */
     private LineReader lineReader;
 
     /** JLine terminal — used to query terminal width for centering and grid layout. */
     private Terminal terminal;
 
     /**
      * Constructs a {@code TUI} bound to the given model.
      *
      * @param model the model to display; may be {@code null} initially.
      */
     public TUI(MiniModel model) {
         this.model    = model;
         this.username = "";
     }
 
 
     /**
      * Sets the username of the local player.
      *
      * @param username the username to assign.
      */
     public void setUsername(String username) {
         this.username = username;
     }
 
     /**
      * Updates the model stored in this view.
      *
      * @param model the latest mini model to display.
      */
     @Override
     public void setModel(MiniModel model) {
         this.model = model;
     }
 
     /**
      * Registers the active JLine {@link LineReader}.
      * Once set, all output routes through {@link LineReader#printAbove} so the
      * prompt survives background-thread renders.
      *
      * @param lineReader the JLine reader to use for output; also provides the terminal reference.
      */
     public void setLineReader(LineReader lineReader) {
         this.lineReader = lineReader;
         this.terminal   = lineReader.getTerminal();
     }
 
 
     /**
      * Default render: dispatches to the right view based on game stage.
      */
     @Override
     public synchronized void render() {
         if (model == null) return;
         GameStages stage = model.currentState.getGameStage();
         if (stage == GameStages.TOTEM_CHOICE) {
             display(buildTotemsContent());
             printLine("\033[2mCommands: totem <pos>\033[0m");
         } else if (stage == GameStages.ENDED) {
             display(buildStandingContent());
             printLine("Type 'rematch' to play again or 'quit' to exit");
         } else {
             display(buildBoardContent());
             printLine("\033[2mCommands: slot <pos> | draw upper/lower tribe/building <pos> | totem <pos> | skip | clear | details buildings/events/characters | quit\033[0m");
         }
     }
 
     /** Renders the board + menu (top) and all players' hands in a grid (bottom). */
     public void renderBoard() {
         display(buildBoardContent());
         printLine("\033[2mCommands: slot <pos> | draw upper/lower tribe/building <pos> | totem <pos> | skip | clear | details buildings/events/characters | quit\033[0m");
     }
 
     /**
      * Shows an error message combined with the current board in one display call,
      * so only one {@code printAbove} is issued and the prompt is redrawn correctly.
      *
      * @param error   the error type.
      * @param message the human-readable message to append.
      */
     public void showError(ErrorType error, String message) {
         String text;
         if (error == ErrorType.WRONG_ACTION
                 && model != null
                 && model.currentState.getCurrentPlayer() != null
                 && !model.currentState.getCurrentPlayer().getUserName().equals(username)) {
             text = "It's not your turn";
         } else {
             text = message;
             if (error == ErrorType.SERVER_CRASHED) {
                 text += "\nPress any key to continue";
             }
         }
         render();
         printLine("\033[31m" + text + "\033[0m");
     }
 
 
     /**
      * Builds the main view: board + menu side by side, followed by all players'
      * hands arranged in a responsive grid.
      */
     private String buildBoardContent() {
         List<String> left  = List.of(boardStamp().split("\n"));
         List<String> right = List.of(buildAllHandsContent().split("\n"));
         return AsciiTable.sideBySide(left, right, 3);
     }
 
     /**
      * Arranges every player's hand side-by-side, wrapping to a new grid row
      * whenever the next panel would exceed the terminal width.
      */
     private String buildAllHandsContent() {
         List<Player> players = new ArrayList<>(model.players.values());
         players.removeIf(p -> model.disconnectedPlayers.contains(p.getUserName()));
         if (players.isEmpty()) return "";
 
         int n = players.size();
         int leftCount = Math.min(3, n);
 
         StringBuilder leftSb  = new StringBuilder();
         StringBuilder rightSb = new StringBuilder();
         for (int i = 0; i < leftCount; i++)  leftSb.append(players.get(i).toString()).append("\n");
         for (int i = leftCount; i < n; i++) rightSb.append(players.get(i).toString()).append("\n");
 
         String handsBlock;
         if (rightSb.isEmpty()) {
             handsBlock = leftSb.toString();
         } else {
             handsBlock = AsciiTable.sideBySide(
                     List.of(leftSb.toString().split("\n", -1)),
                     List.of(rightSb.toString().split("\n", -1)), 3);
         }
 
         if (!model.disconnectedPlayers.isEmpty()) {
             var box = new AsciiTable(BorderStyle.ROUNDED, 1);
             box.addHeader("Disconnected");
             model.disconnectedPlayers.forEach(box::addRow);
             handsBlock += "\n" + box.build();
         }
 
         return handsBlock;
     }
 
     private String buildTotemsContent() {
         var table = new AsciiTable(BorderStyle.ROUNDED, model.availableTotems.size());
         List<String> lines = new ArrayList<>();
         for (int i = 0; i < model.availableTotems.size(); i++) {
             lines.add(i + "." + model.availableTotems.get(i));
         }
         table.addRow(lines);
         String current = model.currentState.getCurrentPlayer() != null
                 ? model.currentState.getCurrentPlayer().getUserName() + " — "
                 : "";
         return current + "TOTEMS AVAILABLE:\n" + table.build();
     }
 
     private String buildStandingContent() {
         if (model.standingPlayers == null || model.standingPlayers.isEmpty()) return "";
         String banner = model.standingPlayers.get(0).getUserName().equals(username)
                 ? "WINNER!" : "GAME OVER";
         List<String> lines = new ArrayList<>();
         for (int i = 0; i < model.standingPlayers.size(); i++) {
             var p = model.standingPlayers.get(i);
             lines.add((i + 1) + ". " + p.getUserName()
                     + " (" + p.getTotem().toString() + ")"
                     + "  \uD83C\uDF56:" + p.getFoodValue()
                     + "  \uD83C\uDFC5:" + p.getPrestigeValue());
         }
         int contentWidth = lines.stream().mapToInt(AsciiTable::displayWidth).max().orElse(0);
         int pad = Math.max(0, (contentWidth - AsciiTable.displayWidth(banner)) / 2);
         var table = new AsciiTable(BorderStyle.ROUNDED, 1);
         table.addHeader(" ".repeat(pad) + banner);
         lines.forEach(table::addRow);
         return table.build();
     }
 
 
     /**
      * Clears the terminal and displays {@code content}, horizontally centered.
      *
      * <p>When a JLine {@link LineReader} is registered, output is routed through
      * {@link LineReader#printAbove} which pauses readline, prints the content,
      * and redraws the prompt — safe to call from any thread.
      * Otherwise, ANSI codes are written directly to stdout.
      */
     private void display(String content) {
         String cleared = "\033[H\033[2J" + center(content);
         if (lineReader != null) {
             lineReader.printAbove(cleared);
         } else {
             System.out.print(cleared);
             System.out.println();
             System.out.flush();
         }
     }
 
     /** Prints {@code text} above the prompt without clearing the screen. */
     private void printLine(String text) {
         if (lineReader != null) {
             lineReader.printAbove(text);
         } else {
             System.out.println(text);
             System.out.flush();
         }
     }
 
     /**
      * Horizontally centers each line of {@code content} within the terminal width.
      * Empty lines are left unpadded. Falls back to the original string when the
      * terminal width is unknown.
      */
     private String center(String content) {
         if (terminal == null) return content;
         int termWidth = terminal.getWidth();
         if (termWidth <= 0) return content;
 
         String[] lines = content.split("\n", -1);
 
         int maxWidth = 0;
         for (String line : lines) {
             int w = visibleLength(line);
             if (w > maxWidth) maxWidth = w;
         }
 
         int pad = Math.max(0, (termWidth - maxWidth) / 2);
         if (pad == 0) return content;
 
         String prefix = " ".repeat(pad);
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < lines.length; i++) {
             if (!lines[i].isEmpty()) sb.append(prefix);
             sb.append(lines[i]);
             if (i < lines.length - 1) sb.append("\n");
         }
         return sb.toString();
     }
 
     /** Returns the display width of a string in terminal columns, stripping ANSI escape codes. */
     private int visibleLength(String line) {
         return AsciiTable.displayWidth(line);
     }
 
 
     /**
      * Returns the board state as a multi-line string: turn order, upper cards,
      * offer track, lower cards.
      *
      * @return multi-line board string.
      */
     public String boardStamp() {
         var offerTrack = new AsciiTable(BorderStyle.ROUNDED, model.slotPlayerMap.size());
         List<String> slotNames   = new ArrayList<>();
         List<String> slotPlayers = new ArrayList<>();
 
         int index = 0;
         for (Map.Entry<Slot, Player> entry : model.slotPlayerMap.entrySet()) {
             slotNames.add((index++) + "." + entry.getKey().toStringTUI());
             slotPlayers.add(entry.getValue() != null ? entry.getValue().getUserName() : " ");
         }
 
         var upperCards = new AsciiTable(BorderStyle.ROUNDED, 2);
         var lowerCards = new AsciiTable(BorderStyle.ROUNDED, 2);
         upperCards.addHeader("Char/\033[38;5;180mEvents\033[0m", "Building");
         lowerCards.addHeader("Char/\033[38;5;180mEvents\033[0m", "Building");
 
         int maxU = Math.max(model.upperListTribeCards.size(), model.upperListBuildingCards.size());
         for (int i = 0; i < maxU; i++) {
             String t = i < model.upperListTribeCards.size() ? i + ":" + model.upperListTribeCards.get(i).toStringBoard() : "";
             String b = i < model.upperListBuildingCards.size() ? i + ":" + model.upperListBuildingCards.get(i).toStringBoard() : "";
             upperCards.addRow(t, b);
         }
 
         int maxL = Math.max(model.lowerListTribeCards.size(), model.lowerListBuildingCards.size());
         for (int i = 0; i < maxL; i++) {
             String t = i < model.lowerListTribeCards.size() ? i + ":" + model.lowerListTribeCards.get(i).toStringBoard() : "";
             String b = i < model.lowerListBuildingCards.size() ? i + ":" + model.lowerListBuildingCards.get(i).toStringBoard() : "";
             lowerCards.addRow(t, b);
         }
 
         offerTrack.addRow(slotPlayers);
         offerTrack.addRow(slotNames);
 
         String upperSection = upperCards.build();
 
         String middleSection = model.orderLogicCard.toString() + offerTrack.build();
 
         String lowerSection = lowerCards.build();
 
         return model.currentState + "\n"
                 + upperSection + "\n"
                 + middleSection + "\n"
                 + lowerSection;
     }
 
 }