From b98d25599b4a703d86f036a0438c1d10129d04a8 Mon Sep 17 00:00:00 2001 From: Jonah Bauer Date: Thu, 4 Nov 2021 20:18:26 +0100 Subject: [PATCH] =?UTF-8?q?-=20Jubil=C3=A4umsedition=20implementiert=20(#1?= =?UTF-8?q?2)=20-=20Tests=20verbessert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 1 + wizard-client/pom.xml | 4 +- wizard-common/pom.xml | 19 ++ .../RuntimeTypeAdapterFactory.java | 0 .../messages/observer/CardMessage.java | 6 +- .../messages/observer/HandMessage.java | 6 +- .../messages/observer/JugglingMessage.java | 7 + .../messages/observer/ObserverMessage.java | 8 +- .../messages/observer/PredictionMessage.java | 4 +- .../messages/observer/ScoreMessage.java | 4 +- .../messages/observer/StateMessage.java | 18 ++ .../messages/observer/TrickMessage.java | 6 +- .../messages/observer/TrumpMessage.java | 7 +- .../messages/observer/UserInputMessage.java | 15 +- .../messages/player/PickTrumpMessage.java | 6 +- .../messages/player/PlayCardMessage.java | 6 +- .../messages/player/PlayerMessage.java | 6 +- .../messages/player/PredictMessage.java | 4 +- .../wizard/common/model/card/Card.java | 31 ++ .../util/SealedClassTypeAdapterFactory.java | 2 +- wizard-core/pom.xml | 7 + .../java/eu/jonahbauer/wizard/core/CLI.java | 28 +- .../wizard/core/machine/Context.java | 184 ++++++----- .../jonahbauer/wizard/core/machine/Game.java | 56 ++-- .../wizard/core/machine/states/GameData.java | 50 ++- .../wizard/core/machine/states/GameState.java | 2 +- .../core/machine/states/game/Finishing.java | 2 +- .../core/machine/states/round/Dealing.java | 17 +- .../states/round/DeterminingTrump.java | 77 +++-- .../machine/states/round/FinishingRound.java | 16 +- .../core/machine/states/round/Predicting.java | 18 +- .../core/machine/states/round/RoundState.java | 2 +- .../states/trick/ChangingPrediction.java | 73 +++++ .../machine/states/trick/FinishingTrick.java | 80 ++--- .../machine/states/trick/PlayingCard.java | 59 ++-- .../core/machine/states/trick/TrickState.java | 13 - .../wizard/core/messages/Observer.java | 2 +- .../core/messages/observer/StateMessage.java | 26 -- .../eu/jonahbauer/wizard/core/model/Card.java | 98 ------ .../jonahbauer/wizard/core/model/Cards.java | 26 -- .../wizard/core/model/Configuration.java | 1 + .../wizard/core/model/Configurations.java | 39 ++- .../wizard/core/model/card/CardUtils.java | 75 +++++ .../core/model/card/ChangelingJesterCard.java | 24 ++ .../core/model/card/ChangelingWizardCard.java | 25 ++ .../wizard/core/model/card/ColoredCard.java | 56 ++++ .../core/model/card/ColoredSubcard.java | 30 ++ .../wizard/core/model/card/GameCard.java | 44 +++ .../wizard/core/model/card/GameCards.java | 123 +++++++ .../wizard/core/model/card/JesterCard.java | 9 + .../core/model/card/NonPlayableCard.java | 38 +++ .../wizard/core/model/card/SimpleCard.java | 16 + .../wizard/core/model/card/Subcard.java | 14 + .../wizard/core/model/card/WizardCard.java | 9 + .../wizard/core/model/{ => deck}/Deck.java | 7 +- .../wizard/core/model/deck/Decks.java | 65 ++++ .../eu/jonahbauer/wizard/core/util/Util.java | 12 + .../wizard/core/machine/GameTest.java | 49 ++- .../wizard/core/machine/GameTestUtils.java | 30 ++ .../wizard/core/machine/MessageQueue.java | 145 +++++++++ .../core/machine/states/GameStateTest.java | 39 +++ .../states/round/DeterminingTrumpTest.java | 163 ++++++++++ .../states/round/FinishingRoundTest.java | 51 --- .../machine/states/round/PredictingTest.java | 179 ++++++++--- .../core/machine/states/round/RoundTest.java | 241 ++++++++++++++ .../states/trick/FinishingTrickTest.java | 78 ----- .../machine/states/trick/PlayingCardTest.java | 86 ----- .../machine/states/trick/TrickStateTest.java | 90 ------ .../core/machine/states/trick/TrickTest.java | 299 ++++++++++++++++++ .../wizard/core/model/card/CardUtilsTest.java | 219 +++++++++++++ wizard-server/pom.xml | 2 +- 71 files changed, 2457 insertions(+), 797 deletions(-) create mode 100644 wizard-common/pom.xml rename {wizard-core => wizard-common}/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java (100%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/CardMessage.java (72%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/HandMessage.java (81%) create mode 100644 wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/JugglingMessage.java rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/ObserverMessage.java (58%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/PredictionMessage.java (82%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/ScoreMessage.java (83%) create mode 100644 wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/StateMessage.java rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/TrickMessage.java (76%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/TrumpMessage.java (78%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/observer/UserInputMessage.java (70%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/player/PickTrumpMessage.java (52%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/player/PlayCardMessage.java (51%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/player/PlayerMessage.java (71%) rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/messages/player/PredictMessage.java (60%) create mode 100644 wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/card/Card.java rename {wizard-core/src/main/java/eu/jonahbauer/wizard/core => wizard-common/src/main/java/eu/jonahbauer/wizard/common}/util/SealedClassTypeAdapterFactory.java (97%) create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/ChangingPrediction.java delete mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/StateMessage.java delete mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Card.java delete mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Cards.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/CardUtils.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingJesterCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingWizardCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredSubcard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCards.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/JesterCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/NonPlayableCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/SimpleCard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/Subcard.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/WizardCard.java rename wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/{ => deck}/Deck.java (82%) create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Decks.java create mode 100644 wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/Util.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTestUtils.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/GameStateTest.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrumpTest.java delete mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRoundTest.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java delete mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrickTest.java delete mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCardTest.java delete mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickStateTest.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java create mode 100644 wizard-core/src/test/java/eu/jonahbauer/wizard/core/model/card/CardUtilsTest.java diff --git a/pom.xml b/pom.xml index a2026d7..b18690d 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ wizard-core wizard-client wizard-server + wizard-common diff --git a/wizard-client/pom.xml b/wizard-client/pom.xml index 31c05c8..e9e8659 100644 --- a/wizard-client/pom.xml +++ b/wizard-client/pom.xml @@ -19,8 +19,8 @@ - eu.jonahbauer - wizard-core + ${project.groupId} + wizard-common ${project.version} diff --git a/wizard-common/pom.xml b/wizard-common/pom.xml new file mode 100644 index 0000000..11dc79c --- /dev/null +++ b/wizard-common/pom.xml @@ -0,0 +1,19 @@ + + + + wizard + eu.jonahbauer + 1.0-SNAPSHOT + + 4.0.0 + + wizard-common + + + 17 + 17 + + + \ No newline at end of file diff --git a/wizard-core/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/wizard-common/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java similarity index 100% rename from wizard-core/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java rename to wizard-common/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/CardMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/CardMessage.java similarity index 72% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/CardMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/CardMessage.java index 1dba761..3dd271d 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/CardMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/CardMessage.java @@ -1,6 +1,7 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; @@ -12,6 +13,7 @@ import java.util.UUID; */ @Getter @RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) public final class CardMessage extends ObserverMessage { /** * The UUID of the player. diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/HandMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/HandMessage.java similarity index 81% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/HandMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/HandMessage.java index 50f186f..396626c 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/HandMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/HandMessage.java @@ -1,6 +1,7 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.jetbrains.annotations.NotNull; @@ -11,6 +12,7 @@ import java.util.UUID; * A {@link HandMessage} is sent when the player receives information about hit own or another player's hand cards. */ @Getter +@EqualsAndHashCode(callSuper = true) public final class HandMessage extends ObserverMessage { /** * The UUID of player whose hand cards are sent. 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 new file mode 100644 index 0000000..20823fd --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/JugglingMessage.java @@ -0,0 +1,7 @@ +package eu.jonahbauer.wizard.common.messages.observer; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class JugglingMessage extends ObserverMessage { +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/ObserverMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java similarity index 58% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/ObserverMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java index a55efcc..76b282a 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/ObserverMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java @@ -1,10 +1,12 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import eu.jonahbauer.wizard.core.util.SealedClassTypeAdapterFactory; +import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; +import lombok.EqualsAndHashCode; -public abstract sealed class ObserverMessage permits CardMessage, HandMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage { +@EqualsAndHashCode +public abstract sealed class ObserverMessage permits CardMessage, HandMessage, JugglingMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage { public static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message")) .create(); diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/PredictionMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/PredictionMessage.java similarity index 82% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/PredictionMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/PredictionMessage.java index 9dd1d6d..6dbcb65 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/PredictionMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/PredictionMessage.java @@ -1,5 +1,6 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; @@ -12,6 +13,7 @@ import java.util.UUID; */ @Getter @RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) public final class PredictionMessage extends ObserverMessage { /** * The UUID of the player who made a prediction. diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/ScoreMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ScoreMessage.java similarity index 83% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/ScoreMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ScoreMessage.java index 5301146..a0ee775 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/ScoreMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ScoreMessage.java @@ -1,5 +1,6 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.jetbrains.annotations.NotNull; @@ -11,6 +12,7 @@ import java.util.UUID; * gained by each player in this round or the final result. */ @Getter +@EqualsAndHashCode(callSuper = true) public final class ScoreMessage extends ObserverMessage { /** * The number of points for each player. diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/StateMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/StateMessage.java new file mode 100644 index 0000000..5973472 --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/StateMessage.java @@ -0,0 +1,18 @@ +package eu.jonahbauer.wizard.common.messages.observer; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * A {@link StateMessage} is sent whenever the game changes its internal state. + */ +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) +public final class StateMessage extends ObserverMessage { + /** + * The name of the new state in snake_case. + */ + private final String state; +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/TrickMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TrickMessage.java similarity index 76% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/TrickMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TrickMessage.java index d6b66fb..f91d570 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/TrickMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TrickMessage.java @@ -1,6 +1,7 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.EqualsAndHashCode; import lombok.Getter; import java.util.List; @@ -11,6 +12,7 @@ import java.util.UUID; * cards played. */ @Getter +@EqualsAndHashCode(callSuper = true) public final class TrickMessage extends ObserverMessage { /** * The UUID of the player who won the trick. diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/TrumpMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TrumpMessage.java similarity index 78% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/TrumpMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TrumpMessage.java index 141f3a2..888647c 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/TrumpMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/TrumpMessage.java @@ -1,6 +1,7 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.jetbrains.annotations.Nullable; @@ -8,6 +9,7 @@ import org.jetbrains.annotations.Nullable; * A {@link TrumpMessage} is sent when the trump suit of the current round is (being) determined. */ @Getter +@EqualsAndHashCode(callSuper = true) public final class TrumpMessage extends ObserverMessage { /** * The {@link Card} that was revealed to determine the {@linkplain Card.Suit trump suit} or {@code null} no cards @@ -20,7 +22,6 @@ public final class TrumpMessage extends ObserverMessage { private final @Nullable Card.Suit suit; public TrumpMessage(@Nullable Card card, @Nullable Card.Suit suit) { - if (suit != null && !suit.isColor()) throw new IllegalArgumentException("The trump suit must be a color or null."); if (card == null && suit == null) throw new IllegalArgumentException("Card and suit must not both be null"); this.card = card; diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/UserInputMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java similarity index 70% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/UserInputMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java index 4b99008..04879b9 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/UserInputMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/UserInputMessage.java @@ -1,8 +1,9 @@ -package eu.jonahbauer.wizard.core.messages.observer; +package eu.jonahbauer.wizard.common.messages.observer; -import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; -import eu.jonahbauer.wizard.core.messages.player.PredictMessage; +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 lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,6 +14,7 @@ import java.util.UUID; */ @Getter @RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) public final class UserInputMessage extends ObserverMessage { /** * The UUID of the player whose input is required. @@ -33,6 +35,11 @@ public final class UserInputMessage extends ObserverMessage { * {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}. */ MAKE_PREDICTION, + /** + * An action that indicates that a player should change his a prediction by ±1. A {@link UserInputMessage} with + * this {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}. + */ + CHANGE_PREDICTION, /** * An action that indicates that a player should play a card. A {@link UserInputMessage} with this * {@link UserInputMessage#getAction()} should be responded to with a {@link PlayCardMessage}. diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PickTrumpMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PickTrumpMessage.java similarity index 52% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PickTrumpMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PickTrumpMessage.java index 6de50f6..a10c544 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PickTrumpMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PickTrumpMessage.java @@ -1,11 +1,13 @@ -package eu.jonahbauer.wizard.core.messages.player; +package eu.jonahbauer.wizard.common.messages.player; -import eu.jonahbauer.wizard.core.model.Card; +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 PickTrumpMessage extends PlayerMessage { private final Card.Suit trumpSuit; } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PlayCardMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayCardMessage.java similarity index 51% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PlayCardMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayCardMessage.java index 8bfeabb..c0f3afb 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PlayCardMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayCardMessage.java @@ -1,11 +1,13 @@ -package eu.jonahbauer.wizard.core.messages.player; +package eu.jonahbauer.wizard.common.messages.player; -import eu.jonahbauer.wizard.core.model.Card; +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 PlayCardMessage extends PlayerMessage { private final Card card; } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PlayerMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java similarity index 71% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PlayerMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java index 677dde5..044e677 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PlayerMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java @@ -1,9 +1,11 @@ -package eu.jonahbauer.wizard.core.messages.player; +package eu.jonahbauer.wizard.common.messages.player; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import eu.jonahbauer.wizard.core.util.SealedClassTypeAdapterFactory; +import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode public abstract sealed class PlayerMessage permits PickTrumpMessage, PlayCardMessage, PredictMessage { public static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message")) diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PredictMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PredictMessage.java similarity index 60% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PredictMessage.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PredictMessage.java index f930c20..7fbe509 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/player/PredictMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PredictMessage.java @@ -1,10 +1,12 @@ -package eu.jonahbauer.wizard.core.messages.player; +package eu.jonahbauer.wizard.common.messages.player; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) public final class PredictMessage extends PlayerMessage { private final int prediction; } diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/card/Card.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/card/Card.java new file mode 100644 index 0000000..f785866 --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/model/card/Card.java @@ -0,0 +1,31 @@ +package eu.jonahbauer.wizard.common.model.card; + +public enum Card { + HIDDEN, + BLUE_1, RED_1, GREEN_1, YELLOW_1, + BLUE_2, RED_2, GREEN_2, YELLOW_2, + BLUE_3, RED_3, GREEN_3, YELLOW_3, + BLUE_4, RED_4, GREEN_4, YELLOW_4, + BLUE_5, RED_5, GREEN_5, YELLOW_5, + BLUE_6, RED_6, GREEN_6, YELLOW_6, + BLUE_7, RED_7, GREEN_7, YELLOW_7, + BLUE_8, RED_8, GREEN_8, YELLOW_8, + BLUE_9, RED_9, GREEN_9, YELLOW_9, + BLUE_10, RED_10, GREEN_10, YELLOW_10, + BLUE_11, RED_11, GREEN_11, YELLOW_11, + BLUE_12, RED_12, GREEN_12, YELLOW_12, + BLUE_13, RED_13, GREEN_13, YELLOW_13, + BLUE_WIZARD, RED_WIZARD, GREEN_WIZARD, YELLOW_WIZARD, + BLUE_JESTER, RED_JESTER, GREEN_JESTER, YELLOW_JESTER, + CHANGELING, CHANGELING_WIZARD, CHANGELING_JESTER, + BOMB, + WEREWOLF, + DRAGON, + FAIRY, + CLOUD, CLOUD_BLUE, CLOUD_RED, CLOUD_GREEN, CLOUD_YELLOW, + JUGGLER, JUGGLER_BLUE, JUGGLER_RED, JUGGLER_GREEN, JUGGLER_YELLOW; + + public enum Suit { + NONE, YELLOW, RED, GREEN, BLUE + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/SealedClassTypeAdapterFactory.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java similarity index 97% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/SealedClassTypeAdapterFactory.java rename to wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java index 40a08ab..f483de7 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/SealedClassTypeAdapterFactory.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java @@ -1,4 +1,4 @@ -package eu.jonahbauer.wizard.core.util; +package eu.jonahbauer.wizard.common.util; import com.google.gson.Gson; import com.google.gson.TypeAdapter; diff --git a/wizard-core/pom.xml b/wizard-core/pom.xml index 76b9c2b..48302c1 100644 --- a/wizard-core/pom.xml +++ b/wizard-core/pom.xml @@ -16,4 +16,11 @@ ${java.version} + + + ${project.groupId} + wizard-common + ${project.version} + + \ No newline at end of file diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/CLI.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/CLI.java index c792460..f1d3c4d 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/CLI.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/CLI.java @@ -1,12 +1,11 @@ package eu.jonahbauer.wizard.core; +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 eu.jonahbauer.wizard.core.machine.Game; -import eu.jonahbauer.wizard.core.machine.states.GameState; import eu.jonahbauer.wizard.core.messages.Observer; -import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; -import eu.jonahbauer.wizard.core.messages.player.PredictMessage; -import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.Configuration; import eu.jonahbauer.wizard.core.model.Configurations; @@ -30,15 +29,13 @@ public class CLI { game.start(players); - GameState state = null; - Scanner scanner = new Scanner(System.in); Pattern pattern = Pattern.compile("(\\d) ([a-z]+) (.*)"); while (scanner.hasNextLine()) { try { Matcher matcher = pattern.matcher(scanner.nextLine()); if (!matcher.find()) { - System.err.println("Format is \"(\\\\d) ([a-z]+) (.*)\""); + System.err.println("Format is \"(\\d) ([a-z]+) (.*)\""); continue; } String player = matcher.group(1); @@ -55,15 +52,24 @@ public class CLI { switch (command) { case "predict" -> { int prediction = Integer.parseInt(param); - game.onMessage(players.get(id), new PredictMessage(prediction)); + game.onMessage(players.get(id), new PredictMessage(prediction)) + .whenComplete((v, err) -> { + if (err != null) err.printStackTrace(); + }); } case "play" -> { Card card = Card.valueOf(param); - game.onMessage(players.get(id), new PlayCardMessage(card)); + game.onMessage(players.get(id), new PlayCardMessage(card)) + .whenComplete((v, err) -> { + if (err != null) err.printStackTrace(); + }); } case "trump" -> { Card.Suit suit = Card.Suit.valueOf(param); - game.onMessage(players.get(id), new PickTrumpMessage(suit)); + game.onMessage(players.get(id), new PickTrumpMessage(suit)) + .whenComplete((v, err) -> { + if (err != null) err.printStackTrace(); + }); } default -> System.err.println("Unknown command: " + command); } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Context.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Context.java index 90617a8..0eb6e91 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Context.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Context.java @@ -1,123 +1,155 @@ package eu.jonahbauer.wizard.core.machine; import eu.jonahbauer.wizard.core.machine.states.State; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NonBlocking; import org.jetbrains.annotations.NotNull; +import java.util.Comparator; import java.util.concurrent.*; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.AtomicReference; public abstract class Context, C extends Context> { - protected final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final ThreadPoolExecutor executor = new ThreadPoolExecutor( + 1, 1, + 0, TimeUnit.SECONDS, + new PriorityBlockingQueue<>( + 11, + Comparator.comparingInt(r -> r instanceof PriorityRunnable prio ? prio.getPriority() : Integer.MAX_VALUE) + .thenComparingLong(r -> r instanceof PriorityRunnable prio ? prio.getTimestamp() : Long.MAX_VALUE) + ), + r -> { + var t = new Thread(r); + t.setUncaughtExceptionHandler((t1, e) -> finish(e)); + return t; + } + ); + protected S state; - protected final ReentrantLock lock = new ReentrantLock(); - private final Condition finishCondition = lock.newCondition(); - private boolean finished; - private Throwable exception; - protected void start(@NotNull S state) { - lock.lock(); - try { - if (finished) throw new IllegalStateException("Context has already finished."); - transition(null, state); - } finally { - lock.unlock(); - } + private final CompletableFuture future = new CompletableFuture<>(); + private final CompletableFuture finished = future.whenComplete((v, t) -> { + executor.shutdownNow(); + scheduler.shutdownNow(); + }); + + @NonBlocking + protected CompletableFuture submit(Runnable runnable) { + var future = new CompletableFuture(); + executor.execute(new PriorityRunnable(100, () -> { + try { + runnable.run(); + future.complete(null); + } catch (Throwable t) { + future.completeExceptionally(t); + } + })); + return future; } - public void transition(S currentState, S newState) { - lock.lock(); - try { - if (state == currentState) { - state = newState; - if (currentState != null) //noinspection unchecked - currentState.onExit((C) this); - onTransition(currentState, newState); - if (newState != null) //noinspection unchecked - newState.onEnter((C) this); + @Blocking + protected void start(@NotNull S state) { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference exception = new AtomicReference<>(); + + executor.execute(new PriorityRunnable(0, () -> { + if (future.isDone()) { + exception.set(new IllegalStateException("Context has already finished.")); + latch.countDown(); } else { - throw new IllegalStateException("Current state does not match."); + latch.countDown(); + doTransition(null, state); } - } catch (Throwable t) { - handleError(t); - } finally { - lock.unlock(); + })); + + while (true) { + try { + latch.await(); + if (exception.get() != null) { + throw exception.get(); + } + break; + } catch (InterruptedException ignored) {} } } + @NonBlocking + public void transition(S currentState, S newState) { + executor.execute(new PriorityRunnable(0, () -> doTransition(currentState, newState))); + } + + @NonBlocking public void finish() { finish(null); } + @NonBlocking public void finish(Throwable exception) { - lock.lock(); - try { - finished = true; - this.exception = exception; - finishCondition.signalAll(); - transition(state, null); - scheduler.shutdown(); - } finally { - lock.unlock(); - } + executor.execute(new PriorityRunnable(0, () -> doFinish(exception))); } + @NonBlocking public void cancel() { finish(new CancellationException()); } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isDone() { - return finished; + /* + * internal methods that are called on the executor + */ + + private void doTransition(S currentState, S newState) { + if (state == currentState) { + state = newState; + if (currentState != null) //noinspection unchecked + currentState.onExit((C) this); + onTransition(currentState, newState); + if (newState != null) //noinspection unchecked + newState.onEnter((C) this); + } + } + + private void doFinish(Throwable t) { + if (future.isDone()) return; + + doTransition(state, null); + if (t != null) { + future.completeExceptionally(t); + } else { + future.complete(null); + } } + protected void onTransition(S from, S to) {} + @Blocking public void await() throws InterruptedException, ExecutionException, CancellationException { - lock.lock(); - try { - while (!finished) { - finishCondition.await(); - } - if (exception != null) { - if (exception instanceof CancellationException cancelled) { - throw cancelled; - } else { - throw new ExecutionException(exception); - } - } - } finally { - lock.unlock(); - } + finished.get(); } public void timeout(@NotNull S currentState, long delay) { scheduler.schedule(() -> { - lock.lock(); - try { + submit(() -> { if (state == currentState) { //noinspection unchecked state.onTimeout((C) this); } - } catch (Throwable t) { - handleError(t); - } finally { - lock.unlock(); - } + }); }, delay, TimeUnit.MILLISECONDS); } - protected void handleError(Throwable t) { - lock.lock(); - try { - if (!isDone()) { - finish(t); - t.printStackTrace(); - } - } finally { - lock.unlock(); + @Getter + @RequiredArgsConstructor + private static class PriorityRunnable implements Runnable { + private final int priority; + private final long timestamp = System.nanoTime(); + private final Runnable runnable; + + @Override + public void run() { + runnable.run(); } } - - protected void onTransition(S from, S to) {} } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Game.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Game.java index 33c04c6..df635e1 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Game.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Game.java @@ -4,75 +4,73 @@ import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.states.GameState; import eu.jonahbauer.wizard.core.machine.states.game.Starting; import eu.jonahbauer.wizard.core.messages.Observer; -import eu.jonahbauer.wizard.core.messages.observer.ObserverMessage; -import eu.jonahbauer.wizard.core.messages.observer.StateMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; +import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage; +import eu.jonahbauer.wizard.common.messages.observer.StateMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.model.Configuration; +import eu.jonahbauer.wizard.core.util.Util; import lombok.Getter; import java.util.List; +import java.util.Random; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; public final class Game extends Context { + @Getter + private final Random random; @Getter private final Configuration config; private final Observer observer; public Game(Configuration config, Observer observer) { + this.random = new Random(); + this.config = config; + this.observer = observer; + } + + public Game(long seed, Configuration config, Observer observer) { + this.random = new Random(seed); this.config = config; this.observer = observer; } public void start(List players) { - start(new Starting(new GameData().with(PLAYERS, List.copyOf(players)))); + start(new Starting(GameData.EMPTY.with(PLAYERS, List.copyOf(players)))); } public void resume(GameState state) { start(state); } - public GameState stop() { - GameState state = this.state; - if (state != null) { - finish(); - return state; - } - return null; - } - - public void onMessage(UUID player, PlayerMessage message) { - lock.lock(); - try { - state.onMessage(this, player, message); - } catch (IllegalStateException | IllegalArgumentException e) { - throw e; - } catch (Throwable t) { - handleError(t); - } finally { - lock.unlock(); - } + public CompletableFuture onMessage(UUID player, PlayerMessage message) { + return submit(() -> { + if (state != null) { + state.onMessage(this, player, message); + } + }); } @Override protected void onTransition(GameState from, GameState to) { - notify(new StateMessage(to != null ? to.getClass() : null)); + notify(new StateMessage(to != null ? Util.toSnakeCase(to.getClass().getSimpleName()) : "null")); } public void notify(ObserverMessage message) { try { observer.notify(message); - } catch (Exception e) { - e.printStackTrace(); + } catch (Throwable t) { + t.printStackTrace(); } } public void notify(UUID player, ObserverMessage message) { try { observer.notify(player, message); - } catch (Exception e) { - e.printStackTrace(); + } catch (Throwable t) { + t.printStackTrace(); } } } 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 c777951..88e9af8 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 @@ -1,6 +1,6 @@ package eu.jonahbauer.wizard.core.machine.states; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.model.card.Card; import eu.jonahbauer.wizard.core.util.Pair; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -14,10 +14,12 @@ import org.jetbrains.annotations.Unmodifiable; import java.util.*; @Unmodifiable -@EqualsAndHashCode(of = "values") +@EqualsAndHashCode(of = {"values", "present"}) public final class GameData { private static final int SIZE = 11; + 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); @@ -34,7 +36,7 @@ public final class GameData { private final boolean[] present; private transient final boolean[] required = new boolean[SIZE]; - public GameData() { + private GameData() { this.values = new Object[SIZE]; this.present = new boolean[SIZE]; } @@ -81,6 +83,37 @@ public final class GameData { return new GameData(newValues, newPresent); } + /** + * @see #with(Key, Object) + */ + public GameData with(@NotNull TypedValue@NotNull... entries) { + boolean equal = true; + for (TypedValue entry : entries) { + int index = entry.key().index(); + if (!present[index] || !Objects.equals(values[index], entry.value())) { + equal = false; + break; + } + } + + if (equal) { + return this; + } + + Object[] newValues = Arrays.copyOf(this.values, SIZE); + boolean[] newPresent = Arrays.copyOf(this.present, SIZE); + for (TypedValue entry : entries) { + int index = entry.key().index(); + newValues[index] = entry.value(); + newPresent[index] = true; + } + + return new GameData(newValues, newPresent); + } + + /** + * @see #with(Key, Object) + */ public GameData with(@NotNull Key key1, T1 value1, Key key2, T2 value2) { int index1 = key1.index(); int index2 = key2.index(); @@ -101,6 +134,9 @@ public final class GameData { return new GameData(newValues, newPresent); } + /** + * @see #with(Key, Object) + */ public GameData with(@NotNull Key key1, T1 value1, Key key2, T2 value2, Key key3, T3 value3) { int index1 = key1.index(); int index2 = key2.index(); @@ -177,7 +213,7 @@ public final class GameData { var mapValue = get(map); var listValue = get(list); for (K k : listValue) { - if (!mapValue.containsKey(k)) throw new NoSuchElementException(); + if (!mapValue.containsKey(k)) throw new NoSuchElementException("Could not find required value: " + map.name() + "[" + k + "]."); } return this; } @@ -216,4 +252,10 @@ public final class GameData { return name(); } } + + public record TypedValue(Key key, T value) { + public static TypedValue entry(Key key, T value) { + return new TypedValue<>(key, value); + } + } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameState.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameState.java index 2d3c9bd..ad85a7b 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameState.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameState.java @@ -1,7 +1,7 @@ package eu.jonahbauer.wizard.core.machine.states; import eu.jonahbauer.wizard.core.machine.Game; -import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import lombok.Getter; import org.jetbrains.annotations.Unmodifiable; diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finishing.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finishing.java index f6ceb60..408dcde 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finishing.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finishing.java @@ -3,7 +3,7 @@ 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.GameState; -import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage; +import eu.jonahbauer.wizard.common.messages.observer.ScoreMessage; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Dealing.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Dealing.java index acb2c59..1fbc592 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Dealing.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/Dealing.java @@ -1,12 +1,15 @@ package eu.jonahbauer.wizard.core.machine.states.round; -import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.common.messages.observer.HandMessage; +import eu.jonahbauer.wizard.common.model.card.Card; import eu.jonahbauer.wizard.core.machine.Game; -import eu.jonahbauer.wizard.core.messages.observer.HandMessage; -import eu.jonahbauer.wizard.core.model.Card; -import eu.jonahbauer.wizard.core.model.Deck; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.model.deck.Deck; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; @@ -19,7 +22,7 @@ public final class Dealing extends RoundState { @Override public void onEnter(Game game) { Deck deck = new Deck(game.getConfig().cards()); - deck.shuffle(); + deck.shuffle(game.getRandom()); var hands = new HashMap>(); @@ -32,7 +35,7 @@ public final class Dealing extends RoundState { } for (UUID player : get(PLAYERS)) { - game.notify(player, new HandMessage(player, hands.get(player))); + game.notify(player, new HandMessage(player, hands.get(player).stream().toList())); } Card trumpCard = deck.draw(); 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 2d61e89..7535bf6 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 @@ -1,34 +1,54 @@ package eu.jonahbauer.wizard.core.machine.states.round; +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.messages.observer.TrumpMessage; -import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; -import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.messages.observer.HandMessage; +import eu.jonahbauer.wizard.common.messages.observer.TrumpMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; +import eu.jonahbauer.wizard.core.model.card.GameCards; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.VisibleForTesting; -import java.util.Arrays; -import java.util.Map; -import java.util.UUID; +import java.util.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PICK_TRUMP; +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.PICK_TRUMP; public final class DeterminingTrump extends RoundState { + private transient UUID player; + private transient boolean werewolf; + public DeterminingTrump(GameData data) { super(data.require(TRUMP_CARD).requireEach(PLAYERS, HANDS)); } @Override public void onEnter(Game game) { - Card trumpCard = getData().get(TRUMP_CARD); - Card.Suit trumpSuit = trumpCard != null ? trumpCard.getTrumpSuit() : Card.Suit.NONE; + Card trumpCard = get(TRUMP_CARD); + + // handle werewolf + for (Map.Entry> entry : get(HANDS).entrySet()) { + var player = entry.getKey(); + var hand = entry.getValue(); + if (hand.contains(Card.WEREWOLF)) { + this.player = player; + this.werewolf = true; + game.notify(new TrumpMessage(trumpCard, null)); + game.notify(new TrumpMessage(Card.WEREWOLF, null)); + game.notify(new UserInputMessage(this.player, PICK_TRUMP, getTimeout(game, true))); + timeout(game); + return; + } + } + + // default trump handling + Card.Suit trumpSuit = trumpCard != null ? GameCards.get(trumpCard).getTrumpSuit() : Card.Suit.NONE; if (trumpSuit == null) { + this.player = getDealer(); game.notify(new TrumpMessage(trumpCard, null)); - game.notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(game, true))); + game.notify(new UserInputMessage(this.player, PICK_TRUMP, getTimeout(game, true))); timeout(game); } else { transition(game, trumpSuit); @@ -38,12 +58,12 @@ public final class DeterminingTrump extends RoundState { @Override public void onTimeout(Game game) { Card.Suit[] suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW}; - transition(game, suits[(int) (Math.random() * suits.length)]); + transition(game, suits[game.getRandom().nextInt(suits.length)]); } @Override public void onMessage(Game game, UUID player, PlayerMessage message) { - if (getDealer().equals(player) && message instanceof PickTrumpMessage trumpMessage) { + if (this.player.equals(player) && message instanceof PickTrumpMessage trumpMessage) { checkTrumpSuit(trumpMessage.getTrumpSuit()); transition(game, trumpMessage.getTrumpSuit()); } else { @@ -51,21 +71,30 @@ public final class DeterminingTrump extends RoundState { } } - @VisibleForTesting - void checkTrumpSuit(Card.Suit suit) { - Card.Suit[] suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW}; - for (Card.Suit s : suits) { - if (s == suit) return; + private void checkTrumpSuit(Card.Suit suit) { + if (suit == Card.Suit.NONE) { + throw new IllegalArgumentException("Trump suit must not be " + Card.Suit.NONE + "."); } - throw new IllegalArgumentException("Trump suit must be one of " + Arrays.toString(suits) + "."); } private void transition(Game game, @NotNull Card.Suit trumpSuit) { - game.notify(new TrumpMessage(getData().get(TRUMP_CARD), trumpSuit)); - transition(game, new Predicting(getData().with( + GameData data = getData().with( TRUMP_SUIT, trumpSuit, PREDICTIONS, Map.of(), CURRENT_PLAYER, getNextPlayer(getDealer()) - ))); + ); + + if (werewolf) { + var mutableHands = new HashMap<>(get(HANDS)); + var mutableHand = new ArrayList<>(mutableHands.get(player)); + mutableHand.set(mutableHand.indexOf(Card.WEREWOLF), get(TRUMP_CARD)); + mutableHands.put(player, List.copyOf(mutableHand)); + data = data.with(HANDS, Map.copyOf(mutableHands)); + game.notify(new TrumpMessage(Card.WEREWOLF, trumpSuit)); + game.notify(player, new HandMessage(player, mutableHands.get(player))); + } else { + game.notify(new TrumpMessage(get(TRUMP_CARD), trumpSuit)); + } + transition(game, new Predicting(data)); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRound.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRound.java index 49b347e..9be3411 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRound.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRound.java @@ -2,10 +2,8 @@ package eu.jonahbauer.wizard.core.machine.states.round; 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.game.Finishing; -import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage; -import org.jetbrains.annotations.VisibleForTesting; +import eu.jonahbauer.wizard.common.messages.observer.ScoreMessage; import java.util.HashMap; import java.util.Map; @@ -17,7 +15,6 @@ public final class FinishingRound extends RoundState { public FinishingRound(GameData data) { super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE)); - checkData(data); } @Override @@ -37,8 +34,7 @@ public final class FinishingRound extends RoundState { } } - @VisibleForTesting - Map getPoints() { + private Map getPoints() { var points = new HashMap(); for (UUID player : get(PLAYERS)) { int prediction = get(PREDICTIONS).get(player); @@ -52,12 +48,4 @@ public final class FinishingRound extends RoundState { } return points; } - - private static void checkData(GameData data) { - // the number of tricks played should be equal to the number of tricks in total - int tricks = data.get(TRICKS).values().stream().mapToInt(i -> i).sum(); - if (tricks != data.get(ROUND) + 1) { - throw new InvalidDataException("Unexpected number of tricks in round " + data.get(ROUND) + ": " + tricks + "."); - } - } } 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 2142031..6221e2e 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 @@ -3,19 +3,18 @@ package eu.jonahbauer.wizard.core.machine.states.round; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick; -import eu.jonahbauer.wizard.core.messages.observer.PredictionMessage; -import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; -import eu.jonahbauer.wizard.core.messages.player.PredictMessage; +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 lombok.Getter; -import org.jetbrains.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; import java.util.UUID; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.MAKE_PREDICTION; +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.MAKE_PREDICTION; @Getter public final class Predicting extends RoundState { @@ -25,7 +24,7 @@ public final class Predicting extends RoundState { @Override public void onEnter(Game game) { - game.notify(new UserInputMessage(getData().get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true))); + game.notify(new UserInputMessage(get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true))); timeout(game); } @@ -46,7 +45,7 @@ public final class Predicting extends RoundState { @Override public void onMessage(Game game, UUID player, PlayerMessage message) { - if (getData().get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { + if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { checkPrediction(game, predictMessage.getPrediction()); transition(game, predictMessage.getPrediction()); } else { @@ -54,8 +53,7 @@ public final class Predicting extends RoundState { } } - @VisibleForTesting - void checkPrediction(Game game, int prediction) { + private void checkPrediction(Game game, int prediction) { int round = get(ROUND); if (prediction < 0 || prediction > round + 1) { throw new IllegalArgumentException("Prediction must be between 0 and " + (round + 1) + "."); diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/RoundState.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/RoundState.java index 88588a8..dca30fd 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/RoundState.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/RoundState.java @@ -13,6 +13,6 @@ public abstract class RoundState extends GameState { } protected UUID getDealer() { - return getPlayer(getData().get(ROUND) % getPlayerCount()); + return getPlayer(get(ROUND) % getPlayerCount()); } } 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/trick/ChangingPrediction.java new file mode 100644 index 0000000..afc388f --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/ChangingPrediction.java @@ -0,0 +1,73 @@ +package eu.jonahbauer.wizard.core.machine.states.trick; + +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 java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +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 { + private transient final int oldPrediction; + + public ChangingPrediction(GameData data) { + super(data); + oldPrediction = get(PREDICTIONS).get(get(CURRENT_PLAYER)); + } + + @Override + public void onEnter(Game game) { + game.notify(new UserInputMessage(get(CURRENT_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) { + checkPrediction(predictMessage.getPrediction()); + transition(game, predictMessage.getPrediction()); + } else { + super.onMessage(game, player, message); + } + } + + @Override + public void onTimeout(Game game) { + transition(game, oldPrediction + 1); + } + + private void checkPrediction(int prediction) { + if (prediction < 0) { + throw new IllegalArgumentException("Prediction must be greater than or equal to 0."); + } else if (Math.abs(prediction - oldPrediction) != 1) { + throw new IllegalArgumentException("Prediction must differ from your old prediction by exactly one."); + } + } + + private void transition(Game game, int prediction) { + game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction)); + + // add prediction + var predictions = new HashMap<>(get(PREDICTIONS)); + predictions.put(get(CURRENT_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)); + } + } +} 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 231e3c2..cac3d8e 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 @@ -1,13 +1,15 @@ package eu.jonahbauer.wizard.core.machine.states.trick; +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.FinishingRound; -import eu.jonahbauer.wizard.core.messages.observer.TrickMessage; -import eu.jonahbauer.wizard.core.model.Card; +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; -import org.jetbrains.annotations.VisibleForTesting; import java.util.*; @@ -22,48 +24,52 @@ public final class FinishingTrick extends TrickState { @Override public void onEnter(Game game) { - var winner = getWinner(); - game.notify(new TrickMessage(winner, get(STACK).stream().map(Pair::second).toList())); + var stack = get(STACK); + var cards = stack.stream().map(Pair::second).toList(); - var tricks = new HashMap<>(get(TRICKS)); - tricks.merge(winner, 1, Integer::sum); + var winner = CardUtils.getWinner(stack, get(TRUMP_SUIT)); + game.notify(new TrickMessage(winner, cards)); - GameData data = getData().with(TRICKS, Map.copyOf(tricks)); + GameData data = getData(); - if (get(TRICK) < get(ROUND)) { - transition(game, new StartingTrick(data.with( - CURRENT_PLAYER, winner, - TRICK, get(TRICK) + 1 - ))); - } else { - transition(game, new FinishingRound(data)); - } - } + boolean bomb = cards.contains(Card.BOMB); + boolean juggler = cards.contains(Card.JUGGLER_BLUE) + || cards.contains(Card.JUGGLER_RED) + || cards.contains(Card.JUGGLER_GREEN) + || cards.contains(Card.JUGGLER_YELLOW); + boolean cloud = cards.contains(Card.CLOUD_BLUE) + || cards.contains(Card.CLOUD_RED) + || cards.contains(Card.CLOUD_GREEN) + || cards.contains(Card.CLOUD_YELLOW); + boolean hasNextTrick = get(TRICK) < get(ROUND); - @VisibleForTesting - UUID getWinner() { - var wizard = get(STACK).stream() - .filter(pair -> pair.second().getSuit() == Card.Suit.WIZARD) - .findFirst() - .orElse(null); - if (wizard != null) { - return wizard.first(); + // 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))); } - if (get(STACK).stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) { - return get(STACK).get(0).first(); + // trick is not counted when a bomb is present + if (!bomb) { + var tricks = new HashMap<>(get(TRICKS)); + tricks.merge(winner, 1, Integer::sum); + data = data.with(TRICKS, Map.copyOf(tricks)); } - var trumpSuit = get(TRUMP_SUIT); - var suit = getTrickSuit(); - return get(STACK).stream() - .max( - Comparator.>comparingInt(pair -> pair.second().getSuit() == trumpSuit ? 1 : 0) - .thenComparing(pair -> pair.second().getSuit() == suit ? 1 : 0) - .thenComparing(pair -> pair.second().getValue()) - ) - .orElseThrow(AssertionError::new) - .first(); + 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))); + } else { + transition(game, new FinishingRound(data)); + } } private static void checkData(GameData data) { diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCard.java index 9d2b738..adea1bf 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCard.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCard.java @@ -1,21 +1,21 @@ package eu.jonahbauer.wizard.core.machine.states.trick; +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.messages.observer.CardMessage; -import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; -import eu.jonahbauer.wizard.core.model.Card; +import eu.jonahbauer.wizard.common.messages.observer.CardMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage; +import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; +import eu.jonahbauer.wizard.core.model.card.GameCards; import eu.jonahbauer.wizard.core.util.Pair; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.VisibleForTesting; import java.util.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PLAY_CARD; +import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.PLAY_CARD; public final class PlayingCard extends TrickState { @@ -33,7 +33,6 @@ public final class PlayingCard extends TrickState { @Override public void onMessage(Game game, UUID player, PlayerMessage message) { if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) { - checkCard(cardMessage.getCard()); transition(game, cardMessage.getCard()); } else { super.onMessage(game, player, message); @@ -43,55 +42,43 @@ public final class PlayingCard extends TrickState { @Override public void onTimeout(Game game) { var hand = get(HANDS).get(get(CURRENT_PLAYER)); - var card = hand.stream().filter(c -> { - try { - checkCard(c); - return true; - } catch (IllegalArgumentException e) { - return false; - } - }).findAny().orElseThrow(() -> new AssertionError("Cannot play any card.")); - transition(game, card); - } + var stack = get(STACK); - @VisibleForTesting - void checkCard(Card card) { - var hand = get(HANDS).get(get(CURRENT_PLAYER)); - if (!hand.contains(card)) { - throw new IllegalArgumentException("You do not have this card on your hand."); - } + var card = GameCards.values().stream() + .filter(c -> c.isPlayable(hand, stack)) + .findAny() + .orElseThrow(() -> new AssertionError("Cannot play any card.")); - Card.Suit suit = getTrickSuit(); - if (card.getSuit().isColor() && suit != null && card.getSuit() != suit) { - if (hand.stream().anyMatch(c -> c.getSuit() == suit)) { - throw new IllegalArgumentException("Must follow suit."); - } - } + transition(game, card.getCard()); } private void transition(Game game, @NotNull Card card) { var currentPlayer = get(CURRENT_PLAYER); - game.notify(new CardMessage(currentPlayer, card)); - // add card to stack + // create mutable stack var stack = get(STACK); var mutableStack = new ArrayList>(stack.size() + 1); mutableStack.addAll(stack); - mutableStack.add(Pair.of(currentPlayer, card)); - // remove card from hand + // create mutable hand var hands = get(HANDS); var mutableHands = new HashMap<>(hands); var hand = hands.get(currentPlayer); var mutableHand = new ArrayList<>(hand); - mutableHand.remove(card); - mutableHands.put(currentPlayer, List.copyOf(mutableHand)); + GameCards.get(card).play(currentPlayer, mutableHand, mutableStack); + + // when card was played successfully + game.notify(new CardMessage(currentPlayer, card)); + + // apply modifications + mutableHands.put(currentPlayer, List.copyOf(mutableHand)); GameData data = getData().with( STACK, List.copyOf(mutableStack), HANDS, Map.copyOf(mutableHands) ); + // check whether the trick is finished var summary = data.get(HANDS).values().stream() .mapToInt(Collection::size) .summaryStatistics(); 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 eddc5c1..61cfae3 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 @@ -2,7 +2,6 @@ package eu.jonahbauer.wizard.core.machine.states.trick; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.states.round.RoundState; -import eu.jonahbauer.wizard.core.model.Card; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; @@ -14,16 +13,4 @@ public abstract class TrickState extends RoundState { .require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER) ); } - - protected Card.Suit getTrickSuit() { - for (var pair : get(STACK)) { - Card.Suit suit = pair.second().getSuit(); - if (suit == Card.Suit.WIZARD) { - return Card.Suit.NONE; - } else if (suit.isColor()) { - return suit; - } - } - return Card.Suit.NONE; - } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/Observer.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/Observer.java index 0021324..f46cfcd 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/Observer.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/Observer.java @@ -1,6 +1,6 @@ package eu.jonahbauer.wizard.core.messages; -import eu.jonahbauer.wizard.core.messages.observer.ObserverMessage; +import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage; import java.util.UUID; diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/StateMessage.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/StateMessage.java deleted file mode 100644 index 4072629..0000000 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/messages/observer/StateMessage.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.jonahbauer.wizard.core.messages.observer; - -import eu.jonahbauer.wizard.core.machine.Game; -import eu.jonahbauer.wizard.core.machine.states.GameState; -import lombok.Getter; - -import java.util.Locale; - -/** - * A {@link StateMessage} is sent whenever the {@link Game} changes its internal {@link GameState}. - */ -@Getter -public final class StateMessage extends ObserverMessage { - /** - * The name of the new state in snake_case. - */ - private final String state; - - public StateMessage(Class state) { - if (state == null) { - this.state = "null"; - } else { - this.state = state.getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT); - } - } -} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Card.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Card.java deleted file mode 100644 index 3ed29e9..0000000 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Card.java +++ /dev/null @@ -1,98 +0,0 @@ -package eu.jonahbauer.wizard.core.model; - -import lombok.Getter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@Getter -public enum Card { - HIDDEN(0, Suit.NONE, Suit.NONE), - RED_JESTER(0, Suit.JESTER, Suit.NONE), - RED_1(1, Suit.RED, Suit.RED), - RED_2(2, Suit.RED, Suit.RED), - RED_3(3, Suit.RED, Suit.RED), - RED_4(4, Suit.RED, Suit.RED), - RED_5(5, Suit.RED, Suit.RED), - RED_6(6, Suit.RED, Suit.RED), - RED_7(7, Suit.RED, Suit.RED), - RED_8(8, Suit.RED, Suit.RED), - RED_9(9, Suit.RED, Suit.RED), - RED_10(10, Suit.RED, Suit.RED), - RED_11(11, Suit.RED, Suit.RED), - RED_12(12, Suit.RED, Suit.RED), - RED_13(13, Suit.RED, Suit.RED), - RED_WIZARD(14, Suit.WIZARD, null), - YELLOW_JESTER(0, Suit.JESTER, Suit.NONE), - YELLOW_1(1, Suit.YELLOW, Suit.YELLOW), - YELLOW_2(2, Suit.YELLOW, Suit.YELLOW), - YELLOW_3(3, Suit.YELLOW, Suit.YELLOW), - YELLOW_4(4, Suit.YELLOW, Suit.YELLOW), - YELLOW_5(5, Suit.YELLOW, Suit.YELLOW), - YELLOW_6(6, Suit.YELLOW, Suit.YELLOW), - YELLOW_7(7, Suit.YELLOW, Suit.YELLOW), - YELLOW_8(8, Suit.YELLOW, Suit.YELLOW), - YELLOW_9(9, Suit.YELLOW, Suit.YELLOW), - YELLOW_10(10, Suit.YELLOW, Suit.YELLOW), - YELLOW_11(11, Suit.YELLOW, Suit.YELLOW), - YELLOW_12(12, Suit.YELLOW, Suit.YELLOW), - YELLOW_13(13, Suit.YELLOW, Suit.YELLOW), - YELLOW_WIZARD(14, Suit.WIZARD, null), - GREEN_JESTER(0, Suit.JESTER, Suit.NONE), - GREEN_1(1, Suit.GREEN, Suit.GREEN), - GREEN_2(2, Suit.GREEN, Suit.GREEN), - GREEN_3(3, Suit.GREEN, Suit.GREEN), - GREEN_4(4, Suit.GREEN, Suit.GREEN), - GREEN_5(5, Suit.GREEN, Suit.GREEN), - GREEN_6(6, Suit.GREEN, Suit.GREEN), - GREEN_7(7, Suit.GREEN, Suit.GREEN), - GREEN_8(8, Suit.GREEN, Suit.GREEN), - GREEN_9(9, Suit.GREEN, Suit.GREEN), - GREEN_10(10, Suit.GREEN, Suit.GREEN), - GREEN_11(11, Suit.GREEN, Suit.GREEN), - GREEN_12(12, Suit.GREEN, Suit.GREEN), - GREEN_13(13, Suit.GREEN, Suit.GREEN), - GREEN_WIZARD(14, Suit.WIZARD, null), - BLUE_JESTER(0, Suit.JESTER, Suit.NONE), - BLUE_1(1, Suit.BLUE, Suit.BLUE), - BLUE_2(2, Suit.BLUE, Suit.BLUE), - BLUE_3(3, Suit.BLUE, Suit.BLUE), - BLUE_4(4, Suit.BLUE, Suit.BLUE), - BLUE_5(5, Suit.BLUE, Suit.BLUE), - BLUE_6(6, Suit.BLUE, Suit.BLUE), - BLUE_7(7, Suit.BLUE, Suit.BLUE), - BLUE_8(8, Suit.BLUE, Suit.BLUE), - BLUE_9(9, Suit.BLUE, Suit.BLUE), - BLUE_10(10, Suit.BLUE, Suit.BLUE), - BLUE_11(11, Suit.BLUE, Suit.BLUE), - BLUE_12(12, Suit.BLUE, Suit.BLUE), - BLUE_13(13, Suit.BLUE, Suit.BLUE), - BLUE_WIZARD(14, Suit.WIZARD, null); - - private final int value; - private final @NotNull Suit suit; - private final @Nullable Suit trumpSuit; - - Card(int value, @NotNull Suit suit, @Nullable Suit trumpSuit) { - if (trumpSuit != null && !trumpSuit.isColor()) throw new IllegalArgumentException(); - this.value = value; - this.suit = suit; - this.trumpSuit = trumpSuit; - } - - @Getter - public enum Suit { - NONE(true), - YELLOW(true), - RED(true), - GREEN(true), - BLUE(true), - JESTER(false), - WIZARD(false); - - private final boolean color; - - Suit(boolean color) { - this.color = color; - } - } -} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Cards.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Cards.java deleted file mode 100644 index ffb3450..0000000 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Cards.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.jonahbauer.wizard.core.model; - -import lombok.experimental.UtilityClass; - -import java.util.Set; - -@UtilityClass -public class Cards { - public static final Set DEFAULT = Set.of( - Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER, Card.BLUE_JESTER, - Card.RED_1, Card.GREEN_1, Card.YELLOW_1, Card.BLUE_1, - Card.RED_2, Card.GREEN_2, Card.YELLOW_2, Card.BLUE_2, - Card.RED_3, Card.GREEN_3, Card.YELLOW_3, Card.BLUE_3, - Card.RED_4, Card.GREEN_4, Card.YELLOW_4, Card.BLUE_4, - Card.RED_5, Card.GREEN_5, Card.YELLOW_5, Card.BLUE_5, - Card.RED_6, Card.GREEN_6, Card.YELLOW_6, Card.BLUE_6, - Card.RED_7, Card.GREEN_7, Card.YELLOW_7, Card.BLUE_7, - Card.RED_8, Card.GREEN_8, Card.YELLOW_8, Card.BLUE_8, - Card.RED_9, Card.GREEN_9, Card.YELLOW_9, Card.BLUE_9, - Card.RED_10, Card.GREEN_10, Card.YELLOW_10, Card.BLUE_10, - Card.RED_11, Card.GREEN_11, Card.YELLOW_11, Card.BLUE_11, - Card.RED_12, Card.GREEN_12, Card.YELLOW_12, Card.BLUE_12, - Card.RED_13, Card.GREEN_13, Card.YELLOW_13, Card.BLUE_13, - Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD, Card.BLUE_WIZARD - ); -} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configuration.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configuration.java index 27bef22..561d340 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configuration.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configuration.java @@ -1,5 +1,6 @@ package eu.jonahbauer.wizard.core.model; +import eu.jonahbauer.wizard.common.model.card.Card; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configurations.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configurations.java index 2814a32..dd382ed 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configurations.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Configurations.java @@ -1,5 +1,6 @@ package eu.jonahbauer.wizard.core.model; +import eu.jonahbauer.wizard.core.model.deck.Decks; import lombok.experimental.UtilityClass; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -13,15 +14,45 @@ import java.util.NoSuchElementException; public class Configurations { private static final Map CONFIGURATIONS = new HashMap<>(); - public static final Configuration DEFAULT = register("default", new Configuration( - Cards.DEFAULT, + public static final Configuration DEFAULT = register("DEFAULT", new Configuration( + Decks.DEFAULT, true, 10 * 60 * 1000 )); + public static final Configuration DEFAULT_PM1 = register("DEFAULT_PM1", new Configuration( + Decks.DEFAULT, + false, + 10 * 60 * 1000 + )); + + public static final Configuration ANNIVERSARY_2016 = register("ANNIVERSARY_2016", new Configuration( + Decks.ANNIVERSARY_2016, + true, + 10 * 60 * 1000 + )); + + public static final Configuration ANNIVERSARY_2016_PM1 = register("ANNIVERSARY_2016_PM1", new Configuration( + Decks.ANNIVERSARY_2016, + false, + 10 * 60 * 1000 + )); + + public static final Configuration ANNIVERSARY_2021 = register("ANNIVERSARY_2021", new Configuration( + Decks.ANNIVERSARY_2021, + true, + 10 * 60 * 1000 + )); + + public static final Configuration ANNIVERSARY_2021_PM1 = register("ANNIVERSARY_2021_PM1", new Configuration( + Decks.ANNIVERSARY_2021, + false, + 10 * 60 * 1000 + )); + @Contract("_,_ -> param2") private static Configuration register(@NotNull String name, @NotNull Configuration configuration) { - if (CONFIGURATIONS.putIfAbsent(name.toLowerCase(Locale.ROOT), configuration) != null) { + if (CONFIGURATIONS.putIfAbsent(name.toUpperCase(Locale.ROOT), configuration) != null) { throw new IllegalArgumentException("Name already taken."); } return configuration; @@ -29,7 +60,7 @@ public class Configurations { @NotNull public static Configuration get(@NotNull String name) { - var out = CONFIGURATIONS.get(name.toLowerCase(Locale.ROOT)); + var out = CONFIGURATIONS.get(name.toUpperCase(Locale.ROOT)); if (out == null) throw new NoSuchElementException("Configuration with name '" + name + "' does not exist."); return out; } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/CardUtils.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/CardUtils.java new file mode 100644 index 0000000..4c15d88 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/CardUtils.java @@ -0,0 +1,75 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; +import lombok.experimental.UtilityClass; + +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +@UtilityClass +public class CardUtils { + public Card.Suit getTrickSuit(List> stack) { + for (var pair : stack) { + Card.Suit suit = GameCards.get(pair.second()).getTrickSuit(); + + if (suit != null) { + return suit; + } + } + + return Card.Suit.NONE; + } + + public UUID getWinner(List> stack, Card.Suit trumpSuit) { + var suit = getTrickSuit(stack); + + Pair fairy = null, dragon = null; + + for (Pair pair : stack) { + Card card = pair.second(); + if (card == Card.FAIRY) { + fairy = pair; + } else if (card == Card.DRAGON) { + dragon = pair; + } + } + + if (fairy != null && dragon != null) { + return fairy.first(); + } else if (dragon != null) { + return dragon.first(); + } + + Comparator> comparator = Comparator.comparingDouble(pair -> { + GameCard card = GameCards.get(pair.second()); + if (card instanceof WizardCard) { + return 1000; + } else if (card instanceof JesterCard) { + return 0; + } else if (card == GameCards.BOMB) { + return -1; + } else if (card == GameCards.DRAGON) { + return 2000; + } else if (card == GameCards.FAIRY) { + return -1; + } else if (card instanceof ColoredCard coloredCard) { + if (coloredCard.getSuit() == trumpSuit) { + return 200 + coloredCard.getValue(); + } else if (coloredCard.getSuit() == suit) { + return 100 + coloredCard.getValue(); + } else { + return coloredCard.getValue(); + } + } else { + throw new IllegalArgumentException("Dont know how to handle " + pair.second() + "."); + } + }); + + return stack.stream() + .max(comparator) + .orElseThrow(() -> new RuntimeException("Could not determine trick winner.")) + .first(); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingJesterCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingJesterCard.java new file mode 100644 index 0000000..1eb3579 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingJesterCard.java @@ -0,0 +1,24 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; + +import java.util.List; +import java.util.UUID; + +public class ChangelingJesterCard extends JesterCard implements Subcard { + public ChangelingJesterCard(Card card) { + super(card); + } + + @Override + public void play(UUID player, List hand, List> stack) { + GameCards.CHANGELING.checkHand(hand); + hand.remove(Card.CHANGELING); + stack.add(Pair.of(player, getCard())); + } + + public boolean isPlayable(List hand, List> stack) { + return hand.contains(Card.CHANGELING); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingWizardCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingWizardCard.java new file mode 100644 index 0000000..7061ab7 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ChangelingWizardCard.java @@ -0,0 +1,25 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; + +import java.util.List; +import java.util.UUID; + +public class ChangelingWizardCard extends WizardCard implements Subcard { + public ChangelingWizardCard(Card card) { + super(card); + } + + @Override + public void play(UUID player, List hand, List> stack) { + GameCards.CHANGELING.checkHand(hand); + hand.remove(Card.CHANGELING); + stack.add(Pair.of(player, getCard())); + } + + @Override + public boolean isPlayable(List hand, List> stack) { + return hand.contains(Card.CHANGELING); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredCard.java new file mode 100644 index 0000000..2355787 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredCard.java @@ -0,0 +1,56 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; + +@Getter +public class ColoredCard extends GameCard { + private final double value; + private final Card.Suit suit; + + public ColoredCard(Card card, double value, Card.Suit suit) { + super(card); + this.value = value; + this.suit = suit; + } + + @Override + public void play(UUID player, List hand, List> stack) { + Card.Suit suit = CardUtils.getTrickSuit(stack); + if (this.suit != suit && canFollowSuit(hand, suit)) { + throw new IllegalArgumentException("Must follow suit."); + } + + super.play(player, hand, stack); + } + + @Override + public Card.Suit getTrumpSuit() { + return getSuit(); + } + + @Override + public Card.Suit getTrickSuit() { + return getSuit(); + } + + @Override + public boolean isPlayable(List hand, List> stack) { + Card.Suit suit = CardUtils.getTrickSuit(stack); + if (this.suit != suit && canFollowSuit(hand, suit)) { + return false; + } + + return super.isPlayable(hand, stack); + } + + private boolean canFollowSuit(List hand, Card.Suit suit) { + return hand.stream() + .map(GameCards::get) + .anyMatch(card -> card instanceof ColoredCard coloredCard && coloredCard.getSuit() == suit); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredSubcard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredSubcard.java new file mode 100644 index 0000000..d7d3962 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/ColoredSubcard.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; + +@Getter +public class ColoredSubcard extends ColoredCard implements Subcard { + private final GameCard parent; + + public ColoredSubcard(Card card, GameCard parent, double value, Card.Suit suit) { + super(card, value, suit); + this.parent = parent; + } + + @Override + public void play(UUID player, List hand, List> stack) { + parent.checkHand(hand); + hand.remove(parent.getCard()); + stack.add(Pair.of(player, getCard())); + } + + @Override + public boolean isPlayable(List hand, List> stack) { + return hand.contains(parent.getCard()); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCard.java new file mode 100644 index 0000000..6a754c6 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCard.java @@ -0,0 +1,44 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Contract; + +import java.util.List; +import java.util.UUID; + +@Getter +@RequiredArgsConstructor +public abstract class GameCard { + private final Card card; + + public abstract Card.Suit getTrumpSuit(); + public abstract Card.Suit getTrickSuit(); + + /** + * Standard implementation of playing a card. + *
+ * Removes the card from the hand checking that the hand contains it and then adds a new entry to the stack. + * @param player the player playing this card + * @param hand the player's hand cards + * @param stack the stack + */ + @Contract(mutates = "param2, param3") + public void play(UUID player, List hand, List> stack) { + checkHand(hand); + hand.remove(getCard()); + stack.add(Pair.of(player, getCard())); + } + + public boolean isPlayable(List hand, List> stack) { + return hand.contains(getCard()); + } + + protected void checkHand(List hand) { + if (!hand.contains(getCard())) { + throw new IllegalArgumentException("You do not have this card on your hand."); + } + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCards.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCards.java new file mode 100644 index 0000000..b33b53e --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/GameCards.java @@ -0,0 +1,123 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +@UtilityClass +@SuppressWarnings("unused") +public class GameCards { + private static final Map CARDS = new HashMap<>(); + + public final GameCard HIDDEN = register(new NonPlayableCard(Card.HIDDEN, null)); + + public final GameCard BLUE_1 = register(new ColoredCard(Card.BLUE_1, 1, Card.Suit.BLUE)); + public final GameCard BLUE_2 = register(new ColoredCard(Card.BLUE_2, 2, Card.Suit.BLUE)); + public final GameCard BLUE_3 = register(new ColoredCard(Card.BLUE_3, 3, Card.Suit.BLUE)); + public final GameCard BLUE_4 = register(new ColoredCard(Card.BLUE_4, 4, Card.Suit.BLUE)); + public final GameCard BLUE_5 = register(new ColoredCard(Card.BLUE_5, 5, Card.Suit.BLUE)); + public final GameCard BLUE_6 = register(new ColoredCard(Card.BLUE_6, 6, Card.Suit.BLUE)); + public final GameCard BLUE_7 = register(new ColoredCard(Card.BLUE_7, 7, Card.Suit.BLUE)); + public final GameCard BLUE_8 = register(new ColoredCard(Card.BLUE_8, 8, Card.Suit.BLUE)); + public final GameCard BLUE_9 = register(new ColoredCard(Card.BLUE_9, 9, Card.Suit.BLUE)); + public final GameCard BLUE_10 = register(new ColoredCard(Card.BLUE_10, 10, Card.Suit.BLUE)); + public final GameCard BLUE_11 = register(new ColoredCard(Card.BLUE_11, 11, Card.Suit.BLUE)); + public final GameCard BLUE_12 = register(new ColoredCard(Card.BLUE_12, 12, Card.Suit.BLUE)); + public final GameCard BLUE_13 = register(new ColoredCard(Card.BLUE_13, 13, Card.Suit.BLUE)); + + public final GameCard RED_1 = register(new ColoredCard(Card.RED_1, 1, Card.Suit.RED)); + public final GameCard RED_2 = register(new ColoredCard(Card.RED_2, 2, Card.Suit.RED)); + public final GameCard RED_3 = register(new ColoredCard(Card.RED_3, 3, Card.Suit.RED)); + public final GameCard RED_4 = register(new ColoredCard(Card.RED_4, 4, Card.Suit.RED)); + public final GameCard RED_5 = register(new ColoredCard(Card.RED_5, 5, Card.Suit.RED)); + public final GameCard RED_6 = register(new ColoredCard(Card.RED_6, 6, Card.Suit.RED)); + public final GameCard RED_7 = register(new ColoredCard(Card.RED_7, 7, Card.Suit.RED)); + public final GameCard RED_8 = register(new ColoredCard(Card.RED_8, 8, Card.Suit.RED)); + public final GameCard RED_9 = register(new ColoredCard(Card.RED_9, 9, Card.Suit.RED)); + public final GameCard RED_10 = register(new ColoredCard(Card.RED_10, 10, Card.Suit.RED)); + public final GameCard RED_11 = register(new ColoredCard(Card.RED_11, 11, Card.Suit.RED)); + public final GameCard RED_12 = register(new ColoredCard(Card.RED_12, 12, Card.Suit.RED)); + public final GameCard RED_13 = register(new ColoredCard(Card.RED_13, 13, Card.Suit.RED)); + + public final GameCard GREEN_1 = register(new ColoredCard(Card.GREEN_1, 1, Card.Suit.GREEN)); + public final GameCard GREEN_2 = register(new ColoredCard(Card.GREEN_2, 2, Card.Suit.GREEN)); + public final GameCard GREEN_3 = register(new ColoredCard(Card.GREEN_3, 3, Card.Suit.GREEN)); + public final GameCard GREEN_4 = register(new ColoredCard(Card.GREEN_4, 4, Card.Suit.GREEN)); + public final GameCard GREEN_5 = register(new ColoredCard(Card.GREEN_5, 5, Card.Suit.GREEN)); + public final GameCard GREEN_6 = register(new ColoredCard(Card.GREEN_6, 6, Card.Suit.GREEN)); + public final GameCard GREEN_7 = register(new ColoredCard(Card.GREEN_7, 7, Card.Suit.GREEN)); + public final GameCard GREEN_8 = register(new ColoredCard(Card.GREEN_8, 8, Card.Suit.GREEN)); + public final GameCard GREEN_9 = register(new ColoredCard(Card.GREEN_9, 9, Card.Suit.GREEN)); + public final GameCard GREEN_10 = register(new ColoredCard(Card.GREEN_10, 10, Card.Suit.GREEN)); + public final GameCard GREEN_11 = register(new ColoredCard(Card.GREEN_11, 11, Card.Suit.GREEN)); + public final GameCard GREEN_12 = register(new ColoredCard(Card.GREEN_12, 12, Card.Suit.GREEN)); + public final GameCard GREEN_13 = register(new ColoredCard(Card.GREEN_13, 13, Card.Suit.GREEN)); + + public final GameCard YELLOW_1 = register(new ColoredCard(Card.YELLOW_1, 1, Card.Suit.YELLOW)); + public final GameCard YELLOW_2 = register(new ColoredCard(Card.YELLOW_2, 2, Card.Suit.YELLOW)); + public final GameCard YELLOW_3 = register(new ColoredCard(Card.YELLOW_3, 3, Card.Suit.YELLOW)); + public final GameCard YELLOW_4 = register(new ColoredCard(Card.YELLOW_4, 4, Card.Suit.YELLOW)); + public final GameCard YELLOW_5 = register(new ColoredCard(Card.YELLOW_5, 5, Card.Suit.YELLOW)); + public final GameCard YELLOW_6 = register(new ColoredCard(Card.YELLOW_6, 6, Card.Suit.YELLOW)); + public final GameCard YELLOW_7 = register(new ColoredCard(Card.YELLOW_7, 7, Card.Suit.YELLOW)); + public final GameCard YELLOW_8 = register(new ColoredCard(Card.YELLOW_8, 8, Card.Suit.YELLOW)); + public final GameCard YELLOW_9 = register(new ColoredCard(Card.YELLOW_9, 9, Card.Suit.YELLOW)); + public final GameCard YELLOW_10 = register(new ColoredCard(Card.YELLOW_10, 10, Card.Suit.YELLOW)); + public final GameCard YELLOW_11 = register(new ColoredCard(Card.YELLOW_11, 11, Card.Suit.YELLOW)); + public final GameCard YELLOW_12 = register(new ColoredCard(Card.YELLOW_12, 12, Card.Suit.YELLOW)); + public final GameCard YELLOW_13 = register(new ColoredCard(Card.YELLOW_13, 13, Card.Suit.YELLOW)); + + public final GameCard BLUE_WIZARD = register(new WizardCard(Card.BLUE_WIZARD)); + public final GameCard RED_WIZARD = register(new WizardCard(Card.RED_WIZARD)); + public final GameCard GREEN_WIZARD = register(new WizardCard(Card.GREEN_WIZARD)); + public final GameCard YELLOW_WIZARD = register(new WizardCard(Card.YELLOW_WIZARD)); + + public final GameCard BLUE_JESTER = register(new JesterCard(Card.BLUE_JESTER)); + public final GameCard RED_JESTER = register(new JesterCard(Card.RED_JESTER)); + public final GameCard GREEN_JESTER = register(new JesterCard(Card.GREEN_JESTER)); + public final GameCard YELLOW_JESTER = register(new JesterCard(Card.YELLOW_JESTER)); + + public final GameCard CHANGELING = register(new NonPlayableCard(Card.CHANGELING, null)); + public final GameCard CHANGELING_WIZARD = register(new ChangelingWizardCard(Card.CHANGELING_WIZARD)); + public final GameCard CHANGELING_JESTER = register(new ChangelingJesterCard(Card.CHANGELING_JESTER)); + + public final GameCard BOMB = register(new SimpleCard(Card.BOMB, Card.Suit.NONE, null)); + public final GameCard WEREWOLF = register(new NonPlayableCard(Card.WEREWOLF, null)); + public final GameCard DRAGON = register(new SimpleCard(Card.DRAGON, null, Card.Suit.NONE)); + public final GameCard FAIRY = register(new SimpleCard(Card.FAIRY, Card.Suit.NONE, null)); + + public final GameCard CLOUD = register(new NonPlayableCard(Card.CLOUD, null)); + public final GameCard CLOUD_BLUE = register(new ColoredSubcard(Card.CLOUD_BLUE, CLOUD, 9.75, Card.Suit.BLUE)); + public final GameCard CLOUD_RED = register(new ColoredSubcard(Card.CLOUD_RED, CLOUD, 9.75, Card.Suit.RED)); + public final GameCard CLOUD_GREEN = register(new ColoredSubcard(Card.CLOUD_GREEN, CLOUD, 9.75, Card.Suit.GREEN)); + public final GameCard CLOUD_YELLOW = register(new ColoredSubcard(Card.CLOUD_YELLOW, CLOUD, 9.75, Card.Suit.YELLOW)); + + public final GameCard JUGGLER = register(new NonPlayableCard(Card.JUGGLER, null)); + public final GameCard JUGGLER_BLUE = register(new ColoredSubcard(Card.JUGGLER_BLUE, JUGGLER, 7.5, Card.Suit.BLUE)); + public final GameCard JUGGLER_RED = register(new ColoredSubcard(Card.JUGGLER_RED, JUGGLER, 7.5, Card.Suit.RED)); + public final GameCard JUGGLER_GREEN = register(new ColoredSubcard(Card.JUGGLER_GREEN, JUGGLER, 7.5, Card.Suit.GREEN)); + public final GameCard JUGGLER_YELLOW = register(new ColoredSubcard(Card.JUGGLER_YELLOW, JUGGLER, 7.5, Card.Suit.YELLOW)); + + @Contract("_ -> param1") + private static GameCard register(@NotNull GameCard card) { + if (CARDS.containsKey(card.getCard())) { + throw new IllegalArgumentException("Name already taken."); + } + + CARDS.put(card.getCard(), card); + + return card; + } + + @NotNull + public static GameCard get(@NotNull Card name) { + return CARDS.get(name); + } + + public static Collection values() { + return Collections.unmodifiableCollection(CARDS.values()); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/JesterCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/JesterCard.java new file mode 100644 index 0000000..f18ff2f --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/JesterCard.java @@ -0,0 +1,9 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; + +public class JesterCard extends SimpleCard { + public JesterCard(Card card) { + super(card, Card.Suit.NONE, null); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/NonPlayableCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/NonPlayableCard.java new file mode 100644 index 0000000..395bc82 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/NonPlayableCard.java @@ -0,0 +1,38 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; + +@Getter +public final class NonPlayableCard extends GameCard { + private final Card.Suit suit; + + public NonPlayableCard(Card card, Card.Suit suit) { + super(card); + this.suit = suit; + } + + @Override + public void play(UUID player, List hand, List> stack) { + throw new IllegalArgumentException("This card cannot be played."); + } + + @Override + public Card.Suit getTrumpSuit() { + return getSuit(); + } + + @Override + public Card.Suit getTrickSuit() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayable(List hand, List> stack) { + return false; + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/SimpleCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/SimpleCard.java new file mode 100644 index 0000000..9104d54 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/SimpleCard.java @@ -0,0 +1,16 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.Getter; + +@Getter +public class SimpleCard extends GameCard { + private final Card.Suit trumpSuit; + private final Card.Suit trickSuit; + + public SimpleCard(Card card, Card.Suit trumpSuit, Card.Suit trickSuit) { + super(card); + this.trumpSuit = trumpSuit; + this.trickSuit = trickSuit; + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/Subcard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/Subcard.java new file mode 100644 index 0000000..712fb03 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/Subcard.java @@ -0,0 +1,14 @@ +package eu.jonahbauer.wizard.core.model.card; + +/** + * Marker interface for subcards. A subcard is an internal variant of another card. It contains additional information + * that would otherwise need to be provided in a seperate message. + *
+ * A subcard may not be on any player's hand. + *

+ * For example: when a player has the {@link GameCards#CLOUD} on his hand, he may not play the card directly but can only + * play one of its subcards {@link GameCards#CLOUD_BLUE}, {@link GameCards#CLOUD_GREEN}, {@link GameCards#CLOUD_RED} or + * {@link GameCards#CLOUD_YELLOW}. + */ +public interface Subcard { +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/WizardCard.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/WizardCard.java new file mode 100644 index 0000000..8463caa --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/card/WizardCard.java @@ -0,0 +1,9 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; + +public class WizardCard extends SimpleCard { + public WizardCard(Card card) { + super(card, null, Card.Suit.NONE); + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Deck.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Deck.java similarity index 82% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Deck.java rename to wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Deck.java index f07b0df..e1b053d 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/Deck.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Deck.java @@ -1,5 +1,6 @@ -package eu.jonahbauer.wizard.core.model; +package eu.jonahbauer.wizard.core.model.deck; +import eu.jonahbauer.wizard.common.model.card.Card; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -13,8 +14,8 @@ public final class Deck { this.cards.addAll(cards); } - public void shuffle() { - Collections.shuffle(cards); + public void shuffle(Random random) { + Collections.shuffle(cards, random); next = 0; } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Decks.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Decks.java new file mode 100644 index 0000000..ca72060 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/model/deck/Decks.java @@ -0,0 +1,65 @@ +package eu.jonahbauer.wizard.core.model.deck; + +import eu.jonahbauer.wizard.common.model.card.Card; +import lombok.experimental.UtilityClass; + +import java.util.*; + +@UtilityClass +public class Decks { + public static final Set DEFAULT = Collections.unmodifiableSet(new LinkedHashSet<>(List.of( + Card.BLUE_1, Card.RED_1, Card.GREEN_1, Card.YELLOW_1, + Card.BLUE_2, Card.RED_2, Card.GREEN_2, Card.YELLOW_2, + Card.BLUE_3, Card.RED_3, Card.GREEN_3, Card.YELLOW_3, + Card.BLUE_4, Card.RED_4, Card.GREEN_4, Card.YELLOW_4, + Card.BLUE_5, Card.RED_5, Card.GREEN_5, Card.YELLOW_5, + Card.BLUE_6, Card.RED_6, Card.GREEN_6, Card.YELLOW_6, + Card.BLUE_7, Card.RED_7, Card.GREEN_7, Card.YELLOW_7, + Card.BLUE_8, Card.RED_8, Card.GREEN_8, Card.YELLOW_8, + Card.BLUE_9, Card.RED_9, Card.GREEN_9, Card.YELLOW_9, + Card.BLUE_10, Card.RED_10, Card.GREEN_10, Card.YELLOW_10, + Card.BLUE_11, Card.RED_11, Card.GREEN_11, Card.YELLOW_11, + Card.BLUE_12, Card.RED_12, Card.GREEN_12, Card.YELLOW_12, + Card.BLUE_13, Card.RED_13, Card.GREEN_13, Card.YELLOW_13, + Card.BLUE_JESTER, Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER, + Card.BLUE_WIZARD, Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD + ))); + + public static final Set ANNIVERSARY_2016 = Collections.unmodifiableSet(new LinkedHashSet<>(List.of( + Card.BLUE_1, Card.RED_1, Card.GREEN_1, Card.YELLOW_1, + Card.BLUE_2, Card.RED_2, Card.GREEN_2, Card.YELLOW_2, + Card.BLUE_3, Card.RED_3, Card.GREEN_3, Card.YELLOW_3, + Card.BLUE_4, Card.RED_4, Card.GREEN_4, Card.YELLOW_4, + Card.BLUE_5, Card.RED_5, Card.GREEN_5, Card.YELLOW_5, + Card.BLUE_6, Card.RED_6, Card.GREEN_6, Card.YELLOW_6, + Card.BLUE_7, Card.RED_7, Card.GREEN_7, Card.YELLOW_7, + Card.BLUE_8, Card.RED_8, Card.GREEN_8, Card.YELLOW_8, + Card.BLUE_9, Card.RED_9, Card.GREEN_9, Card.YELLOW_9, + Card.BLUE_10, Card.RED_10, Card.GREEN_10, Card.YELLOW_10, + Card.BLUE_11, Card.RED_11, Card.GREEN_11, Card.YELLOW_11, + Card.BLUE_12, Card.RED_12, Card.GREEN_12, Card.YELLOW_12, + Card.BLUE_13, Card.RED_13, Card.GREEN_13, Card.YELLOW_13, + Card.BLUE_JESTER, Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER, + Card.BLUE_WIZARD, Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD, + Card.WEREWOLF, Card.BOMB, Card.DRAGON, Card.FAIRY, Card.JUGGLER, Card.CLOUD + ))); + + public static final Set ANNIVERSARY_2021 = Collections.unmodifiableSet(new LinkedHashSet<>(List.of( + Card.BLUE_1, Card.RED_1, Card.GREEN_1, Card.YELLOW_1, + Card.BLUE_2, Card.RED_2, Card.GREEN_2, Card.YELLOW_2, + Card.BLUE_3, Card.RED_3, Card.GREEN_3, Card.YELLOW_3, + Card.BLUE_4, Card.RED_4, Card.GREEN_4, Card.YELLOW_4, + Card.BLUE_5, Card.RED_5, Card.GREEN_5, Card.YELLOW_5, + Card.BLUE_6, Card.RED_6, Card.GREEN_6, Card.YELLOW_6, + Card.BLUE_7, Card.RED_7, Card.GREEN_7, Card.YELLOW_7, + Card.BLUE_8, Card.RED_8, Card.GREEN_8, Card.YELLOW_8, + Card.BLUE_9, Card.RED_9, Card.GREEN_9, Card.YELLOW_9, + Card.BLUE_10, Card.RED_10, Card.GREEN_10, Card.YELLOW_10, + Card.BLUE_11, Card.RED_11, Card.GREEN_11, Card.YELLOW_11, + Card.BLUE_12, Card.RED_12, Card.GREEN_12, Card.YELLOW_12, + Card.BLUE_13, Card.RED_13, Card.GREEN_13, Card.YELLOW_13, + Card.BLUE_JESTER, Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER, + Card.BLUE_WIZARD, Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD, + Card.WEREWOLF, Card.BOMB, Card.DRAGON, Card.FAIRY, Card.JUGGLER, Card.CLOUD, Card.CHANGELING + ))); +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/Util.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/Util.java new file mode 100644 index 0000000..0d572c3 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/util/Util.java @@ -0,0 +1,12 @@ +package eu.jonahbauer.wizard.core.util; + +import lombok.experimental.UtilityClass; + +import java.util.Locale; + +@UtilityClass +public class Util { + public static String toSnakeCase(String str) { + return str.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT); + } +} 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 7ea734c..ca83e6e 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 @@ -1,16 +1,57 @@ package eu.jonahbauer.wizard.core.machine; import eu.jonahbauer.wizard.core.model.Configurations; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.RepetitionInfo; import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; public class GameTest { - @Test - public void run() throws InterruptedException, ExecutionException { - Game game = new Game(Configurations.DEFAULT.withTimeout(0), (player, msg) -> System.out.println(msg)); + @RepeatedTest(3) + public void runDefault(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException { + Game game = new Game( + repetitionInfo.getCurrentRepetition(), + Configurations.DEFAULT.withTimeout(0), + (player, msg) -> System.out.println(msg) + ); + var players = List.of( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID() + ); + + game.start(players); + game.await(); + } + + @RepeatedTest(3) + public void runAnniversary2016(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException { + Game game = new Game( + repetitionInfo.getCurrentRepetition(), + Configurations.ANNIVERSARY_2016.withTimeout(0), + (player, msg) -> System.out.println(msg) + ); + var players = List.of( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID() + ); + + game.start(players); + game.await(); + } + + @RepeatedTest(3) + public void runAnniversary2021(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException { + Game game = new Game( + repetitionInfo.getCurrentRepetition(), + Configurations.ANNIVERSARY_2021.withTimeout(0), + (player, msg) -> System.out.println(msg) + ); var players = List.of( UUID.randomUUID(), UUID.randomUUID(), diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTestUtils.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTestUtils.java new file mode 100644 index 0000000..13f01db --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/GameTestUtils.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.wizard.core.machine; + +import lombok.experimental.UtilityClass; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.mockito.stubbing.Stubber; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@UtilityClass +public class GameTestUtils { + public static Map emptyToIntMap(List players) { + return players.stream().collect(Collectors.toUnmodifiableMap(player -> player, player -> 0)); + } + + public static Stubber doFinish() { + return Mockito.doAnswer(finish()); + } + + public static Answer finish() { + return invocation -> { + Game game = (Game) invocation.getMock(); + game.finish(); + return null; + }; + } +} 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 new file mode 100644 index 0000000..8183a93 --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java @@ -0,0 +1,145 @@ +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.model.card.Card; +import eu.jonahbauer.wizard.core.messages.Observer; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.junit.jupiter.api.Assertions; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * A queue of {@link PlayerMessage}s that will automatically be sent to a {@link Game} when + * {@linkplain UserInputMessage input is required}. The {@code PlayerMessage}s are ordered and it is asserted that + * the incoming {@code UserInputMessage}s match the given {@code PlayerMessage}s. + */ +public class MessageQueue implements Observer { + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final LinkedList messages = new LinkedList<>(); + @Setter + private Game game; + + public MessageQueue add(UUID player, UserInputMessage.Action action, PlayerMessage message) { + messages.add(new QueuedMessage(player, action, message)); + return this; + } + + public MessageQueue addCard(UUID player, Card card) { + return add(player, UserInputMessage.Action.PLAY_CARD, new PlayCardMessage(card)); + } + + public MessageQueue addPrediction(UUID player, int prediction) { + return add(player, UserInputMessage.Action.MAKE_PREDICTION, new PredictMessage(prediction)); + } + + public MessageQueue addChangePrediction(UUID player, int prediction) { + return add(player, UserInputMessage.Action.CHANGE_PREDICTION, new PredictMessage(prediction)); + } + + public MessageQueue addPickTrump(UUID player, Card.Suit trumpSuit) { + return add(player, UserInputMessage.Action.PICK_TRUMP, new PickTrumpMessage(trumpSuit)); + } + + public MessageQueue addCards(List players, Map> cards, int startPlayer) { + Map nextCards = new HashMap<>(); + int size = players.size(); + boolean changed = true; + while (changed) { + changed = false; + for (int i = 0; i < size; i++) { + var player = players.get((i + startPlayer) % size); + var hand = cards.get(player); + + int next = nextCards.getOrDefault(player, 0); + + if (next < hand.size()) { + nextCards.merge(player, 1, Integer::sum); + var card = hand.get(next); + addCard(player, card); + changed = true; + } + } + } + return this; + } + + public MessageQueue assertThrows(Class exception) { + messages.getLast().setException(exception); + return this; + } + + public void doNotify(ObserverMessage om) { + try { + 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."); + + var queued = messages.poll(); + + var queuedPlayer = queued.getPlayer(); + var queuedAction = queued.getAction(); + var queuedMessage = queued.getMessage(); + var exception = queued.getException(); + + if (exception == null) { + assertEquals(queuedPlayer, player); + assertEquals(queuedAction, message.getAction()); + } + + System.out.println(queuedPlayer + ": " + queuedMessage); + if (exception != null) { + var executionException = Assertions.assertThrows( + ExecutionException.class, + () -> game.onMessage(queuedPlayer, queuedMessage).get(), + "Excepted exception for message " + queuedMessage + " from player " + queuedPlayer + "." + ); + assertInstanceOf( + exception, + executionException.getCause(), + "Excepted exception for message " + queuedMessage + " from player " + queuedPlayer + "." + ); + } else { + Assertions.assertDoesNotThrow(() -> game.onMessage(queuedPlayer, queuedMessage).get()); + return; + } + } + } + } catch (Throwable t) { + game.finish(t); + } + } + + @Override + public void notify(ObserverMessage message) { + executor.execute(() -> doNotify(message)); + } + + @Override + public void notify(UUID player, ObserverMessage message) { + + } + + @Getter + @Setter + @RequiredArgsConstructor + private static class QueuedMessage { + private final UUID player; + private final UserInputMessage.Action action; + private final PlayerMessage message; + private Class exception; + } +} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/GameStateTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/GameStateTest.java new file mode 100644 index 0000000..fe3fbe9 --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/GameStateTest.java @@ -0,0 +1,39 @@ +package eu.jonahbauer.wizard.core.machine.states; + +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GameStateTest { + + @Test + public void getNextPlayer_IsPermutation() { + List players = List.of( + new UUID(0, 1), + new UUID(0, 2), + new UUID(0, 3), + new UUID(0, 4), + new UUID(0, 5), + new UUID(0, 6) + ); + + GameState gameState = mock(GameState.class); + when(gameState.getNextPlayer(any())).thenCallRealMethod(); + when(gameState.get(GameData.PLAYERS)).thenReturn(players); + + Set a = new HashSet<>(players); + Set b = new HashSet<>(); + + a.forEach(player -> b.add(gameState.getNextPlayer(player))); + + assertEquals(a, b); + } +} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrumpTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrumpTest.java new file mode 100644 index 0000000..90f6e8b --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/DeterminingTrumpTest.java @@ -0,0 +1,163 @@ +package eu.jonahbauer.wizard.core.machine.states.round; + +import eu.jonahbauer.wizard.common.messages.observer.HandMessage; +import eu.jonahbauer.wizard.common.messages.observer.StateMessage; +import eu.jonahbauer.wizard.common.messages.observer.TrumpMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.MessageQueue; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.model.Configuration; +import eu.jonahbauer.wizard.core.model.Configurations; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish; +import static eu.jonahbauer.wizard.core.machine.states.GameData.*; +import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; + +public class DeterminingTrumpTest { + private final UUID[] players = new UUID[] { + new UUID(0, 0), + new UUID(0, 1), + new UUID(0, 2), + new UUID(0, 3) + }; + + @SneakyThrows + @SuppressWarnings("SameParameterValue") + private Game performTest(Configuration configuration, int round, Map> hands, Card trumpCard, MessageQueue queue) { + Game game = spy(new Game(configuration, queue)); + doFinish().when(game).transition(any(), any(Predicting.class)); + queue.setGame(game); + + var playerList = List.of(players); + + GameData data = GameData.EMPTY.with( + entry(PLAYERS, playerList), + entry(ROUND, round), + entry(SCORE, Map.of()), + entry(TRUMP_CARD, trumpCard), + entry(HANDS, hands) + ); + + game.resume(new DeterminingTrump(data)); + game.await(); + + verify(game, never()).transition(any(), isNull()); + return game; + } + + @Test + public void run_Simple() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.CLOUD), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue(); + + Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.YELLOW_1, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // determining trump + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.YELLOW_1 && trump.getSuit() == Card.Suit.YELLOW)); + order.verify(game).transition(any(), any(Predicting.class)); // round is finished + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_Simple2() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.CLOUD), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue(); + + Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.GREEN_JESTER, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // determining trump + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.GREEN_JESTER && trump.getSuit() == Card.Suit.NONE)); + order.verify(game).transition(any(), any(Predicting.class)); + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_WithUserInput() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.CLOUD), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addPickTrump(players[0], Card.Suit.GREEN); + + Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.BLUE_WIZARD, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // determining trump + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.BLUE_WIZARD && trump.getSuit() == null)); + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.BLUE_WIZARD && trump.getSuit() == Card.Suit.GREEN)); + order.verify(game).transition(any(), any(Predicting.class)); + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_WithWerewolf() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.CLOUD), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.WEREWOLF) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addPickTrump(players[3], Card.Suit.YELLOW); + + Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.GREEN_1, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // determining trump + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.GREEN_1 && trump.getSuit() == null)); + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.WEREWOLF && trump.getSuit() == null)); + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.WEREWOLF && trump.getSuit() == Card.Suit.YELLOW)); + order.verify(game).notify(eq(players[3]), any(HandMessage.class)); // swap trump card and werewolf + order.verify(game).transition(any(), any(Predicting.class)); + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } +} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRoundTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRoundTest.java deleted file mode 100644 index fa2b354..0000000 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/FinishingRoundTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package eu.jonahbauer.wizard.core.machine.states.round; - -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class FinishingRoundTest { - @Test - public void getPoints() { - UUID player0 = new UUID(0, 0); - UUID player1 = new UUID(0, 1); - UUID player2 = new UUID(0, 2); - - List players = List.of( - player0, - player1, - player2 - ); - - Map predictions = Map.of( - player0, 0, - player1, 3, - player2, 3 - ); - - Map tricks = Map.of( - player0, 0, - player1, 3, - player2, 5 - ); - - FinishingRound state = mock(FinishingRound.class); - when(state.getPoints()).thenCallRealMethod(); - when(state.get(PREDICTIONS)).thenReturn(predictions); - when(state.get(TRICKS)).thenReturn(tricks); - when(state.get(PLAYERS)).thenReturn(players); - - assertEquals(Map.of( - player0, 20, - player1, 50, - player2, -20 - ), state.getPoints()); - } -} 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 0e350ed..e983af3 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 @@ -1,65 +1,170 @@ package eu.jonahbauer.wizard.core.machine.states.round; +import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage; +import eu.jonahbauer.wizard.common.messages.observer.StateMessage; +import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; +import eu.jonahbauer.wizard.common.model.card.Card; import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.GameTestUtils; +import eu.jonahbauer.wizard.core.machine.MessageQueue; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick; import eu.jonahbauer.wizard.core.model.Configuration; +import eu.jonahbauer.wizard.core.model.Configurations; +import lombok.SneakyThrows; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; +import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; public class PredictingTest { - @Test - public void checkPrediction_ThrowsIllegalArgument_IfOutOfBounds() { - Map predictions = Map.of(); + private final UUID[] players = new UUID[] { + new UUID(0, 0), + new UUID(0, 1), + new UUID(0, 2), + new UUID(0, 3) + }; + + @SneakyThrows + @SuppressWarnings("SameParameterValue") + private Game performTest(Configuration configuration, int round, MessageQueue queue) { + Map> hands = Map.of( + players[0], Collections.nCopies(round + 1, Card.HIDDEN), + players[1], Collections.nCopies(round + 1, Card.HIDDEN), + players[2], Collections.nCopies(round + 1, Card.HIDDEN), + players[3], Collections.nCopies(round + 1, Card.HIDDEN) + ); - Configuration config = mock(Configuration.class); - when(config.allowExactPredictions()).thenReturn(true); + Game game = spy(new Game(configuration, queue)); + doFinish().when(game).transition(any(), any(StartingTrick.class)); + queue.setGame(game); - Game game = mock(Game.class); - when(game.getConfig()).thenReturn(config); + var playerList = List.of(players); - Predicting state = mock(Predicting.class); - doCallRealMethod().when(state).checkPrediction(any(), anyInt()); - when(state.get(PREDICTIONS)).thenReturn(predictions); - when(state.getDealer()).thenReturn(new UUID(0,0)); - when(state.get(CURRENT_PLAYER)).thenReturn(new UUID(0,1)); - when(state.get(ROUND)).thenReturn(10); + GameData data = GameData.EMPTY.with( + entry(PLAYERS, playerList), + entry(ROUND, round), + entry(SCORE, Map.of()), + entry(TRUMP_SUIT, Card.Suit.NONE), + entry(PREDICTIONS, GameTestUtils.emptyToIntMap(playerList)), + entry(HANDS, hands), + entry(CURRENT_PLAYER, players[(round + 1) % players.length]) + ); - assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, -1)); - assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 12)); + game.resume(new Predicting(data)); + game.await(); - for (int i = 0; i < 12; i++) { - state.checkPrediction(game, i); - } + verify(game, never()).transition(any(), isNull()); + return game; } @Test - public void checkPrediction_ThrowsIllegalArgument_IfAddsUp() { - UUID player = new UUID(0,0); - Map predictions = Map.of(new UUID(0,1), 10); + public void predicting_Simple() { + // play cards in given order + MessageQueue queue = new MessageQueue() + .addPrediction(players[0], 4) + .addPrediction(players[1], 3) + .addPrediction(players[2], 3) + .addPrediction(players[3], 0); - Configuration config = mock(Configuration.class); - when(config.allowExactPredictions()).thenReturn(false); + Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue); - Game game = mock(Game.class); - when(game.getConfig()).thenReturn(config); + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // predicting + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + 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); - Predicting state = mock(Predicting.class); - doCallRealMethod().when(state).checkPrediction(any(), anyInt()); - when(state.get(PREDICTIONS)).thenReturn(predictions); - when(state.getDealer()).thenReturn(player); - when(state.get(CURRENT_PLAYER)).thenReturn(player); - when(state.get(ROUND)).thenReturn(10); + Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // predicting + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void predicting_AddUp() { + // play cards in given order + MessageQueue queue = new MessageQueue() + .addPrediction(players[0], 1) + .addPrediction(players[1], 1) + .addPrediction(players[2], 1) + .addPrediction(players[3], 1).assertThrows(IllegalArgumentException.class) + .addPrediction(players[3], 0); - assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 1)); + Game game = performTest(Configurations.ANNIVERSARY_2021_PM1, 3, queue); - for (int i = 0; i < 12; i++) { - if (i == 1) continue; - state.checkPrediction(game, i); - } + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // predicting + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(Predicting.class)); // next player + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); } } 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 new file mode 100644 index 0000000..93fe26d --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java @@ -0,0 +1,241 @@ +package eu.jonahbauer.wizard.core.machine.states.round; + +import eu.jonahbauer.wizard.common.messages.observer.*; +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.MessageQueue; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.model.Configuration; +import eu.jonahbauer.wizard.core.model.Configurations; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish; +import static eu.jonahbauer.wizard.core.machine.states.GameData.*; +import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; + +public class RoundTest { + private final UUID[] players = new UUID[] { + new UUID(0, 0), + new UUID(0, 1), + new UUID(0, 2), + new UUID(0, 3) + }; + + @SneakyThrows + private Game performTest(long seed, Configuration configuration, int round, MessageQueue queue) { + Game game = spy(new Game(seed, configuration, queue)); + doFinish().when(game).transition(any(), any(StartingRound.class)); + queue.setGame(game); + + var playerList = List.of(players); + + GameData data = GameData.EMPTY.with( + entry(PLAYERS, playerList), + entry(ROUND, round), + entry(SCORE, Map.of()) + ); + + game.resume(new StartingRound(data)); + game.await(); + + verify(game, never()).transition(any(), isNull()); + return game; + } + + @Test + public void run_Simple() throws ExecutionException, InterruptedException { + MessageQueue queue = new MessageQueue() + .addPrediction(players[3], 3) + .addPrediction(players[0], 0) + .addPrediction(players[1], 3) + .addPrediction(players[2], 1) + // trick 0 + .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 + .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 + .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 + .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 + .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 + .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 + .addCard(players[3], Card.YELLOW_2) + .addCard(players[0], Card.BLUE_2) + .addCard(players[1], Card.BLUE_10) + .addCard(players[2], Card.BLUE_6); + + int round = 6; + Game game = performTest(0L, Configurations.DEFAULT, round, queue); + game.await(); + + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting_round + order.verify(game).notify(any(StateMessage.class)); // dealing + order.verify(game, atLeast(4)).notify(any(), any(HandMessage.class)); // hands + order.verify(game).notify(any(StateMessage.class)); // determining_trump + order.verify(game).notify(any(TrumpMessage.class)); // trump + for (int i = 0; i < players.length; i++) { + order.verify(game).notify(any(StateMessage.class)); // predicting + order.verify(game).notify(any(UserInputMessage.class)); // user input + order.verify(game).notify(any(PredictionMessage.class)); // user input + } + + for (int i = 0; i < round + 1; i++) { + order.verify(game).notify(any(StateMessage.class)); // starting trick + for (int j = 0; j < players.length; j++) { + order.verify(game).notify(any(StateMessage.class)); // playing_card + order.verify(game).notify(any(UserInputMessage.class)); // user input + order.verify(game).notify(any(CardMessage.class)); // user input + } + order.verify(game).notify(any(StateMessage.class)); // finishing_trick + order.verify(game).notify(any(TrickMessage.class)); // trick + } + order.verify(game).notify(any(StateMessage.class)); // finishing_round + order.verify(game).notify(argThat(message -> + message instanceof ScoreMessage score + && score.getPoints().get(players[0]) == -10 + && score.getPoints().get(players[1]) == 50 + && score.getPoints().get(players[2]) == 30 + && score.getPoints().get(players[3]) == -10 + )); // score + order.verify(game).transition(any(), any(StartingRound.class)); // next round + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_Anniversary() throws ExecutionException, InterruptedException { + MessageQueue queue = new MessageQueue() + .addPickTrump(players[2], Card.Suit.YELLOW) + .addPrediction(players[3], 2) + .addPrediction(players[0], 2) + .addPrediction(players[1], 2) + .addPrediction(players[2], 1).assertThrows(IllegalArgumentException.class) + .addPrediction(players[2], 3) + // 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) + .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) + .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) + .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) + .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) + // trick 5 + .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 + .addCard(players[2], Card.BLUE_4) + .addCard(players[3], Card.BLUE_11) + .addCard(players[0], Card.GREEN_1) + .addCard(players[1], Card.GREEN_11); + + int round = 6; + Game game = performTest(227L, Configurations.ANNIVERSARY_2021_PM1, round, queue); + game.await(); + + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting round + order.verify(game).notify(any(StateMessage.class)); // dealing + order.verify(game, times(4)).notify(any(), any(HandMessage.class)); // hands + order.verify(game).notify(any(StateMessage.class)); // determining trump + order.verify(game, times(2)).notify(any(TrumpMessage.class)); // werewolf + order.verify(game).notify(any(UserInputMessage.class)); // user input + order.verify(game).notify(any(TrumpMessage.class)); // user input + order.verify(game).notify(any(), any(HandMessage.class)); // update hand + for (int i = 0; i < players.length; i++) { + order.verify(game).notify(any(StateMessage.class)); + order.verify(game).notify(any(UserInputMessage.class)); + order.verify(game).notify(any(PredictionMessage.class)); + } + for (int i = 0; i < round + 1; i++) { + order.verify(game).notify(any(StateMessage.class)); // starting trick + for (int j = 0; j < players.length; j++) { + order.verify(game).notify(any(StateMessage.class)); // playing_card + order.verify(game).notify(any(UserInputMessage.class)); // user input + order.verify(game).notify(any(CardMessage.class)); // user input + } + 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)); // finishing_round + order.verify(game).notify(argThat(message -> + message instanceof ScoreMessage score + && score.getPoints().get(players[0]) == -10 + && score.getPoints().get(players[1]) == 30 + && score.getPoints().get(players[2]) == -10 + && score.getPoints().get(players[3]) == 40 + )); // score + order.verify(game).transition(any(), any(StartingRound.class)); // next round + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } +} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrickTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrickTest.java deleted file mode 100644 index 2937513..0000000 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/FinishingTrickTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package eu.jonahbauer.wizard.core.machine.states.trick; - -import eu.jonahbauer.wizard.core.machine.states.GameData; -import eu.jonahbauer.wizard.core.model.Card; -import eu.jonahbauer.wizard.core.util.Pair; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.UUID; - -import static eu.jonahbauer.wizard.core.machine.states.GameData.STACK; -import static eu.jonahbauer.wizard.core.machine.states.GameData.TRUMP_SUIT; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class FinishingTrickTest { - private void performTest(List> stack, Card.Suit trumpSuit, Card.Suit trickSuit) { - FinishingTrick state = mock(FinishingTrick.class); - when(state.getWinner()).thenCallRealMethod(); - when(state.get(STACK)).thenReturn(stack); - when(state.get(TRUMP_SUIT)).thenReturn(trumpSuit); - when(state.getTrickSuit()).thenReturn(trickSuit); - - assertNotNull(state.getWinner()); - } - - @Test - public void getWinner_ReturnsFirstWizard() { - List> stack = List.of( - Pair.of(null, Card.RED_1), - Pair.of(null, Card.YELLOW_1), - Pair.of(UUID.randomUUID(), Card.BLUE_WIZARD), - Pair.of(null, Card.BLUE_JESTER), - Pair.of(null, Card.GREEN_11) - ); - - performTest(stack, Card.Suit.YELLOW, Card.Suit.RED); - } - - @Test - public void getWinner_ReturnsHighestTrump_IfNoWizard() { - List> stack = List.of( - Pair.of(null, Card.RED_1), - Pair.of(null, Card.YELLOW_1), - Pair.of(null, Card.BLUE_JESTER), - Pair.of(UUID.randomUUID(), Card.YELLOW_13), - Pair.of(null, Card.GREEN_11) - ); - - performTest(stack, Card.Suit.YELLOW, Card.Suit.RED); - } - - @Test - public void getWinner_ReturnsHighestTrickSuit_IfNeitherWizardNorTrump() { - List> stack = List.of( - Pair.of(null, Card.YELLOW_1), - Pair.of(null, Card.BLUE_JESTER), - Pair.of(UUID.randomUUID(), Card.RED_1), - Pair.of(null, Card.YELLOW_13), - Pair.of(null, Card.GREEN_11) - ); - - performTest(stack, Card.Suit.BLUE, Card.Suit.RED); - } - - @Test - public void getWinner_ReturnsFirstJester_IfOnlyJester() { - List> stack = List.of( - Pair.of(UUID.randomUUID(), Card.GREEN_JESTER), - Pair.of(null, Card.BLUE_JESTER), - Pair.of(null, Card.RED_JESTER), - Pair.of(null, Card.YELLOW_JESTER) - ); - - performTest(stack, Card.Suit.NONE, Card.Suit.NONE); - } -} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCardTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCardTest.java deleted file mode 100644 index 49be417..0000000 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/PlayingCardTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package eu.jonahbauer.wizard.core.machine.states.trick; - -import eu.jonahbauer.wizard.core.machine.Game; -import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; -import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; -import eu.jonahbauer.wizard.core.messages.player.PredictMessage; -import eu.jonahbauer.wizard.core.model.Card; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static eu.jonahbauer.wizard.core.machine.states.GameData.*; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -public class PlayingCardTest { - @Test - public void checkCard_ThrowsIllegalArgument_IfCardNotInHand() { - List hand = List.of(Card.BLUE_1, Card.RED_1, Card.YELLOW_1, Card.GREEN_JESTER, Card.BLUE_WIZARD); - UUID player = new UUID(0, 0); - - PlayingCard state = mock(PlayingCard.class); - doCallRealMethod().when(state).checkCard(any()); - when(state.get(HANDS)).thenReturn(Map.of(player, hand)); - when(state.get(STACK)).thenReturn(List.of()); - when(state.get(CURRENT_PLAYER)).thenReturn(player); - - for (Card value : Card.values()) { - if (hand.contains(value)) continue; - assertThrows(IllegalArgumentException.class, () -> state.checkCard(value)); - } - - verify(state, times(0)).getData(); - } - - @Test - public void checkCard_ThrowsIllegalArgument_IfNotFollowingSuit() { - List hand = List.of(Card.BLUE_1, Card.RED_1, Card.YELLOW_1, Card.GREEN_JESTER, Card.BLUE_WIZARD); - UUID player = new UUID(0, 0); - - PlayingCard state = mock(PlayingCard.class); - doCallRealMethod().when(state).checkCard(any()); - when(state.getTrickSuit()).thenReturn(Card.Suit.YELLOW); - when(state.get(HANDS)).thenReturn(Map.of(player, hand)); - when(state.get(CURRENT_PLAYER)).thenReturn(player); - - assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.BLUE_1)); - assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.RED_1)); - - state.checkCard(Card.YELLOW_1); - state.checkCard(Card.GREEN_JESTER); - state.checkCard(Card.BLUE_WIZARD); - } - - @Test - public void onMessage_ThrowsIllegalState_IfNotCurrentPlayer() { - UUID player = new UUID(0, 0); - UUID player2 = new UUID(0, 1); - - Game game = mock(Game.class); - - PlayingCard state = mock(PlayingCard.class); - doCallRealMethod().when(state).onMessage(any(), any(), any()); - when(state.get(CURRENT_PLAYER)).thenReturn(player); - - assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PlayCardMessage(Card.BLUE_WIZARD))); - assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PredictMessage(1))); - assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PickTrumpMessage(Card.Suit.BLUE))); - } - - @Test - public void onMessage_ThrowsIllegalState_IfNotPlayCard() { - UUID player = new UUID(0, 0); - - Game game = mock(Game.class); - - PlayingCard state = mock(PlayingCard.class); - doCallRealMethod().when(state).onMessage(any(), any(), any()); - when(state.get(CURRENT_PLAYER)).thenReturn(player); - - assertThrows(IllegalStateException.class, () -> state.onMessage(game, player, new PredictMessage(1))); - assertThrows(IllegalStateException.class, () -> state.onMessage(game, player, new PickTrumpMessage(Card.Suit.BLUE))); - } -} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickStateTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickStateTest.java deleted file mode 100644 index 12c8171..0000000 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickStateTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package eu.jonahbauer.wizard.core.machine.states.trick; - -import eu.jonahbauer.wizard.core.model.Card; -import eu.jonahbauer.wizard.core.util.Pair; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.UUID; - -import static eu.jonahbauer.wizard.core.machine.states.GameData.STACK; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class TrickStateTest { - @Test - public void getTrickSuit_ReturnsNone_IfEmpty() { - List> stack = List.of(); - - TrickState trickState = mock(TrickState.class); - when(trickState.getTrickSuit()).thenCallRealMethod(); - when(trickState.get(STACK)).thenReturn(stack); - - assertEquals(trickState.getTrickSuit(), Card.Suit.NONE); - } - - @Test - public void getTrickSuit_ReturnsColor() { - List> stack = List.of( - Pair.of(null, Card.RED_1), - Pair.of(null, Card.YELLOW_1), - Pair.of(null, Card.BLUE_WIZARD), - Pair.of(null, Card.BLUE_JESTER), - Pair.of(null, Card.GREEN_11) - ); - - TrickState trickState = mock(TrickState.class); - when(trickState.getTrickSuit()).thenCallRealMethod(); - when(trickState.get(STACK)).thenReturn(stack); - - assertEquals(trickState.getTrickSuit(), Card.Suit.RED); - } - - @Test - public void getTickSuit_ReturnsNone_IfFirstCardIsWizard() { - List> stack = List.of( - Pair.of(null, Card.BLUE_WIZARD), - Pair.of(null, Card.RED_1), - Pair.of(null, Card.YELLOW_1), - Pair.of(null, Card.BLUE_JESTER), - Pair.of(null, Card.GREEN_11) - ); - - TrickState trickState = mock(TrickState.class); - when(trickState.getTrickSuit()).thenCallRealMethod(); - when(trickState.get(STACK)).thenReturn(stack); - - assertEquals(trickState.getTrickSuit(), Card.Suit.NONE); - } - - @Test - public void getTrickSuit_IgnoresJesters() { - List> stack = List.of( - Pair.of(null, Card.BLUE_JESTER), - Pair.of(null, Card.BLUE_WIZARD), - Pair.of(null, Card.RED_1), - Pair.of(null, Card.YELLOW_1), - Pair.of(null, Card.GREEN_11) - ); - - TrickState trickState = mock(TrickState.class); - when(trickState.getTrickSuit()).thenCallRealMethod(); - when(trickState.get(STACK)).thenReturn(stack); - - assertEquals(trickState.getTrickSuit(), Card.Suit.NONE); - - List> stack2 = List.of( - Pair.of(null, Card.BLUE_JESTER), - Pair.of(null, Card.YELLOW_1), - Pair.of(null, Card.BLUE_WIZARD), - Pair.of(null, Card.RED_1), - Pair.of(null, Card.GREEN_11) - ); - TrickState trickState2 = mock(TrickState.class); - when(trickState2.getTrickSuit()).thenCallRealMethod(); - when(trickState2.get(STACK)).thenReturn(stack2); - - assertEquals(trickState2.getTrickSuit(), Card.Suit.YELLOW); - } -} 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 new file mode 100644 index 0000000..6934597 --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java @@ -0,0 +1,299 @@ +package eu.jonahbauer.wizard.core.machine.states.trick; + +import eu.jonahbauer.wizard.common.messages.observer.*; +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.GameTestUtils; +import eu.jonahbauer.wizard.core.machine.MessageQueue; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound; +import eu.jonahbauer.wizard.core.model.Configuration; +import eu.jonahbauer.wizard.core.model.Configurations; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish; +import static eu.jonahbauer.wizard.core.machine.states.GameData.*; +import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class TrickTest { + + private final UUID[] players = new UUID[] { + new UUID(0, 0), + new UUID(0, 1), + new UUID(0, 2), + new UUID(0, 3) + }; + + @SneakyThrows + @SuppressWarnings("SameParameterValue") + private Game performTest(Configuration configuration, int round, int trick, Map> hands, Card.Suit trump, MessageQueue queue) { + Game game = spy(new Game(configuration, queue)); + doFinish().when(game).transition(any(), any(StartingTrick.class)); + doFinish().when(game).transition(any(), any(FinishingRound.class)); + queue.setGame(game); + + var playerList = List.of(players); + + GameData data = GameData.EMPTY.with( + entry(PLAYERS, playerList), + entry(ROUND, round), + entry(SCORE, Map.of()), + entry(HANDS, hands), + entry(PREDICTIONS, GameTestUtils.emptyToIntMap(playerList)), + entry(TRICKS, GameTestUtils.emptyToIntMap(playerList)), + entry(TRICK, trick), + entry(TRUMP_SUIT, trump), + entry(CURRENT_PLAYER, players[0]) + ); + + game.resume(new StartingTrick(data)); + game.await(); + + return game; + } + + @Test + public void run_Simple() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.YELLOW_1), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addCards(List.of(players), hands, 0); + + Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting trick + for (int i = 0; i < players.length; i++) { + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + 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[3])); // trick with correct winner + order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + @Test + public void run_WithWrongInput() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.YELLOW_1), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // 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) + .addCard(players[2], Card.GREEN_1) + .addPrediction(players[3], 1).assertThrows(IllegalStateException.class) + .addCard(players[3], Card.BLUE_1); + + Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + 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[3])); // trick with correct winner + order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_WithCloud() { + Map> hands = Map.of( + players[0], List.of(Card.RED_1), + players[1], List.of(Card.CLOUD), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addCard(players[0], Card.RED_1) + .addCard(players[1], Card.CLOUD_BLUE) + .addCard(players[2], Card.GREEN_1) + .addCard(players[3], Card.BLUE_1) + .addChangePrediction(players[2], 1); + + Game game = performTest(Configurations.ANNIVERSARY_2021, 0, 0, hands, Card.Suit.GREEN, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting trick + for (int i = 0; i < players.length; i++) { + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + 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[2])); // trick with correct winner + order.verify(game).notify(any(StateMessage.class)); // changing prediction + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(PredictionMessage.class)); // user response + order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_WithJuggler() { + Map> hands = Map.of( + 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) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addCard(players[0], Card.RED_1) + .addCard(players[1], Card.JUGGLER_RED) + .addCard(players[2], Card.GREEN_1) + .addCard(players[3], Card.RED_5); + + Game game = performTest(Configurations.ANNIVERSARY_2021, 1, 0, hands, Card.Suit.YELLOW, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + 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, 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 + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_WithChangeling() { + Map> hands = Map.of( + players[0], List.of(Card.CHANGELING), + players[1], List.of(Card.RED_1), + players[2], List.of(Card.GREEN_1), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addCard(players[0], Card.CHANGELING_JESTER) + .addCard(players[1], Card.RED_1) + .addCard(players[2], Card.GREEN_1) + .addCard(players[3], Card.BLUE_1); + + Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + 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[3])); // trick with correct winner + order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } + + @Test + public void run_WithChangeling2() { + Map> hands = Map.of( + players[0], List.of(Card.CHANGELING), + players[1], List.of(Card.RED_1), + players[2], List.of(Card.GREEN_WIZARD), + players[3], List.of(Card.BLUE_1) + ); + + // play cards in given order + MessageQueue queue = new MessageQueue() + .addCard(players[0], Card.CHANGELING_WIZARD) + .addCard(players[1], Card.RED_1) + .addCard(players[2], Card.GREEN_WIZARD) + .addCard(players[3], Card.BLUE_1); + + Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue); + + // validate messages + InOrder order = inOrder(game); + order.verify(game).notify(any(StateMessage.class)); // starting trick + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + order.verify(game).notify(any(CardMessage.class)); // user response + order.verify(game).notify(any(StateMessage.class)); // playing card + order.verify(game).notify(any(UserInputMessage.class)); // user input request + 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[0])); // trick with correct winner + order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).notify(any(StateMessage.class)); // finish + order.verify(game, never()).notify(any()); + order.verify(game, never()).notify(any(), any()); + } +} diff --git a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/model/card/CardUtilsTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/model/card/CardUtilsTest.java new file mode 100644 index 0000000..8aa6005 --- /dev/null +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/model/card/CardUtilsTest.java @@ -0,0 +1,219 @@ +package eu.jonahbauer.wizard.core.model.card; + +import eu.jonahbauer.wizard.common.model.card.Card; +import eu.jonahbauer.wizard.core.util.Pair; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class CardUtilsTest { + private void performWinnerTest(List> stack, Card.Suit trumpSuit) { + assertNotNull(CardUtils.getWinner(stack, trumpSuit)); + } + + @Test + public void getWinner_ReturnsFirstWizard() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(UUID.randomUUID(), Card.BLUE_WIZARD), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.YELLOW); + } + + @Test + public void getWinner_ReturnsDragon() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.BLUE_WIZARD), + Pair.of(UUID.randomUUID(), Card.DRAGON), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.GREEN_11) + ); + + performWinnerTest(stack, Card.Suit.YELLOW); + } + + @Test + public void getWinner_ReturnsFairy_IfDragon() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.BLUE_WIZARD), + Pair.of(UUID.randomUUID(), Card.FAIRY), + Pair.of(null, Card.DRAGON), + Pair.of(null, Card.GREEN_11) + ); + + performWinnerTest(stack, Card.Suit.YELLOW); + } + + @Test + public void getWinner_ReturnsHighestTrump_IfNoWizard() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(UUID.randomUUID(), Card.YELLOW_13), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.YELLOW); + } + + @Test + public void getWinner_ReturnsHighestSpecialTrump_IfNoWizard() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(UUID.randomUUID(), Card.JUGGLER_YELLOW), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.YELLOW); + } + + @Test + public void getWinner_ReturnsHighestSpecialTrump_IfNoWizard2() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(UUID.randomUUID(), Card.CLOUD_YELLOW), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.YELLOW); + } + + @Test + public void getWinner_ReturnsHighestTrickSuit_IfNeitherWizardNorTrump() { + List> stack = List.of( + Pair.of(null, Card.RED_5), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(UUID.randomUUID(), Card.RED_13), + Pair.of(null, Card.YELLOW_13), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.RED_7), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.BLUE); + } + + @Test + public void getWinner_ReturnsHighestSpecialTrickSuit_IfNeitherWizardNorTrump() { + List> stack = List.of( + Pair.of(null, Card.RED_5), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(UUID.randomUUID(), Card.CLOUD_RED), + Pair.of(null, Card.YELLOW_13), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.RED_7), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.BLUE); + } + + @Test + public void getWinner_ReturnsHighestSpecialTrickSuit_IfNeitherWizardNorTrump2() { + List> stack = List.of( + Pair.of(null, Card.RED_5), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(UUID.randomUUID(), Card.JUGGLER_RED), + Pair.of(null, Card.YELLOW_13), + Pair.of(null, Card.GREEN_11), + Pair.of(null, Card.RED_7), + Pair.of(null, Card.FAIRY) + ); + + performWinnerTest(stack, Card.Suit.BLUE); + } + + @Test + public void getWinner_ReturnsFirstJester_IfOnlyJester() { + List> stack = List.of( + Pair.of(UUID.randomUUID(), Card.GREEN_JESTER), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.RED_JESTER), + Pair.of(null, Card.FAIRY), + Pair.of(null, Card.YELLOW_JESTER) + ); + + performWinnerTest(stack, Card.Suit.RED); + } + + @Test + public void getWinner_ReturnsJester_IfOnlyFairyAndBomb() { + List> stack = List.of( + Pair.of(null, Card.FAIRY), + Pair.of(null, Card.BOMB), + Pair.of(UUID.randomUUID(), Card.RED_JESTER) + ); + + performWinnerTest(stack, Card.Suit.RED); + } + + @Test + public void getTrickSuit_ReturnsNone_IfEmpty() { + List> stack = List.of(); + assertEquals(Card.Suit.NONE, CardUtils.getTrickSuit(stack)); + } + + @Test + public void getTrickSuit_ReturnsColor() { + List> stack = List.of( + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.BLUE_WIZARD), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.GREEN_11) + ); + assertEquals(Card.Suit.RED, CardUtils.getTrickSuit(stack)); + } + + @Test + public void getTickSuit_ReturnsNone_IfFirstCardIsWizard() { + List> stack = List.of( + Pair.of(null, Card.BLUE_WIZARD), + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.GREEN_11) + ); + assertEquals(Card.Suit.NONE, CardUtils.getTrickSuit(stack)); + } + + @Test + public void getTrickSuit_IgnoresJesters() { + List> stack = List.of( + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.BLUE_WIZARD), + Pair.of(null, Card.RED_1), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.GREEN_11) + ); + assertEquals(Card.Suit.NONE, CardUtils.getTrickSuit(stack)); + + List> stack2 = List.of( + Pair.of(null, Card.BLUE_JESTER), + Pair.of(null, Card.YELLOW_1), + Pair.of(null, Card.BLUE_WIZARD), + Pair.of(null, Card.RED_1), + Pair.of(null, Card.GREEN_11) + ); + assertEquals(Card.Suit.YELLOW, CardUtils.getTrickSuit(stack2)); + } +} diff --git a/wizard-server/pom.xml b/wizard-server/pom.xml index e0843cc..e60c456 100644 --- a/wizard-server/pom.xml +++ b/wizard-server/pom.xml @@ -19,7 +19,7 @@ - eu.jonahbauer + ${project.groupId} wizard-core ${project.version}