Reworked state machine
This commit is contained in:
@@ -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) {
|
||||
start(state);
|
||||
transition(new Created(), state);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> onMessage(UUID player, PlayerMessage message) {
|
||||
return submit(() -> {
|
||||
if (state != null) {
|
||||
state.onMessage(this, player, message);
|
||||
}
|
||||
});
|
||||
public void start(List<UUID> players) {
|
||||
execute(s -> s.start(this, players));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected final void timeout(Game game) {
|
||||
//<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 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.");
|
||||
|
Reference in New Issue
Block a user