318 lines
14 KiB
Java
318 lines
14 KiB
Java
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 <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();
|
||
}
|
||
}
|