diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/Context.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/Context.java new file mode 100644 index 0000000..b91dec4 --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/Context.java @@ -0,0 +1,123 @@ +package eu.jonahbauer.wizard.common.machine; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract class Context, C extends Context> { + @Getter + private @NotNull S state; + private final ReentrantLock lock = new ReentrantLock(); + + public Context(@NotNull S initialState) { + this.state = initialState; + } + + /** + * Atomically executes the given supplier and transitions to the returned state if present. + * @see #transition(State) + */ + public void execute(Supplier> transition) { + lock.lock(); + try { + Optional next = transition.get(); + next.ifPresent(this::transition); + } finally { + lock.unlock(); + } + } + + /** + * Atomically applies the given function to the current state and transitions to the returned state if present. + * @see #transition(State) + */ + public void execute(Function<@NotNull S, Optional<@NotNull S>> transition) { + lock.lock(); + try { + Optional next = transition.apply(state); + next.ifPresent(this::transition); + } finally { + lock.unlock(); + } + } + + /** + * Atomically transitions to the specified state calling the state lifecycle methods. + * When an error occurs during execution of the lifecycle methods {@link #handleError(Throwable)} is called. + * @param state the next state + * @see State#onExit(Context) + * @see State#onEnter(Context) + * @see #handleError(Throwable) + */ + public void transition(@NotNull S state) { + lock.lock(); + try { + //noinspection unchecked + this.state.onExit((C) this); + onTransition(this.state, state); + this.state = state; + //noinspection unchecked + state.onEnter((C) this).ifPresent(this::transition); + } catch (Throwable t) { + handleError(t); + } finally { + lock.unlock(); + } + } + + /** + * Atomically checks that the current state is {@linkplain Object#equals(Object) equal to} the specified expected + * state and transitions to the specified next state. + * @param expected the expected current state + * @param next the next state + * @see #transition(State) + * @throws IllegalStateException if the current state is not equal to the expected state + */ + public void transition(@NotNull S expected, @NotNull S next) { + lock.lock(); + try { + if (Objects.equals(expected, this.state)) { + transition(next); + } else { + throw new IllegalStateException("Current state is not " + expected + "."); + } + } finally { + lock.unlock(); + } + } + + /** + * Atomically transitions to the specified state without calling the state lifecycle methods. This method can + * be used to recover from errors occurring during {@link State#onExit(Context)} which would otherwise prevent + * the context from transitioning to another state. + * @param state the next state + */ + protected void forceTransition(@NotNull S state) { + lock.lock(); + try { + onTransition(this.state, state); + this.state = state; + } finally { + lock.unlock(); + } + } + + /** + * Callback method that will synchronously be called on transitioning between two states. + * @param from the previous state + * @param to the next state + */ + protected void onTransition(S from, S to) {} + + /** + * Callback method that will synchronously be called when an error occurs during execution of state lifecycle + * methods. + * @param t the cause + */ + protected abstract void handleError(Throwable t); +} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/State.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/State.java new file mode 100644 index 0000000..43ac6c6 --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/State.java @@ -0,0 +1,11 @@ +package eu.jonahbauer.wizard.common.machine; + +import java.util.Optional; + +public interface State, C extends Context> { + default Optional onEnter(C context) { + return Optional.empty(); + } + + default void onExit(C context) {} +} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/TimeoutContext.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/TimeoutContext.java new file mode 100644 index 0000000..4d1cc31 --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/TimeoutContext.java @@ -0,0 +1,36 @@ +package eu.jonahbauer.wizard.common.machine; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public abstract class TimeoutContext, C extends TimeoutContext> extends Context { + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + public TimeoutContext(@NotNull S initialState) { + super(initialState); + } + + public void timeout(@NotNull S currentState, long delay) { + scheduler.schedule(() -> { + if (Objects.equals(getState(), currentState)) { + execute(() -> { + if (Objects.equals(getState(), currentState)) { + //noinspection unchecked + return currentState.onTimeout((C) this); + } else { + return Optional.empty(); + } + }); + } + }, delay, TimeUnit.MILLISECONDS); + } + + public void shutdownNow() { + scheduler.shutdownNow(); + } +} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/TimeoutState.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/TimeoutState.java new file mode 100644 index 0000000..34a4f6c --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/machine/TimeoutState.java @@ -0,0 +1,9 @@ +package eu.jonahbauer.wizard.common.machine; + +import java.util.Optional; + +public interface TimeoutState, C extends TimeoutContext> extends State { + default Optional onTimeout(C context) { + return Optional.empty(); + } +} 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 f53da5b..65148d0 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 @@ -17,7 +17,7 @@ import java.util.regex.Pattern; public class CLI { public static void main(String[] args) { - GameConfiguration config = Configurations.DEFAULT.withTimeout(0); + GameConfiguration config = Configurations.DEFAULT; Observer observer = (player, msg) -> System.out.println(msg); Game game = new Game(config, observer); var players = List.of( @@ -52,24 +52,15 @@ public class CLI { switch (command) { case "predict" -> { int prediction = Integer.parseInt(param); - game.onMessage(players.get(id), new PredictMessage(prediction)) - .whenComplete((v, err) -> { - if (err != null) err.printStackTrace(); - }); + game.onMessage(players.get(id), new PredictMessage(prediction)); } case "play" -> { Card card = Card.valueOf(param); - game.onMessage(players.get(id), new PlayCardMessage(card)) - .whenComplete((v, err) -> { - if (err != null) err.printStackTrace(); - }); + game.onMessage(players.get(id), new PlayCardMessage(card)); } case "trump" -> { Card.Suit suit = Card.Suit.valueOf(param); - game.onMessage(players.get(id), new PickTrumpMessage(suit)) - .whenComplete((v, err) -> { - if (err != null) err.printStackTrace(); - }); + game.onMessage(players.get(id), new PickTrumpMessage(suit)); } 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 deleted file mode 100644 index 0eb6e91..0000000 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/Context.java +++ /dev/null @@ -1,155 +0,0 @@ -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.atomic.AtomicReference; - -public abstract class Context, C extends Context> { - 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; - - 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; - } - - @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 { - latch.countDown(); - doTransition(null, state); - } - })); - - 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) { - executor.execute(new PriorityRunnable(0, () -> doFinish(exception))); - } - - @NonBlocking - public void cancel() { - finish(new CancellationException()); - } - - /* - * 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 { - finished.get(); - } - - public void timeout(@NotNull S currentState, long delay) { - scheduler.schedule(() -> { - submit(() -> { - if (state == currentState) { - //noinspection unchecked - state.onTimeout((C) this); - } - }); - }, delay, TimeUnit.MILLISECONDS); - } - - @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(); - } - } -} 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 f056586..10e2e88 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 @@ -1,56 +1,58 @@ package eu.jonahbauer.wizard.core.machine; -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.common.machine.TimeoutContext; 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.machine.states.game.Created; +import eu.jonahbauer.wizard.core.machine.states.game.Error; +import eu.jonahbauer.wizard.core.messages.Observer; import eu.jonahbauer.wizard.core.model.GameConfiguration; import eu.jonahbauer.wizard.core.util.Util; import lombok.Getter; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Random; import java.util.UUID; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; -import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; - -public final class Game extends Context { +public final class Game extends TimeoutContext { @Getter private final Random random; @Getter private final GameConfiguration config; private final Observer observer; + private final CompletableFuture future = new CompletableFuture<>(); + public Game(GameConfiguration config, Observer observer) { + super(new Created()); this.random = new Random(); this.config = config; this.observer = observer; } public Game(long seed, GameConfiguration config, Observer observer) { + super(new Created()); this.random = new Random(seed); this.config = config; this.observer = observer; } - public void start(List players) { - start(new Starting(GameData.EMPTY.with(PLAYERS, List.copyOf(players)))); + public void resume(GameState state) { + transition(new Created(), state); } - public void resume(GameState state) { - start(state); + public void start(List players) { + execute(s -> s.start(this, players)); } - public CompletableFuture onMessage(UUID player, PlayerMessage message) { - return submit(() -> { - if (state != null) { - state.onMessage(this, player, message); - } - }); + public void onMessage(UUID player, PlayerMessage message) { + execute(s -> s.onMessage(this, player, message)); } @Override @@ -58,6 +60,13 @@ public final class Game extends Context { notify(new StateMessage(to != null ? Util.toSnakeCase(to.getClass().getSimpleName()) : "null")); } + @Override + protected void handleError(Throwable t) { + // don't use usual transition procedure to prevent errors during onExit from causing stack overflow + forceTransition(new Error(t)); + this.complete(t); + } + public void notify(ObserverMessage message) { try { observer.notify(message); @@ -73,4 +82,18 @@ public final class Game extends Context { t.printStackTrace(); } } + + public void complete(@Nullable Throwable t) { + shutdownNow(); + if (t != null) { + future.completeExceptionally(t); + } else { + future.complete(null); + } + } + + @Blocking + public void await() throws InterruptedException, ExecutionException, CancellationException { + future.get(); + } } 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/GameState.java similarity index 62% rename from wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/GameState.java rename to wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/GameState.java index ad85a7b..0054f31 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/GameState.java @@ -1,45 +1,57 @@ -package eu.jonahbauer.wizard.core.machine.states; +package eu.jonahbauer.wizard.core.machine; -import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.common.machine.TimeoutState; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; +import eu.jonahbauer.wizard.core.machine.states.GameData; import lombok.Getter; import org.jetbrains.annotations.Unmodifiable; +import java.util.List; +import java.util.Optional; import java.util.UUID; import static eu.jonahbauer.wizard.core.machine.states.GameData.CURRENT_PLAYER; import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; @Unmodifiable -public abstract class GameState implements State { +public abstract class GameState implements TimeoutState { @Getter private final GameData data; public GameState(GameData data) { - this.data = data.require(PLAYERS).clean(); + this.data = data.clean(); } - protected final void transition(Game game, GameState state) { - game.transition(this, state); + // + protected final Optional timeout(Game game) { + game.timeout(this, getTimeout(game, false)); + return Optional.empty(); } - protected final void timeout(Game game) { - game.timeout(this, getTimeout(game, false)); + protected final Optional transition(GameState state) { + return Optional.of(state); } protected final long getTimeout(Game game, boolean absolute) { return (absolute ? System.currentTimeMillis() : 0) + game.getConfig().timeout(); } + // - public void onMessage(Game game, UUID player, PlayerMessage message) { + public Optional onMessage(Game game, UUID player, PlayerMessage message) { throw new IllegalStateException("You cannot do that right now."); } + public Optional start(Game game, List players) { + throw new IllegalStateException("Game has already started."); + } + + @Override public String toString() { return getClass().getSimpleName(); } + // public T get(GameData.Key key) { return getData().get(key); } @@ -68,4 +80,5 @@ public abstract class GameState implements State { } } + // } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/State.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/State.java deleted file mode 100644 index 92eadae..0000000 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/State.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.jonahbauer.wizard.core.machine.states; - -import eu.jonahbauer.wizard.core.machine.Context; - -public interface State, C extends Context> { - default void onEnter(C context) {} - default void onTimeout(C context) {} - default void onExit(C context) {} -} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Created.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Created.java new file mode 100644 index 0000000..1665e2b --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Created.java @@ -0,0 +1,25 @@ +package eu.jonahbauer.wizard.core.machine.states.game; + +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.GameState; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public final class Created extends GameState { + public Created() { + super(GameData.EMPTY); + } + + @Override + public Optional start(Game game, List players) { + return transition(new Starting(GameData.EMPTY.with(GameData.PLAYERS, players))); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Created; + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Error.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Error.java new file mode 100644 index 0000000..d5832a9 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Error.java @@ -0,0 +1,15 @@ +package eu.jonahbauer.wizard.core.machine.states.game; + +import eu.jonahbauer.wizard.core.machine.GameState; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import lombok.Getter; + +@Getter +public final class Error extends GameState { + private final Throwable cause; + + public Error(Throwable cause) { + super(GameData.EMPTY); + this.cause = cause; + } +} diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finished.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finished.java new file mode 100644 index 0000000..519cd68 --- /dev/null +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Finished.java @@ -0,0 +1,19 @@ +package eu.jonahbauer.wizard.core.machine.states.game; + +import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.GameState; + +import java.util.Optional; + +public final class Finished extends GameState { + public Finished() { + super(GameData.EMPTY); + } + + @Override + public Optional onEnter(Game context) { + context.complete(null); + return Optional.empty(); + } +} 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 408dcde..fc00dc7 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 @@ -2,9 +2,11 @@ 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.machine.GameState; import eu.jonahbauer.wizard.common.messages.observer.ScoreMessage; +import java.util.Optional; + import static eu.jonahbauer.wizard.core.machine.states.GameData.*; public final class Finishing extends GameState { @@ -14,8 +16,8 @@ public final class Finishing extends GameState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { game.notify(new ScoreMessage(get(SCORE))); - game.finish(); + return transition(new Finished()); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java index cde92a3..1920225 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/game/Starting.java @@ -2,17 +2,21 @@ package eu.jonahbauer.wizard.core.machine.states.game; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.GameData; -import eu.jonahbauer.wizard.core.machine.states.GameState; +import eu.jonahbauer.wizard.core.machine.GameState; import eu.jonahbauer.wizard.core.machine.states.round.StartingRound; +import java.util.Optional; + +import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; + public final class Starting extends GameState { public Starting(GameData data) { - super(data); + super(data.require(PLAYERS)); } @Override - public void onEnter(Game game) { - transition(game, new StartingRound(getData())); + public Optional onEnter(Game game) { + return transition(new StartingRound(getData())); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java index d633346..cf7486e 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/ChangingPrediction.java @@ -6,10 +6,12 @@ import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PredictMessage; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.GameState; import eu.jonahbauer.wizard.core.machine.states.InvalidDataException; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.CHANGE_PREDICTION; @@ -25,24 +27,24 @@ public final class ChangingPrediction extends RoundState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { game.notify(new UserInputMessage(get(CLOUDED_PLAYER), CHANGE_PREDICTION, getTimeout(game, true))); - timeout(game); + return timeout(game); } @Override - public void onMessage(Game game, UUID player, PlayerMessage message) { + public Optional onMessage(Game game, UUID player, PlayerMessage message) { if (get(CLOUDED_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { checkPrediction(predictMessage.getPrediction()); - transition(game, predictMessage.getPrediction()); + return transition(game, predictMessage.getPrediction()); } else { - super.onMessage(game, player, message); + return super.onMessage(game, player, message); } } @Override - public void onTimeout(Game game) { - transition(game, oldPrediction + 1); + public Optional onTimeout(Game game) { + return transition(game, oldPrediction + 1); } private void checkPrediction(int prediction) { @@ -53,7 +55,7 @@ public final class ChangingPrediction extends RoundState { } } - private void transition(Game game, int prediction) { + private Optional transition(Game game, int prediction) { game.notify(new PredictionMessage(get(CLOUDED_PLAYER), prediction)); // add prediction @@ -64,7 +66,7 @@ public final class ChangingPrediction extends RoundState { PREDICTIONS, Map.copyOf(predictions) ); - transition(game, new FinishingRound(data)); + return transition(new FinishingRound(data)); } private void checkData(GameData data) { 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 27cff7f..7d8bb3f 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 @@ -4,12 +4,10 @@ import eu.jonahbauer.wizard.common.messages.observer.HandMessage; import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.GameState; import eu.jonahbauer.wizard.core.model.deck.Deck; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; @@ -20,7 +18,7 @@ public final class Dealing extends RoundState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { Deck deck = new Deck(game.getConfig().cards()); deck.shuffle(game.getRandom()); @@ -39,7 +37,7 @@ public final class Dealing extends RoundState { } Card trumpCard = deck.draw(); - transition(game, new DeterminingTrump( + return transition(new DeterminingTrump( getData().with( HANDS, Map.copyOf(hands), TRUMP_CARD, trumpCard 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 20490fb..838e45b 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 @@ -8,6 +8,7 @@ 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.machine.GameState; import eu.jonahbauer.wizard.core.model.card.GameCards; import org.jetbrains.annotations.NotNull; @@ -25,7 +26,7 @@ public final class DeterminingTrump extends RoundState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { Card trumpCard = get(TRUMP_CARD); // handle werewolf @@ -38,8 +39,7 @@ public final class DeterminingTrump extends RoundState { 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; + return timeout(game); } } @@ -49,30 +49,30 @@ public final class DeterminingTrump extends RoundState { this.player = getDealer(); game.notify(new TrumpMessage(trumpCard, null)); game.notify(new UserInputMessage(this.player, PICK_TRUMP, getTimeout(game, true))); - timeout(game); + return timeout(game); } else { - transition(game, trumpSuit); + return transition(game, trumpSuit); } } @Override - public void onTimeout(Game game) { + public Optional onTimeout(Game game) { Card.Suit[] suits; if (werewolf) { suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW, Card.Suit.NONE}; } else { suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW}; } - transition(game, suits[game.getRandom().nextInt(suits.length)]); + return transition(game, suits[game.getRandom().nextInt(suits.length)]); } @Override - public void onMessage(Game game, UUID player, PlayerMessage message) { + public Optional onMessage(Game game, UUID player, PlayerMessage message) { if (this.player.equals(player) && message instanceof PickTrumpMessage trumpMessage) { checkTrumpSuit(trumpMessage.getTrumpSuit()); - transition(game, trumpMessage.getTrumpSuit()); + return transition(game, trumpMessage.getTrumpSuit()); } else { - super.onMessage(game, player, message); + return super.onMessage(game, player, message); } } @@ -84,7 +84,7 @@ public final class DeterminingTrump extends RoundState { } } - private void transition(Game game, @NotNull Card.Suit trumpSuit) { + private Optional transition(Game game, @NotNull Card.Suit trumpSuit) { GameData data = getData().with( TRUMP_SUIT, trumpSuit, CURRENT_PLAYER, getNextPlayer(getDealer()) @@ -101,6 +101,6 @@ public final class DeterminingTrump extends RoundState { } else { game.notify(new TrumpMessage(get(TRUMP_CARD), trumpSuit)); } - transition(game, new Predicting(data)); + return transition(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 9be3411..12f446d 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,11 +2,13 @@ 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.GameState; import eu.jonahbauer.wizard.core.machine.states.game.Finishing; import eu.jonahbauer.wizard.common.messages.observer.ScoreMessage; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; @@ -18,7 +20,7 @@ public final class FinishingRound extends RoundState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { var points = getPoints(); game.notify(new ScoreMessage(points)); @@ -28,9 +30,9 @@ public final class FinishingRound extends RoundState { GameData data = getData().with(SCORE, Map.copyOf(score)); if (60 / getPlayerCount() == get(ROUND) + 1) { - transition(game, new Finishing(data)); + return transition(new Finishing(data)); } else { - transition(game, new StartingRound(data.with(ROUND, get(ROUND) + 1))); + return transition(new StartingRound(data.with(ROUND, get(ROUND) + 1))); } } 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 05e3d8c..b13082c 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 @@ -2,6 +2,7 @@ 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.GameState; import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick; import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; @@ -11,6 +12,7 @@ import lombok.Getter; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import static eu.jonahbauer.wizard.core.machine.states.GameData.*; @@ -23,20 +25,20 @@ public final class Predicting extends RoundState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { game.notify(new UserInputMessage(get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true))); - timeout(game); + return timeout(game); } @Override - public void onTimeout(Game game) { + public Optional onTimeout(Game game) { try { checkPrediction(game, 0); - transition(game, 0); + return transition(game, 0); } catch (IllegalArgumentException e) { try { checkPrediction(game, 1); - transition(game, 1); + return transition(game, 1); } catch (IllegalArgumentException e2) { throw new AssertionError(e2); } @@ -44,12 +46,12 @@ public final class Predicting extends RoundState { } @Override - public void onMessage(Game game, UUID player, PlayerMessage message) { + public Optional onMessage(Game game, UUID player, PlayerMessage message) { if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { checkPrediction(game, predictMessage.getPrediction()); - transition(game, predictMessage.getPrediction()); + return transition(game, predictMessage.getPrediction()); } else { - super.onMessage(game, player, message); + return super.onMessage(game, player, message); } } @@ -67,7 +69,7 @@ public final class Predicting extends RoundState { } } - private void transition(Game game, int prediction) { + private Optional transition(Game game, int prediction) { game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction)); // add prediction @@ -80,9 +82,9 @@ public final class Predicting extends RoundState { ); if (isLastPlayer()) { - transition(game, new StartingTrick(data)); + return transition(new StartingTrick(data)); } else { - transition(game, new Predicting(data)); + return transition(new Predicting(data)); } } 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 dca30fd..02f822b 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 @@ -1,7 +1,7 @@ package eu.jonahbauer.wizard.core.machine.states.round; import eu.jonahbauer.wizard.core.machine.states.GameData; -import eu.jonahbauer.wizard.core.machine.states.GameState; +import eu.jonahbauer.wizard.core.machine.GameState; import java.util.UUID; @@ -9,7 +9,7 @@ import static eu.jonahbauer.wizard.core.machine.states.GameData.*; public abstract class RoundState extends GameState { public RoundState(GameData data) { - super(data.require(ROUND, SCORE)); + super(data.require(PLAYERS, ROUND, SCORE)); } protected UUID getDealer() { diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java index d8242ae..dd21ea4 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/round/StartingRound.java @@ -2,6 +2,9 @@ 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.GameState; + +import java.util.Optional; public final class StartingRound extends RoundState { public StartingRound(GameData data) { @@ -9,7 +12,7 @@ public final class StartingRound extends RoundState { } @Override - public void onEnter(Game game) { - transition(game, new Dealing(getData())); + public Optional onEnter(Game game) { + return transition(new Dealing(getData())); } } 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 188b978..ea8ae50 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 @@ -3,6 +3,7 @@ package eu.jonahbauer.wizard.core.machine.states.trick; import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.GameState; import eu.jonahbauer.wizard.core.machine.states.InvalidDataException; import eu.jonahbauer.wizard.core.machine.states.round.ChangingPrediction; import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound; @@ -22,7 +23,7 @@ public final class FinishingTrick extends TrickState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { var stack = get(STACK); var cards = stack.stream().map(Pair::second).toList(); @@ -58,14 +59,14 @@ public final class FinishingTrick extends TrickState { if (!hasNextTrick) { if (data.has(CLOUDED_PLAYER)) { - transition(game, new ChangingPrediction(data)); + return transition(new ChangingPrediction(data)); } else { - transition(game, new FinishingRound(data)); + return transition(new FinishingRound(data)); } } else if (juggler) { - transition(game, new Juggling(data)); + return transition(new Juggling(data)); } else { - transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1))); + return transition(new StartingTrick(data.with(TRICK, get(TRICK) + 1))); } } diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java index 171c368..0a918b4 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/Juggling.java @@ -7,6 +7,7 @@ import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.GameData; +import eu.jonahbauer.wizard.core.machine.GameState; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -22,24 +23,24 @@ public final class Juggling extends TrickState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { game.notify(new UserInputMessage(null, JUGGLE_CARD, getTimeout(game, true))); - timeout(game); + return timeout(game); } @Override - public void onTimeout(Game game) { + public Optional onTimeout(Game game) { for (UUID player : get(PLAYERS)) { juggledCards.computeIfAbsent(player, p -> { var hand = get(HANDS).get(p); return hand.get(game.getRandom().nextInt(hand.size())); }); } - juggle(game); + return juggle(game); } @Override - public void onMessage(Game game, UUID player, PlayerMessage message) { + public Optional onMessage(Game game, UUID player, PlayerMessage message) { if (get(PLAYERS).contains(player) && message instanceof JuggleMessage juggleMessage) { Card card = juggleMessage.getCard(); @@ -49,14 +50,16 @@ public final class Juggling extends TrickState { juggledCards.put(player, card); if (juggledCards.size() == get(PLAYERS).size()) { - juggle(game); + return juggle(game); + } else { + return Optional.empty(); } } else { - super.onMessage(game, player, message); + return super.onMessage(game, player, message); } } - private void juggle(Game game) { + private Optional juggle(Game game) { Map newCards = new HashMap<>(); juggledCards.forEach((player, card) -> newCards.put(getNextPlayer(player), card)); @@ -77,6 +80,6 @@ public final class Juggling extends TrickState { TRICK, get(TRICK) + 1 ); - transition(game, new StartingTrick(data)); + return transition(new StartingTrick(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 3a94213..7e80bcc 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 @@ -3,6 +3,7 @@ package eu.jonahbauer.wizard.core.machine.states.trick; import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.Game; +import eu.jonahbauer.wizard.core.machine.GameState; import eu.jonahbauer.wizard.core.machine.states.InvalidDataException; import eu.jonahbauer.wizard.common.messages.observer.CardMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; @@ -25,25 +26,25 @@ public final class PlayingCard extends TrickState { } @Override - public void onEnter(Game game) { + public Optional onEnter(Game game) { game.notify(new UserInputMessage(get(CURRENT_PLAYER), PLAY_CARD, getTimeout(game, true))); - timeout(game); + return timeout(game); } @Override - public void onMessage(Game game, UUID player, PlayerMessage message) { + public Optional onMessage(Game game, UUID player, PlayerMessage message) { if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) { if (cardMessage.getCard() == null) { throw new IllegalArgumentException("Card must not be null."); } - transition(game, cardMessage.getCard()); + return transition(game, cardMessage.getCard()); } else { - super.onMessage(game, player, message); + return super.onMessage(game, player, message); } } @Override - public void onTimeout(Game game) { + public Optional onTimeout(Game game) { var hand = get(HANDS).get(get(CURRENT_PLAYER)); var stack = get(STACK); @@ -52,10 +53,10 @@ public final class PlayingCard extends TrickState { .findAny() .orElseThrow(() -> new AssertionError("Cannot play any card.")); - transition(game, card.getCard()); + return transition(game, card.getCard()); } - private void transition(Game game, @NotNull Card card) { + private Optional transition(Game game, @NotNull Card card) { var currentPlayer = get(CURRENT_PLAYER); // create mutable stack @@ -87,11 +88,10 @@ public final class PlayingCard extends TrickState { .summaryStatistics(); if (summary.getMax() == summary.getMin()) { // everybody has the same amount of cards - transition(game, new FinishingTrick(data)); + return transition(new FinishingTrick(data)); } else { - transition(game, new PlayingCard(data.with(CURRENT_PLAYER, getNextPlayer()))); + return transition(new PlayingCard(data.with(CURRENT_PLAYER, getNextPlayer()))); } - } private static void checkData(GameData data) { diff --git a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java index a614da8..83e958a 100644 --- a/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java +++ b/wizard-core/src/main/java/eu/jonahbauer/wizard/core/machine/states/trick/StartingTrick.java @@ -2,6 +2,9 @@ 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.GameState; + +import java.util.Optional; public final class StartingTrick extends TrickState { public StartingTrick(GameData data) { @@ -9,7 +12,7 @@ public final class StartingTrick extends TrickState { } @Override - public void onEnter(Game game) { - transition(game, new PlayingCard(getData())); + public Optional onEnter(Game game) { + return transition(new PlayingCard(getData())); } } 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 index d2c22e7..b3fa807 100644 --- 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 @@ -20,6 +20,9 @@ public class ColoredCard extends GameCard { @Override public void play(UUID player, List hand, List> stack) { + // check hand first + checkHand(hand); + Card.Suit suit = CardUtils.getTrickSuit(stack); if (this.suit != suit && canFollowSuit(hand, suit)) { throw new IllegalArgumentException("Must follow suit."); 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 index 13f01db..44dd2c3 100644 --- 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 @@ -1,5 +1,6 @@ package eu.jonahbauer.wizard.core.machine; +import eu.jonahbauer.wizard.core.machine.states.game.Finished; import lombok.experimental.UtilityClass; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -7,6 +8,7 @@ import org.mockito.stubbing.Stubber; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -23,7 +25,7 @@ public class GameTestUtils { public static Answer finish() { return invocation -> { Game game = (Game) invocation.getMock(); - game.finish(); + game.execute(() -> Optional.of(new Finished())); 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 index 1dda2df..04d257d 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/MessageQueue.java @@ -4,6 +4,7 @@ import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.messages.player.*; import eu.jonahbauer.wizard.common.model.Card; +import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.messages.Observer; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -11,7 +12,6 @@ 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; @@ -138,23 +138,23 @@ public class MessageQueue implements Observer { 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( + Assertions.assertThrows( exception, - executionException.getCause(), + () -> game.onMessage(queuedPlayer, queuedMessage), "Excepted exception for message " + queuedMessage + " from player " + queuedPlayer + "." ); } else { - Assertions.assertDoesNotThrow(() -> game.onMessage(queuedPlayer, queuedMessage).get()); + Assertions.assertDoesNotThrow(() -> game.onMessage(queuedPlayer, queuedMessage)); } } } } catch (Throwable t) { - game.finish(t); + game.transition(new GameState(GameData.EMPTY) { + @Override + public Optional onEnter(Game context) { + throw t; + } + }); } } 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 index fe3fbe9..6907b60 100644 --- 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 @@ -1,5 +1,6 @@ package eu.jonahbauer.wizard.core.machine.states; +import eu.jonahbauer.wizard.core.machine.GameState; import org.junit.jupiter.api.Test; import java.util.HashSet; 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 index e8fb89e..f750e12 100644 --- 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 @@ -8,6 +8,7 @@ import eu.jonahbauer.wizard.common.model.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.machine.states.game.Finished; import eu.jonahbauer.wizard.core.model.GameConfiguration; import eu.jonahbauer.wizard.core.model.Configurations; import lombok.SneakyThrows; @@ -37,7 +38,7 @@ public class DeterminingTrumpTest { @SuppressWarnings("SameParameterValue") private Game performTest(GameConfiguration configuration, int round, Map> hands, Card trumpCard, MessageQueue queue) { Game game = spy(new Game(configuration, queue)); - doFinish().when(game).transition(any(), any(Predicting.class)); + doFinish().when(game).transition(any(Predicting.class)); queue.setGame(game); var playerList = List.of(players); @@ -53,7 +54,7 @@ public class DeterminingTrumpTest { game.resume(new DeterminingTrump(data)); game.await(); - verify(game, never()).transition(any(), isNull()); + verify(game).transition(any(Finished.class)); return game; } @@ -75,7 +76,7 @@ public class DeterminingTrumpTest { 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).transition(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()); @@ -99,7 +100,7 @@ public class DeterminingTrumpTest { 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).transition(any(Predicting.class)); order.verify(game).notify(any(StateMessage.class)); // finish order.verify(game, never()).notify(any()); order.verify(game, never()).notify(any(), any()); @@ -126,7 +127,7 @@ public class DeterminingTrumpTest { 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).transition(any(Predicting.class)); order.verify(game).notify(any(StateMessage.class)); // finish order.verify(game, never()).notify(any()); order.verify(game, never()).notify(any(), any()); @@ -155,7 +156,7 @@ public class DeterminingTrumpTest { 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).transition(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/PredictingTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/PredictingTest.java index 29b7401..e0f92d0 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 @@ -8,6 +8,7 @@ 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.game.Finished; import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick; import eu.jonahbauer.wizard.core.model.GameConfiguration; import eu.jonahbauer.wizard.core.model.Configurations; @@ -24,7 +25,6 @@ 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 PredictingTest { @@ -46,7 +46,7 @@ public class PredictingTest { ); Game game = spy(new Game(configuration, queue)); - doFinish().when(game).transition(any(), any(StartingTrick.class)); + doFinish().when(game).transition(any(StartingTrick.class)); queue.setGame(game); var playerList = List.of(players); @@ -64,7 +64,7 @@ public class PredictingTest { game.resume(new Predicting(data)); game.await(); - verify(game, never()).transition(any(), isNull()); + verify(game).transition(any(Finished.class)); return game; } @@ -84,16 +84,16 @@ public class PredictingTest { 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).transition(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).transition(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).transition(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).transition(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()); @@ -126,16 +126,16 @@ public class PredictingTest { 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).transition(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).transition(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).transition(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).transition(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()); @@ -160,16 +160,16 @@ public class PredictingTest { 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).transition(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).transition(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).transition(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).transition(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 index f4ad3b6..bbcbcd2 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/round/RoundTest.java @@ -5,8 +5,9 @@ import eu.jonahbauer.wizard.common.model.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.GameConfiguration; +import eu.jonahbauer.wizard.core.machine.states.game.Finished; import eu.jonahbauer.wizard.core.model.Configurations; +import eu.jonahbauer.wizard.core.model.GameConfiguration; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -16,11 +17,10 @@ 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.GameTestUtils.finish; 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 { @@ -34,7 +34,7 @@ public class RoundTest { @SneakyThrows private Game performTest(long seed, GameConfiguration configuration, int round, MessageQueue queue) { Game game = spy(new Game(seed, configuration, queue)); - doFinish().when(game).transition(any(), any(StartingRound.class)); + doCallRealMethod().doAnswer(finish()).when(game).transition(any(StartingRound.class)); queue.setGame(game); var playerList = List.of(players); @@ -48,7 +48,7 @@ public class RoundTest { game.resume(new StartingRound(data)); game.await(); - verify(game, never()).transition(any(), isNull()); + verify(game).transition(any(Finished.class)); return game; } @@ -129,7 +129,7 @@ public class RoundTest { && 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).transition(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()); @@ -249,7 +249,7 @@ public class RoundTest { && 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).transition(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/TrickTest.java b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java index 18c9f37..cacceca 100644 --- a/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java +++ b/wizard-core/src/test/java/eu/jonahbauer/wizard/core/machine/states/trick/TrickTest.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.UUID; import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish; +import static eu.jonahbauer.wizard.core.machine.GameTestUtils.finish; 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; @@ -36,8 +37,8 @@ public class TrickTest { @SuppressWarnings("SameParameterValue") private Game performTest(GameConfiguration 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)); + doCallRealMethod().doAnswer(finish()).when(game).transition(any(StartingTrick.class)); + doFinish().when(game).transition(any(FinishingRound.class)); queue.setGame(game); var playerList = List.of(players); @@ -85,7 +86,7 @@ public class TrickTest { } 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).transition(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()); @@ -132,7 +133,7 @@ public class TrickTest { order.verify(game).notify(any(CardMessage.class)); // user response order.verify(game).notify(any(StateMessage.class)); // finishing trick order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner - order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).transition(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()); @@ -170,7 +171,7 @@ public class TrickTest { 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).transition(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()); @@ -220,7 +221,7 @@ public class TrickTest { order.verify(game).notify(any(StateMessage.class)); // juggling order.verify(game).notify(any(UserInputMessage.class)); // user input request order.verify(game, times(4)).notify(any(), any(HandMessage.class)); - order.verify(game).transition(any(), any(StartingTrick.class)); // there is another trick + order.verify(game).transition(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()); @@ -261,7 +262,7 @@ public class TrickTest { order.verify(game).notify(any(CardMessage.class)); // user response order.verify(game).notify(any(StateMessage.class)); // finishing trick order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner - order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).transition(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()); @@ -302,7 +303,7 @@ public class TrickTest { order.verify(game).notify(any(CardMessage.class)); // user response order.verify(game).notify(any(StateMessage.class)); // finishing trick order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[0])); // trick with correct winner - order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished + order.verify(game).transition(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());