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.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 { 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 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')."; public static void main(String[] args) throws InterruptedException { new ClientLauncherTUI().start(); } /** * Starts the TUI client. Creates a JLine terminal, loops through login → game → rematch. * * @throws InterruptedException if the thread is interrupted while waiting. */ private void start() throws InterruptedException { 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(controller); 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; } } } 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(); 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) { System.out.println("Invalid number of players."); return false; } currentUsername = username; view.setUsername(username); controller.setMyUsername(username); if (networkStr.equals("rmi")) { String myIP; try { myIP = InterfaceResolver.resolveLocalInterface(ip); } catch (Exception e) { System.out.println("Cannot resolve local interface: " + e.getMessage()); return false; } System.setProperty("java.rmi.server.hostname", myIP); RMIClient client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, myIP); ErrorType err = client.connect(username, nPlayers); if (err != null) { terminal.writer().print("\033[H\033[2J"); terminal.writer().flush(); System.out.println("Errore: " + err); return false; } System.out.println("Connected via RMI."); controller.setClient(client); } else if (networkStr.equals("tcp")) { TCPClient client = new TCPClient(controller, ip, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT); ErrorType err = client.connect(username, nPlayers); if (err != null) { terminal.writer().print("\033[H\033[2J"); terminal.writer().flush(); System.out.println("Errore: " + err); return false; } System.out.println("Connected via TCP."); controller.setClient(client); } else { System.out.println("Unknown network type '" + networkStr + "'. Use rmi or tcp."); return false; } return true; } 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("render"), node("help"), 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(); } private String buildPrompt(ClientController controller) { 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); if (pos >= 0) controller.slotChoice(pos); } case "draw" -> handleDraw(parts, controller); case "totem" -> { int pos = parsePos(parts, 1); if (pos >= 0) controller.totemChoice(pos); } case "skip" -> controller.skipTurn(); case "render" -> handleRender(parts); 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)) { 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; } private void handleDraw(String[] parts, ClientController controller) { if (parts.length < 4) { view.showError(ErrorType.GENERIC_ERROR, "Usage: draw upper|lower tribe|building "); 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); } private void handleRender(String[] parts) { view.renderBoard(); } 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; } } private void safeQuit(ClientController controller) { if (controller.getClient() != null) controller.disconnect(); } }