CLI Client #15
parent
acad8faa0e
commit
37b31869cd
@ -0,0 +1,221 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.ClientState;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Menu;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.util.DelegateCompleter;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.util.StateAwareFactory;
|
||||||
|
import eu.jonahbauer.wizard.common.machine.TimeoutContext;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.java_websocket.framing.CloseFrame;
|
||||||
|
import org.jline.reader.*;
|
||||||
|
import org.jline.reader.impl.DefaultParser;
|
||||||
|
import org.jline.terminal.Terminal;
|
||||||
|
import org.jline.terminal.TerminalBuilder;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
|
import picocli.shell.jline3.PicocliJLineCompleter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.concurrent.locks.Condition;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
public class Client extends TimeoutContext<ClientState, Client> implements Runnable {
|
||||||
|
private LineReader reader;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private ClientSocket socket;
|
||||||
|
|
||||||
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
|
private final Condition ready = lock.newCondition();
|
||||||
|
private boolean isReady = true;
|
||||||
|
|
||||||
|
private final DelegateCompleter completer = new DelegateCompleter();
|
||||||
|
private CommandSpec spec;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
new Client().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client() {
|
||||||
|
super(new Menu());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
update();
|
||||||
|
|
||||||
|
Parser parser = new DefaultParser();
|
||||||
|
try (Terminal terminal = TerminalBuilder.builder().build()) {
|
||||||
|
this.reader = LineReaderBuilder.builder()
|
||||||
|
.terminal(terminal)
|
||||||
|
.completer(completer)
|
||||||
|
.parser(parser)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String prompt = "> ";
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while (true) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
while (!isReady) {
|
||||||
|
ready.await();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
line = reader.readLine(prompt).trim();
|
||||||
|
terminal.flush();
|
||||||
|
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
if (line.equals("exit") || line.equals("quit")) break;
|
||||||
|
|
||||||
|
var parsedLine = parser.parse(line, 0);
|
||||||
|
execute(() -> execute(spec, parsedLine));
|
||||||
|
terminal.flush();
|
||||||
|
} catch (UserInterruptException e) {
|
||||||
|
// ignore
|
||||||
|
} catch (EndOfFileException e) {
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace(reader.getTerminal().writer());
|
||||||
|
terminal.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket != null && socket.isOpen()) {
|
||||||
|
socket.close(CloseFrame.GOING_AWAY);
|
||||||
|
}
|
||||||
|
shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleError(Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTransition(ClientState from, ClientState to) {
|
||||||
|
update(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold desc="Socket" defaultstate="collapsed">
|
||||||
|
public void setSocket(ClientSocket socket) {
|
||||||
|
this.socket = socket;
|
||||||
|
if (socket != null) {
|
||||||
|
this.socket.setAttachment(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onOpen() {
|
||||||
|
execute(s -> s.onOpen(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMessage(ServerMessage message) {
|
||||||
|
execute(s -> s.onMessage(this, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onClose(int code, String reason, boolean remote) {
|
||||||
|
execute(s -> s.onClose(this, code, reason, remote));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(ClientMessage message) {
|
||||||
|
getSocket().send(message.toString());
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
//<editor-fold desc="Output" defaultstate="collapsed">
|
||||||
|
public void println(String str) {
|
||||||
|
reader.printAbove(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void println(Object object) {
|
||||||
|
println(Objects.toString(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printfln(String format, Object...args) {
|
||||||
|
reader.printAbove(format.formatted(args));
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
public void ready() {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
isReady = true;
|
||||||
|
ready.signalAll();
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitForReady() {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
isReady = false;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update() {
|
||||||
|
update(getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(ClientState state) {
|
||||||
|
synchronized (completer) {
|
||||||
|
var commandLine = new CommandLine(state.getCommand(), new StateAwareFactory(this, state));
|
||||||
|
var spec = commandLine.getCommandSpec();
|
||||||
|
if (this.spec != spec) {
|
||||||
|
this.spec = spec;
|
||||||
|
completer.setDelegate(new PicocliJLineCompleter(spec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientState> execute(CommandSpec spec, ParsedLine line) {
|
||||||
|
var commandLine = spec.commandLine();
|
||||||
|
|
||||||
|
AtomicReference<Exception> exceptionRef = new AtomicReference<>();
|
||||||
|
commandLine.setExecutionExceptionHandler((ex, cl, parseResult) -> {
|
||||||
|
exceptionRef.set(ex);
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
commandLine.execute(line.words().toArray(String[]::new));
|
||||||
|
|
||||||
|
Exception exception = exceptionRef.get();
|
||||||
|
if (exception == null) {
|
||||||
|
Object result;
|
||||||
|
CommandLine.ParseResult parseResult = commandLine.getParseResult();
|
||||||
|
if (parseResult.subcommand() != null) {
|
||||||
|
CommandLine sub = parseResult.subcommand().commandSpec().commandLine();
|
||||||
|
result = sub.getExecutionResult();
|
||||||
|
} else {
|
||||||
|
result = commandLine.getExecutionResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof ClientState state) {
|
||||||
|
return Optional.of(state);
|
||||||
|
} else if (result instanceof Optional) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (Optional<ClientState>) result;
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(exception);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import org.java_websocket.client.WebSocketClient;
|
||||||
|
import org.java_websocket.framing.CloseFrame;
|
||||||
|
import org.java_websocket.handshake.ServerHandshake;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public class ClientSocket extends WebSocketClient {
|
||||||
|
public ClientSocket(URI serverUri) {
|
||||||
|
super(serverUri);
|
||||||
|
|
||||||
|
if ("wss".equals(getURI().getScheme())) {
|
||||||
|
setSocketFactory(SSLSocketFactory.getDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(ServerHandshake serverHandshake) {
|
||||||
|
getClient().onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String s) {
|
||||||
|
ServerMessage message = ServerMessage.parse(s);
|
||||||
|
getClient().onMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(int i, String s, boolean b) {
|
||||||
|
getClient().onClose(i, s, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception e) {
|
||||||
|
getClient().println(e.getMessage());
|
||||||
|
close(CloseFrame.ABNORMAL_CLOSE, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Client getClient() {
|
||||||
|
return getAttachment();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.commands;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.AwaitingAcknowledgement;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Game;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Menu;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.player.JuggleMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.model.Card;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static picocli.CommandLine.*;
|
||||||
|
|
||||||
|
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class, ShowCommand.class})
|
||||||
|
public class GameCommand {
|
||||||
|
private final Client client;
|
||||||
|
private final Game game;
|
||||||
|
|
||||||
|
public GameCommand(Client client, Game game) {
|
||||||
|
this.client = client;
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "play")
|
||||||
|
public AwaitingAcknowledgement play(
|
||||||
|
@Parameters(index = "0", paramLabel = "<card>", completionCandidates = HandCompletion.class) Card card
|
||||||
|
) {
|
||||||
|
this.client.send(new InteractionMessage(new PlayCardMessage(card)));
|
||||||
|
return awaitAcknowledgement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "predict")
|
||||||
|
public AwaitingAcknowledgement predict(
|
||||||
|
@Parameters(index = "0", paramLabel = "<prediction>") int prediction
|
||||||
|
) {
|
||||||
|
this.client.send(new InteractionMessage(new PredictMessage(prediction)));
|
||||||
|
return awaitAcknowledgement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "trump")
|
||||||
|
public AwaitingAcknowledgement trump(
|
||||||
|
@Parameters(index = "0", paramLabel = "<suit>") Card.Suit suit
|
||||||
|
) {
|
||||||
|
this.client.send(new InteractionMessage(new PickTrumpMessage(suit)));
|
||||||
|
return awaitAcknowledgement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "juggle")
|
||||||
|
public AwaitingAcknowledgement juggle(
|
||||||
|
@Parameters(index = "0", paramLabel = "<card>", completionCandidates = JuggleCompletion.class) Card card
|
||||||
|
) {
|
||||||
|
this.client.send(new InteractionMessage(new JuggleMessage(card)));
|
||||||
|
return awaitAcknowledgement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AwaitingAcknowledgement awaitAcknowledgement() {
|
||||||
|
return new AwaitingAcknowledgement(
|
||||||
|
() -> game,
|
||||||
|
message -> {
|
||||||
|
if (message instanceof NackMessage nack) {
|
||||||
|
int code = nack.getCode();
|
||||||
|
if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
|
||||||
|
client.println("Error: " + nack.getMessage());
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.println("Fatal: Unexpected message " + message + ". Returning to menu.");
|
||||||
|
return new Menu();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HandCompletion implements Iterable<String> {
|
||||||
|
private final Game game;
|
||||||
|
|
||||||
|
public HandCompletion(Game game) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Iterator<String> iterator() {
|
||||||
|
return game.getHands().get(game.getSelf()).stream().flatMap(c -> {
|
||||||
|
if (c == Card.CLOUD) {
|
||||||
|
return Stream.of(Card.CLOUD_BLUE, Card.CLOUD_GREEN, Card.CLOUD_RED, Card.CLOUD_YELLOW);
|
||||||
|
} else if (c == Card.JUGGLER) {
|
||||||
|
return Stream.of(Card.JUGGLER_BLUE, Card.JUGGLER_GREEN, Card.JUGGLER_RED, Card.JUGGLER_YELLOW);
|
||||||
|
} else if (c == Card.CHANGELING) {
|
||||||
|
return Stream.of(Card.CHANGELING_JESTER, Card.CHANGELING_WIZARD);
|
||||||
|
} else {
|
||||||
|
return Stream.of(c);
|
||||||
|
}
|
||||||
|
}).map(Card::toString).iterator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JuggleCompletion implements Iterable<String> {
|
||||||
|
private final Game game;
|
||||||
|
|
||||||
|
public JuggleCompletion(Game game) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Iterator<String> iterator() {
|
||||||
|
return game.getHands().get(game.getSelf()).stream().map(Card::toString).iterator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.commands;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.AwaitingJoinSession;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Lobby;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Menu;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.data.SessionData;
|
||||||
|
import eu.jonahbauer.wizard.common.model.Configuration;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.ajbrown.namemachine.NameGenerator;
|
||||||
|
import org.java_websocket.framing.CloseFrame;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static picocli.CommandLine.*;
|
||||||
|
|
||||||
|
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class})
|
||||||
|
public class LobbyCommand {
|
||||||
|
@Getter(lazy = true)
|
||||||
|
private static final NameGenerator nameGenerator = new NameGenerator();
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final Lobby lobby;
|
||||||
|
|
||||||
|
public LobbyCommand(Client client, Lobby lobby) {
|
||||||
|
this.client = client;
|
||||||
|
this.lobby = lobby;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "list", description = "Shows a list of available sessions")
|
||||||
|
public void list() {
|
||||||
|
var sessions = lobby.getSessions();
|
||||||
|
var count = sessions.size();
|
||||||
|
if (count > 0) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
if (count == 1) builder.append("There is one open session:\n");
|
||||||
|
else builder.append("There are ").append(count).append(" open sessions:\n");
|
||||||
|
|
||||||
|
sessions.forEach((session) -> builder.append(session).append('\n'));
|
||||||
|
client.println(builder);
|
||||||
|
} else {
|
||||||
|
client.println("No sessions.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "disconnect", description = "Disconnects from the server")
|
||||||
|
public Menu disconnect() {
|
||||||
|
client.getSocket().close(CloseFrame.GOING_AWAY);
|
||||||
|
client.setSocket(null);
|
||||||
|
return new Menu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "join", description = "Joins the specified session")
|
||||||
|
public AwaitingJoinSession join(
|
||||||
|
@Parameters(index = "0", paramLabel = "<session>", description = "session uuid", completionCandidates = SessionCompleter.class) UUID session,
|
||||||
|
@Option(names = "--name", description = "user name") String userName
|
||||||
|
) {
|
||||||
|
if (userName == null) {
|
||||||
|
userName = getNameGenerator().generateName().getFirstName();
|
||||||
|
client.println("Your name will be " + userName + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
client.send(new JoinSessionMessage(session, userName));
|
||||||
|
return new AwaitingJoinSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "create")
|
||||||
|
public AwaitingJoinSession create(
|
||||||
|
@Parameters(index = "0", paramLabel = "<session>", description = "human readable session name") String sessionName,
|
||||||
|
@Option(names = "--name", description = "user name") String userName,
|
||||||
|
@Option(names = "--configuration", description = "game configuration", defaultValue = "DEFAULT") Configuration configuration,
|
||||||
|
@Option(names = "--timeout", description = "interaction timeout", defaultValue = "60000") long timeout
|
||||||
|
) {
|
||||||
|
if (userName == null) {
|
||||||
|
userName = getNameGenerator().generateName().getFirstName();
|
||||||
|
client.println("Your name will be " + userName + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
client.send(new CreateSessionMessage(sessionName, userName, timeout, configuration));
|
||||||
|
return new AwaitingJoinSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SessionCompleter implements Iterable<String> {
|
||||||
|
private final Lobby lobby;
|
||||||
|
|
||||||
|
private SessionCompleter(Lobby lobby) {
|
||||||
|
this.lobby = lobby;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Iterator<String> iterator() {
|
||||||
|
return lobby.getSessions().stream().map(SessionData::getUuid).map(UUID::toString).iterator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.commands;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.ClientSocket;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.AwaitingConnection;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Lobby;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.SessionListMessage;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static picocli.CommandLine.*;
|
||||||
|
|
||||||
|
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class})
|
||||||
|
public class MenuCommand {
|
||||||
|
private final Client client;
|
||||||
|
|
||||||
|
public MenuCommand(Client client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "connect", description = "Connects to the specified server")
|
||||||
|
public AwaitingConnection connect(
|
||||||
|
@Parameters(index = "0", paramLabel = "<uri>", description = "server uri") URI uri
|
||||||
|
) {
|
||||||
|
ClientSocket socket = new ClientSocket(uri);
|
||||||
|
client.setSocket(socket);
|
||||||
|
socket.connect();
|
||||||
|
return new AwaitingConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "dummy")
|
||||||
|
public Lobby dummy() {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(4000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
client.onClose(1000, "Test", false);
|
||||||
|
});
|
||||||
|
return new Lobby(new SessionListMessage(List.of()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.commands;
|
||||||
|
|
||||||
|
import static picocli.CommandLine.Command;
|
||||||
|
|
||||||
|
@Command(name = "quit", aliases = "exit")
|
||||||
|
public class QuitCommand {
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.commands;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.AwaitingAcknowledgement;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.AwaitingJoinLobby;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Session;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.client.ReadyMessage;
|
||||||
|
|
||||||
|
import static picocli.CommandLine.*;
|
||||||
|
|
||||||
|
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class})
|
||||||
|
public class SessionCommand {
|
||||||
|
private final Client client;
|
||||||
|
private final Session session;
|
||||||
|
|
||||||
|
public SessionCommand(Client client, Session session) {
|
||||||
|
this.client = client;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "ready")
|
||||||
|
public AwaitingAcknowledgement ready(
|
||||||
|
@Parameters(index = "0", paramLabel = "<ready>", defaultValue = "true") boolean ready
|
||||||
|
) {
|
||||||
|
client.send(new ReadyMessage(ready));
|
||||||
|
return new AwaitingAcknowledgement(
|
||||||
|
() -> new Session(session.getSelf(), session.getSession(), ready, session.getPlayers())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "leave", description = "Leaves the current session and returns to the lobby")
|
||||||
|
public AwaitingJoinLobby leave() {
|
||||||
|
client.send(new LeaveSessionMessage());
|
||||||
|
return new AwaitingJoinLobby();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "list", description = "Shows a list of players in the current session")
|
||||||
|
public void list() {
|
||||||
|
var players = session.getPlayers();
|
||||||
|
var count = players.size();
|
||||||
|
if (count > 0) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
if (count == 1) builder.append("There is one player in this session:\n");
|
||||||
|
else builder.append("There are ").append(count).append(" players in this session:\n");
|
||||||
|
|
||||||
|
players.forEach((id, session) -> builder.append(session).append('\n'));
|
||||||
|
client.println(builder);
|
||||||
|
} else {
|
||||||
|
client.println("There are no players in this session.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.commands;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.Game;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.util.Pair;
|
||||||
|
import eu.jonahbauer.wizard.common.model.Card;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static picocli.CommandLine.Command;
|
||||||
|
|
||||||
|
@Command(name = "show")
|
||||||
|
public class ShowCommand {
|
||||||
|
private final Client client;
|
||||||
|
private final Game game;
|
||||||
|
|
||||||
|
public ShowCommand(Client client, Game game) {
|
||||||
|
this.client = client;
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "players")
|
||||||
|
public void players() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (Map.Entry<UUID, String> player : game.getPlayers().entrySet()) {
|
||||||
|
builder.append(player.getValue()).append('\t').append(player.getKey()).append('\n');
|
||||||
|
}
|
||||||
|
client.println(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "stack")
|
||||||
|
public void stack() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (Pair<UUID, Card> entry : game.getStack()) {
|
||||||
|
builder.append(entry.getValue()).append('\t').append(game.nameOf(entry.getKey())).append('\n');
|
||||||
|
}
|
||||||
|
client.println(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "hand")
|
||||||
|
public void hand() {
|
||||||
|
client.println(game.getHands().get(game.getSelf()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "predictions")
|
||||||
|
public void predictions() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
game.getPredictions().forEach((player, prediction) -> builder.append(game.nameOf(player)).append('\t').append(prediction).append('\n'));
|
||||||
|
client.println(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command(name = "trump")
|
||||||
|
public void trump() {
|
||||||
|
client.printfln("%s (%s)", game.getTrumpSuit(), game.getTrumpCard());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class Awaiting extends BaseState implements ClientState {
|
||||||
|
private static final CommandSpec COMMAND_SPEC = CommandSpec.create();
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
return unexpectedMessage(client, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
client.waitForReady();
|
||||||
|
client.timeout(this, 10_000);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onTimeout(Client client) {
|
||||||
|
client.println("Timed out. Returning to menu");
|
||||||
|
return Optional.of(new Menu());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCommand() {
|
||||||
|
return COMMAND_SPEC;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.Response;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public final class AwaitingAcknowledgement extends Awaiting {
|
||||||
|
private final Supplier<ClientState> success;
|
||||||
|
private final Function<ServerMessage, ClientState> failure;
|
||||||
|
|
||||||
|
public AwaitingAcknowledgement(Supplier<ClientState> success, Function<ServerMessage, ClientState> failure) {
|
||||||
|
this.success = success;
|
||||||
|
this.failure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AwaitingAcknowledgement(Supplier<ClientState> success) {
|
||||||
|
this.success = success;
|
||||||
|
this.failure = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
client.println("Waiting for acknowledgment...");
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
if (message instanceof AckMessage) {
|
||||||
|
return Optional.of(success.get());
|
||||||
|
} else if (failure != null) {
|
||||||
|
return Optional.of(failure.apply(message));
|
||||||
|
} else {
|
||||||
|
return super.onMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class AwaitingConnection extends Awaiting {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
client.println("Awaiting connection...");
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onOpen(Client client) {
|
||||||
|
client.println("Connection established.");
|
||||||
|
return Optional.of(new AwaitingJoinLobby());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
|
||||||
|
client.printfln("Connection could not be established. (code=%d, reason=%s)", code, reason);
|
||||||
|
return Optional.of(new Menu());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.SessionListMessage;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class AwaitingJoinLobby extends Awaiting {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
client.println("Waiting for session list...");
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
if (message instanceof SessionListMessage list) {
|
||||||
|
return Optional.of(new Lobby(list));
|
||||||
|
} else {
|
||||||
|
return super.onMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.SessionJoinedMessage;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class AwaitingJoinSession extends Awaiting {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
client.println("Waiting for acknowledgment...");
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
if (message instanceof SessionJoinedMessage joined) {
|
||||||
|
return Optional.of(new Session(joined));
|
||||||
|
} else if (message instanceof NackMessage nack) {
|
||||||
|
switch (nack.getCode()) {
|
||||||
|
case NackMessage.GAME_ALREADY_STARTED -> client.println("Error: Game has already started.");
|
||||||
|
case NackMessage.SESSION_FULL -> client.println("Error: The session is full.");
|
||||||
|
case NackMessage.SESSION_NOT_FOUND -> client.println("Error: Session not found.");
|
||||||
|
case NackMessage.NAME_TAKEN -> client.println("Error: Name already taken.");
|
||||||
|
default -> { return super.onMessage(client, message); }
|
||||||
|
}
|
||||||
|
return Optional.of(new AwaitingJoinLobby());
|
||||||
|
} else {
|
||||||
|
return super.onMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class BaseState implements ClientState {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client context) {
|
||||||
|
context.ready();
|
||||||
|
return ClientState.super.onEnter(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onOpen(Client client) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
|
||||||
|
if (remote) {
|
||||||
|
client.printfln("Lost connection (code=%d, reason=%s).", code, reason);
|
||||||
|
} else {
|
||||||
|
client.printfln("Connection closed (code=%d, reason=%s).", code, reason);
|
||||||
|
}
|
||||||
|
return Optional.of(new Menu());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Optional<ClientState> unexpectedMessage(Client client, ServerMessage message) {
|
||||||
|
// return to menu on unexpected message
|
||||||
|
client.println("Fatal: Unexpected message " + message + ". Returning to menu.");
|
||||||
|
return Optional.of(new Menu());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.common.machine.TimeoutState;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ClientState extends TimeoutState<ClientState, Client> {
|
||||||
|
Optional<ClientState> onOpen(Client client);
|
||||||
|
|
||||||
|
Optional<ClientState> onMessage(Client client, ServerMessage message);
|
||||||
|
|
||||||
|
Optional<ClientState> onClose(Client client, int code, String reason, boolean remote);
|
||||||
|
|
||||||
|
Object getCommand();
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.commands.GameCommand;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.util.Pair;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.observer.*;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import eu.jonahbauer.wizard.common.model.Card;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class Game extends BaseState {
|
||||||
|
private final UUID self;
|
||||||
|
private final UUID session;
|
||||||
|
|
||||||
|
private final Map<UUID, String> players;
|
||||||
|
private final Map<UUID, Integer> scores = new HashMap<>();
|
||||||
|
|
||||||
|
private int round = -1;
|
||||||
|
private final Map<UUID, Integer> predictions = new HashMap<>();
|
||||||
|
private final Map<UUID, List<Card>> hands = new HashMap<>();
|
||||||
|
|
||||||
|
private int trick = -1;
|
||||||
|
private final List<Pair<UUID, Card>> stack = new ArrayList<>();
|
||||||
|
|
||||||
|
private Card trumpCard;
|
||||||
|
private Card.Suit trumpSuit;
|
||||||
|
|
||||||
|
public Game(UUID self, UUID session, Map<UUID, String> players) {
|
||||||
|
this.self = self;
|
||||||
|
this.session = session;
|
||||||
|
this.players = players;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
if (message instanceof GameMessage game) {
|
||||||
|
var observerMessage = game.getObserverMessage();
|
||||||
|
if (observerMessage instanceof StateMessage state) {
|
||||||
|
switch (state.getState()) {
|
||||||
|
case "starting_round" -> {
|
||||||
|
client.printfln("Round %d is starting...", ++round);
|
||||||
|
predictions.clear();
|
||||||
|
trumpSuit = null;
|
||||||
|
trumpCard = null;
|
||||||
|
stack.clear();
|
||||||
|
trick = -1;
|
||||||
|
}
|
||||||
|
case "starting_trick" -> {
|
||||||
|
client.printfln("Trick %d is starting...", ++trick);
|
||||||
|
stack.clear();
|
||||||
|
}
|
||||||
|
case "finished" -> {
|
||||||
|
client.println("The game has finished.");
|
||||||
|
return returnToSession();
|
||||||
|
}
|
||||||
|
case "error" -> {
|
||||||
|
client.println("The game has finished with an error.");
|
||||||
|
return returnToSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (observerMessage instanceof HandMessage hand) {
|
||||||
|
hands.put(hand.getPlayer(), hand.getHand());
|
||||||
|
if (hand.getPlayer().equals(self)) {
|
||||||
|
client.printfln("Your hand cards are: %s", hand.getHand());
|
||||||
|
} else {
|
||||||
|
client.printfln("%s's hand cards are: %s", nameOf(hand.getPlayer()), hand.getHand());
|
||||||
|
}
|
||||||
|
} else if (observerMessage instanceof PredictionMessage prediction) {
|
||||||
|
predictions.put(prediction.getPlayer(), prediction.getPrediction());
|
||||||
|
if (prediction.getPlayer().equals(self)) {
|
||||||
|
client.printfln("You predicted: %d", prediction.getPrediction());
|
||||||
|
} else {
|
||||||
|
client.printfln("%s predicted: %d", nameOf(prediction.getPlayer()), prediction.getPrediction());
|
||||||
|
}
|
||||||
|
} else if (observerMessage instanceof TrumpMessage trump) {
|
||||||
|
trumpCard = trump.getCard();
|
||||||
|
trumpSuit = trump.getSuit();
|
||||||
|
if (trumpCard == null) {
|
||||||
|
client.println("There is no trump in this round.");
|
||||||
|
} else {
|
||||||
|
client.printfln("The trump suit is %s (%s).", trumpSuit, trumpCard);
|
||||||
|
}
|
||||||
|
} else if (observerMessage instanceof TrickMessage trick) {
|
||||||
|
this.stack.clear();
|
||||||
|
client.printfln("This trick %s goes to %s.", trick.getCards(), nameOf(trick.getPlayer()));
|
||||||
|
} else if (observerMessage instanceof CardMessage card) {
|
||||||
|
this.stack.add(Pair.of(card.getPlayer(), card.getCard()));
|
||||||
|
|
||||||
|
var hand = this.hands.get(card.getPlayer());
|
||||||
|
if (hand != null) hand.remove(card.getCard());
|
||||||
|
|
||||||
|
if (card.getPlayer().equals(self)) {
|
||||||
|
client.printfln("You played %s.", card.getCard());
|
||||||
|
} else {
|
||||||
|
client.printfln("%s played %s.", nameOf(card.getPlayer()), card.getCard());
|
||||||
|
}
|
||||||
|
} else if (observerMessage instanceof ScoreMessage score) {
|
||||||
|
score.getPoints().forEach((player, points) -> scores.merge(player, points, Integer::sum));
|
||||||
|
StringBuilder builder = new StringBuilder("The scores are as follows:\n");
|
||||||
|
scores.forEach((player, points) -> builder.append(nameOf(player)).append('\t').append(points).append('\n'));
|
||||||
|
client.println(builder.toString());
|
||||||
|
} else if (observerMessage instanceof UserInputMessage input) {
|
||||||
|
if (input.getPlayer().equals(self)) {
|
||||||
|
client.printfln("It is your turn to %s. You have time until %s.", switch (input.getAction()) {
|
||||||
|
case CHANGE_PREDICTION -> "change your prediction";
|
||||||
|
case JUGGLE_CARD -> "juggle a card";
|
||||||
|
case PLAY_CARD -> "play a card";
|
||||||
|
case PICK_TRUMP -> "pick the trump suit";
|
||||||
|
case MAKE_PREDICTION -> "make a prediction";
|
||||||
|
}, LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault()));
|
||||||
|
} else {
|
||||||
|
client.printfln(
|
||||||
|
"Waiting for input %s from %s. (times out at %s)",
|
||||||
|
input.getAction(),
|
||||||
|
nameOf(input.getPlayer()),
|
||||||
|
LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Unknown observer message " + observerMessage.getClass().getSimpleName() + "");
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
return unexpectedMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nameOf(UUID player) {
|
||||||
|
return players.get(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCommand() {
|
||||||
|
return GameCommand.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientState> returnToSession() {
|
||||||
|
return Optional.of(new Session(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
players.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> new PlayerData(entry.getKey(), entry.getValue(), false)
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.commands.LobbyCommand;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.data.SessionData;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public final class Lobby extends BaseState {
|
||||||
|
@Getter
|
||||||
|
private final Set<SessionData> sessions = new HashSet<>();
|
||||||
|
|
||||||
|
public Lobby(SessionListMessage list) {
|
||||||
|
sessions.addAll(list.getSessions());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
if (sessions.size() == 1) {
|
||||||
|
client.println("Successfully joined the server. There is one open session.");
|
||||||
|
} else {
|
||||||
|
client.printfln("Successfully joined the server. There are %d open sessions.", sessions.size());
|
||||||
|
}
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
if (message instanceof SessionCreatedMessage created) {
|
||||||
|
var session = created.getSession();
|
||||||
|
sessions.add(session);
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (message instanceof SessionRemovedMessage removed) {
|
||||||
|
sessions.remove(new SessionData(removed.getSession(), null, 0, null));
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (message instanceof SessionModifiedMessage modified) {
|
||||||
|
sessions.remove(modified.getSession());
|
||||||
|
sessions.add(modified.getSession());
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (message instanceof SessionListMessage list) {
|
||||||
|
sessions.clear();
|
||||||
|
sessions.addAll(list.getSessions());
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
return unexpectedMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCommand() {
|
||||||
|
return LobbyCommand.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.commands.MenuCommand;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.java_websocket.framing.CloseFrame;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class Menu extends BaseState {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
if (client.getSocket() != null && client.getSocket().isOpen()) {
|
||||||
|
client.getSocket().close(CloseFrame.GOING_AWAY);
|
||||||
|
client.waitForReady();
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
throw new AssertionError("Received a ServerMessage while not connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
|
||||||
|
client.ready();
|
||||||
|
super.onClose(client, code, reason, remote);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCommand() {
|
||||||
|
return MenuCommand.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.state;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.commands.SessionCommand;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
|
||||||
|
import eu.jonahbauer.wizard.common.messages.server.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class Session extends BaseState {
|
||||||
|
private static final CommandSpec COMMAND_SPEC = CommandSpec.forAnnotatedObject(new SessionCommand(null, null));
|
||||||
|
|
||||||
|
private final UUID self;
|
||||||
|
private final UUID session;
|
||||||
|
private final boolean ready;
|
||||||
|
private final Map<UUID, PlayerData> players = new HashMap<>();
|
||||||
|
|
||||||
|
public Session(SessionJoinedMessage joined) {
|
||||||
|
this.self = joined.getPlayer();
|
||||||
|
this.session = joined.getSession();
|
||||||
|
this.ready = false;
|
||||||
|
joined.getPlayers().forEach(player -> players.put(player.getUuid(), player));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session(UUID self, UUID session, boolean ready, Map<UUID, PlayerData> players) {
|
||||||
|
this.self = self;
|
||||||
|
this.session = session;
|
||||||
|
this.players.putAll(players);
|
||||||
|
this.ready = ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onEnter(Client client) {
|
||||||
|
if (players.size() - 1 == 1) {
|
||||||
|
client.printfln("Successfully joined session %s. There is one other player.", session);
|
||||||
|
} else {
|
||||||
|
client.printfln("Successfully joined session %s. There are %d other players.", session, players.size() - 1);
|
||||||
|
}
|
||||||
|
return super.onEnter(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
|
||||||
|
if (message instanceof PlayerJoinedMessage join) {
|
||||||
|
var player = join.getPlayer();
|
||||||
|
players.put(player.getUuid(), player);
|
||||||
|
client.printfln("Player \"%s\" joined the session.", player.getName());
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (message instanceof PlayerLeftMessage leave) {
|
||||||
|
var player = players.remove(leave.getPlayer());
|
||||||
|
client.printfln("Player \"%s\" left the session.", player.getName());
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (message instanceof PlayerModifiedMessage modified) {
|
||||||
|
var player = modified.getPlayer();
|
||||||
|
players.put(player.getUuid(), player);
|
||||||
|
client.printfln("Player \"%s\" was modified.", player.getName());
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (message instanceof StartingGameMessage) {
|
||||||
|
return Optional.of(new Game(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
players.values().stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName))
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return unexpectedMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCommand() {
|
||||||
|
return SessionCommand.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.util;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.experimental.Delegate;
|
||||||
|
import org.jline.reader.Completer;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class DelegateCompleter implements Completer {
|
||||||
|
@Delegate
|
||||||
|
private Completer delegate;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.util;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record Pair<F,S>(F first, S second) implements Map.Entry<F, S> {
|
||||||
|
public static <F,S> Pair<F,S> of(F first, S second) {
|
||||||
|
return new Pair<>(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public F getKey() {
|
||||||
|
return first();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public S getValue() {
|
||||||
|
return second();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public S setValue(S value) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package eu.jonahbauer.wizard.client.cli.util;
|
||||||
|
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.client.cli.Client;
|
||||||
|
import eu.jonahbauer.wizard.client.cli.state.ClientState;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
|
||||||
|
public class StateAwareFactory implements CommandLine.IFactory {
|
||||||
|
private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory();
|
||||||
|
private final Client client;
|
||||||
|
private final ClientState clientState;
|
||||||
|
|
||||||
|
public StateAwareFactory(Client client, ClientState clientState) {
|
||||||
|
this.client = client;
|
||||||
|
this.clientState = clientState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <K> K create(Class<K> cls) throws Exception {
|
||||||
|
try {
|
||||||
|
return defaultFactory.create(cls);
|
||||||
|
} catch (Exception e) {
|
||||||
|
c: for (Constructor<K> constructor : (Constructor<K>[]) cls.getDeclaredConstructors()) {
|
||||||
|
Object[] values = new Object[constructor.getParameterCount()];
|
||||||
|
Parameter[] parameters = constructor.getParameters();
|
||||||
|
for (int i = 0; i < parameters.length; i++) {
|
||||||
|
Parameter parameter = parameters[i];
|
||||||
|
if (ClientState.class.isAssignableFrom(parameter.getType())) {
|
||||||
|
values[i] = clientState;
|
||||||
|
} else if (Client.class.isAssignableFrom(parameter.getType())) {
|
||||||
|
values[i] = client;
|
||||||
|
} else {
|
||||||
|
continue c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor.newInstance(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
var e2 = new NoSuchMethodException("Could not find a suitable constructor for class " + cls.getName());
|
||||||
|
e2.addSuppressed(e);
|
||||||
|
throw e2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue