Fix: potential windows solution for TUI

This commit is contained in:
2026-06-10 19:08:39 +02:00
parent 0e28f8c6d8
commit 73153762c3
17 changed files with 90 additions and 116 deletions
+5
View File
@@ -68,6 +68,11 @@
<artifactId>jline-builtins</artifactId> <artifactId>jline-builtins</artifactId>
<version>3.26.3</version> <version>3.26.3</version>
</dependency> </dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal-jansi</artifactId>
<version>3.26.3</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -15,6 +15,8 @@ import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder; import org.jline.terminal.TerminalBuilder;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import static org.jline.builtins.Completers.TreeCompleter.node; import static org.jline.builtins.Completers.TreeCompleter.node;
@@ -35,7 +37,12 @@ public class ClientLauncherTUI {
*/ */
public void main() throws InterruptedException { public void main() throws InterruptedException {
try { try {
terminal = TerminalBuilder.builder().system(true).build(); System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8));
terminal = TerminalBuilder.builder()
.system(true)
.encoding(StandardCharsets.UTF_8)
.build();
} catch (IOException e) { } catch (IOException e) {
System.err.println("Could not initialise terminal: " + e.getMessage()); System.err.println("Could not initialise terminal: " + e.getMessage());
return; return;
@@ -149,6 +156,7 @@ public class ClientLauncherTUI {
node("totem"), node("totem"),
node("skip"), node("skip"),
node("render"), node("render"),
node("help"),
node("rematch"), node("rematch"),
node("quit") node("quit")
); );
@@ -180,6 +188,7 @@ public class ClientLauncherTUI {
} }
case "skip" -> controller.skipTurn(); case "skip" -> controller.skipTurn();
case "render" -> handleRender(parts); case "render" -> handleRender(parts);
case "help" -> view.renderHelp();
case "rematch" -> { case "rematch" -> {
if (controller.miniModel == null if (controller.miniModel == null
|| !controller.miniModel.currentState.getGameStage().equals(GameStages.ENDED)) { || !controller.miniModel.currentState.getGameStage().equals(GameStages.ENDED)) {
@@ -274,6 +274,6 @@ public class BuildingCard extends PlayableCard implements Cloneable , BuildingEf
*/ */
@Override @Override
public String toString() { public String toString() {
return ":" + " ID:" + String.valueOf(getEffectId())+ " 🍗:"+String.valueOf(getPrice())+" \uD83C\uDFC5:"+String.valueOf(getPrestigeValue()); return ":" + " ID:" + String.valueOf(getEffectId())+ " \uD83C\uDF56:"+String.valueOf(getPrice())+" \uD83C\uDFC5:"+String.valueOf(getPrestigeValue());
} }
} }
@@ -93,7 +93,7 @@ public class Inventor extends Character {
*/ */
@Override @Override
public String toString() { public String toString() {
return super.toString() + " I_ID:" + String.valueOf(icon); return super.toString() + "ID:" + String.valueOf(icon);
} }
/** /**
@@ -86,7 +86,7 @@ public class Shaman extends Character {
*/ */
@Override @Override
public String toString() { public String toString() {
return super.toString() + " " + "🌟".repeat(icon); return super.toString() + "🌟:" + icon;
} }
/** /**
@@ -102,7 +102,7 @@ public class Shaman extends Character {
*/ */
@Override @Override
public String toStringBoard() { public String toStringBoard() {
return super.toStringBoard() + " " + "🌟".repeat(icon); return super.toStringBoard() + "🌟:" + icon;
} }
/** /**
@@ -89,7 +89,7 @@ public abstract class EventCard extends TribeCard {
*/ */
@Override @Override
public String toStringBoard() { public String toStringBoard() {
return super.toString() + " " + type.toString(); return super.toStringBoard() + " " + type.toString();
} }
/** /**
@@ -96,7 +96,7 @@ public class Hunt extends EventCard {
*/ */
@Override @Override
public String toStringBoard() { public String toStringBoard() {
return super.toStringBoard()+" 1F+"+prestigeMultiplier+"PP"+" X N Hunter"; return super.toStringBoard()+" 1\uD83C\uDF56+"+prestigeMultiplier+"\uD83C\uDFC5"+" X N Hunter";
} }
} }
@@ -201,11 +201,9 @@ public class CurrentState implements Serializable {
} }
@Override @Override
public String toString(){ public String toString(){
var table= new AsciiTable(BorderStyle.ROUNDED,6); var table= new AsciiTable(BorderStyle.ROUNDED,4);
List<String>header= new ArrayList<>(); List<String>header= new ArrayList<>();
header.add("Player"); header.add("Player");
header.add("NUpper");
header.add("NLower");
header.add("Round"); header.add("Round");
header.add("Era"); header.add("Era");
header.add("GameStage"); header.add("GameStage");
@@ -213,8 +211,6 @@ public class CurrentState implements Serializable {
table.addSeparator(); table.addSeparator();
List<String>values= new ArrayList<>(); List<String>values= new ArrayList<>();
values.add(player.getUserName()); values.add(player.getUserName());
values.add(Integer.toString(NUpper));
values.add(Integer.toString(NLower));
values.add(Integer.toString(round)); values.add(Integer.toString(round));
values.add(Integer.toString(Era)); values.add(Integer.toString(Era));
values.add(GameStage.toString()); values.add(GameStage.toString());
@@ -72,9 +72,9 @@ public class Order2 extends OrderLogicCard {
{ {
try { try {
if(playerList.get(i).played) if(playerList.get(i).played)
stringUp.add(" "); stringUp.add("");
else else
stringUp.add(i+". "+playerList.get(i).player.getUserName()); stringUp.add(playerList.get(i).player.getUserName());
} }
catch (IndexOutOfBoundsException e) { catch (IndexOutOfBoundsException e) {
stringUp.add(""); stringUp.add("");
@@ -83,9 +83,9 @@ public class Order2 extends OrderLogicCard {
table.addRow(stringUp); table.addRow(stringUp);
List<String> stringList=new ArrayList<>(); List<String> stringList=new ArrayList<>();
stringList.add("+1🍗"); stringList.add("+1\uD83C\uDF56");
stringList.add("-1🍗 / -2\uD83C\uDFC5"); stringList.add("-1\uD83C\uDF56/-2\uD83C\uDFC5");
table.addRow(stringList); table.addRow(stringList);
return "TURN ORDER\n" + table.build() + "\n"; return table.build() + "\n";
} }
} }
@@ -75,9 +75,9 @@ public class Order3 extends OrderLogicCard {
{ {
try { try {
if(playerList.get(i).played) if(playerList.get(i).played)
stringUp.add(" "); stringUp.add("");
else else
stringUp.add(i+". "+playerList.get(i).player.getUserName()); stringUp.add(playerList.get(i).player.getUserName());
} }
catch (IndexOutOfBoundsException e) { catch (IndexOutOfBoundsException e) {
stringUp.add(""); stringUp.add("");
@@ -87,12 +87,12 @@ public class Order3 extends OrderLogicCard {
List<String> stringList=new ArrayList<>(); List<String> stringList=new ArrayList<>();
stringList.add("+2 🍗"); stringList.add("+2\uD83C\uDF56");
stringList.add("--"); stringList.add("--");
stringList.add("-1 🍗 / -2 ⭐"); stringList.add("-1\uD83C\uDF56/-2\uD83C\uDFC5");
table.addRow(stringList); table.addRow(stringList);
return "TURN ORDER\n" + table.build() + "\n"; return table.build() + "\n";
} }
} }
@@ -81,9 +81,9 @@ public class Order4 extends OrderLogicCard {
{ {
try { try {
if(playerList.get(i).played) if(playerList.get(i).played)
stringUp.add(" "); stringUp.add("");
else else
stringUp.add(i+". "+playerList.get(i).player.getUserName()); stringUp.add(playerList.get(i).player.getUserName());
} }
catch (IndexOutOfBoundsException e) { catch (IndexOutOfBoundsException e) {
stringUp.add(""); stringUp.add("");
@@ -92,11 +92,11 @@ public class Order4 extends OrderLogicCard {
table.addRow(stringUp); table.addRow(stringUp);
List<String> stringList=new ArrayList<>(); List<String> stringList=new ArrayList<>();
stringList.add("+2 🍗"); stringList.add("+2\uD83C\uDF56");
stringList.add("+1 🍗"); stringList.add("+1\uD83C\uDF56");
stringList.add("--"); stringList.add("--");
stringList.add("-1 🍗 / -2 ⭐"); stringList.add("-1\uD83C\uDF56/-2\uD83C\uDFC5");
table.addRow(stringList); table.addRow(stringList);
return "TURN ORDER\n" + table.build() + "\n"; return table.build() + "\n";
} }
} }
@@ -82,9 +82,9 @@ public class Order5 extends OrderLogicCard {
{ {
try { try {
if(playerList.get(i).played) if(playerList.get(i).played)
stringUp.add(" "); stringUp.add("");
else else
stringUp.add(i+". "+playerList.get(i).player.getUserName()); stringUp.add(playerList.get(i).player.getUserName());
} }
catch (IndexOutOfBoundsException e) { catch (IndexOutOfBoundsException e) {
stringUp.add(""); stringUp.add("");
@@ -93,12 +93,12 @@ public class Order5 extends OrderLogicCard {
table.addRow(stringUp); table.addRow(stringUp);
List<String> stringList=new ArrayList<>(); List<String> stringList=new ArrayList<>();
stringList.add("+3 🍗"); stringList.add("+3\uD83C\uDF56");
stringList.add("+1 🍗"); stringList.add("+1\uD83C\uDF56");
stringList.add("--"); stringList.add("--");
stringList.add("--"); stringList.add("--");
stringList.add("-1 🍗 / -2 ⭐"); stringList.add("-1\uD83C\uDF56/-2\uD83C\uDFC5");
table.addRow(stringList); table.addRow(stringList);
return "TURN ORDER\n" + table.build() + "\n"; return table.build() + "\n";
} }
} }
@@ -74,7 +74,7 @@ public abstract class PlayableCard implements Serializable {
*/ */
@Override @Override
public String toString() { public String toString() {
return ":"; return "";
} }
/** /**
@@ -273,13 +273,8 @@ public class Player implements Serializable {
var table = new AsciiTable(ROUNDED, 1); var table = new AsciiTable(ROUNDED, 1);
table.addHeader(this.getUserName()); String totemStr = (this.totem != null) ? " (" + this.totem.toString() + ")" : "";
table.addRow("RESOURCES:"); table.addHeader(this.getUserName() + totemStr + " | \uD83C\uDF56:" + this.getFoodValue() + " | \uD83C\uDFC5:" + this.getPrestigeValue() + " ");
if(this.totem != null) {
table.addRow("Totem: " + this.totem.toString());
}
table.addRow("🍗 " + this.getFoodValue());
table.addRow("2\uD83C\uDFC5 " + this.getPrestigeValue());
table.addSeparator(); table.addSeparator();
if(!this.hunters.isEmpty()){ if(!this.hunters.isEmpty()){
@@ -182,7 +182,7 @@ public class Slot implements Serializable {
s.append(""); s.append("");
} }
if(getFood()!=0) if(getFood()!=0)
s.append("+"+getFood()+" 🍗"); s.append("+"+getFood()+" \uD83C\uDF56");
return s.toString(); return s.toString();
} }
@@ -14,7 +14,7 @@ import java.util.*;
* <p>Example usage: * <p>Example usage:
* <pre>{@code * <pre>{@code
* AsciiTable table = new AsciiTable(BorderStyle.UNICODE, 3); * AsciiTable table = new AsciiTable(BorderStyle.UNICODE, 3);
* table.addHeader("Name", "🍗", ""); * table.addHeader("Name", "🍖", "");
* table.addRow("Alice", "4", "12"); * table.addRow("Alice", "4", "12");
* table.addRow("Bob", "2", "8"); * table.addRow("Bob", "2", "8");
* System.out.println(table.build()); * System.out.println(table.build());
@@ -122,7 +122,7 @@ public class AsciiTable {
/** /**
* Returns {@code true} when {@code cp} is a wide (2-column) character. * Returns {@code true} when {@code cp} is a wide (2-column) character.
* Covers CJK blocks, Hangul, full-width forms, and emoji (including * Covers CJK blocks, Hangul, full-width forms, and emoji (including
* the specific emoji used in this project: 🍗 U+1F357, U+2B50). * the specific emoji used in this project: 🍖 U+1F357, U+2B50).
*/ */
private static boolean isWide(int cp) { private static boolean isWide(int cp) {
if (cp < 0x1100) return false; if (cp < 0x1100) return false;
@@ -151,7 +151,7 @@ public class AsciiTable {
if (cp <= 0xFFE6) return true; // Fullwidth Signs if (cp <= 0xFFE6) return true; // Fullwidth Signs
if (cp < 0x1F004) return false; if (cp < 0x1F004) return false;
if (cp <= 0x1FAFF) return true; // Emoji (mahjong, playing cards, flags, if (cp <= 0x1FAFF) return true; // Emoji (mahjong, playing cards, flags,
// misc symbols, transport, 🍗 U+1F357, ...) // misc symbols, transport, 🍖 U+1F357, ...)
if (cp < 0x20000) return false; if (cp < 0x20000) return false;
return cp <= 0x3FFFD; // CJK Extension B through G return cp <= 0x3FFFD; // CJK Extension B through G
} }
@@ -9,7 +9,6 @@ import org.jline.reader.LineReader;
import org.jline.terminal.Terminal; import org.jline.terminal.Terminal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -143,8 +142,8 @@ public class TUI implements IView {
*/ */
private String buildBoardContent() { private String buildBoardContent() {
List<String> left = List.of(BoardStamp().split("\n")); List<String> left = List.of(BoardStamp().split("\n"));
List<String> right = List.of(printMenuOptions().split("\n")); List<String> right = List.of(buildAllHandsContent().split("\n"));
return AsciiTable.sideBySide(left, right, 3) + "\n" + buildAllHandsContent(); return AsciiTable.sideBySide(left, right, 3);
} }
/** /**
@@ -154,35 +153,9 @@ public class TUI implements IView {
private String buildAllHandsContent() { private String buildAllHandsContent() {
List<Player> players = new ArrayList<>(model.players.values()); List<Player> players = new ArrayList<>(model.players.values());
if (players.isEmpty()) return ""; if (players.isEmpty()) return "";
int termWidth = (terminal != null && terminal.getWidth() > 0) ? terminal.getWidth() : 80;
int gap = 3;
List<List<String>> panels = new ArrayList<>();
List<Integer> panelWidths = new ArrayList<>();
for (Player p : players) {
List<String> 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(); StringBuilder result = new StringBuilder();
int i = 0; for (Player p : players) {
while (i < panels.size()) { result.append(p.toString()).append("\n");
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<String> 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(); return result.toString();
} }
@@ -317,62 +290,58 @@ public class TUI implements IView {
slotPlayers.add(entry.getValue() != null ? entry.getValue().getUserName() : " "); slotPlayers.add(entry.getValue() != null ? entry.getValue().getUserName() : " ");
} }
var TribeUpper = new AsciiTable(BorderStyle.ROUNDED, 1); var UpperCards = new AsciiTable(BorderStyle.ROUNDED, 2);
var TribeLower = new AsciiTable(BorderStyle.ROUNDED, 1); var LowerCards = new AsciiTable(BorderStyle.ROUNDED, 2);
var BuildUpper = new AsciiTable(BorderStyle.ROUNDED, 1); UpperCards.addHeader("Char/Events", "Building");
BuildUpper.addHeader("Building "); LowerCards.addHeader("Char/Events", "Building");
var BuildLower = new AsciiTable(BorderStyle.ROUNDED, 1);
BuildLower.addHeader("Building ");
List<String> upperTribeRows = new ArrayList<>(); int maxU = Math.max(model.upperListTribeCards.size(), model.upperListBuildingCards.size());
List<String> lowerTribeRows = new ArrayList<>(); for (int i = 0; i < maxU; i++) {
upperTribeRows.add("Char/Events"); String t = i < model.upperListTribeCards.size() ? i + model.upperListTribeCards.get(i).toStringBoard() : "";
lowerTribeRows.add("Char/Events"); String b = i < model.upperListBuildingCards.size() ? i + model.upperListBuildingCards.get(i).toString() : "";
UpperCards.addRow(t, b);
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()); int maxL = Math.max(model.lowerListTribeCards.size(), model.lowerListBuildingCards.size());
for (int i = 0; i < maxB; i++) { for (int i = 0; i < maxL; i++) {
if (i < model.upperListBuildingCards.size()) String t = i < model.lowerListTribeCards.size() ? i + model.lowerListTribeCards.get(i).toStringBoard() : "";
BuildUpper.addRow(i + model.upperListBuildingCards.get(i).toString()); String b = i < model.lowerListBuildingCards.size() ? i + model.lowerListBuildingCards.get(i).toString() : "";
if (i < model.lowerListBuildingCards.size()) LowerCards.addRow(t, b);
BuildLower.addRow(i + model.lowerListBuildingCards.get(i).toString());
} }
upperTribeRows.forEach(TribeUpper::addRow);
lowerTribeRows.forEach(TribeLower::addRow);
offerTrack.addRow(slotPlayers); offerTrack.addRow(slotPlayers);
offerTrack.addRow(slotNames); offerTrack.addRow(slotNames);
return "CURRENT STATE\n" + model.currentState + "\n" + model.orderLogicCard + "\n" String upperSection = UpperCards.build();
+ AsciiTable.sideBySide(
Arrays.stream(TribeUpper.build().split("\n")).toList(), String middleSection = AsciiTable.sideBySide(
Arrays.stream(BuildUpper.build().split("\n")).toList(), 2) List.of(model.orderLogicCard.toString().split("\n")),
+ "\n" + offerTrack.build() + "\n" List.of(offerTrack.build().split("\n")), 3);
+ AsciiTable.sideBySide(
List.of(TribeLower.build().split("\n")), String lowerSection = LowerCards.build();
Arrays.stream(BuildLower.build().split("\n")).toList(), 2);
return model.currentState + "\n"
+ upperSection + "\n"
+ middleSection + "\n"
+ lowerSection;
} }
// Menu options // Help
private String printMenuOptions() { /** Displays the command help panel. */
public void renderHelp() {
display(buildHelpContent());
}
private String buildHelpContent() {
var table = new AsciiTable(BorderStyle.ROUNDED, 1); var table = new AsciiTable(BorderStyle.ROUNDED, 1);
table.addHeader("Commands"); table.addHeader("Commands");
table.addRow("slot <pos>"); table.addRow("slot <pos>");
table.addRow("draw upper tribe <pos>"); table.addRow("draw upper|lower tribe|building <pos>");
table.addRow("draw upper building <pos>");
table.addRow("draw lower tribe <pos>");
table.addRow("draw lower building <pos>");
table.addRow("totem <pos>"); table.addRow("totem <pos>");
table.addRow("skip"); table.addRow("skip");
table.addRow("render"); table.addRow("render");
table.addRow("help");
table.addRow("rematch | quit"); table.addRow("rematch | quit");
return table.build(); return table.build();
} }