Files
Progetto-ingegneria-del-sof…/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java
T

318 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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').";
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 <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);
}
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();
}
}