Fix: complete refactor of TUI

Tests updated
This commit is contained in:
2026-06-10 22:15:32 +02:00
parent 8bd740e68a
commit 4ffe4c5352
31 changed files with 242 additions and 187 deletions
@@ -28,8 +28,57 @@ public class ClientLauncherTUI {
private TUI view;
private Terminal terminal;
private LineReader gameReader;
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 13 \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').";
/**
* Starts the TUI client. Creates a JLine terminal, loops through login → game → rematch.
*
@@ -52,7 +101,7 @@ public class ClientLauncherTUI {
view = new TUI(null);
ClientController controller = new ClientController(view);
LineReader gameReader = buildGameReader();
gameReader = buildGameReader();
view.setLineReader(gameReader);
while (true) {
@@ -157,6 +206,7 @@ public class ClientLauncherTUI {
node("skip"),
node("render"),
node("help"),
node("details", node("buildings"), node("events"), node("characters")),
node("rematch"),
node("quit")
);
@@ -188,7 +238,17 @@ public class ClientLauncherTUI {
}
case "skip" -> controller.skipTurn();
case "render" -> handleRender(parts);
case "help" -> view.renderHelp();
case "help" -> gameReader.printAbove("Commands: slot, draw, totem, skip, render, help, details, rematch, quit");
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.miniModel == null
|| !controller.miniModel.currentState.getGameStage().equals(GameStages.ENDED)) {
@@ -74,6 +74,11 @@ public class Building1 extends BuildingCard {
*/
@Override
public String toString() {
return super.toString() + " Icon: " + this.icon.toString().charAt(0);
return super.toString() + " (Icon:" + this.icon.toString().substring(0, 3) + ")";
}
@Override
public String toStringPlayer() {
return super.toStringPlayer() + " (Icon:" + this.icon.toString().substring(0, 3) + ")";
}
}
@@ -105,6 +105,11 @@ public class Building11 extends BuildingCard {
*/
@Override
public String toString() {
return super.toString() + " Icon: " + this.icon.toString().charAt(0) + " MP: " + this.PrestigeMul;
return super.toString() + "+" + this.icon.toString().substring(0, 3) + "×" + this.PrestigeMul;
}
@Override
public String toStringPlayer() {
return super.toStringPlayer() + " (\uD83C\uDFC5:" + this.icon.toString().substring(0, 3) + "×" + this.PrestigeMul + ")";
}
}
@@ -141,7 +141,7 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf
* if {@code price <= 0}, or if {@code prestigeValue < 0}
*/
public BuildingCard(int effectId ,int era,int price,int prestigeValue) throws IllegalArgumentException{
this.effectId = effectId;
this(era,price,prestigeValue);
switch (effectId){
case 2:
this.effectType=EffectType.ON_EVENT;
@@ -168,7 +168,7 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf
throw new IllegalArgumentException();
}
this(era,price,prestigeValue);
this.effectId = effectId;
}
/**
@@ -186,7 +186,7 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf
* if {@code price <= 0}, or if {@code prestigeValue < 0}.
*/
public BuildingCard(String idIMG,int effectId ,int era,int price,int prestigeValue) throws IllegalArgumentException{
this.effectId = effectId;
this(idIMG,era,price,prestigeValue);
switch (effectId){
case 2:
this.effectType=EffectType.ON_EVENT;
@@ -213,7 +213,7 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf
throw new IllegalArgumentException();
}
this(idIMG,era,price,prestigeValue);
this.effectId = effectId;
}
/**
@@ -274,6 +274,10 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf
*/
@Override
public String toString() {
return ":" + " ID:" + String.valueOf(getEffectId())+ " \uD83C\uDF56:"+String.valueOf(getPrice())+" \uD83C\uDFC5:"+String.valueOf(getPrestigeValue());
return ":" + " ID:" + getEffectId()+ " \uD83C\uDF56:"+String.valueOf(getPrice())+" \uD83C\uDFC5:"+String.valueOf(getPrestigeValue());
}
public String toStringPlayer() {
return "ID:" + getEffectId();
}
}
@@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Model.Orders;
import it.polimi.ingsw.gc14.Model.OrderLogicCard;
import it.polimi.ingsw.gc14.Model.Player;
import it.polimi.ingsw.gc14.Model.Totems;
import it.polimi.ingsw.gc14.View.TUI.AsciiTable;
import it.polimi.ingsw.gc14.View.TUI.BorderStyle;
@@ -23,10 +24,9 @@ public class Order2 extends OrderLogicCard {
* @throws NoSuchElementException if the number of players is not equal to 2.
*/
public Order2(ArrayList<Player> players) throws NoSuchElementException {
super(players);
if(players.size()!=2)
throw new NoSuchElementException();
super(players);
}
/**
@@ -74,7 +74,7 @@ public class Order2 extends OrderLogicCard {
if(playerList.get(i).played)
stringUp.add("");
else
stringUp.add(playerList.get(i).player.getUserName());
stringUp.add(playerList.get(i).player.getTotem() != null ? playerList.get(i).player.getTotem().toString() : playerList.get(i).player.getUserName());
}
catch (IndexOutOfBoundsException e) {
stringUp.add("");
@@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Model.Orders;
import it.polimi.ingsw.gc14.Model.OrderLogicCard;
import it.polimi.ingsw.gc14.Model.Player;
import it.polimi.ingsw.gc14.Model.Totems;
import it.polimi.ingsw.gc14.View.TUI.AsciiTable;
import it.polimi.ingsw.gc14.View.TUI.BorderStyle;
@@ -24,9 +25,9 @@ public class Order3 extends OrderLogicCard {
* @throws NoSuchElementException if the number of players is not equal to 3.
*/
public Order3(ArrayList<Player> players) throws NoSuchElementException{
super(players);
if(players.size()!=3)
throw new NoSuchElementException();
super(players);
}
/**
@@ -77,7 +78,7 @@ public class Order3 extends OrderLogicCard {
if(playerList.get(i).played)
stringUp.add("");
else
stringUp.add(playerList.get(i).player.getUserName());
stringUp.add(i + ". " + (playerList.get(i).player.getTotem() != null ? playerList.get(i).player.getTotem().toString() : playerList.get(i).player.getUserName()));
}
catch (IndexOutOfBoundsException e) {
stringUp.add("");
@@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Model.Orders;
import it.polimi.ingsw.gc14.Model.OrderLogicCard;
import it.polimi.ingsw.gc14.Model.Player;
import it.polimi.ingsw.gc14.Model.Totems;
import it.polimi.ingsw.gc14.View.TUI.AsciiTable;
import it.polimi.ingsw.gc14.View.TUI.BorderStyle;
@@ -24,9 +25,9 @@ public class Order4 extends OrderLogicCard {
* @throws NoSuchElementException if the number of players is not equal to 4.
*/
public Order4(ArrayList<Player> players)throws NoSuchElementException {
super(players);
if(players.size()!=4)
throw new NoSuchElementException();
super(players);
}
/**
@@ -83,7 +84,7 @@ public class Order4 extends OrderLogicCard {
if(playerList.get(i).played)
stringUp.add("");
else
stringUp.add(playerList.get(i).player.getUserName());
stringUp.add(i + ". " + (playerList.get(i).player.getTotem() != null ? playerList.get(i).player.getTotem().toString() : playerList.get(i).player.getUserName()));
}
catch (IndexOutOfBoundsException e) {
stringUp.add("");
@@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Model.Orders;
import it.polimi.ingsw.gc14.Model.OrderLogicCard;
import it.polimi.ingsw.gc14.Model.Player;
import it.polimi.ingsw.gc14.Model.Totems;
import it.polimi.ingsw.gc14.View.TUI.AsciiTable;
import it.polimi.ingsw.gc14.View.TUI.BorderStyle;
@@ -24,9 +25,9 @@ public class Order5 extends OrderLogicCard {
* @throws NoSuchElementException if the number of players is not equal to 5.
*/
public Order5(ArrayList<Player> players) throws NoSuchElementException{
super(players);
if(players.size()!=5)
throw new NoSuchElementException();
super(players);
}
/**
@@ -84,7 +85,7 @@ public class Order5 extends OrderLogicCard {
if(playerList.get(i).played)
stringUp.add("");
else
stringUp.add(playerList.get(i).player.getUserName());
stringUp.add(i + ". " + (playerList.get(i).player.getTotem() != null ? playerList.get(i).player.getTotem().toString() : playerList.get(i).player.getUserName()));
}
catch (IndexOutOfBoundsException e) {
stringUp.add("");
@@ -276,7 +276,7 @@ public class Player implements Serializable {
var table = new AsciiTable(ROUNDED, 1);
String totemStr = (this.totem != null) ? " (" + this.totem.toString() + ")" : "";
table.addHeader(this.getUserName() + totemStr + " | \uD83C\uDF56:" + this.getFoodValue() + " | \uD83C\uDFC5:" + this.getPrestigeValue() + " ");
table.addHeader(this.getUserName() + totemStr + " | \uD83C\uDF56:" + this.getFoodValue() + " | \uD83C\uDFC5:" + this.getPrestigeValue());
table.addSeparator();
if(!this.hunters.isEmpty()){
@@ -304,7 +304,7 @@ public class Player implements Serializable {
if(Last == 0){
table.addSeparator();
}
table.addRow("CHARACTERS:");
table.addRow("CHARACTERS");
if(!this.artists.isEmpty()){
table.addRow(this.artists.size() + " Artists: \uD83C\uDFA8:" + this.artists.size());
if(Last == 1){
@@ -313,7 +313,7 @@ public class Player implements Serializable {
}
if(!this.builders.isEmpty()){
table.addRow(this.builders.size() + " Builders: " + "-\uD83C\uDF56:" + this.builders.stream().mapToInt(Builder::getReductionValue).sum() + " \uD83C\uDFC5:" + this.builders.stream().mapToInt(Builder::getPrestigeValue).sum());
table.addRow(this.builders.size() + " Builders: " + "\uD83D\uDD28:" + this.builders.stream().mapToInt(Builder::getReductionValue).sum() + " \uD83C\uDFC5:" + this.builders.stream().mapToInt(Builder::getPrestigeValue).sum());
if(Last == 2){
table.addSeparator();
}
@@ -342,7 +342,7 @@ public class Player implements Serializable {
.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + ":" + e.getValue())
.collect(Collectors.joining(" ")));
.collect(Collectors.joining(", ")));
if(Last == 5){
table.addSeparator();
}
@@ -353,10 +353,13 @@ public class Player implements Serializable {
table.addRow(this.hunters.size() + " Hunters: \uD83C\uDF56" + this.hunters.stream().filter(Hunter::getIcon).count());
table.addSeparator();
}
table.addRow("BUILDING CARDS:");
table.addSeparator();
table.addRow("BUILDING CARDS" +
(this.buildingCards.stream().mapToInt(BuildingCard::getPrestigeValue).sum() != 0
? " 🏅:" + this.buildingCards.stream().mapToInt(BuildingCard::getPrestigeValue).sum()
: ""));
if(!this.buildingCards.isEmpty()){
table.addRow(this.buildingCards.size() + " Buildings: " + this.buildingCards.toString());
table.addRow(this.buildingCards.size() + " Buildings: " + this.buildingCards.stream().map(BuildingCard::toStringPlayer).collect(Collectors.joining(", ")));
}
return table.build();
@@ -171,18 +171,11 @@ public class Slot implements Serializable {
*/
public String toStringTUI()
{
StringBuilder s = new StringBuilder();
//s.append(this.getSlotId()+" ");
for(int i=0;i<this.getNUpper();i++)
{
s.append("");
}
for(int i=0;i<this.getNLower();i++)
{
s.append("");
}
if(getFood()!=0)
s.append("+"+getFood()+" \uD83C\uDF56");
StringBuilder s = new StringBuilder(" ");
for(int i=0;i<this.getNUpper();i++) s.append("");
for(int i=0;i<this.getNLower();i++) s.append("");
if(getFood()!=0) s.append("+"+getFood()+" \uD83C\uDF56");
s.append(" ");
return s.toString();
}
@@ -110,6 +110,7 @@ public class AsciiTable {
* Wide characters (emoji, CJK, full-width) count as 2; all others as 1.
*/
public static int displayWidth(String s) {
s = s.replaceAll("\033\\[[^m]*m", "");
int w = 0;
for (int i = 0; i < s.length(); ) {
int cp = s.codePointAt(i);
@@ -106,32 +106,19 @@ public class TUI implements IView {
* @param message the human-readable message to append.
*/
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();
}
String text;
if (error.equals(ErrorType.WRONG_ACTION)
&& model != null
&& model.currentState.getCurrentPlayer() != null
&& !model.currentState.getCurrentPlayer().getUserName().equals(username)) {
display(base + "\nIt's not your turn");
return;
text = "It's not your turn";
} else {
text = message;
if (error == ErrorType.SERVER_CRASHED) {
text += "\nPress any key to continue";
}
}
String full = base + "\n" + message;
if (error == ErrorType.SERVER_CRASHED) {
full += "\nPress any key to continue";
}
display(full);
printLine(text);
}
// ── Content builders (return strings, do not print) ───────────────────────
@@ -153,11 +140,20 @@ public class TUI implements IView {
private String buildAllHandsContent() {
List<Player> players = new ArrayList<>(model.players.values());
if (players.isEmpty()) return "";
StringBuilder result = new StringBuilder();
for (Player p : players) {
result.append(p.toString()).append("\n");
}
return result.toString();
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");
if (rightSb.isEmpty()) return leftSb.toString();
return AsciiTable.sideBySide(
List.of(leftSb.toString().split("\n", -1)),
List.of(rightSb.toString().split("\n", -1)), 3);
}
private String buildTotemsContent() {
@@ -215,6 +211,16 @@ public class TUI implements IView {
}
}
/** 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
@@ -314,9 +320,7 @@ public class TUI implements IView {
String upperSection = UpperCards.build();
String middleSection = AsciiTable.sideBySide(
List.of(model.orderLogicCard.toString().split("\n")),
List.of(offerTrack.build().split("\n")), 3);
String middleSection = model.orderLogicCard.toString() + offerTrack.build();
String lowerSection = LowerCards.build();
@@ -326,23 +330,4 @@ public class TUI implements IView {
+ lowerSection;
}
// ── Help ──────────────────────────────────────────────────────────────────
/** Displays the command help panel. */
public void renderHelp() {
display(buildHelpContent());
}
private String buildHelpContent() {
var table = new AsciiTable(BorderStyle.ROUNDED, 1);
table.addHeader("Commands");
table.addRow("slot <pos>");
table.addRow("draw upper|lower tribe|building <pos>");
table.addRow("totem <pos>");
table.addRow("skip");
table.addRow("render");
table.addRow("help");
table.addRow("rematch | quit");
return table.build();
}
}