Fix: TUI refactor non complete

This commit is contained in:
2026-06-10 18:24:33 +02:00
parent d041067159
commit 0e28f8c6d8
16 changed files with 174 additions and 197 deletions
@@ -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) {
@@ -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());
}
}
@@ -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);
}
@@ -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);
}
/**
@@ -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();
}
/**
@@ -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;
}
}
@@ -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);
List<String>header= new ArrayList<>();
header.add("Player");
header.add("NUpper");
@@ -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<String> stringUp=new ArrayList<>();
for(int i=0;i<2;i++)
{
@@ -83,8 +83,8 @@ public class Order2 extends OrderLogicCard {
table.addRow(stringUp);
List<String> 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";
}
@@ -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<String> stringUp=new ArrayList<>();
for(int i=0;i<3;i++)
@@ -87,9 +87,9 @@ public class Order3 extends OrderLogicCard {
List<String> 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);
@@ -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<String> stringUp=new ArrayList<>();
for(int i=0;i<4;i++)
{
@@ -92,10 +92,10 @@ public class Order4 extends OrderLogicCard {
table.addRow(stringUp);
List<String> 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";
}
@@ -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<String> stringUp=new ArrayList<>();
for(int i=0;i<5;i++)
{
@@ -93,11 +93,11 @@ public class Order5 extends OrderLogicCard {
table.addRow(stringUp);
List<String> 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";
}
@@ -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 ":";
}
}
@@ -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()){
@@ -182,7 +182,7 @@ public class Slot implements Serializable {
s.append("");
}
if(getFood()!=0)
s.append("+"+getFood()+" Food");
s.append("+"+getFood()+" 🍗");
return s.toString();
}
@@ -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.
*
* <p>Cell widths are measured in terminal columns, not Java {@code char} units:
* wide characters (emoji, CJK) count as 2 columns each.
*
* <p>Example usage:
* <pre>{@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<List<String>> rows = new ArrayList<>();
/**
* Row indices after which a horizontal separator line is drawn.
* Populated by {@link #addHeader} and {@link #addSeparator}.
*/
private final List<Integer> 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<String> 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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>Example:
* <pre>{@code
* List<String> left = Arrays.asList(table1.build().split("\n"));
* List<String> right = Arrays.asList(table2.build().split("\n"));
* System.out.print(AsciiTable.sideBySide(left, right, 2));
* }</pre>
*
* @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<String> left, List<String> 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();
}
}
// 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
}
}
@@ -16,8 +16,9 @@ import java.util.Map;
/**
* Text-based User Interface (TUI) implementation of {@link IView}.
*
* <p>Renders the current state of a {@link MiniModel} directly to the
* terminal using Unicode box-drawing characters and fixed-width ASCII tables.
* <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
@@ -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.
*
* <p>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<String> left = List.of(BoardStamp().split("\n"));
List<String> right = List.of((printMenuOptions() + "\nYOUR HAND\n"
+ model.players.get(username)).split("\n"));
return AsciiTable.sideBySide(left, right, 3);
}
private String buildFullContent() {
List<String> left = List.of(BoardStamp().split("\n"));
List<String> right = List.of((printMenuOptions() + "\n"
+ model.players.get(username)).split("\n"));
return PlayersStamp() + "\n" + AsciiTable.sideBySide(left, right, 3);
}
private String buildPlayerContent() {
List<String> left = List.of(PlayersStamp().split("\n"));
List<String> 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<Player> players = new ArrayList<>(model.players.values());
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();
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<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();
}
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<String> slotNames = new ArrayList<>();
List<String> 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<String> 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 <pos>", "render full"));
table.addRow(List.of("draw upper tribe <pos>", "render board"));
table.addRow(List.of("draw upper building <pos>", "render players"));
table.addRow(List.of("draw lower tribe <pos>", ""));
table.addRow(List.of("draw lower building <pos>", ""));
table.addRow(List.of("totem <pos>", ""));
table.addRow(List.of("skip", ""));
table.addRow(List.of("rematch | quit", ""));
var table = new AsciiTable(BorderStyle.ROUNDED, 1);
table.addHeader("Commands");
table.addRow("slot <pos>");
table.addRow("draw upper tribe <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("skip");
table.addRow("render");
table.addRow("rematch | quit");
return table.build();
}
}