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;
}
}