");
+ 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);
+ }
+
+ private void handleRender(String[] parts) {
+ String sub = parts.length > 1 ? parts[1].toLowerCase() : "board";
+ switch (sub) {
+ case "full" -> view.fullRender();
+ case "players" -> view.renderPlayer();
+ default -> view.renderBoard();
+ }
+ }
+
+ 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;
+ }
+ }
+
+ private void safeQuit(ClientController controller) {
+ if (controller.getClient() != null) controller.disconnect();
+ }
+}
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 9c771ea..a3b5403 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
@@ -5,6 +5,9 @@ import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.View.IView;
+import org.jline.reader.LineReader;
+import org.jline.terminal.Terminal;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -13,358 +16,347 @@ import java.util.Map;
/**
* Text-based User Interface (TUI) implementation of {@link IView}.
*
- * Renders the current state of a {@link Game} model directly to the
- * standard output using Unicode box-drawing characters and fixed-width
- * ASCII tables (see {@link AsciiTable}).
+ *
Renders the current state of a {@link MiniModel} directly to the
+ * terminal using Unicode box-drawing characters and fixed-width ASCII tables.
*
- *
The display is split into two side-by-side panels:
- *
- * - Left panel — board or player status, depending on the render method called.
- * - Right panel — menu options legend and the current player's hand.
- *
- *
- * The terminal is cleared before each render via a platform-aware
- * {@code cls} / {@code clear} system call.
- *
- *
Typical usage:
- *
{@code
- * TUI tui = new TUI(game);
- * tui.setUsername("Alice");
- * tui.fullRender();
- * }
+ * 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 {
- /**
- * The {@link MiniModel} model whose state is rendered.
- */
private MiniModel model;
-
- /**
- * The username of the local player, used to retrieve and display
- * that player's hand in the right panel.
- *
- * @see #setUsername(String)
- */
private String username;
/**
- * Constructs a {@code TUI} bound to the given game model.
- * The username is initialised to an empty string and must be set
- * separately via {@link #setUsername(String)} before calling any
- * render method that displays the player's hand.
+ * 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. */
+ private Terminal terminal;
+
+ /**
+ * Constructs a {@code TUI} bound to the given model.
*
- * @param model the {@link Game} model to display; must not be {@code null}
+ * @param model the model to display; may be {@code null} initially.
*/
public TUI(MiniModel model) {
- this.model = model;
+ this.model = model;
this.username = "";
}
- /**
- * Sets the username of the local player.
- * This value is used by {@link #renderBoard()} and {@link #fullRender()}
- * to look up the correct player hand via
- * {@link Game#getPlayerByUsername(String)}.
- *
- * @param username the player's username; must match an existing player
- * in the current {@link Game} model
- */
+ // ── Setters ───────────────────────────────────────────────────────────────
+
+ /** Sets the username of the local player. */
public void setUsername(String username) {
this.username = username;
}
- /**
- * Updates the game model stored in this view.
- * Should be called whenever the game state changes so that the next
- * render reflects the latest state.
- *
- * @param model the new {@link Game} model; must not be {@code null}
- */
+ /** Updates the model stored in this view. */
@Override
public void setModel(MiniModel model) {
this.model = model;
}
/**
- * Default render entry point, as required by {@link IView}.
- * Delegates to {@link #renderBoard()}.
+ * Registers the active JLine {@link LineReader}.
+ * Once set, all output routes through {@link LineReader#printAbove} so the
+ * prompt survives background-thread renders.
+ */
+ public void setLineReader(LineReader lineReader) {
+ this.lineReader = lineReader;
+ this.terminal = lineReader.getTerminal();
+ }
+
+ // ── Public render entry points ────────────────────────────────────────────
+
+ /**
+ * Default render: dispatches to the right sub-renderer based on game stage.
*/
@Override
public synchronized void render() {
- if(model != null)
- {
- if(model.currentState.getGameStage().equals(GameStages.TOTEM_CHOICE))
- {
- renderTotems();
- }
- else if(model.currentState.getGameStage().equals(GameStages.ENDED))
- {
- renderStanding();
- System.out.println("Press 8 to start another game or press 0 to exit");
- return;
- }
- else
- {
- renderBoard();
- }
- System.out.println("\nYOUR ACTION:");
+ if (model == null) return;
+ GameStages stage = model.currentState.getGameStage();
+ if (stage.equals(GameStages.TOTEM_CHOICE)) {
+ display(buildTotemsContent());
+ } else if (stage.equals(GameStages.ENDED)) {
+ display(buildStandingContent() + "\nType 'rematch' to play again or 'quit' to exit");
+ } else {
+ display(buildBoardContent());
}
}
- /** Prints the final standings side-by-side in pairs, then announces the winner or game-over result. */
- private void renderStanding() {
- if(model.standingPlayers!=null)
- {
- StringBuilder stringBuilder=new StringBuilder();
- for(int i = 0; i< model.standingPlayers.size()/2; i++)
- {
- stringBuilder.append(AsciiTable.sideBySide(List.of(model.standingPlayers.get((i*2)).toString().split("\n")),List.of(model.standingPlayers.get((i*2+1)).toString().split("\n")),2));
- }
- stringBuilder.append("\n");
- if(model.standingPlayers.size()%2!=0)
- {
- stringBuilder.append(model.standingPlayers.get(model.standingPlayers.size()-1).toString());
- stringBuilder.append("\n");
- }
- System.out.println(stringBuilder);
- if(model.standingPlayers.get(0).getUserName().equals(username))
- {
- System.out.println("GAME ENDED: !!!YOU WON!!!");
- }
- else {
- System.out.println("GAME ENDED: !!!YOU LOST!!!");
- }
- }
- }
-
- /**
- * Renders a full view of the game, combining both player status
- * and board status.
- *
- *
Layout:
- *
- * - Top — player status table produced by
- * {@link #PlayersStamp()}.
- * - Bottom-left — board status produced by
- * {@link #BoardStamp()}.
- * - Bottom-right — menu options legend and the local
- * player's hand.
- *
- *
- * The terminal is cleared before rendering.
- */
- public void fullRender() {
- clearTerminal();
- List lines = List.of(BoardStamp().split("\n"));
- List lines2 = List.of((printMenuOptions() + "\n" +
- model.players.get(username)).split("\n"));
- System.out.println(PlayersStamp() + "\n" +
- AsciiTable.sideBySide(lines, lines2, 3));
- }
-
- /**
- * Renders the board status only.
- *
- * Layout:
- *
- * - Left panel — turn order, upper card row, offer track,
- * and lower card row, as produced by {@link #BoardStamp()}.
- * - Right panel — menu options legend followed by the local
- * player's hand.
- *
- *
- * The terminal is cleared before rendering.
- */
+ /** Renders the board panel + menu + player hand side by side. */
public void renderBoard() {
- clearTerminal();
- List lines = List.of(BoardStamp().split("\n"));
- List lines2 = List.of((printMenuOptions() + "\nYOUR HAND\n" +
- model.players.get(username)).split("\n"));
- System.out.println(AsciiTable.sideBySide(lines, lines2, 3));
+ display(buildBoardContent());
+ }
+
+ /** Renders players panel + board panel + menu + player hand. */
+ public void fullRender() {
+ display(buildFullContent());
+ }
+
+ /** Renders the totem-choice panel. */
+ public void renderTotems() {
+ display(buildTotemsContent());
+ }
+
+ /** Renders the players panel + menu side by side. */
+ public void renderPlayer() {
+ display(buildPlayerContent());
}
/**
- * Renders the available totems in the terminal interface.
+ * Shows an error message above (or below) the current board.
*
- * The method clears the terminal, builds a table containing the currently
- * available totems together with their selection positions, and displays the
- * instructions required to choose one.
+ *
Board and message are combined into a single {@link #display} call so
+ * that only one {@code printAbove} is issued — avoiding the double-clear
+ * that would occur if render and message were displayed separately.
+ *
+ * @param error the error type.
+ * @param message the human-readable message to append.
*/
- public void renderTotems() {
- clearTerminal();
+ public void showError(ErrorType error, String message) {
+ if (model == null) {
+ display(message);
+ return;
+ }
+ GameStages stage = model.currentState.getGameStage();
+ String base;
+ if (stage.equals(GameStages.TOTEM_CHOICE)) {
+ base = buildTotemsContent();
+ } else if (stage.equals(GameStages.ENDED)) {
+ base = buildStandingContent() + "\nType 'rematch' to play again or 'quit' to exit";
+ } else {
+ base = buildBoardContent();
+ }
+
+ if (error.equals(ErrorType.WRONG_ACTION)
+ && model.currentState.getCurrentPlayer() != null
+ && !model.currentState.getCurrentPlayer().getUserName().equals(username)) {
+ display(base + "\nIt's not your turn");
+ return;
+ }
+
+ String full = base + "\n" + message;
+ if (error == ErrorType.SERVER_CRASHED) {
+ full += "\nPress any key to continue";
+ }
+ display(full);
+ }
+
+ // ── Content builders (return strings, do not print) ───────────────────────
+
+ private String buildBoardContent() {
+ List left = List.of(BoardStamp().split("\n"));
+ List right = List.of((printMenuOptions() + "\nYOUR HAND\n"
+ + model.players.get(username)).split("\n"));
+ return AsciiTable.sideBySide(left, right, 3);
+ }
+
+ private String buildFullContent() {
+ List left = List.of(BoardStamp().split("\n"));
+ List right = List.of((printMenuOptions() + "\n"
+ + model.players.get(username)).split("\n"));
+ return PlayersStamp() + "\n" + AsciiTable.sideBySide(left, right, 3);
+ }
+
+ private String buildPlayerContent() {
+ List left = List.of(PlayersStamp().split("\n"));
+ List right = List.of(printMenuOptions().split("\n"));
+ return AsciiTable.sideBySide(left, right, 3);
+ }
+
+ private String buildTotemsContent() {
var table = new AsciiTable(BorderStyle.ROUNDED, model.availableTotems.size());
- List lines =new ArrayList<>();
- for(int i=0;i lines = new ArrayList<>();
+ for (int i = 0; i < model.availableTotems.size(); i++) {
+ lines.add(i + "." + model.availableTotems.get(i));
}
table.addRow(lines);
- System.out.println(model.currentState+"\nTOTEMS AVAILABLE:\n"+table.build()+"\nPress 7 and then write the position");
+ String current = model.currentState.getCurrentPlayer() != null
+ ? model.currentState.getCurrentPlayer().getUserName() + " — "
+ : "";
+ return current + "TOTEMS AVAILABLE:\n" + table.build() + "\nType: totem ";
}
- /**
- * Renders the player status table only.
- *
- * Layout:SkipTurn
- *
- * - Left panel — prestige, food, character deck, and building
- * deck for all players, as produced by {@link #PlayersStamp()}.
- * - Right panel — menu options legend.
- *
- *
- * The terminal is cleared before rendering.
- */
- public void renderPlayer() {
- clearTerminal();
- List lines = List.of(PlayersStamp().split("\n"));
- List lines2 = List.of(printMenuOptions().split("\n"));
- System.out.println(AsciiTable.sideBySide(lines, lines2, 3));
- }
-
-
- /**
- * Prints an error message to standard output.
- *
- * @param error the error message to display
- */
- public void showError(ErrorType error,String message) {
- clearTerminal();
- render();
- if(error.equals(ErrorType.WRONG_ACTION))
- {
- if(!model.currentState.getCurrentPlayer().getUserName().equals(username)){
- System.out.println("It's not your turn");
- return;
- }
+ private String buildStandingContent() {
+ if (model.standingPlayers == null) return "";
+ StringBuilder sb = new StringBuilder();
+ int size = model.standingPlayers.size();
+ for (int i = 0; i < size / 2; i++) {
+ sb.append(AsciiTable.sideBySide(
+ List.of(model.standingPlayers.get(i * 2).toString().split("\n")),
+ List.of(model.standingPlayers.get(i * 2 + 1).toString().split("\n")), 2));
+ }
+ sb.append("\n");
+ if (size % 2 != 0) {
+ sb.append(model.standingPlayers.get(size - 1)).append("\n");
+ }
+ if (model.standingPlayers.get(0).getUserName().equals(username)) {
+ sb.append("GAME ENDED: !!!YOU WON!!!");
+ } else {
+ sb.append("GAME ENDED: !!!YOU LOST!!!");
+ }
+ return sb.toString();
+ }
+
+ // ── Core display primitive ────────────────────────────────────────────────
+
+ /**
+ * Clears the terminal and displays {@code content}, horizontally centered.
+ *
+ * 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.
+ *
+ * @param content the text to display (board, error, etc.).
+ */
+ 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();
}
- System.out.println(message);
- if(error==ErrorType.SERVER_CRASHED)
- System.out.println("Press any key to continue");
}
/**
- * Builds and returns the menu options panel as a two-column
- * {@link AsciiTable} with rounded borders.
- *
- *
The left column lists game action commands (slot choice, draw, pick…),
- * the right column lists render shortcuts (full, board, players).
- *
- * @return the rendered menu table as a multi-line string
+ * 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 visible (printable) length of a string, stripping ANSI escape codes. */
+ private int visibleLength(String line) {
+ return line.replaceAll("\033\\[[^m]*m", "").length();
+ }
+
+ // ── Board / players stamp helpers ─────────────────────────────────────────
+
+ /**
+ * Returns all players rendered side-by-side in pairs.
+ *
+ * @return multi-line string with all player panels.
+ */
+ public String PlayersStamp() {
+ StringBuilder sb = new StringBuilder();
+ ArrayList list = new ArrayList<>(model.players.values());
+ for (int i = 0; i < list.size() / 2; i++) {
+ sb.append(AsciiTable.sideBySide(
+ List.of(list.get(i * 2).toString().split("\n")),
+ List.of(list.get(i * 2 + 1).toString().split("\n")), 2));
+ }
+ sb.append("\n");
+ if (list.size() % 2 != 0) {
+ sb.append(list.get(list.size() - 1)).append("\n");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 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.UNICODE, model.slotPlayerMap.size());
+ List slotNames = new ArrayList<>();
+ List slotPlayers = new ArrayList<>();
+
+ int index = 0;
+ for (Map.Entry entry : model.slotPlayerMap.entrySet()) {
+ slotNames.add((index++) + "." + entry.getKey().toStringTUI());
+ slotPlayers.add(entry.getValue() != null ? entry.getValue().getUserName() : " ");
+ }
+
+ var TribeUpper = new AsciiTable(BorderStyle.UNICODE, 1);
+ var TribeLower = new AsciiTable(BorderStyle.UNICODE, 1);
+ var BuildUpper = new AsciiTable(BorderStyle.UNICODE, 1);
+ BuildUpper.addHeader("Building ");
+ var BuildLower = new AsciiTable(BorderStyle.UNICODE, 1);
+ BuildLower.addHeader("Building ");
+
+ List upperTribeRows = new ArrayList<>();
+ List lowerTribeRows = new ArrayList<>();
+ upperTribeRows.add("Char/Events");
+ lowerTribeRows.add("Char/Events");
+
+ int max = Math.max(model.upperListTribeCards.size(), model.lowerListTribeCards.size());
+ for (int i = 0; i < max; i++) {
+ if (i < model.upperListTribeCards.size())
+ upperTribeRows.add(i + "-" + model.upperListTribeCards.get(i).toStringBoard());
+ if (i < model.lowerListTribeCards.size())
+ lowerTribeRows.add(i + "-" + model.lowerListTribeCards.get(i).toStringBoard());
+ }
+
+ int maxB = Math.max(model.upperListBuildingCards.size(), model.lowerListBuildingCards.size());
+ for (int i = 0; i < maxB; i++) {
+ if (i < model.upperListBuildingCards.size())
+ BuildUpper.addRow(i + "-" + model.upperListBuildingCards.get(i).toString());
+ if (i < model.lowerListBuildingCards.size())
+ BuildLower.addRow(i + "-" + model.lowerListBuildingCards.get(i).toString());
+ }
+
+ upperTribeRows.forEach(TribeUpper::addRow);
+ lowerTribeRows.forEach(TribeLower::addRow);
+ offerTrack.addRow(slotPlayers);
+ offerTrack.addRow(slotNames);
+
+ return "CURRENT STATE\n" + model.currentState + "\n" + model.orderLogicCard + "\n"
+ + AsciiTable.sideBySide(
+ Arrays.stream(TribeUpper.build().split("\n")).toList(),
+ Arrays.stream(BuildUpper.build().split("\n")).toList(), 2)
+ + "\n" + offerTrack.build() + "\n"
+ + AsciiTable.sideBySide(
+ List.of(TribeLower.build().split("\n")),
+ Arrays.stream(BuildLower.build().split("\n")).toList(), 2);
+ }
+
+ // ── Menu options ──────────────────────────────────────────────────────────
+
private String printMenuOptions() {
var table = new AsciiTable(BorderStyle.ROUNDED, 2);
- table.addHeader("Menu Options", "Render Options");
- table.addRow(List.of("1-SlotChoice(pos)", "A-Full Render"));
- table.addRow(List.of("2-DrawUpperTribe(pos)", "B-Board Render"));
- table.addRow(List.of("3-DrawUpperBuilding(pos)", "C-Players Render"));
- table.addRow(List.of("4-DrawLowerTribe(pos)", ""));
- table.addRow(List.of("5-DrawLowerBuilding(pos)", ""));
- table.addRow(List.of("6-SkipTurn", ""));
- table.addRow(List.of("0-Disconnect", ""));
+ table.addHeader("Commands", "Render");
+ table.addRow(List.of("slot ", "render full"));
+ table.addRow(List.of("draw upper tribe ", "render board"));
+ table.addRow(List.of("draw upper building ", "render players"));
+ table.addRow(List.of("draw lower tribe ", ""));
+ table.addRow(List.of("draw lower building ", ""));
+ table.addRow(List.of("totem ", ""));
+ table.addRow(List.of("skip", ""));
+ table.addRow(List.of("rematch | quit", ""));
return table.build();
}
-
- /**
- * Clears the terminal using a platform-aware system call.
- * Uses {@code cls} on Windows and {@code clear} on Unix-like systems.
- * Failures are silently ignored to avoid interrupting the render flow.
- */
- private void clearTerminal() {
- try {
- String os = System.getProperty("os.name").toLowerCase();
- ProcessBuilder pb = os.contains("win")
- ? new ProcessBuilder("cmd", "/c", "cls")
- : new ProcessBuilder("clear");
- pb.inheritIO().start().waitFor();
- } catch (Exception ignored) {}
- }
-
- /**
- * Returns a string representation of all the players currently in the game,
- * arranged side by side in pairs.
- * If the number of players is odd, the last player is printed on its own line.
- *
- * @return {@code String} - a string representation of all the players.
- */
- public String PlayersStamp()
- {
- StringBuilder stringBuilder=new StringBuilder();
- ArrayList playersList=new ArrayList<>(model.players.values());
- for(int i = 0; i< model.players.size()/2; i++)
- {
- stringBuilder.append(AsciiTable.sideBySide(List.of(playersList.get((i*2)).toString().split("\n")),List.of(playersList.get((i*2+1)).toString().split("\n")),2));
- }
- stringBuilder.append("\n");
- if(playersList.size()%2!=0)
- {
- stringBuilder.append(playersList.get(playersList.size()-1).toString());
- stringBuilder.append("\n");
- }
- return stringBuilder.toString();
- }
-
- /**
- * Returns a string representation of the board, including the current state,
- * the offer track with slot assignments, the upper and lower tribe card lists,
- * and the upper and lower building card lists.
- *
- * @return {@code String} - a string representation of the board.
- */
- public String BoardStamp()
- {
- var offerTrack = new AsciiTable(BorderStyle.UNICODE, model.slotPlayerMap.size());
- List stringUpOffer=new ArrayList<>();
- List stringDownOffer=new ArrayList<>();
-
- int index=0;
- for(Map.Entry entry:model.slotPlayerMap.entrySet())
- {
- stringDownOffer.add((index++)+"."+entry.getKey().toStringTUI());
- if(entry.getValue()!=null)
- stringUpOffer.add(entry.getValue().getUserName());
- else
- stringUpOffer.add(" ");
- }
-
- var TribeTableUpper = new AsciiTable(BorderStyle.UNICODE,1);
- var TribeTableLower = new AsciiTable(BorderStyle.UNICODE,1);
-
- List stringUpperListTribe=new ArrayList<>();
- List stringLowerListTribe=new ArrayList<>();
- stringUpperListTribe.add("Char/Events");
- stringLowerListTribe.add("Char/Events");
- for(int i=0;i