Reworked state machine

main
Jonah Bauer 3 years ago
parent a9e2b1ddd9
commit d037ada8c5

@ -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<S extends State<S,C>, C extends Context<S,C>> {
@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<Optional<@NotNull S>> transition) {
lock.lock();
try {
Optional<S> 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<S> 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);
}

@ -0,0 +1,11 @@
package eu.jonahbauer.wizard.common.machine;
import java.util.Optional;
public interface State<S extends State<S,C>, C extends Context<S,C>> {
default Optional<S> onEnter(C context) {
return Optional.empty();
}
default void onExit(C context) {}
}

@ -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<S extends TimeoutState<S,C>, C extends TimeoutContext<S,C>> extends Context<S,C> {
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();
}
}

@ -0,0 +1,9 @@
package eu.jonahbauer.wizard.common.machine;
import java.util.Optional;
public interface TimeoutState<S extends TimeoutState<S,C>, C extends TimeoutContext<S,C>> extends State<S,C> {
default Optional<S> onTimeout(C context) {
return Optional.empty();
}
}

@ -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);
}

@ -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<S extends State<S,C>, C extends Context<S,C>> {
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<Void> future = new CompletableFuture<>();
private final CompletableFuture<Void> finished = future.whenComplete((v, t) -> {
executor.shutdownNow();
scheduler.shutdownNow();
});
@NonBlocking
protected CompletableFuture<Void> submit(Runnable runnable) {
var future = new CompletableFuture<Void>();
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<RuntimeException> 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();
}
}
}

@ -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<GameState, Game> {
public final class Game extends TimeoutContext<GameState, Game> {
@Getter
private final Random random;
@Getter
private final GameConfiguration config;
private final Observer observer;
private final CompletableFuture<Void> 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<UUID> 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<UUID> players) {
execute(s -> s.start(this, players));
}
public CompletableFuture<Void> 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<GameState, Game> {
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<GameState, Game> {
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();
}
}

@ -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<GameState, Game> {
public abstract class GameState implements TimeoutState<GameState, Game> {
@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);
//<editor-fold desc="Utility Methods" defaultstate="collapsed">
protected final Optional<GameState> 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<GameState> transition(GameState state) {
return Optional.of(state);
}
protected final long getTimeout(Game game, boolean absolute) {
return (absolute ? System.currentTimeMillis() : 0) + game.getConfig().timeout();
}
//</editor-fold>
public void onMessage(Game game, UUID player, PlayerMessage message) {
public Optional<GameState> onMessage(Game game, UUID player, PlayerMessage message) {
throw new IllegalStateException("You cannot do that right now.");
}
public Optional<GameState> start(Game game, List<UUID> players) {
throw new IllegalStateException("Game has already started.");
}
@Override
public String toString() {
return getClass().getSimpleName();
}
//<editor-fold desc="GameData accessors" defaultstate="collapsed">
public <T> T get(GameData.Key<T> key) {
return getData().get(key);
}
@ -68,4 +80,5 @@ public abstract class GameState implements State<GameState, Game> {
}
}
//</editor-fold>
}

@ -1,9 +0,0 @@
package eu.jonahbauer.wizard.core.machine.states;
import eu.jonahbauer.wizard.core.machine.Context;
public interface State<S extends State<S,C>, C extends Context<S,C>> {
default void onEnter(C context) {}
default void onTimeout(C context) {}
default void onExit(C context) {}
}

@ -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<GameState> start(Game game, List<UUID> players) {
return transition(new Starting(GameData.EMPTY.with(GameData.PLAYERS, players)));
}
@Override
public boolean equals(Object obj) {
return obj instanceof Created;
}
}

@ -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;
}
}

@ -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<GameState> onEnter(Game context) {
context.complete(null);
return Optional.empty();
}
}

@ -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<GameState> onEnter(Game game) {
game.notify(new ScoreMessage(get(SCORE)));
game.finish();
return transition(new Finished());
}
}

@ -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<GameState> onEnter(Game game) {
return transition(new StartingRound(getData()));
}
}

@ -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<GameState> 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<GameState> 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<GameState> 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<GameState> 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) {

@ -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<GameState> 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

@ -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<GameState> 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<GameState> 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<GameState> 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<GameState> 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));
}
}

@ -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<GameState> 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)));
}
}

@ -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<GameState> 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<GameState> 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<GameState> 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<GameState> 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));
}
}

@ -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() {

@ -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<GameState> onEnter(Game game) {
return transition(new Dealing(getData()));
}
}

@ -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<GameState> 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)));
}
}

@ -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<GameState> 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<GameState> 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<GameState> 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<GameState> juggle(Game game) {
Map<UUID, Card> 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));
}
}

@ -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<GameState> 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<GameState> 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<GameState> 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<GameState> 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) {

@ -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<GameState> onEnter(Game game) {
return transition(new PlayingCard(getData()));
}
}

@ -20,6 +20,9 @@ public class ColoredCard extends GameCard {
@Override
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> 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.");

@ -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 <T> Answer<T> finish() {
return invocation -> {
Game game = (Game) invocation.getMock();
game.finish();
game.execute(() -> Optional.of(new Finished()));
return null;
};
}

@ -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<GameState> onEnter(Game context) {
throw t;
}
});
}
}

@ -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;

@ -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<UUID, List<Card>> 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());

@ -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());

@ -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());

@ -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<UUID, List<Card>> 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());

Loading…
Cancel
Save