diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java index 0719c64..3446072 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java @@ -3,8 +3,10 @@ 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.client.InteractionMessage; import eu.jonahbauer.wizard.common.messages.data.PlayerData; import eu.jonahbauer.wizard.common.messages.observer.*; +import eu.jonahbauer.wizard.common.messages.player.ContinueMessage; import eu.jonahbauer.wizard.common.messages.server.AckMessage; import eu.jonahbauer.wizard.common.messages.server.GameMessage; import eu.jonahbauer.wizard.common.messages.server.NackMessage; @@ -133,20 +135,25 @@ public final class Game extends BaseState { client.print("The scores are as follows:", "", col0, col1); } else if (observerMessage instanceof UserInputMessage input) { - if (self.equals(input.getPlayer())) { + if (input.getAction() == UserInputMessage.Action.SYNC) { + client.send(new InteractionMessage(new ContinueMessage())); + } else if (self.equals(input.getPlayer())) { 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"; + default -> throw new AssertionError(); }, 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()) + LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), + ZoneId.systemDefault() + ) ); } } else if (observerMessage instanceof TimeoutMessage) { diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TimeoutMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TimeoutMessage.java index a46cae1..a59066c 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TimeoutMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TimeoutMessage.java @@ -1,4 +1,7 @@ package eu.jonahbauer.wizard.common.messages.observer; +/** + * A {@link TimeoutMessage} is sent when an user input times out. + */ public final class TimeoutMessage extends ObserverMessage { } diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java index 36118b4..f34b3af 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java @@ -58,6 +58,7 @@ public final class UserInputMessage extends ObserverMessage { * An action that indicates that a player should pick a trump suit. A {@link UserInputMessage} with this * {@link UserInputMessage#getAction()} should be responded to with a {@link PickTrumpMessage}. */ - PICK_TRUMP + PICK_TRUMP, + SYNC } } diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/ContinueMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/ContinueMessage.java new file mode 100644 index 0000000..eac8d0b --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/ContinueMessage.java @@ -0,0 +1,4 @@ +package eu.jonahbauer.wizard.common.messages.player; + +public final class ContinueMessage extends PlayerMessage { +} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java index 3067a4e..180c7f1 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java @@ -7,7 +7,7 @@ import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; import lombok.EqualsAndHashCode; @EqualsAndHashCode -public abstract sealed class PlayerMessage permits JuggleMessage, PickTrumpMessage, PlayCardMessage, PredictMessage { +public abstract sealed class PlayerMessage permits ContinueMessage, JuggleMessage, PickTrumpMessage, PlayCardMessage, PredictMessage { private static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message")) .create(); diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java index 0054f31..ae49259 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java @@ -28,6 +28,11 @@ public abstract class GameState implements TimeoutState { return Optional.empty(); } + protected final Optional syncTimeout(Game game) { + game.timeout(this, getSyncTimeout(game, false)); + return Optional.empty(); + } + protected final Optional transition(GameState state) { return Optional.of(state); } @@ -35,6 +40,10 @@ public abstract class GameState implements TimeoutState { protected final long getTimeout(Game game, boolean absolute) { return (absolute ? System.currentTimeMillis() : 0) + game.getConfig().timeout(); } + + protected final long getSyncTimeout(Game game, boolean absolute) { + return (absolute ? System.currentTimeMillis() : 0) + game.getConfig().syncTimeout(); + } // public Optional onMessage(Game game, UUID player, PlayerMessage message) { diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java index dd21ea4..2ab2e49 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java @@ -1,18 +1,52 @@ package eu.jonahbauer.wizard.core.machine.states.round; -import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.common.messages.observer.TimeoutMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.messages.player.ContinueMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.GameState; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.SYNC; +import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; public final class StartingRound extends RoundState { + private final transient Set ready = new HashSet<>(); + public StartingRound(GameData data) { super(data); } @Override public Optional onEnter(Game game) { - return transition(new Dealing(getData())); + game.notify(new UserInputMessage(null, SYNC, getSyncTimeout(game, true))); + return syncTimeout(game); + } + + @Override + public Optional onMessage(Game game, UUID player, PlayerMessage message) { + if (message instanceof ContinueMessage) { + ready.add(player); + + if (ready.size() == get(PLAYERS).size()) { + return Optional.of(new Dealing(getData())); + } else { + return Optional.empty(); + } + } else { + return super.onMessage(game, player, message); + } + } + + @Override + public Optional onTimeout(Game game) { + game.notify(new TimeoutMessage()); + return Optional.of(new Dealing(getData())); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java index 83e958a..ed98858 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java @@ -1,18 +1,52 @@ package eu.jonahbauer.wizard.core.machine.states.trick; +import eu.jonahbauer.wizard.common.messages.observer.TimeoutMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.messages.player.ContinueMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.GameState; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.SYNC; +import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; public final class StartingTrick extends TrickState { + private final transient Set ready = new HashSet<>(); + public StartingTrick(GameData data) { super(data); } @Override public Optional onEnter(Game game) { - return transition(new PlayingCard(getData())); + game.notify(new UserInputMessage(null, SYNC, getSyncTimeout(game, true))); + return syncTimeout(game); + } + + @Override + public Optional onMessage(Game game, UUID player, PlayerMessage message) { + if (message instanceof ContinueMessage) { + ready.add(player); + + if (ready.size() == get(PLAYERS).size()) { + return Optional.of(new PlayingCard(getData())); + } else { + return Optional.empty(); + } + } else { + return super.onMessage(game, player, message); + } + } + + @Override + public Optional onTimeout(Game game) { + game.notify(new TimeoutMessage()); + return Optional.of(new PlayingCard(getData())); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/GameConfiguration.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/GameConfiguration.java index 6d65c22..34d08e4 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/GameConfiguration.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/GameConfiguration.java @@ -16,4 +16,9 @@ public class GameConfiguration { Set cards; boolean allowExactPredictions; @With long timeout; + @With long syncTimeout; + + public GameConfiguration(Set cards, boolean allowExactPredictions, long timeout) { + this(cards, allowExactPredictions, timeout, 10_000); + } } diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTest.java index ca83e6e..670ca76 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTest.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTest.java @@ -13,7 +13,7 @@ public class GameTest { public void runDefault(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException { Game game = new Game( repetitionInfo.getCurrentRepetition(), - Configurations.DEFAULT.withTimeout(0), + Configurations.DEFAULT.withTimeout(0).withSyncTimeout(0), (player, msg) -> System.out.println(msg) ); var players = List.of( @@ -31,7 +31,7 @@ public class GameTest { public void runAnniversary2016(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException { Game game = new Game( repetitionInfo.getCurrentRepetition(), - Configurations.ANNIVERSARY_2016.withTimeout(0), + Configurations.ANNIVERSARY_2016.withTimeout(0).withSyncTimeout(0), (player, msg) -> System.out.println(msg) ); var players = List.of( @@ -49,7 +49,7 @@ public class GameTest { public void runAnniversary2021(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException { Game game = new Game( repetitionInfo.getCurrentRepetition(), - Configurations.ANNIVERSARY_2021.withTimeout(0), + Configurations.ANNIVERSARY_2021.withTimeout(0).withSyncTimeout(0), (player, msg) -> System.out.println(msg) ); var players = List.of( diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java index 04d257d..182ec2f 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java @@ -107,6 +107,15 @@ public class MessageQueue implements Observer { return this; } + public MessageQueue sync(UUID... players) { + var bulk = new BulkQueuedMessage(); + Arrays.stream(players) + .map(player -> new SingleQueuedMessage(player, UserInputMessage.Action.SYNC, new ContinueMessage())) + .forEach(bulk.getMessages()::add); + messages.add(bulk); + return this; + } + public void doNotify(ObserverMessage om) { try { System.out.println(om); diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java index bbcbcd2..89012d2 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java @@ -55,41 +55,49 @@ public class RoundTest { @Test public void run_Simple() throws ExecutionException, InterruptedException { MessageQueue queue = new MessageQueue() + .sync(players) .addPrediction(players[3], 3) .addPrediction(players[0], 0) .addPrediction(players[1], 3) .addPrediction(players[2], 1) // trick 0 + .sync(players) .addCard(players[3], Card.RED_7) .addCard(players[0], Card.GREEN_WIZARD) .addCard(players[1], Card.GREEN_10) .addCard(players[2], Card.RED_1) // trick 1 + .sync(players) .addCard(players[0], Card.RED_9) .addCard(players[1], Card.YELLOW_WIZARD) .addCard(players[2], Card.RED_3) .addCard(players[3], Card.RED_10) // trick 2 + .sync(players) .addCard(players[1], Card.YELLOW_JESTER) .addCard(players[2], Card.GREEN_12) .addCard(players[3], Card.GREEN_5) .addCard(players[0], Card.BLUE_7) // trick 3 + .sync(players) .addCard(players[2], Card.GREEN_1) .addCard(players[3], Card.YELLOW_10) .addCard(players[0], Card.BLUE_5) .addCard(players[1], Card.GREEN_2) // trick 4 + .sync(players) .addCard(players[1], Card.GREEN_7) .addCard(players[2], Card.GREEN_3) .addCard(players[3], Card.YELLOW_5) .addCard(players[0], Card.RED_13) // trick 5 + .sync(players) .addCard(players[1], Card.YELLOW_8) .addCard(players[2], Card.BLUE_9) .addCard(players[3], Card.YELLOW_9) .addCard(players[0], Card.GREEN_JESTER) // trick 5 + .sync(players) .addCard(players[3], Card.YELLOW_2) .addCard(players[0], Card.BLUE_2) .addCard(players[1], Card.BLUE_10) @@ -138,6 +146,7 @@ public class RoundTest { @Test public void run_Anniversary() throws ExecutionException, InterruptedException { MessageQueue queue = new MessageQueue() + .sync(players) .addPickTrump(players[2], Card.Suit.YELLOW) .addPrediction(players[3], 2) .addPrediction(players[0], 2) @@ -147,6 +156,7 @@ public class RoundTest { .addPrediction(players[2], 3) .end() // trick 0 + .sync(players) .begin() .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class) .addCard(players[3], Card.BLUE_2) @@ -157,6 +167,7 @@ public class RoundTest { .addCard(players[1], Card.BLUE_9) .addCard(players[2], Card.GREEN_WIZARD) // trick 1 + .sync(players) .begin() .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) .addCard(players[2], Card.YELLOW_4) @@ -165,6 +176,7 @@ public class RoundTest { .addCard(players[0], Card.YELLOW_WIZARD) .addCard(players[1], Card.BOMB) // trick 2 + .sync(players) .addCard(players[0], Card.RED_3) .begin() .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) @@ -173,6 +185,7 @@ public class RoundTest { .addCard(players[2], Card.RED_2) .addCard(players[3], Card.DRAGON) // trick 3 + .sync(players) .addCard(players[3], Card.BLUE_13) .begin() .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class) @@ -183,6 +196,7 @@ public class RoundTest { .end() .addCard(players[2], Card.BLUE_1) // trick 4 + .sync(players) .addCard(players[1], Card.RED_7) .addCard(players[2], Card.YELLOW_11) .begin() @@ -193,11 +207,13 @@ public class RoundTest { .addCard(players[0], Card.CHANGELING_WIZARD) .end() // trick 5 + .sync(players) .addCard(players[0], Card.GREEN_7) .addCard(players[1], Card.FAIRY) .addCard(players[2], Card.YELLOW_7) .addCard(players[3], Card.BLUE_6) // trick 6 + .sync(players) .addCard(players[2], Card.BLUE_4) .addCard(players[3], Card.BLUE_11) .addCard(players[0], Card.GREEN_1) diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java index cacceca..dec642f 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java @@ -72,6 +72,7 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() + .sync(players) .addCards(List.of(players), hands, 0); Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue); @@ -102,6 +103,7 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() + .sync(players) .addCard(players[0], Card.RED_1) .begin() .addCard(players[2], Card.GREEN_1).assertThrows(IllegalStateException.class) @@ -150,6 +152,7 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() + .sync(players) .addCard(players[0], Card.RED_1) .addCard(players[1], Card.CLOUD_BLUE) .addCard(players[2], Card.GREEN_1) @@ -188,6 +191,7 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() + .sync(players) .addCard(players[0], Card.RED_1) .addCard(players[1], Card.JUGGLER_RED) .addCard(players[2], Card.GREEN_1) @@ -238,6 +242,7 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() + .sync(players) .addCard(players[0], Card.CHANGELING_JESTER) .addCard(players[1], Card.RED_1) .addCard(players[2], Card.GREEN_1) @@ -279,6 +284,7 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() + .sync(players) .addCard(players[0], Card.CHANGELING_WIZARD) .addCard(players[1], Card.RED_1) .addCard(players[2], Card.GREEN_WIZARD)