diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/JugglingMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/JugglingMessage.java index 22ffc2b..e69de29 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/JugglingMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/JugglingMessage.java @@ -1,8 +0,0 @@ -package eu.jonahbauer.wizard.common.messages.observer; - -import lombok.EqualsAndHashCode; - -@EqualsAndHashCode(callSuper = true) -public final class JugglingMessage extends ObserverMessage { - -} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java index 76b282a..7d656a2 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java @@ -6,7 +6,7 @@ import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; import lombok.EqualsAndHashCode; @EqualsAndHashCode -public abstract sealed class ObserverMessage permits CardMessage, HandMessage, JugglingMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage { +public abstract sealed class ObserverMessage permits CardMessage, HandMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage { public static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message")) .create(); 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 04879b9..e0ba829 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 @@ -1,8 +1,10 @@ package eu.jonahbauer.wizard.common.messages.observer; +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.model.card.Card; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -17,7 +19,8 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) public final class UserInputMessage extends ObserverMessage { /** - * The UUID of the player whose input is required. + * The UUID of the player whose input is required. May be {@code null} to indicate that an input is required from + * every player. */ private final UUID player; /** @@ -45,6 +48,12 @@ public final class UserInputMessage extends ObserverMessage { * {@link UserInputMessage#getAction()} should be responded to with a {@link PlayCardMessage}. */ PLAY_CARD, + /** + * An action that indicates that a player should choose a card that will be given to his left neighbor as a + * result of a played {@link Card#JUGGLER}. A {@link UserInputMessage} with this + * {@link UserInputMessage#getAction()} should be responded to with a {@link JuggleMessage}. + */ + JUGGLE_CARD, /** * 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}. diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/JuggleMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/JuggleMessage.java new file mode 100644 index 0000000..c0b650a --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/JuggleMessage.java @@ -0,0 +1,13 @@ +package eu.jonahbauer.wizard.common.messages.player; + +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) +public final class JuggleMessage extends PlayerMessage { + private final Card card; +} 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 044e677..47ba4bc 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 @@ -6,7 +6,7 @@ import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; import lombok.EqualsAndHashCode; @EqualsAndHashCode -public abstract sealed class PlayerMessage permits PickTrumpMessage, PlayCardMessage, PredictMessage { +public abstract sealed class PlayerMessage permits JuggleMessage, PickTrumpMessage, PlayCardMessage, PredictMessage { public 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/states/GameData.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameData.java index 88e9af8..a895614 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameData.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameData.java @@ -12,26 +12,28 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.*; +import java.util.function.Supplier; @Unmodifiable @EqualsAndHashCode(of = {"values", "present"}) public final class GameData { - private static final int SIZE = 11; + private static final int SIZE = 12; public static final GameData EMPTY = new GameData(); - public static final Key> PLAYERS = new Key<>("players", 0); - public static final Key> SCORE = new Key<>("score", 1); - public static final Key ROUND = new Key<>("round", 2); - public static final Key>> HANDS = new Key<>("hands", 3); - public static final Key TRUMP_CARD = new Key<>("trumpCard", 4); - public static final Key TRUMP_SUIT = new Key<>("trumpSuit", 5); - public static final Key> PREDICTIONS = new Key<>("predictions", 6); - public static final Key> TRICKS = new Key<>("tricks", 7); - public static final Key TRICK = new Key<>("trick", 8); - public static final Key>> STACK = new Key<>("stack", 9); - public static final Key CURRENT_PLAYER = new Key<>("currentPlayer", 10); - + public static final Key> PLAYERS = new Key<>("players", 0, null); + public static final Key> SCORE = new Key<>("score", 1, Map::of); + public static final Key ROUND = new Key<>("round", 2, () -> 0); + public static final Key>> HANDS = new Key<>("hands", 3, null); + public static final Key TRUMP_CARD = new Key<>("trumpCard", 4, null); + public static final Key TRUMP_SUIT = new Key<>("trumpSuit", 5, null); + public static final Key> PREDICTIONS = new Key<>("predictions", 6, Map::of); + public static final Key> TRICKS = new Key<>("tricks", 7, Map::of); + public static final Key TRICK = new Key<>("trick", 8, () -> 0); + public static final Key>> STACK = new Key<>("stack", 9, List::of); + public static final Key CURRENT_PLAYER = new Key<>("currentPlayer", 10, null); + public static final Key CLOUDED_PLAYER = new Key<>("cloudedPlayer", 11, null); + private final Object[] values; private final boolean[] present; private transient final boolean[] required = new boolean[SIZE]; @@ -47,18 +49,35 @@ public final class GameData { } /** - * Returns the value to which the specified key is mapped or {@code null} if this map contains no mapping for the - * key. + * Returns the value to which the specified key is mapped, the default value for the specified key, or throws + * a {@link NoSuchElementException} if this map neither contains a mapping for the key nor does the key have a + * default value. * @param key the key whose associated value is to be returned * @param the value type * @return the value to which the specified key is mapped, or null if this map contains no mapping for the key */ public T get(@NotNull Key key) { int index = key.index(); - if (!present[index]) throw new NoSuchElementException(); + if (present[index]) { + //noinspection unchecked + return (T) values[index]; + } else if (key.defaultValue() != null) { + present[index] = true; + values[index] = key.defaultValue().get(); + //noinspection unchecked + return (T) values[index]; + } else { + throw new NoSuchElementException(); + } + } - //noinspection unchecked - return (T) values[index]; + /** + * Returns {@code true} if this map contains a mapping for the specified key. + * @param key key whose presence in this map is to be tested + * @return {@code true} if this map contains a mapping for the specified key + */ + public boolean has(@NotNull Key key) { + return present[key.index()]; } /** @@ -172,13 +191,21 @@ public final class GameData { */ @Contract("_ -> this") public GameData require(@NotNull Key key) { - if (!present[key.index()]) { - throw new NoSuchElementException("Could not find required value '" + key + "'."); + if (!present[key.index()] && key.defaultValue() == null) { + throw new InvalidDataException("Could not find required value '" + key + "'."); } required[key.index()] = true; return this; } + /** + * @see #clean() + */ + public GameData keep(@NotNull Key key) { + required[key.index()] = true; + return this; + } + /** * Returns {@code this} if this map contains a mapping for each of the specified keys or throws a * {@link NoSuchElementException} otherwise. @@ -213,13 +240,13 @@ public final class GameData { var mapValue = get(map); var listValue = get(list); for (K k : listValue) { - if (!mapValue.containsKey(k)) throw new NoSuchElementException("Could not find required value: " + map.name() + "[" + k + "]."); + if (!mapValue.containsKey(k)) throw new InvalidDataException("Could not find required value: " + map.name() + "[" + k + "]."); } return this; } /** - * Retains only the mappings that have been required since object creation. + * Retains only the mappings that have been required or kept since object creation. * @return {@code this} */ public GameData clean() { @@ -246,6 +273,7 @@ public final class GameData { public static class Key { String name; int index; + Supplier defaultValue; @Override public String toString() { diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java index 9b2dc1c..cde92a3 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java @@ -1,14 +1,10 @@ package eu.jonahbauer.wizard.core.machine.states.game; -import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.states.GameState; import eu.jonahbauer.wizard.core.machine.states.round.StartingRound; -import java.util.Map; - -import static eu.jonahbauer.wizard.core.machine.states.GameData.*; - public final class Starting extends GameState { public Starting(GameData data) { @@ -17,9 +13,6 @@ public final class Starting extends GameState { @Override public void onEnter(Game game) { - transition(game, new StartingRound(getData().with( - ROUND, 0, - SCORE, Map.of() - ))); + transition(game, new StartingRound(getData())); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/ChangingPrediction.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java similarity index 68% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/ChangingPrediction.java rename to wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java index afc388f..d633346 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/ChangingPrediction.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java @@ -1,37 +1,38 @@ -package eu.jonahbauer.wizard.core.machine.states.trick; +package eu.jonahbauer.wizard.core.machine.states.round; -import eu.jonahbauer.wizard.core.machine.Game; -import eu.jonahbauer.wizard.core.machine.states.GameData; -import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound; import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PredictMessage; +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.states.InvalidDataException; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.CHANGE_PREDICTION; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*; -public final class ChangingPrediction extends TrickState { +public final class ChangingPrediction extends RoundState { private transient final int oldPrediction; public ChangingPrediction(GameData data) { - super(data); - oldPrediction = get(PREDICTIONS).get(get(CURRENT_PLAYER)); + super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE, CLOUDED_PLAYER)); + checkData(data); + oldPrediction = get(PREDICTIONS).get(get(CLOUDED_PLAYER)); } @Override public void onEnter(Game game) { - game.notify(new UserInputMessage(get(CURRENT_PLAYER), CHANGE_PREDICTION, getTimeout(game, true))); + game.notify(new UserInputMessage(get(CLOUDED_PLAYER), CHANGE_PREDICTION, getTimeout(game, true))); timeout(game); } @Override public void onMessage(Game game, UUID player, PlayerMessage message) { - if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { + if (get(CLOUDED_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { checkPrediction(predictMessage.getPrediction()); transition(game, predictMessage.getPrediction()); } else { @@ -53,21 +54,22 @@ public final class ChangingPrediction extends TrickState { } private void transition(Game game, int prediction) { - game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction)); + game.notify(new PredictionMessage(get(CLOUDED_PLAYER), prediction)); // add prediction var predictions = new HashMap<>(get(PREDICTIONS)); - predictions.put(get(CURRENT_PLAYER), prediction); + predictions.put(get(CLOUDED_PLAYER), prediction); GameData data = getData().with( PREDICTIONS, Map.copyOf(predictions) ); - boolean hasNextTrick = get(TRICK) < get(ROUND); - if (hasNextTrick) { - transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1))); - } else { - transition(game, new FinishingRound(data)); + transition(game, new FinishingRound(data)); + } + + private void checkData(GameData data) { + if (data.get(CLOUDED_PLAYER) == null) { + throw new InvalidDataException("Clouded player is null."); } } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrump.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrump.java index 7535bf6..05596df 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrump.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrump.java @@ -80,7 +80,6 @@ public final class DeterminingTrump extends RoundState { private void transition(Game game, @NotNull Card.Suit trumpSuit) { GameData data = getData().with( TRUMP_SUIT, trumpSuit, - PREDICTIONS, Map.of(), CURRENT_PLAYER, getNextPlayer(getDealer()) ); diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Predicting.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Predicting.java index 6221e2e..05e3d8c 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Predicting.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Predicting.java @@ -80,10 +80,6 @@ public final class Predicting extends RoundState { ); if (isLastPlayer()) { - data = data.with( - TRICK, 0, - TRICKS, Map.of() - ); transition(game, new StartingTrick(data)); } else { transition(game, new Predicting(data)); diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrick.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrick.java index cac3d8e..37b6061 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrick.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrick.java @@ -4,9 +4,8 @@ import eu.jonahbauer.wizard.common.model.card.Card; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.InvalidDataException; +import eu.jonahbauer.wizard.core.machine.states.round.ChangingPrediction; import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound; -import eu.jonahbauer.wizard.common.messages.observer.HandMessage; -import eu.jonahbauer.wizard.common.messages.observer.JugglingMessage; import eu.jonahbauer.wizard.common.messages.observer.TrickMessage; import eu.jonahbauer.wizard.core.model.card.*; import eu.jonahbauer.wizard.core.util.Pair; @@ -43,32 +42,30 @@ public final class FinishingTrick extends TrickState { || cards.contains(Card.CLOUD_YELLOW); boolean hasNextTrick = get(TRICK) < get(ROUND); - // juggle hands - if (juggler && hasNextTrick) { - game.notify(new JugglingMessage()); - var hands = get(HANDS); - Map> juggledHands = new HashMap<>(); - hands.forEach((player, hand) -> juggledHands.put(getNextPlayer(player), hand)); - data = data.with(HANDS, Map.copyOf(juggledHands)); - juggledHands.forEach((player, hand) -> game.notify(player, new HandMessage(player, hand))); - } - - // trick is not counted when a bomb is present if (!bomb) { + // trick is not counted when a bomb is present var tricks = new HashMap<>(get(TRICKS)); tricks.merge(winner, 1, Integer::sum); data = data.with(TRICKS, Map.copyOf(tricks)); + + // mark "clouded player" + if (cloud) { + data = data.with(CLOUDED_PLAYER, winner); + } } data = data.with(CURRENT_PLAYER, winner); - if (cloud && !bomb) { - // adjust prediction - transition(game, new ChangingPrediction(data)); - } else if (hasNextTrick) { - transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1))); + if (!hasNextTrick) { + if (data.has(CLOUDED_PLAYER)) { + transition(game, new ChangingPrediction(data)); + } else { + transition(game, new FinishingRound(data)); + } + } else if (juggler) { + transition(game, new Juggling(data)); } else { - transition(game, new FinishingRound(data)); + transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1))); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java new file mode 100644 index 0000000..f0f58f6 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java @@ -0,0 +1,82 @@ +package eu.jonahbauer.wizard.core.machine.states.trick; + +import eu.jonahbauer.wizard.common.messages.observer.HandMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.messages.player.JuggleMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.states.GameData; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*; +import static eu.jonahbauer.wizard.core.machine.states.GameData.*; + +public final class Juggling extends TrickState { + private final transient Map juggledCards = new ConcurrentHashMap<>(); + + public Juggling(GameData data) { + super(data); + } + + @Override + public void onEnter(Game game) { + game.notify(new UserInputMessage(null, JUGGLE_CARD, getTimeout(game, true))); + timeout(game); + } + + @Override + public void onTimeout(Game game) { + for (UUID player : get(PLAYERS)) { + juggledCards.computeIfAbsent(player, p -> { + var hand = get(HANDS).get(p); + return hand.get(game.getRandom().nextInt(hand.size())); + }); + } + juggle(game); + } + + @Override + public void onMessage(Game game, UUID player, PlayerMessage message) { + if (get(PLAYERS).contains(player) && message instanceof JuggleMessage juggleMessage) { + Card card = juggleMessage.getCard(); + + if (!get(HANDS).get(player).contains(card)) { + throw new IllegalArgumentException("You do not have this card on your hand."); + } + + juggledCards.put(player, card); + if (juggledCards.size() == get(PLAYERS).size()) { + juggle(game); + } + } else { + super.onMessage(game, player, message); + } + } + + private void juggle(Game game) { + Map newCards = new HashMap<>(); + juggledCards.forEach((player, card) -> newCards.put(getNextPlayer(player), card)); + + var mutableHands = new HashMap<>(get(HANDS)); + + for (UUID player : get(PLAYERS)) { + var mutableHand = new ArrayList<>(mutableHands.get(player)); + var oldCard = juggledCards.get(player); + var newCard = newCards.get(player); + mutableHand.set(mutableHand.indexOf(oldCard), newCard); + var immutableHand = List.copyOf(mutableHand); + mutableHands.put(player, immutableHand); + game.notify(player, new HandMessage(player, immutableHand)); + } + + var data = getData().with( + HANDS, Map.copyOf(mutableHands), + TRICK, get(TRICK) + 1 + ); + + transition(game, new StartingTrick(data)); + } +} 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 50923b2..a614da8 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,11 +1,7 @@ package eu.jonahbauer.wizard.core.machine.states.trick; -import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.Game; - -import java.util.List; - -import static eu.jonahbauer.wizard.core.machine.states.GameData.*; +import eu.jonahbauer.wizard.core.machine.states.GameData; public final class StartingTrick extends TrickState { public StartingTrick(GameData data) { @@ -14,6 +10,6 @@ public final class StartingTrick extends TrickState { @Override public void onEnter(Game game) { - transition(game, new PlayingCard(getData().with(STACK, List.of()))); + transition(game, new PlayingCard(getData())); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickState.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickState.java index 61cfae3..b5a4241 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickState.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickState.java @@ -11,6 +11,7 @@ public abstract class TrickState extends RoundState { data.requireEach(PLAYERS, HANDS) .requireEach(PLAYERS, PREDICTIONS) .require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER) + .keep(CLOUDED_PLAYER) ); } } 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 8183a93..bc20cb8 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 @@ -2,10 +2,7 @@ package eu.jonahbauer.wizard.core.machine; import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; -import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage; -import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage; -import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; -import eu.jonahbauer.wizard.common.messages.player.PredictMessage; +import eu.jonahbauer.wizard.common.messages.player.*; import eu.jonahbauer.wizard.common.model.card.Card; import eu.jonahbauer.wizard.core.messages.Observer; import lombok.Getter; @@ -31,8 +28,14 @@ public class MessageQueue implements Observer { @Setter private Game game; + private BulkQueuedMessage bulk; + public MessageQueue add(UUID player, UserInputMessage.Action action, PlayerMessage message) { - messages.add(new QueuedMessage(player, action, message)); + if (bulk != null) { + bulk.getMessages().add(new SingleQueuedMessage(player, action, message)); + } else { + messages.add(new SingleQueuedMessage(player, action, message)); + } return this; } @@ -40,6 +43,10 @@ public class MessageQueue implements Observer { return add(player, UserInputMessage.Action.PLAY_CARD, new PlayCardMessage(card)); } + public MessageQueue addJuggle(UUID player, Card card) { + return add(player, UserInputMessage.Action.JUGGLE_CARD, new JuggleMessage(card)); + } + public MessageQueue addPrediction(UUID player, int prediction) { return add(player, UserInputMessage.Action.MAKE_PREDICTION, new PredictMessage(prediction)); } @@ -76,7 +83,27 @@ public class MessageQueue implements Observer { } public MessageQueue assertThrows(Class exception) { - messages.getLast().setException(exception); + var message = messages.getLast(); + if (message instanceof SingleQueuedMessage singleMessage) { + singleMessage.setException(exception); + } else if (message instanceof BulkQueuedMessage bulkMessage) { + bulkMessage.getMessages().get(bulkMessage.getMessages().size() - 1).setException(exception); + } + return this; + } + + public MessageQueue begin() { + if (bulk != null) { + end(); + } + bulk = new BulkQueuedMessage(); + messages.add(bulk); + return this; + } + + public MessageQueue end() { + if (bulk == null) throw new IllegalStateException(); + bulk = null; return this; } @@ -85,18 +112,27 @@ public class MessageQueue implements Observer { System.out.println(om); if (om instanceof UserInputMessage message) { UUID player = message.getPlayer(); - while (true) { - assertFalse(messages.isEmpty(), "User input is required but none is provided."); + assertFalse(messages.isEmpty(), "User input is required but none is provided."); - var queued = messages.poll(); + var queued = messages.poll(); + + List list; + if (queued instanceof SingleQueuedMessage s) { + list = List.of(s); + } else if (queued instanceof BulkQueuedMessage b) { + list = b.getMessages(); + } else { + throw new AssertionError(); + } - var queuedPlayer = queued.getPlayer(); - var queuedAction = queued.getAction(); - var queuedMessage = queued.getMessage(); - var exception = queued.getException(); + for (SingleQueuedMessage queuedSingle : list) { + var queuedPlayer = queuedSingle.getPlayer(); + var queuedAction = queuedSingle.getAction(); + var queuedMessage = queuedSingle.getMessage(); + var exception = queuedSingle.getException(); if (exception == null) { - assertEquals(queuedPlayer, player); + if (player != null) assertEquals(queuedPlayer, player); assertEquals(queuedAction, message.getAction()); } @@ -114,7 +150,6 @@ public class MessageQueue implements Observer { ); } else { Assertions.assertDoesNotThrow(() -> game.onMessage(queuedPlayer, queuedMessage).get()); - return; } } } @@ -133,13 +168,20 @@ public class MessageQueue implements Observer { } + private interface QueuedMessage {} + @Getter @Setter @RequiredArgsConstructor - private static class QueuedMessage { + private static class SingleQueuedMessage implements QueuedMessage { private final UUID player; private final UserInputMessage.Action action; private final PlayerMessage message; private Class exception; } + + @Getter + private static class BulkQueuedMessage implements QueuedMessage { + private final List messages = new ArrayList<>(); + } } diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/PredictingTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/PredictingTest.java index e983af3..c876286 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/PredictingTest.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/PredictingTest.java @@ -103,16 +103,21 @@ public class PredictingTest { public void predicting_WithWrongInput() { // play cards in given order MessageQueue queue = new MessageQueue() - .addPrediction(players[0], -1).assertThrows(IllegalArgumentException.class) - .addPrediction(players[0], 6).assertThrows(IllegalArgumentException.class) - .addPrediction(players[0], 4) - .addPrediction(players[2], 3).assertThrows(IllegalStateException.class) - .addPrediction(players[1], 3) - .addPrediction(players[2], 5).assertThrows(IllegalArgumentException.class) - .addPrediction(players[2], 3) - .addCard(players[3], Card.GREEN_WIZARD).assertThrows(IllegalStateException.class) - .addPickTrump(players[3], Card.Suit.GREEN).assertThrows(IllegalStateException.class) - .addPrediction(players[3], 0); + .begin() + .addPrediction(players[0], -1).assertThrows(IllegalArgumentException.class) + .addPrediction(players[0], 6).assertThrows(IllegalArgumentException.class) + .addPrediction(players[0], 4) + .begin() + .addPrediction(players[2], 3).assertThrows(IllegalStateException.class) + .addPrediction(players[1], 3) + .begin() + .addPrediction(players[2], 5).assertThrows(IllegalArgumentException.class) + .addPrediction(players[2], 3) + .begin() + .addCard(players[3], Card.GREEN_WIZARD).assertThrows(IllegalStateException.class) + .addPickTrump(players[3], Card.Suit.GREEN).assertThrows(IllegalStateException.class) + .addPrediction(players[3], 0) + .end(); Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue); @@ -143,8 +148,10 @@ public class PredictingTest { .addPrediction(players[0], 1) .addPrediction(players[1], 1) .addPrediction(players[2], 1) - .addPrediction(players[3], 1).assertThrows(IllegalArgumentException.class) - .addPrediction(players[3], 0); + .begin() + .addPrediction(players[3], 1).assertThrows(IllegalArgumentException.class) + .addPrediction(players[3], 0) + .end(); Game game = performTest(Configurations.ANNIVERSARY_2021_PM1, 3, queue); 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 93fe26d..de24d38 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 @@ -142,45 +142,56 @@ public class RoundTest { .addPrediction(players[3], 2) .addPrediction(players[0], 2) .addPrediction(players[1], 2) - .addPrediction(players[2], 1).assertThrows(IllegalArgumentException.class) - .addPrediction(players[2], 3) + .begin() + .addPrediction(players[2], 1).assertThrows(IllegalArgumentException.class) + .addPrediction(players[2], 3) + .end() // trick 0 - .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class) - .addCard(players[3], Card.BLUE_2) - .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) - .addCard(players[0], Card.YELLOW_8) + .begin() + .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class) + .addCard(players[3], Card.BLUE_2) + .begin() + .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) + .addCard(players[0], Card.YELLOW_8) + .end() .addCard(players[1], Card.BLUE_9) .addCard(players[2], Card.GREEN_WIZARD) // trick 1 - .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) - .addCard(players[2], Card.YELLOW_4) + .begin() + .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) + .addCard(players[2], Card.YELLOW_4) + .end() .addCard(players[3], Card.YELLOW_3) .addCard(players[0], Card.YELLOW_WIZARD) .addCard(players[1], Card.BOMB) // trick 2 .addCard(players[0], Card.RED_3) - .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) - .addCard(players[1], Card.RED_12) + .begin() + .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) + .addCard(players[1], Card.RED_12) + .end() .addCard(players[2], Card.RED_2) .addCard(players[3], Card.DRAGON) // trick 3 .addCard(players[3], Card.BLUE_13) - .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class) - .addCard(players[0], Card.CLOUD_YELLOW) - .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) - .addCard(players[1], Card.YELLOW_13) + .begin() + .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class) + .addCard(players[0], Card.CLOUD_YELLOW) + .begin() + .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class) + .addCard(players[1], Card.YELLOW_13) + .end() .addCard(players[2], Card.BLUE_1) - .addChangePrediction(players[1], 0).assertThrows(IllegalArgumentException.class) - .addChangePrediction(players[1], 2).assertThrows(IllegalArgumentException.class) - .addChangePrediction(players[1], 4).assertThrows(IllegalArgumentException.class) - .addChangePrediction(players[1], 1) // trick 4 .addCard(players[1], Card.RED_7) .addCard(players[2], Card.YELLOW_11) - .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class) - .addCard(players[3], Card.RED_5) - .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class) - .addCard(players[0], Card.CHANGELING_WIZARD) + .begin() + .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class) + .addCard(players[3], Card.RED_5) + .begin() + .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class) + .addCard(players[0], Card.CHANGELING_WIZARD) + .end() // trick 5 .addCard(players[0], Card.GREEN_7) .addCard(players[1], Card.FAIRY) @@ -190,7 +201,14 @@ public class RoundTest { .addCard(players[2], Card.BLUE_4) .addCard(players[3], Card.BLUE_11) .addCard(players[0], Card.GREEN_1) - .addCard(players[1], Card.GREEN_11); + .addCard(players[1], Card.GREEN_11) + // cloud + .begin() + .addChangePrediction(players[1], 0).assertThrows(IllegalArgumentException.class) + .addChangePrediction(players[1], 2).assertThrows(IllegalArgumentException.class) + .addChangePrediction(players[1], 4).assertThrows(IllegalArgumentException.class) + .addChangePrediction(players[1], 1) + .end(); int round = 6; Game game = performTest(227L, Configurations.ANNIVERSARY_2021_PM1, round, queue); @@ -219,12 +237,10 @@ public class RoundTest { } order.verify(game).notify(any(StateMessage.class)); // finishing_trick order.verify(game).notify(any(TrickMessage.class)); // trick - if (i == 3) { - order.verify(game).notify(any(StateMessage.class)); // change prediction - order.verify(game).notify(any(UserInputMessage.class)); - order.verify(game).notify(any(PredictionMessage.class)); - } } + order.verify(game).notify(any(StateMessage.class)); // change prediction + order.verify(game).notify(any(UserInputMessage.class)); + order.verify(game).notify(any(PredictionMessage.class)); order.verify(game).notify(any(StateMessage.class)); // finishing_round order.verify(game).notify(argThat(message -> message instanceof ScoreMessage score 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 6934597..b737c2c 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 @@ -102,12 +102,16 @@ public class TrickTest { // play cards in given order MessageQueue queue = new MessageQueue() .addCard(players[0], Card.RED_1) - .addCard(players[2], Card.GREEN_1).assertThrows(IllegalStateException.class) - .addCard(players[1], Card.GREEN_1).assertThrows(IllegalArgumentException.class) - .addCard(players[1], Card.YELLOW_1) + .begin() + .addCard(players[2], Card.GREEN_1).assertThrows(IllegalStateException.class) + .addCard(players[1], Card.GREEN_1).assertThrows(IllegalArgumentException.class) + .addCard(players[1], Card.YELLOW_1) + .end() .addCard(players[2], Card.GREEN_1) - .addPrediction(players[3], 1).assertThrows(IllegalStateException.class) - .addCard(players[3], Card.BLUE_1); + .begin() + .addPrediction(players[3], 1).assertThrows(IllegalStateException.class) + .addCard(players[3], Card.BLUE_1) + .end(); Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue); @@ -178,7 +182,7 @@ public class TrickTest { players[0], List.of(Card.RED_1, Card.GREEN_12), players[1], List.of(Card.JUGGLER, Card.YELLOW_3), players[2], List.of(Card.GREEN_1, Card.BLUE_4), - players[3], List.of(Card.BLUE_1, Card.RED_5) + players[3], List.of(Card.RED_5, Card.BLUE_1) ); // play cards in given order @@ -186,7 +190,13 @@ public class TrickTest { .addCard(players[0], Card.RED_1) .addCard(players[1], Card.JUGGLER_RED) .addCard(players[2], Card.GREEN_1) - .addCard(players[3], Card.RED_5); + .addCard(players[3], Card.RED_5) + .begin() + .addJuggle(players[0], Card.GREEN_12) + .addJuggle(players[1], Card.YELLOW_3) + .addJuggle(players[2], Card.BLUE_4) + .addJuggle(players[3], Card.BLUE_1) + .end(); Game game = performTest(Configurations.ANNIVERSARY_2021, 1, 0, hands, Card.Suit.YELLOW, queue); @@ -207,7 +217,8 @@ public class TrickTest { order.verify(game).notify(any(CardMessage.class)); // user response order.verify(game).notify(any(StateMessage.class)); // finishing trick order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[1])); // trick with correct winner - order.verify(game).notify(any(JugglingMessage.class)); + order.verify(game).notify(any(StateMessage.class)); // juggling + order.verify(game).notify(any(UserInputMessage.class)); // user input request order.verify(game, times(4)).notify(any(), any(HandMessage.class)); order.verify(game).transition(any(), any(StartingTrick.class)); // there is another trick order.verify(game).notify(any(StateMessage.class)); // finish