Coverage Summary for Class: ClientLauncherTUI (it.polimi.ingsw.gc14)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| ClientLauncherTUI |
0%
(0/1)
|
0%
(0/12)
|
0%
(0/62)
|
0%
(0/139)
|
package it.polimi.ingsw.gc14;
import it.polimi.ingsw.gc14.Controller.ClientController;
import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
import it.polimi.ingsw.gc14.Network.IClient;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
import it.polimi.ingsw.gc14.Network.InterfaceResolver;
import it.polimi.ingsw.gc14.Network.RMI.Client.RMIClient;
import it.polimi.ingsw.gc14.Network.TCP.Client.TCPClient;
import it.polimi.ingsw.gc14.View.TUI.TUI;
import org.jline.builtins.Completers.TreeCompleter;
import org.jline.reader.*;
import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import static org.jline.builtins.Completers.TreeCompleter.node;
/**
* Entry point for the TUI-based game client.
* Handles login, connects to the server, and drives the JLine-powered command loop.
*/
public class ClientLauncherTUI {
/** TUI view shared with the client controller. */
private TUI view;
/** JLine terminal used for all input/output. */
private Terminal terminal;
/** Line reader used during active gameplay, with tab-completion wired. */
private LineReader gameReader;
/** Username of the currently logged-in player; empty string before login. */
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 1–3 \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').";
/**
* Launches the TUI client.
*
* @param args command-line arguments (unused).
* @throws InterruptedException if the thread is interrupted during startup.
*/
static void main(String[] args) throws InterruptedException {
new ClientLauncherTUI().start();
}
/**
* Starts the TUI client. Creates a JLine terminal, loops through login → game → rematch.
*
*/
private void start() {
try {
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) {
System.err.println("Could not initialise terminal: " + e.getMessage());
return;
}
terminal.writer().print("\033[H\033[2J");
terminal.writer().flush();
view = new TUI(null);
ClientController controller = new ClientController(view);
gameReader = buildGameReader();
view.setLineReader(gameReader);
while (true) {
if (!doLogin(controller)) continue;
while (controller.getClient() != null) {
String prompt = buildPrompt();
String line;
try {
line = gameReader.readLine(prompt);
} catch (UserInterruptException e) {
safeQuit(controller);
return;
} catch (EndOfFileException e) {
safeQuit(controller);
return;
}
if (line == null || line.isBlank()) continue;
boolean rematch = handleCommand(line.trim(), controller);
if (rematch) break;
}
}
}
/**
* Prompts for username, player count, network type, and server IP, then connects.
*
* @param controller the client controller to connect.
* @return {@code true} on successful connection; {@code false} if input is invalid or connection fails.
*/
private boolean doLogin(ClientController controller) {
LineReader loginReader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(new StringsCompleter("rmi", "tcp"))
.build();
LineReader ipReader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(new StringsCompleter("localhost"))
.build();
view.setLineReader(loginReader);
try {
return doLoginInner(controller, loginReader, ipReader);
} finally {
view.setLineReader(gameReader);
}
}
/**
* Reads login fields from the terminal and connects to the server.
*
* @param controller the client controller to connect.
* @param loginReader line reader used for username, player count, and network type.
* @param ipReader line reader with localhost auto-complete for the server IP.
* @return {@code true} on successful connection; {@code false} if input is invalid or connection fails.
*/
private boolean doLoginInner(ClientController controller, LineReader loginReader, LineReader ipReader) {
String username, networkStr, ip;
int nPlayers;
try {
username = loginReader.readLine("Username: ").trim();
nPlayers = Integer.parseInt(loginReader.readLine("Number of players [2-5]: ").trim());
networkStr = loginReader.readLine("Network [rmi/tcp]: ").trim().toLowerCase();
ip = ipReader.readLine("Server IP [localhost]: ").trim();
if (ip.isEmpty()) ip = "localhost";
} catch (UserInterruptException | EndOfFileException e) {
System.exit(0);
return false;
} catch (NumberFormatException e) {
loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Invalid number of players.\033[0m");
return false;
}
currentUsername = username;
view.setUsername(username);
controller.setMyUsername(username);
IClient client;
if (networkStr.equals("rmi")) {
String myIP;
try {
myIP = InterfaceResolver.resolveLocalInterface(ip);
} catch (Exception e) {
loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Cannot resolve local interface: " + e.getMessage() + "\033[0m");
return false;
}
System.setProperty("java.rmi.server.hostname", myIP);
client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, myIP);
ErrorType err = client.connect(username, nPlayers);
if (err != null) {
loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: " + err + "\033[0m");
return false;
}
System.out.println("Connected via RMI, waiting for other players...");
controller.setClient(client);
} else if (networkStr.equals("tcp")) {
client = new TCPClient(controller, ip, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT);
ErrorType err = client.connect(username, nPlayers);
if (err != null) {
loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: " + err + "\033[0m");
return false;
}
System.out.println("Connected via TCP, waiting for other players...");
controller.setClient(client);
} else {
loginReader.printAbove("\033[H\033[2J" + "\033[31mERROR: Unknown network type '" + networkStr + "'. Use rmi or tcp.\033[0m");
return false;
}
return true;
}
/**
* Builds the JLine {@link LineReader} used during gameplay, with tab-completion
* for all valid commands.
*
* @return the configured {@link LineReader}.
*/
private LineReader buildGameReader() {
Completer completer = new TreeCompleter(
node("slot"),
node("draw",
node("upper",
node("tribe"),
node("building")),
node("lower",
node("tribe"),
node("building"))),
node("totem"),
node("skip"),
node("clear"),
node("details", node("buildings"), node("events"), node("characters")),
node("rematch"),
node("quit")
);
return LineReaderBuilder.builder()
.terminal(terminal)
.completer(completer)
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
.build();
}
/**
* Returns the command-line prompt string showing the current username.
*
* @return the prompt string.
*/
private String buildPrompt() {
return currentUsername.isEmpty() ? "> " : currentUsername + "> ";
}
/** Returns {@code true} when the caller should break out of the game loop (rematch requested). */
private boolean handleCommand(String line, ClientController controller) {
String[] parts = line.split("\\s+");
String cmd = parts[0].toLowerCase();
switch (cmd) {
case "slot" -> {
int pos = parsePos(parts, 1);
controller.slotChoice(pos);
}
case "draw" -> handleDraw(parts, controller);
case "totem" -> {
int pos = parsePos(parts, 1);
controller.totemChoice(pos);
}
case "skip" -> controller.skipTurn();
case "clear" -> handleRender();
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.getMiniModel() == null
|| !controller.getMiniModel().currentState.getGameStage().equals(GameStages.ENDED)) {
System.out.println("Game not ended yet. Use 'quit' to disconnect.");
return false;
}
controller.disconnect();
controller.setClient(null);
controller.setModel(null);
return true;
}
case "quit" -> {
safeQuit(controller);
System.exit(0);
}
default -> view.showError(ErrorType.GENERIC_ERROR, "Unknown command: " + cmd);
}
return false;
}
/**
* Handles the {@code draw} command, dispatching to the appropriate controller draw method.
*
* @param parts the tokenized command: {@code [draw, upper|lower, tribe|building, pos]}.
* @param controller the client controller to invoke.
*/
private void handleDraw(String[] parts, ClientController controller) {
if (parts.length < 4) {
view.showError(ErrorType.GENERIC_ERROR, "Usage: draw upper|lower tribe|building <pos>");
return;
}
boolean upper = parts[1].equalsIgnoreCase("upper");
boolean tribe = parts[2].equalsIgnoreCase("tribe");
int pos = parsePos(parts, 3);
if (pos < 0) return;
if (upper && tribe) controller.drawUpperTribeCard(pos);
else if (upper) controller.drawUpperBuildingCard(pos);
else if (tribe) controller.drawLowerTribeCard(pos);
else controller.drawLowerBuildingCard(pos);
}
/**
* Handles the {@code clear} command by triggering a full board re-render.
*
*/
private void handleRender() {
view.renderBoard();
}
/**
* Parses an integer position argument at {@code idx} in {@code parts}.
*
* @param parts the tokenized command array.
* @param idx index of the position argument.
* @return the parsed integer, or {@code -1} if missing or malformed.
*/
private int parsePos(String[] parts, int idx) {
if (parts.length <= idx) {
view.showError(ErrorType.GENERIC_ERROR, "Missing position argument.");
return -1;
}
try {
return Integer.parseInt(parts[idx]);
} catch (NumberFormatException e) {
view.showError(ErrorType.GENERIC_ERROR,
"Invalid position '" + parts[idx] + "' — expected integer.");
return -1;
}
}
/**
* Disconnects the client if one is currently connected; safe to call when already disconnected.
*
* @param controller the client controller to disconnect.
*/
private void safeQuit(ClientController controller) {
if (controller.getClient() != null) controller.disconnect();
}
}