diff --git a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java index 40b5806..39b7bf3 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java @@ -148,10 +148,7 @@ public class ClientLauncherTUI { node("building"))), node("totem"), node("skip"), - node("render", - node("full"), - node("board"), - node("players")), + node("render"), node("rematch"), node("quit") ); @@ -220,12 +217,7 @@ public class ClientLauncherTUI { } 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(); - } + view.renderBoard(); } private int parsePos(String[] parts, int idx) { diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/BuildingCard.java b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/BuildingCard.java index 1fbe1e6..220fd5b 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/BuildingCard.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/BuildingCard.java @@ -274,6 +274,6 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf */ @Override public String toString() { - return "⎕:" + " ID:" + String.valueOf(getEffectId())+ " $:"+String.valueOf(getPrice())+" PV:"+String.valueOf(getPrestigeValue()); + return ":" + " ID:" + String.valueOf(getEffectId())+ " 🍗:"+String.valueOf(getPrice())+" \uD83C\uDFC5:"+String.valueOf(getPrestigeValue()); } } \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Builder.java b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Builder.java index aab81f5..5d397f5 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Builder.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Builder.java @@ -139,7 +139,7 @@ public class Builder extends Character { */ @Override public String toString() { - return super.toString() + " RV:" + String.valueOf(reductionValue) + " PV:" + String.valueOf(prestigeValue); + return super.toString() + " \uD83D\uDD28:" + String.valueOf(reductionValue) + " \uD83C\uDFC5:" + String.valueOf(prestigeValue); } /** @@ -154,7 +154,7 @@ public class Builder extends Character { @Override public String toStringBoard() { - return super.toStringBoard()+" "+ " RV:" + String.valueOf(reductionValue) + " PV:" + String.valueOf(prestigeValue); + return super.toStringBoard()+" \uD83D\uDD28:" + String.valueOf(reductionValue) + " \uD83C\uDFC5:" + String.valueOf(prestigeValue); } diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Shaman.java b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Shaman.java index 1f1c937..b0c4409 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Shaman.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Characters/Shaman.java @@ -86,7 +86,7 @@ public class Shaman extends Character { */ @Override public String toString() { - return super.toString() + " *:" + String.valueOf(icon); + return super.toString() + " " + "🌟".repeat(icon); } /** @@ -102,7 +102,7 @@ public class Shaman extends Character { */ @Override public String toStringBoard() { - return super.toStringBoard() + " *:" + String.valueOf(icon); + return super.toStringBoard() + " " + "🌟".repeat(icon); } /** diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/EventCard.java b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/EventCard.java index 07bd61e..2a3329b 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/EventCard.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/EventCard.java @@ -74,7 +74,7 @@ public abstract class EventCard extends TribeCard { */ @Override public String toString() { - return super.toString()+"(Event)"+type.toString(); + return super.toString() + " " + type.toString(); } /** @@ -89,7 +89,7 @@ public abstract class EventCard extends TribeCard { */ @Override public String toStringBoard() { - return super.toString()+"(Event)"+type.toString(); + return super.toString() + " " + type.toString(); } /** diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Events/ShamanicRitual.java b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Events/ShamanicRitual.java index 3e1544c..91515da 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Events/ShamanicRitual.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Cards/TribeCards/Events/ShamanicRitual.java @@ -128,7 +128,7 @@ public class ShamanicRitual extends EventCard { */ @Override public String toStringBoard() { - return super.toStringBoard()+" *>:"+prestigeToAdd+" *<:"+prestigeToRemove; + return super.toStringBoard()+" \uD83C\uDF1F>:"+prestigeToAdd+" \uD83C\uDF1F<:"+prestigeToRemove; } } diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/GamePackage/CurrentState.java b/src/main/java/it/polimi/ingsw/gc14/Model/GamePackage/CurrentState.java index f464e02..fa369c0 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/GamePackage/CurrentState.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/GamePackage/CurrentState.java @@ -201,7 +201,7 @@ public class CurrentState implements Serializable { } @Override public String toString(){ - var table= new AsciiTable(BorderStyle.UNICODE,6); + var table= new AsciiTable(BorderStyle.ROUNDED,6); Listheader= new ArrayList<>(); header.add("Player"); header.add("NUpper"); diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order2.java b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order2.java index 4a73da0..7555247 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order2.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order2.java @@ -66,7 +66,7 @@ public class Order2 extends OrderLogicCard { @Override public String toString() { - var table = new AsciiTable(BorderStyle.UNICODE, 2); + var table = new AsciiTable(BorderStyle.ROUNDED, 2); List stringUp=new ArrayList<>(); for(int i=0;i<2;i++) { @@ -83,8 +83,8 @@ public class Order2 extends OrderLogicCard { table.addRow(stringUp); List stringList=new ArrayList<>(); - stringList.add("+1 Food"); - stringList.add("-1 Food / -2 PP"); + stringList.add("+1🍗"); + stringList.add("-1🍗 / -2\uD83C\uDFC5"); table.addRow(stringList); return "TURN ORDER\n" + table.build() + "\n"; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order3.java b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order3.java index 3629b3b..0ac675d 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order3.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order3.java @@ -68,7 +68,7 @@ public class Order3 extends OrderLogicCard { public String toString() { - var table = new AsciiTable(BorderStyle.UNICODE, 3); + var table = new AsciiTable(BorderStyle.ROUNDED, 3); List stringUp=new ArrayList<>(); for(int i=0;i<3;i++) @@ -87,9 +87,9 @@ public class Order3 extends OrderLogicCard { List stringList=new ArrayList<>(); - stringList.add("+2 Food"); + stringList.add("+2 🍗"); stringList.add("--"); - stringList.add("-1 Food / -2 PP"); + stringList.add("-1 🍗 / -2 ⭐"); table.addRow(stringList); diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order4.java b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order4.java index 4b4db23..517e061 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order4.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order4.java @@ -75,7 +75,7 @@ public class Order4 extends OrderLogicCard { public String toString() { - var table = new AsciiTable(BorderStyle.UNICODE, 4); + var table = new AsciiTable(BorderStyle.ROUNDED, 4); List stringUp=new ArrayList<>(); for(int i=0;i<4;i++) { @@ -92,10 +92,10 @@ public class Order4 extends OrderLogicCard { table.addRow(stringUp); List stringList=new ArrayList<>(); - stringList.add("+2 Food"); - stringList.add("+1 Food"); + stringList.add("+2 🍗"); + stringList.add("+1 🍗"); stringList.add("--"); - stringList.add("-1 Food / -2 PP"); + stringList.add("-1 🍗 / -2 ⭐"); table.addRow(stringList); return "TURN ORDER\n" + table.build() + "\n"; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order5.java b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order5.java index 64e31da..221dae6 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order5.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Orders/Order5.java @@ -76,7 +76,7 @@ public class Order5 extends OrderLogicCard { public String toString() { - var table = new AsciiTable(BorderStyle.UNICODE, 5); + var table = new AsciiTable(BorderStyle.ROUNDED, 5); List stringUp=new ArrayList<>(); for(int i=0;i<5;i++) { @@ -93,11 +93,11 @@ public class Order5 extends OrderLogicCard { table.addRow(stringUp); List stringList=new ArrayList<>(); - stringList.add("+3 Food"); - stringList.add("+1 Food"); + stringList.add("+3 🍗"); + stringList.add("+1 🍗"); stringList.add("--"); stringList.add("--"); - stringList.add("-1 Food / -2 PP"); + stringList.add("-1 🍗 / -2 ⭐"); table.addRow(stringList); return "TURN ORDER\n" + table.build() + "\n"; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/PlayableCard.java b/src/main/java/it/polimi/ingsw/gc14/Model/PlayableCard.java index ff0e353..5e805ca 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/PlayableCard.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/PlayableCard.java @@ -74,7 +74,7 @@ public abstract class PlayableCard implements Serializable { */ @Override public String toString() { - return "⎕:"; + return ":"; } /** @@ -86,6 +86,6 @@ public abstract class PlayableCard implements Serializable { */ public String toStringBoard() { - return "⎕:"; + return ":"; } } 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 02daf2d..c185a05 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Player.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Player.java @@ -271,15 +271,15 @@ public class Player implements Serializable { public String toString() { int Last = -1; - var table = new AsciiTable(UNICODE, 1); + var table = new AsciiTable(ROUNDED, 1); table.addHeader(this.getUserName()); table.addRow("RESOURCES:"); if(this.totem != null) { table.addRow("Totem: " + this.totem.toString()); } - table.addRow("Food: " + this.getFoodValue()); - table.addRow("Prestige: " + this.getPrestigeValue()); + table.addRow("🍗 " + this.getFoodValue()); + table.addRow("2\uD83C\uDFC5 " + this.getPrestigeValue()); table.addSeparator(); if(!this.hunters.isEmpty()){ diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Slot.java b/src/main/java/it/polimi/ingsw/gc14/Model/Slot.java index b2e35b2..b114a76 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Slot.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Slot.java @@ -182,7 +182,7 @@ public class Slot implements Serializable { s.append("↓"); } if(getFood()!=0) - s.append("+"+getFood()+" Food"); + s.append("+"+getFood()+" 🍗"); return s.toString(); } diff --git a/src/main/java/it/polimi/ingsw/gc14/View/TUI/AsciiTable.java b/src/main/java/it/polimi/ingsw/gc14/View/TUI/AsciiTable.java index 813708c..79e419d 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/TUI/AsciiTable.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/TUI/AsciiTable.java @@ -8,10 +8,13 @@ import java.util.*; * to obtain the fully rendered string. Column width is computed automatically * from the widest cell across all rows. * + *

Cell widths are measured in terminal columns, not Java {@code char} units: + * wide characters (emoji, CJK) count as 2 columns each. + * *

Example usage: *

{@code
  * AsciiTable table = new AsciiTable(BorderStyle.UNICODE, 3);
- * table.addHeader("Name", "Food", "PP");
+ * table.addHeader("Name", "🍗", "⭐");
  * table.addRow("Alice", "4", "12");
  * table.addRow("Bob",   "2", "8");
  * System.out.println(table.build());
@@ -19,92 +22,33 @@ import java.util.*;
  */
 public class AsciiTable {
 
-    /**
-     * The border style used to draw the table (e.g. Unicode box-drawing,
-     * plain ASCII, rounded corners).
-     */
     private final BorderStyle s;
-
-    /**
-     * The number of columns in the table.
-     * Every row added via {@link #addRow} must contain exactly this many cells.
-     */
     private final int cols;
-
-    /**
-     * The rows of the table, each represented as a list of cell strings.
-     * Rows are stored in display order; {@link #addHeader} inserts at index 0.
-     */
     private final List> rows = new ArrayList<>();
-
-    /**
-     * Row indices after which a horizontal separator line is drawn.
-     * Populated by {@link #addHeader} and {@link #addSeparator}.
-     */
     private final List separators = new ArrayList<>();
 
-    /**
-     * Constructs an empty {@code AsciiTable} with the given border style
-     * and column count.
-     *
-     * @param s    the {@link BorderStyle} to use for box-drawing characters
-     * @param cols the number of columns; every row must supply exactly this
-     *             many cells
-     */
     public AsciiTable(BorderStyle s, int cols) {
         this.s = s; this.cols = cols;
     }
 
-    /**
-     * Appends a row at the bottom of the table.
-     *
-     * @param cells one string per column, in left-to-right order
-     */
     public void addRow(String... cells) { rows.add(Arrays.asList(cells)); }
-
-    /**
-     * Appends a row at the bottom of the table.
-     *
-     * @param cells a list of strings, one per column, in left-to-right order
-     */
     public void addRow(List cells) { rows.add(cells); }
 
-    /**
-     * Inserts a header row at the top of the table and marks it with a
-     * horizontal separator so that a dividing line is drawn below it.
-     *
-     * 

Calling this method after rows have already been added will push - * all existing rows down by one position. - * - * @param cells one string per column, in left-to-right order - */ public void addHeader(String... cells) { rows.add(0, Arrays.asList(cells)); separators.add(0); } - /** - * Marks the current last row so that a horizontal separator line is - * drawn below it when the table is built. - * - *

Call this method immediately after the row that should be followed - * by the separator. - */ public void addSeparator() { separators.add(rows.size() - 1); } /** * Renders the table to a multi-line string. - * - *

Column width is determined dynamically as the length of the longest - * cell across all rows, plus one padding space. All cells are left-aligned - * and padded or truncated to the same width. - * - * @return the fully rendered table as a single string with embedded newlines + * Column width is the widest cell in display columns (wide chars = 2), plus 1. */ public String build() { var sb = new StringBuilder(); int maxWidth = rows.stream() - .mapToInt(x -> x.stream().mapToInt(y -> y.length()).max().getAsInt()) + .mapToInt(x -> x.stream().mapToInt(y -> displayWidth(y)).max().getAsInt()) .max().getAsInt() + 1; sb.append(hline(s.tl(), s.mt(), s.tr(), maxWidth)).append('\n'); @@ -120,16 +64,6 @@ public class AsciiTable { return sb.toString(); } - /** - * Builds a single horizontal border line spanning all columns. - * - * @param l the left-end character (e.g. {@code ╔}, {@code ╠}, {@code ╚}) - * @param m the column-junction character (e.g. {@code ╦}, {@code ╬}, {@code ╩}) - * @param r the right-end character (e.g. {@code ╗}, {@code ╣}, {@code ╝}) - * @param maxWidth the width in characters of each column segment, - * filled with the horizontal line character of the current style - * @return the rendered horizontal line as a string - */ private String hline(String l, String m, String r, int maxWidth) { var sb = new StringBuilder(l); for (int i = 0; i < cols; i++) { @@ -140,41 +74,22 @@ public class AsciiTable { } /** - * Right-pads a string with spaces to the given width, or truncates it - * if it exceeds that width. - * - * @param s the input string - * @param w the desired output width in characters - * @return a string of exactly {@code w} characters + * Pads {@code s} with trailing spaces so its display width equals {@code w}. + * If the string is already at or over {@code w} columns, it is returned as-is. */ private static String rpad(String s, int w) { - if (s.length() >= w) return s.substring(0, w); - return s + " ".repeat(w - s.length()); + int dw = displayWidth(s); + if (dw >= w) return s; + return s + " ".repeat(w - dw); } /** * Places two pre-rendered text blocks side by side, separated by a gap. - * - *

Each block is a list of lines as returned by {@link #build()}. - * Lines in the left block are padded to a uniform width so that the - * right block always starts at the same horizontal position. If one - * block is taller than the other, the shorter one is padded with blank - * lines. - * - *

Example: - *

{@code
-     * List left  = Arrays.asList(table1.build().split("\n"));
-     * List right = Arrays.asList(table2.build().split("\n"));
-     * System.out.print(AsciiTable.sideBySide(left, right, 2));
-     * }
- * - * @param left lines of the left block - * @param right lines of the right block - * @param gap number of blank spaces between the two blocks - * @return the merged string with embedded newlines + * Left-block lines are padded to a uniform display width so the right block + * always starts at the same column. Uses {@link #displayWidth} for measurement. */ public static String sideBySide(List left, List right, int gap) { - int leftWidth = left.stream().mapToInt(String::length).max().orElse(0); + int leftWidth = left.stream().mapToInt(AsciiTable::displayWidth).max().orElse(0); int maxHeight = Math.max(left.size(), right.size()); String padding = " ".repeat(gap); @@ -187,4 +102,57 @@ public class AsciiTable { } return sb.toString(); } -} \ No newline at end of file + + // ── Wide-character width ────────────────────────────────────────────────── + + /** + * Returns the number of terminal columns required to display {@code s}. + * Wide characters (emoji, CJK, full-width) count as 2; all others as 1. + */ + public static int displayWidth(String s) { + int w = 0; + for (int i = 0; i < s.length(); ) { + int cp = s.codePointAt(i); + w += isWide(cp) ? 2 : 1; + i += Character.charCount(cp); + } + return w; + } + + /** + * Returns {@code true} when {@code cp} is a wide (2-column) character. + * Covers CJK blocks, Hangul, full-width forms, and emoji (including + * the specific emoji used in this project: 🍗 U+1F357, ⭐ U+2B50). + */ + private static boolean isWide(int cp) { + if (cp < 0x1100) return false; + if (cp <= 0x115F) return true; // Hangul Jamo + if (cp < 0x2E80) { + // Specific wide symbols below 0x2E80 + return cp == 0x2B50 // ⭐ STAR + || cp == 0x2B55; // ⭕ HEAVY LARGE CIRCLE + } + if (cp <= 0x303E) return true; // CJK Radicals, Kangxi, CJK Symbols + if (cp < 0x3041) return false; + if (cp <= 0xA4CF) return true; // Hiragana, Katakana, Bopomofo, CJK Unified + if (cp < 0xA960) return false; + if (cp <= 0xA97F) return true; // Hangul Jamo Extended-A + if (cp < 0xAC00) return false; + if (cp <= 0xD7AF) return true; // Hangul Syllables + if (cp < 0xF900) return false; + if (cp <= 0xFAFF) return true; // CJK Compatibility Ideographs + if (cp < 0xFE10) return false; + if (cp <= 0xFE1F) return true; // Vertical Forms + if (cp < 0xFE30) return false; + if (cp <= 0xFE6F) return true; // CJK Compatibility Forms + if (cp < 0xFF00) return false; + if (cp <= 0xFF60) return true; // Fullwidth Latin, punctuation + if (cp < 0xFFE0) return false; + if (cp <= 0xFFE6) return true; // Fullwidth Signs + if (cp < 0x1F004) return false; + if (cp <= 0x1FAFF) return true; // Emoji (mahjong, playing cards, flags, + // misc symbols, transport, 🍗 U+1F357, ...) + if (cp < 0x20000) return false; + return cp <= 0x3FFFD; // CJK Extension B through G + } +} 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 a3b5403..45c2af1 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 @@ -16,8 +16,9 @@ import java.util.Map; /** * Text-based User Interface (TUI) implementation of {@link IView}. * - *

Renders the current state of a {@link MiniModel} directly to the - * terminal using Unicode box-drawing characters and fixed-width ASCII tables. + *

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. * *

All output goes through {@link #display(String)}, which uses * {@link LineReader#printAbove} when a JLine reader is set so that the @@ -34,7 +35,7 @@ public class TUI implements IView { */ private LineReader lineReader; - /** JLine terminal — used to query terminal width for centering. */ + /** JLine terminal — used to query terminal width for centering and grid layout. */ private Terminal terminal; /** @@ -73,7 +74,7 @@ public class TUI implements IView { // ── Public render entry points ──────────────────────────────────────────── /** - * Default render: dispatches to the right sub-renderer based on game stage. + * Default render: dispatches to the right view based on game stage. */ @Override public synchronized void render() { @@ -88,32 +89,19 @@ public class TUI implements IView { } } - /** Renders the board panel + menu + player hand side by side. */ + /** Renders the board + menu (top) and all players' hands in a grid (bottom). */ public void renderBoard() { 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()); - } - /** - * Shows an error message above (or below) the current board. - * - *

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. + * 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. @@ -149,24 +137,54 @@ public class TUI implements IView { // ── Content builders (return strings, do not print) ─────────────────────── + /** + * Builds the main view: board + menu side by side, followed by all players' + * hands arranged in a responsive grid. + */ 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); + return AsciiTable.sideBySide(left, right, 3) + "\n" + buildAllHandsContent(); + } + + /** + * 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 players = new ArrayList<>(model.players.values()); + if (players.isEmpty()) return ""; + + int termWidth = (terminal != null && terminal.getWidth() > 0) ? terminal.getWidth() : 80; + int gap = 3; + + List> panels = new ArrayList<>(); + List panelWidths = new ArrayList<>(); + for (Player p : players) { + List lines = Arrays.asList(p.toString().split("\n")); + panels.add(lines); + panelWidths.add(lines.stream().mapToInt(l -> visibleLength(l)).max().orElse(0)); + } + + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < panels.size()) { + int rowWidth = panelWidths.get(i); + int j = i + 1; + while (j < panels.size() + && rowWidth + gap + panelWidths.get(j) <= termWidth) { + rowWidth += gap + panelWidths.get(j); + j++; + } + List merged = panels.get(i); + for (int k = i + 1; k < j; k++) { + merged = Arrays.asList( + AsciiTable.sideBySide(merged, panels.get(k), gap).split("\n")); + } + result.append(String.join("\n", merged)).append("\n"); + i = j; + } + return result.toString(); } private String buildTotemsContent() { @@ -212,8 +230,6 @@ public class TUI implements IView { * {@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); @@ -257,9 +273,9 @@ public class TUI implements IView { return sb.toString(); } - /** Returns the visible (printable) length of a string, stripping ANSI escape codes. */ + /** Returns the display width of a string in terminal columns, stripping ANSI escape codes. */ private int visibleLength(String line) { - return line.replaceAll("\033\\[[^m]*m", "").length(); + return AsciiTable.displayWidth(line.replaceAll("\033\\[[^m]*m", "")); } // ── Board / players stamp helpers ───────────────────────────────────────── @@ -291,7 +307,7 @@ public class TUI implements IView { * @return multi-line board string. */ public String BoardStamp() { - var offerTrack = new AsciiTable(BorderStyle.UNICODE, model.slotPlayerMap.size()); + var offerTrack = new AsciiTable(BorderStyle.ROUNDED, model.slotPlayerMap.size()); List slotNames = new ArrayList<>(); List slotPlayers = new ArrayList<>(); @@ -301,11 +317,11 @@ public class TUI implements IView { 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); + var TribeUpper = new AsciiTable(BorderStyle.ROUNDED, 1); + var TribeLower = new AsciiTable(BorderStyle.ROUNDED, 1); + var BuildUpper = new AsciiTable(BorderStyle.ROUNDED, 1); BuildUpper.addHeader("Building "); - var BuildLower = new AsciiTable(BorderStyle.UNICODE, 1); + var BuildLower = new AsciiTable(BorderStyle.ROUNDED, 1); BuildLower.addHeader("Building "); List upperTribeRows = new ArrayList<>(); @@ -316,17 +332,17 @@ public class TUI implements IView { 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()); + upperTribeRows.add(i + model.upperListTribeCards.get(i).toStringBoard()); if (i < model.lowerListTribeCards.size()) - lowerTribeRows.add(i + "-" + model.lowerListTribeCards.get(i).toStringBoard()); + 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()); + BuildUpper.addRow(i + model.upperListBuildingCards.get(i).toString()); if (i < model.lowerListBuildingCards.size()) - BuildLower.addRow(i + "-" + model.lowerListBuildingCards.get(i).toString()); + BuildLower.addRow(i + model.lowerListBuildingCards.get(i).toString()); } upperTribeRows.forEach(TribeUpper::addRow); @@ -347,16 +363,17 @@ public class TUI implements IView { // ── Menu options ────────────────────────────────────────────────────────── private String printMenuOptions() { - var table = new AsciiTable(BorderStyle.ROUNDED, 2); - 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", "")); + var table = new AsciiTable(BorderStyle.ROUNDED, 1); + table.addHeader("Commands"); + table.addRow("slot "); + table.addRow("draw upper tribe "); + table.addRow("draw upper building "); + table.addRow("draw lower tribe "); + table.addRow("draw lower building "); + table.addRow("totem "); + table.addRow("skip"); + table.addRow("render"); + table.addRow("rematch | quit"); return table.build(); } }