- refactored GameData
- removed reference to Context in State
This commit is contained in:
parent
d04e894937
commit
1775604e1d
@ -1,10 +1,13 @@
|
||||
package eu.jonahbauer.wizard.core;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameState;
|
||||
import eu.jonahbauer.wizard.core.messages.Observer;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.model.Configuration;
|
||||
import eu.jonahbauer.wizard.core.model.Configurations;
|
||||
|
||||
import java.util.List;
|
||||
@ -15,7 +18,9 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class CLI {
|
||||
public static void main(String[] args) {
|
||||
Game game = new Game(Configurations.DEFAULT, (player, msg) -> System.out.println(msg));
|
||||
Configuration config = Configurations.DEFAULT.withTimeout(0);
|
||||
Observer observer = (player, msg) -> System.out.println(msg);
|
||||
Game game = new Game(config, observer);
|
||||
var players = List.of(
|
||||
UUID.randomUUID(),
|
||||
UUID.randomUUID(),
|
||||
@ -25,13 +30,15 @@ public class CLI {
|
||||
|
||||
game.start(players);
|
||||
|
||||
GameState state = null;
|
||||
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
Pattern pattern = Pattern.compile("(\\d) (predict|play|trump) (.*)");
|
||||
Pattern pattern = Pattern.compile("(\\d) ([a-z]+) (.*)");
|
||||
while (scanner.hasNextLine()) {
|
||||
try {
|
||||
Matcher matcher = pattern.matcher(scanner.nextLine());
|
||||
if (!matcher.find()) {
|
||||
System.err.println("Format is \"(\\\\d) (predict|play|trump) (.*)\"");
|
||||
System.err.println("Format is \"(\\\\d) ([a-z]+) (.*)\"");
|
||||
continue;
|
||||
}
|
||||
String player = matcher.group(1);
|
||||
|
@ -8,15 +8,15 @@ import java.util.concurrent.*;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public abstract class Context<T extends State> {
|
||||
public abstract class Context<S extends State<S,C>, C extends Context<S,C>> {
|
||||
protected final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
protected T state;
|
||||
protected S state;
|
||||
protected final ReentrantLock lock = new ReentrantLock();
|
||||
private final Condition finishCondition = lock.newCondition();
|
||||
private boolean finished;
|
||||
private Throwable exception;
|
||||
|
||||
protected void start(@NotNull T state) {
|
||||
protected void start(@NotNull S state) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (finished) throw new IllegalStateException("Context has already finished.");
|
||||
@ -26,14 +26,16 @@ public abstract class Context<T extends State> {
|
||||
}
|
||||
}
|
||||
|
||||
public void transition(T currentState, T newState) {
|
||||
public void transition(S currentState, S newState) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (state == currentState) {
|
||||
state = newState;
|
||||
if (currentState != null) currentState.onExit();
|
||||
if (currentState != null) //noinspection unchecked
|
||||
currentState.onExit((C) this);
|
||||
onTransition(currentState, newState);
|
||||
if (newState != null) newState.onEnter();
|
||||
if (newState != null) //noinspection unchecked
|
||||
newState.onEnter((C) this);
|
||||
} else {
|
||||
throw new IllegalStateException("Current state does not match.");
|
||||
}
|
||||
@ -65,6 +67,11 @@ public abstract class Context<T extends State> {
|
||||
finish(new CancellationException());
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isDone() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void await() throws InterruptedException, ExecutionException, CancellationException {
|
||||
lock.lock();
|
||||
@ -84,12 +91,13 @@ public abstract class Context<T extends State> {
|
||||
}
|
||||
}
|
||||
|
||||
public void timeout(@NotNull T currentState, long delay) {
|
||||
public void timeout(@NotNull S currentState, long delay) {
|
||||
scheduler.schedule(() -> {
|
||||
lock.lock();
|
||||
try {
|
||||
if (state == currentState) {
|
||||
state.onTimeout();
|
||||
//noinspection unchecked
|
||||
state.onTimeout((C) this);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
handleError(t);
|
||||
@ -100,8 +108,16 @@ public abstract class Context<T extends State> {
|
||||
}
|
||||
|
||||
protected void handleError(Throwable t) {
|
||||
finish(t);
|
||||
lock.lock();
|
||||
try {
|
||||
if (!isDone()) {
|
||||
finish(t);
|
||||
t.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onTransition(T from, T to) {}
|
||||
protected void onTransition(S from, S to) {}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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;
|
||||
@ -12,7 +13,9 @@ import lombok.Getter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class Game extends Context<GameState> {
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS;
|
||||
|
||||
public final class Game extends Context<GameState, Game> {
|
||||
@Getter
|
||||
private final Configuration config;
|
||||
private final Observer observer;
|
||||
@ -23,17 +26,26 @@ public final class Game extends Context<GameState> {
|
||||
}
|
||||
|
||||
public void start(List<UUID> players) {
|
||||
start(new Starting(this, GameData.builder().players(players).build()));
|
||||
start(new Starting(new GameData().with(PLAYERS, List.copyOf(players))));
|
||||
}
|
||||
|
||||
public void resume(GameState state) {
|
||||
start(state);
|
||||
}
|
||||
|
||||
public GameState stop() {
|
||||
GameState state = this.state;
|
||||
if (state != null) {
|
||||
finish();
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void onMessage(UUID player, PlayerMessage message) {
|
||||
lock.lock();
|
||||
try {
|
||||
state.onMessage(player, message);
|
||||
state.onMessage(this, player, message);
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (Throwable t) {
|
||||
|
@ -1,334 +0,0 @@
|
||||
package eu.jonahbauer.wizard.core.machine;
|
||||
|
||||
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@With
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue", "OptionalUsedAsFieldOrParameterType", "OptionalAssignedToNull"})
|
||||
public final class GameData {
|
||||
private final @Unmodifiable List<UUID> players;
|
||||
private final @Unmodifiable Map<UUID, Integer> score;
|
||||
|
||||
private final Integer round;
|
||||
private final @Unmodifiable Map<UUID, @Unmodifiable List<Card>> hands;
|
||||
private final Optional<Card> trumpCard;
|
||||
private final Card.Suit trumpSuit;
|
||||
private final @Unmodifiable Map<UUID, Integer> predictions;
|
||||
private final @Unmodifiable Map<UUID, Integer> tricks;
|
||||
|
||||
private final Integer trick;
|
||||
private final @Unmodifiable List<Pair<UUID, Card>> stack;
|
||||
|
||||
private final UUID currentPlayer;
|
||||
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredPlayers;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredRound;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredTrick;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredHands;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredTrumpCard;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredTrumpSuit;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredStack;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredPredictions;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredTricks;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredScore;
|
||||
private @EqualsAndHashCode.Exclude transient boolean requiredCurrentPlayer;
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
private GameData(@Unmodifiable List<UUID> players,
|
||||
@Unmodifiable Map<UUID, Integer> score,
|
||||
Integer round,
|
||||
@Unmodifiable Map<UUID, @Unmodifiable List<Card>> hands,
|
||||
Optional<Card> trumpCard,
|
||||
Card.Suit trumpSuit,
|
||||
@Unmodifiable Map<UUID, Integer> predictions,
|
||||
@Unmodifiable Map<UUID, Integer> tricks,
|
||||
Integer trick,
|
||||
@Unmodifiable List<Pair<UUID, Card>> stack,
|
||||
UUID currentPlayer)
|
||||
{
|
||||
this.players = players != null ? List.copyOf(players) : null;
|
||||
this.score = score != null ? Map.copyOf(score) : null;
|
||||
|
||||
this.round = round;
|
||||
this.hands = hands != null ? Map.copyOf(hands) : null;
|
||||
this.trumpCard = trumpCard;
|
||||
this.trumpSuit = trumpSuit;
|
||||
this.predictions = predictions != null ? Map.copyOf(predictions) : null;
|
||||
this.tricks = tricks != null ? Map.copyOf(tricks) : null;
|
||||
|
||||
this.trick = trick;
|
||||
this.stack = stack != null ? List.copyOf(stack) : null;
|
||||
|
||||
this.currentPlayer = currentPlayer;
|
||||
}
|
||||
|
||||
//<editor-fold desc="Getters" defaultstate="collapsed">
|
||||
public List<UUID> getPlayers() {
|
||||
if (players == null) throw new UnsupportedOperationException();
|
||||
return players;
|
||||
}
|
||||
|
||||
public int getRound() {
|
||||
if (round == null) throw new UnsupportedOperationException();
|
||||
return round;
|
||||
}
|
||||
|
||||
public int getTrick() {
|
||||
if (trick == null) throw new UnsupportedOperationException();
|
||||
return trick;
|
||||
}
|
||||
|
||||
public Map<UUID, List<Card>> getHands() {
|
||||
if (hands == null) throw new UnsupportedOperationException();
|
||||
return hands;
|
||||
}
|
||||
|
||||
public Card getTrumpCard() {
|
||||
if (trumpCard == null) throw new UnsupportedOperationException();
|
||||
return trumpCard.orElse(null);
|
||||
}
|
||||
|
||||
public Card.Suit getTrumpSuit() {
|
||||
if (trumpSuit == null) throw new UnsupportedOperationException();
|
||||
return trumpSuit;
|
||||
}
|
||||
|
||||
public List<Pair<UUID, Card>> getStack() {
|
||||
if (stack == null) throw new UnsupportedOperationException();
|
||||
return stack;
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getPredictions() {
|
||||
if (predictions == null) throw new UnsupportedOperationException();
|
||||
return predictions;
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getTricks() {
|
||||
if (tricks == null) throw new UnsupportedOperationException();
|
||||
return tricks;
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getScore() {
|
||||
if (score == null) throw new UnsupportedOperationException();
|
||||
return score;
|
||||
}
|
||||
|
||||
public UUID getCurrentPlayer() {
|
||||
if (currentPlayer == null) throw new UnsupportedOperationException();
|
||||
return currentPlayer;
|
||||
}
|
||||
|
||||
public UUID getPlayer(int index) {
|
||||
return getPlayers().get(index);
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return getPlayers().size();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="Requirements" defaultstate="collapsed">
|
||||
public GameData requirePlayers() {
|
||||
if (players == null) throw new AssertionError();
|
||||
requiredPlayers = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireRound() {
|
||||
if (round == null) throw new AssertionError();
|
||||
requiredRound = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireTrick() {
|
||||
if (trick == null) throw new AssertionError();
|
||||
requiredTrick = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireHands() {
|
||||
if (hands == null) throw new AssertionError();
|
||||
requiredHands = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireAllHands() {
|
||||
requirePlayers();
|
||||
requireHands();
|
||||
for (UUID uuid : players) {
|
||||
if (!hands.containsKey(uuid)) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireTrumpCard() {
|
||||
if (trumpCard == null) throw new AssertionError();
|
||||
requiredTrumpCard = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireTrumpSuit() {
|
||||
if (trumpSuit == null) throw new AssertionError();
|
||||
requiredTrumpSuit = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireStack() {
|
||||
if (stack == null) throw new AssertionError();
|
||||
requiredStack = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requirePredictions() {
|
||||
if (predictions == null) throw new AssertionError();
|
||||
requiredPredictions = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireAllPredictions() {
|
||||
requirePlayers();
|
||||
requirePredictions();
|
||||
for (UUID uuid : players) {
|
||||
if (!predictions.containsKey(uuid)) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireTricks() {
|
||||
if (tricks == null) throw new AssertionError();
|
||||
requiredTricks = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireScore() {
|
||||
if (score == null) throw new AssertionError();
|
||||
requiredScore = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData requireCurrentPlayer() {
|
||||
if (currentPlayer == null) throw new AssertionError();
|
||||
requiredCurrentPlayer = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameData onlyRequired() {
|
||||
var builder = toBuilder();
|
||||
if (!requiredPlayers) builder.players(null);
|
||||
if (!requiredRound) builder.round(null);
|
||||
if (!requiredTrick) builder.trick(null);
|
||||
if (!requiredHands) builder.hands(null);
|
||||
if (!requiredTrumpCard) builder.trumpCard(null);
|
||||
if (!requiredTrumpSuit) builder.trumpSuit(null);
|
||||
if (!requiredStack) builder.stack(null);
|
||||
if (!requiredPredictions) builder.predictions(null);
|
||||
if (!requiredTricks) builder.tricks(null);
|
||||
if (!requiredScore) builder.score(null);
|
||||
if (!requiredCurrentPlayer) builder.currentPlayer(null);
|
||||
requiredPlayers = false;
|
||||
requiredRound = false;
|
||||
requiredTrick = false;
|
||||
requiredHands = false;
|
||||
requiredTrumpCard = false;
|
||||
requiredTrumpSuit = false;
|
||||
requiredStack = false;
|
||||
requiredPredictions = false;
|
||||
requiredTricks = false;
|
||||
requiredScore = false;
|
||||
requiredCurrentPlayer = false;
|
||||
return builder.build();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="Wither" defaultstate="collapsed">
|
||||
public GameData withPrediction(UUID player, int prediction) {
|
||||
Map<UUID, Integer> predictions = new HashMap<>(getPredictions());
|
||||
predictions.put(player, prediction);
|
||||
return withPredictions(predictions);
|
||||
}
|
||||
|
||||
public GameData withNextPlayer() {
|
||||
int index = getPlayers().indexOf(getCurrentPlayer());
|
||||
UUID next;
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException();
|
||||
} else if (index == getPlayerCount() - 1) {
|
||||
next = getPlayers().get(0);
|
||||
} else {
|
||||
next = getPlayers().get(index + 1);
|
||||
}
|
||||
return withCurrentPlayer(next);
|
||||
}
|
||||
|
||||
public GameData withNextTrick() {
|
||||
int trick = getTrick();
|
||||
return withTrick(trick + 1);
|
||||
}
|
||||
|
||||
public GameData withNextRound() {
|
||||
int round = getRound();
|
||||
return withRound(round + 1);
|
||||
}
|
||||
|
||||
public GameData withCardPlayed(UUID player, Card card) {
|
||||
List<Card> hand = new ArrayList<>(getHands().get(player));
|
||||
hand.remove(card);
|
||||
Map<UUID, List<Card>> hands = new HashMap<>(getHands());
|
||||
hands.put(player, hand);
|
||||
List<Pair<UUID, Card>> stack = new ArrayList<>(getStack());
|
||||
stack.add(Pair.of(player, card));
|
||||
return withStack(stack).withHands(hands);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder("(");
|
||||
if (players != null) builder.append("players=").append(players).append(", ");
|
||||
if (round != null) builder.append("round=").append(round).append(", ");
|
||||
if (trick != null) builder.append("trick=").append(trick).append(", ");
|
||||
if (hands != null) builder.append("hands=").append(hands).append(", ");
|
||||
if (trumpCard != null) builder.append("trumpCard=").append(trumpCard).append(", ");
|
||||
if (trumpSuit != null) builder.append("trumpSuit=").append(trumpSuit).append(", ");
|
||||
if (stack != null) builder.append("stack=").append(stack).append(", ");
|
||||
if (predictions != null) builder.append("predictions=").append(predictions).append(", ");
|
||||
if (tricks != null) builder.append("tricks=").append(tricks).append(", ");
|
||||
if (currentPlayer != null) builder.append("currentPlayer=").append(currentPlayer).append(", ");
|
||||
if (builder.length() > 1) builder.setLength(builder.length() - 2);
|
||||
builder.append(")");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
// An interface for @lombok.experimental.Delegate
|
||||
public interface Getters {
|
||||
@NotNull List<UUID> getPlayers();
|
||||
@NotNull Map<UUID, Integer> getScore();
|
||||
|
||||
int getRound();
|
||||
@NotNull Map<UUID, List<Card>> getHands();
|
||||
@Nullable Card getTrumpCard();
|
||||
@NotNull Card.Suit getTrumpSuit();
|
||||
@NotNull Map<UUID, Integer> getPredictions();
|
||||
@NotNull Map<UUID, Integer> getTricks();
|
||||
|
||||
int getTrick();
|
||||
@NotNull List<Pair<UUID, Card>> getStack();
|
||||
|
||||
@NotNull UUID getCurrentPlayer();
|
||||
@NotNull UUID getPlayer(int index);
|
||||
int getPlayerCount();
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states;
|
||||
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Unmodifiable
|
||||
@EqualsAndHashCode(of = "values")
|
||||
public final class GameData {
|
||||
private static final int SIZE = 11;
|
||||
|
||||
public static final Key<List<UUID>> PLAYERS = new Key<>("players", 0);
|
||||
public static final Key<Map<UUID, Integer>> SCORE = new Key<>("score", 1);
|
||||
public static final Key<Integer> ROUND = new Key<>("round", 2);
|
||||
public static final Key<Map<UUID, List<Card>>> HANDS = new Key<>("hands", 3);
|
||||
public static final Key<Card> TRUMP_CARD = new Key<>("trumpCard", 4);
|
||||
public static final Key<Card.Suit> TRUMP_SUIT = new Key<>("trumpSuit", 5);
|
||||
public static final Key<Map<UUID, Integer>> PREDICTIONS = new Key<>("predictions", 6);
|
||||
public static final Key<Map<UUID, Integer>> TRICKS = new Key<>("tricks", 7);
|
||||
public static final Key<Integer> TRICK = new Key<>("trick", 8);
|
||||
public static final Key<List<Pair<UUID, Card>>> STACK = new Key<>("stack", 9);
|
||||
public static final Key<UUID> CURRENT_PLAYER = new Key<>("currentPlayer", 10);
|
||||
|
||||
private final Object[] values;
|
||||
private final boolean[] present;
|
||||
private transient final boolean[] required = new boolean[SIZE];
|
||||
|
||||
public GameData() {
|
||||
this.values = new Object[SIZE];
|
||||
this.present = new boolean[SIZE];
|
||||
}
|
||||
|
||||
private GameData(Object[] values, boolean[] present) {
|
||||
this.values = values;
|
||||
this.present = present;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped or {@code null} if this map contains no mapping for the
|
||||
* key.
|
||||
* @param key the key whose associated value is to be returned
|
||||
* @param <T> the value type
|
||||
* @return the value to which the specified key is mapped, or null if this map contains no mapping for the key
|
||||
*/
|
||||
public <T> T get(@NotNull Key<T> key) {
|
||||
int index = key.index();
|
||||
if (!present[index]) throw new NoSuchElementException();
|
||||
|
||||
//noinspection unchecked
|
||||
return (T) values[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this map with the value for the specified key changed or {@code this} if the current value
|
||||
* is equal to the new value.
|
||||
* @param key the key whose associated value is to be changed
|
||||
* @param value value to be associated with the specified key
|
||||
* @param <T> the value type
|
||||
* @return the newly generated map or {@code this} is no changes had to be made
|
||||
*/
|
||||
public <T> GameData with(@NotNull Key<T> key, T value) {
|
||||
int index = key.index();
|
||||
|
||||
if (present[index] && Objects.equals(values[index], value)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Object[] newValues = Arrays.copyOf(this.values, SIZE);
|
||||
boolean[] newPresent = Arrays.copyOf(this.present, SIZE);
|
||||
newValues[index] = value;
|
||||
newPresent[index] = true;
|
||||
return new GameData(newValues, newPresent);
|
||||
}
|
||||
|
||||
public <T1, T2> GameData with(@NotNull Key<T1> key1, T1 value1, Key<T2> key2, T2 value2) {
|
||||
int index1 = key1.index();
|
||||
int index2 = key2.index();
|
||||
|
||||
if (present[index1]
|
||||
&& present[index2]
|
||||
&& Objects.equals(values[index1], value1)
|
||||
&& Objects.equals(values[index2], value2)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Object[] newValues = Arrays.copyOf(this.values, SIZE);
|
||||
boolean[] newPresent = Arrays.copyOf(this.present, SIZE);
|
||||
newValues[index1] = value1;
|
||||
newPresent[index1] = true;
|
||||
newValues[index2] = value2;
|
||||
newPresent[index2] = true;
|
||||
return new GameData(newValues, newPresent);
|
||||
}
|
||||
|
||||
public <T1, T2, T3> GameData with(@NotNull Key<T1> key1, T1 value1, Key<T2> key2, T2 value2, Key<T3> key3, T3 value3) {
|
||||
int index1 = key1.index();
|
||||
int index2 = key2.index();
|
||||
int index3 = key3.index();
|
||||
|
||||
if (present[index1]
|
||||
&& present[index2]
|
||||
&& present[index3]
|
||||
&& Objects.equals(values[index1], value1)
|
||||
&& Objects.equals(values[index2], value2)
|
||||
&& Objects.equals(values[index3], value3)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Object[] newValues = Arrays.copyOf(this.values, SIZE);
|
||||
boolean[] newPresent = Arrays.copyOf(this.present, SIZE);
|
||||
newValues[index1] = value1;
|
||||
newPresent[index1] = true;
|
||||
newValues[index2] = value2;
|
||||
newPresent[index2] = true;
|
||||
newValues[index3] = value3;
|
||||
newPresent[index3] = true;
|
||||
return new GameData(newValues, newPresent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code this} if this map contains a mapping for the specified key or throws a
|
||||
* {@link NoSuchElementException} otherwise.
|
||||
* @param key key whose presence in this map is to be tested
|
||||
* @return {@code this}
|
||||
* @throws NoSuchElementException if this map contains no mapping for the specified key
|
||||
* @see Map#containsKey(Object)
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public GameData require(@NotNull Key<?> key) {
|
||||
if (!present[key.index()]) {
|
||||
throw new NoSuchElementException("Could not find required value '" + key + "'.");
|
||||
}
|
||||
required[key.index()] = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code this} if this map contains a mapping for each of the specified keys or throws a
|
||||
* {@link NoSuchElementException} otherwise.
|
||||
* @param keys keys whose presence in this map is to be tested
|
||||
* @return {@code this}
|
||||
* @throws NoSuchElementException if this map contains no mapping for at least one of the specified keys
|
||||
* @see #require(Key)
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public GameData require(Key<?>...keys) {
|
||||
for (Key<?> key : keys) {
|
||||
require(key);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code this} if this map contains a mapping for both the specified keys and the specified map contains
|
||||
* a mapping for each entry in the specified list.
|
||||
* @param list key to a list whose entries' presence is to be tested
|
||||
* @param map key to a map which must contain the specified keys
|
||||
* @param <K> key type
|
||||
* @param <V> value type
|
||||
* @return {@code this}
|
||||
* @throws NoSuchElementException if this map contains no mapping for at least one of the specified keys or if
|
||||
* the associated map contains no mapping for at least one of the entries of
|
||||
* the associated list
|
||||
*/
|
||||
@Contract("_,_ -> this")
|
||||
public <K, V> GameData requireEach(Key<? extends Collection<K>> list, Key<Map<K,V>> map) {
|
||||
require(list, map);
|
||||
var mapValue = get(map);
|
||||
var listValue = get(list);
|
||||
for (K k : listValue) {
|
||||
if (!mapValue.containsKey(k)) throw new NoSuchElementException();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retains only the mappings that have been required since object creation.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public GameData clean() {
|
||||
Object[] newValues = Arrays.copyOf(this.values, SIZE);
|
||||
boolean[] newPresent = Arrays.copyOf(this.present, SIZE);
|
||||
|
||||
boolean modified = false;
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
if (!required[i]) {
|
||||
newValues[i] = null;
|
||||
newPresent[i] = false;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) return new GameData(newValues, newPresent);
|
||||
else return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Value
|
||||
@Accessors(fluent = true)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Key<T> {
|
||||
String name;
|
||||
int index;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +1,37 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Delegate;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.CURRENT_PLAYER;
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS;
|
||||
|
||||
@Unmodifiable
|
||||
public abstract class GameState implements State {
|
||||
public abstract class GameState implements State<GameState, Game> {
|
||||
@Getter
|
||||
private final Game game;
|
||||
@Getter
|
||||
@Delegate(types = GameData.Getters.class)
|
||||
private final GameData data;
|
||||
|
||||
public GameState(Game game, GameData data) {
|
||||
this.game = game;
|
||||
this.data = data.requirePlayers().onlyRequired();
|
||||
public GameState(GameData data) {
|
||||
this.data = data.require(PLAYERS).clean();
|
||||
}
|
||||
|
||||
protected final void transition(GameState state) {
|
||||
getGame().transition(this, state);
|
||||
protected final void transition(Game game, GameState state) {
|
||||
game.transition(this, state);
|
||||
}
|
||||
|
||||
protected final void timeout() {
|
||||
getGame().timeout(this, getTimeout(false));
|
||||
protected final void timeout(Game game) {
|
||||
game.timeout(this, getTimeout(game, false));
|
||||
}
|
||||
|
||||
protected final long getTimeout(boolean absolute) {
|
||||
return (absolute ? System.currentTimeMillis() : 0) + getGame().getConfig().timeout();
|
||||
protected final long getTimeout(Game game, boolean absolute) {
|
||||
return (absolute ? System.currentTimeMillis() : 0) + game.getConfig().timeout();
|
||||
}
|
||||
|
||||
public void onMessage(UUID player, PlayerMessage message) {
|
||||
public void onMessage(Game game, UUID player, PlayerMessage message) {
|
||||
throw new IllegalStateException("You cannot do that right now.");
|
||||
}
|
||||
|
||||
@ -42,4 +39,33 @@ public abstract class GameState implements State {
|
||||
public String toString() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
public <T> T get(GameData.Key<T> key) {
|
||||
return getData().get(key);
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return get(PLAYERS).size();
|
||||
}
|
||||
|
||||
public UUID getPlayer(int index) {
|
||||
return get(PLAYERS).get(index);
|
||||
}
|
||||
|
||||
public UUID getNextPlayer() {
|
||||
return getNextPlayer(get(CURRENT_PLAYER));
|
||||
}
|
||||
|
||||
public UUID getNextPlayer(UUID player) {
|
||||
var players = get(PLAYERS);
|
||||
var index = players.indexOf(player);
|
||||
if (index == -1) {
|
||||
throw new AssertionError("Player is not a valid player.");
|
||||
} else if (index == players.size() - 1) {
|
||||
return players.get(0);
|
||||
} else {
|
||||
return players.get(index + 1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states;
|
||||
|
||||
public class InvalidDataException extends RuntimeException {
|
||||
public InvalidDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states;
|
||||
|
||||
public interface State {
|
||||
default void onEnter() {}
|
||||
default void onTimeout() {}
|
||||
default void onExit() {}
|
||||
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) {}
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
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.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameState;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class Finishing extends GameState {
|
||||
|
||||
public Finishing(Game game, GameData data) {
|
||||
super(game, data.requireScore());
|
||||
public Finishing(GameData data) {
|
||||
super(data.require(SCORE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
getGame().notify(new ScoreMessage(getScore()));
|
||||
getGame().finish();
|
||||
public void onEnter(Game game) {
|
||||
game.notify(new ScoreMessage(get(SCORE)));
|
||||
game.finish();
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,25 @@
|
||||
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.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameState;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.StartingRound;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class Starting extends GameState {
|
||||
|
||||
public Starting(Game game, GameData data) {
|
||||
super(game, data);
|
||||
public Starting(GameData data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
transition(new StartingRound(getGame(), getData().withRound(0).withScore(Collections.emptyMap())));
|
||||
public void onEnter(Game game) {
|
||||
transition(game, new StartingRound(getData().with(
|
||||
ROUND, 0,
|
||||
SCORE, Map.of()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,46 @@
|
||||
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.GameData;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.HandMessage;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.model.Deck;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class Dealing extends RoundState {
|
||||
private transient Map<UUID, List<Card>> hands;
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public Dealing(Game game, GameData data) {
|
||||
super(game, data);
|
||||
public final class Dealing extends RoundState {
|
||||
|
||||
public Dealing(GameData data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
Deck deck = new Deck(getGame().getConfig().cards());
|
||||
public void onEnter(Game game) {
|
||||
Deck deck = new Deck(game.getConfig().cards());
|
||||
deck.shuffle();
|
||||
|
||||
hands = new HashMap<>();
|
||||
var hands = new HashMap<UUID, List<Card>>();
|
||||
|
||||
int dealer = getRound();
|
||||
int round = get(ROUND);
|
||||
int playerCount = getPlayerCount();
|
||||
int cardCount = getRound() + 1;
|
||||
int cardCount = round + 1;
|
||||
for (int i = 1; i <= playerCount; i++) {
|
||||
int player = (dealer + i) % playerCount;
|
||||
int player = (round + i) % playerCount;
|
||||
hands.put(getPlayer(player), deck.draw(cardCount));
|
||||
}
|
||||
|
||||
Optional<Card> trumpCard = Optional.ofNullable(deck.draw());
|
||||
for (UUID player : get(PLAYERS)) {
|
||||
game.notify(player, new HandMessage(player, hands.get(player)));
|
||||
}
|
||||
|
||||
transition(new DeterminingTrump(
|
||||
getGame(),
|
||||
getData().withHands(hands)
|
||||
.withTrumpCard(trumpCard)
|
||||
Card trumpCard = deck.draw();
|
||||
transition(game, new DeterminingTrump(
|
||||
getData().with(
|
||||
HANDS, Map.copyOf(hands),
|
||||
TRUMP_CARD, trumpCard
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit() {
|
||||
if (hands != null) {
|
||||
for (UUID player : getPlayers()) {
|
||||
getGame().notify(player, new HandMessage(player, hands.get(player)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,87 +1,71 @@
|
||||
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.GameData;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.TrumpMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PICK_TRUMP;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public final class DeterminingTrump extends RoundState {
|
||||
private transient Card.Suit trumpSuit;
|
||||
|
||||
public DeterminingTrump(Game game, GameData data) {
|
||||
super(game, data.requireAllHands().requireTrumpCard());
|
||||
public DeterminingTrump(GameData data) {
|
||||
super(data.require(TRUMP_CARD).requireEach(PLAYERS, HANDS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
Card trumpCard = getTrumpCard();
|
||||
public void onEnter(Game game) {
|
||||
Card trumpCard = getData().get(TRUMP_CARD);
|
||||
Card.Suit trumpSuit = trumpCard != null ? trumpCard.getTrumpSuit() : Card.Suit.NONE;
|
||||
if (trumpSuit == null) {
|
||||
getGame().notify(new TrumpMessage(getTrumpCard(), null));
|
||||
getGame().notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(true)));
|
||||
timeout();
|
||||
game.notify(new TrumpMessage(trumpCard, null));
|
||||
game.notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(game, true)));
|
||||
timeout(game);
|
||||
} else {
|
||||
this.trumpSuit = trumpSuit;
|
||||
transition();
|
||||
transition(game, trumpSuit);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout() {
|
||||
Card.Suit[] suits = new Card.Suit[] {Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW};
|
||||
this.trumpSuit = suits[(int)(Math.random() * suits.length)];
|
||||
transition();
|
||||
public void onTimeout(Game game) {
|
||||
Card.Suit[] suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW};
|
||||
transition(game, suits[(int) (Math.random() * suits.length)]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit() {
|
||||
if (trumpSuit != null) {
|
||||
getGame().notify(new TrumpMessage(getTrumpCard(), trumpSuit));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(UUID player, PlayerMessage message) {
|
||||
public void onMessage(Game game, UUID player, PlayerMessage message) {
|
||||
if (getDealer().equals(player) && message instanceof PickTrumpMessage trumpMessage) {
|
||||
checkTrumpSuit(trumpMessage.getTrumpSuit());
|
||||
this.trumpSuit = trumpMessage.getTrumpSuit();
|
||||
transition();
|
||||
transition(game, trumpMessage.getTrumpSuit());
|
||||
} else {
|
||||
super.onMessage(player, message);
|
||||
super.onMessage(game, player, message);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void checkTrumpSuit(Card.Suit suit) {
|
||||
Card.Suit[] suits = new Card.Suit[] {Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW};
|
||||
Card.Suit[] suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW};
|
||||
for (Card.Suit s : suits) {
|
||||
if (s == suit) return;
|
||||
}
|
||||
throw new IllegalArgumentException("Trump suit must be one of " + Arrays.toString(suits) + ".");
|
||||
}
|
||||
|
||||
private void transition() {
|
||||
if (trumpSuit == null) throw new AssertionError();
|
||||
|
||||
transition(new Predicting(
|
||||
getGame(),
|
||||
getData().withTrumpSuit(trumpSuit)
|
||||
.withPredictions(Collections.emptyMap())
|
||||
.withCurrentPlayer(getDealer())
|
||||
.withNextPlayer()
|
||||
));
|
||||
private void transition(Game game, @NotNull Card.Suit trumpSuit) {
|
||||
game.notify(new TrumpMessage(getData().get(TRUMP_CARD), trumpSuit));
|
||||
transition(game, new Predicting(getData().with(
|
||||
TRUMP_SUIT, trumpSuit,
|
||||
PREDICTIONS, Map.of(),
|
||||
CURRENT_PLAYER, getNextPlayer(getDealer())
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.round;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
|
||||
import eu.jonahbauer.wizard.core.machine.states.game.Finishing;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
@ -10,47 +11,38 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class FinishingRound extends RoundState {
|
||||
private transient Map<UUID, Integer> points;
|
||||
|
||||
public FinishingRound(Game game, GameData data) {
|
||||
super(game, data.requireAllPredictions().requireTricks().requireScore());
|
||||
|
||||
int tricks = data.getTricks().values().stream().mapToInt(i -> i).sum();
|
||||
if (tricks != data.getRound() + 1) {
|
||||
throw new AssertionError("Unexpected number of tricks in round " + data.getRound() + ": " + tricks + ".");
|
||||
}
|
||||
public FinishingRound(GameData data) {
|
||||
super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE));
|
||||
checkData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
points = getPoints();
|
||||
public void onEnter(Game game) {
|
||||
var points = getPoints();
|
||||
game.notify(new ScoreMessage(points));
|
||||
|
||||
var score = new HashMap<>(getScore());
|
||||
points.forEach((uuid, p) -> score.compute(uuid, (u, total) -> total == null ? p : total + p));
|
||||
var score = new HashMap<>(get(SCORE));
|
||||
points.forEach((uuid, amount) -> score.merge(uuid, amount, Integer::sum));
|
||||
|
||||
GameData data = getData().withScore(score);
|
||||
GameData data = getData().with(SCORE, Map.copyOf(score));
|
||||
|
||||
if (60 / getPlayerCount() == getRound() + 1) {
|
||||
transition(new Finishing(getGame(), data));
|
||||
if (60 / getPlayerCount() == get(ROUND) + 1) {
|
||||
transition(game, new Finishing(data));
|
||||
} else {
|
||||
transition(new StartingRound(getGame(), data.withNextRound()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit() {
|
||||
if (points != null) {
|
||||
getGame().notify(new ScoreMessage(points));
|
||||
transition(game, new StartingRound(data.with(ROUND, get(ROUND) + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Map<UUID, Integer> getPoints() {
|
||||
var points = new HashMap<UUID, Integer>();
|
||||
for (UUID player : getPlayers()) {
|
||||
int prediction = getPredictions().get(player);
|
||||
int tricks = getTricks().getOrDefault(player, 0);
|
||||
for (UUID player : get(PLAYERS)) {
|
||||
int prediction = get(PREDICTIONS).get(player);
|
||||
int tricks = get(TRICKS).getOrDefault(player, 0);
|
||||
|
||||
if (tricks == prediction) {
|
||||
points.put(player, 20 + 10 * tricks);
|
||||
@ -60,4 +52,12 @@ public final class FinishingRound extends RoundState {
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private static void checkData(GameData data) {
|
||||
// the number of tricks played should be equal to the number of tricks in total
|
||||
int tricks = data.get(TRICKS).values().stream().mapToInt(i -> i).sum();
|
||||
if (tricks != data.get(ROUND) + 1) {
|
||||
throw new InvalidDataException("Unexpected number of tricks in round " + data.get(ROUND) + ": " + tricks + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.PredictionMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
|
||||
@ -10,36 +10,34 @@ import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.MAKE_PREDICTION;
|
||||
|
||||
@Getter
|
||||
public final class Predicting extends RoundState {
|
||||
private transient Integer prediction;
|
||||
|
||||
public Predicting(Game game, GameData data) {
|
||||
super(game, data.requireAllHands().requirePredictions().requireTrumpSuit().requireCurrentPlayer());
|
||||
public Predicting(GameData data) {
|
||||
super(data.requireEach(PLAYERS, HANDS).require(PREDICTIONS, TRUMP_SUIT, CURRENT_PLAYER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
getGame().notify(new UserInputMessage(getCurrentPlayer(), MAKE_PREDICTION, getTimeout(true)));
|
||||
timeout();
|
||||
public void onEnter(Game game) {
|
||||
game.notify(new UserInputMessage(getData().get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true)));
|
||||
timeout(game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout() {
|
||||
public void onTimeout(Game game) {
|
||||
try {
|
||||
checkPrediction(0);
|
||||
this.prediction = 0;
|
||||
transition();
|
||||
checkPrediction(game, 0);
|
||||
transition(game, 0);
|
||||
} catch (IllegalArgumentException e) {
|
||||
try {
|
||||
checkPrediction(1);
|
||||
this.prediction = 1;
|
||||
transition();
|
||||
checkPrediction(game, 1);
|
||||
transition(game, 1);
|
||||
} catch (IllegalArgumentException e2) {
|
||||
throw new AssertionError(e2);
|
||||
}
|
||||
@ -47,57 +45,54 @@ public final class Predicting extends RoundState {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit() {
|
||||
if (prediction != null) {
|
||||
getGame().notify(new PredictionMessage(getCurrentPlayer(), prediction));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(UUID player, PlayerMessage message) {
|
||||
if (getCurrentPlayer().equals(player) && message instanceof PredictMessage predictMessage) {
|
||||
checkPrediction(predictMessage.getPrediction());
|
||||
this.prediction = predictMessage.getPrediction();
|
||||
transition();
|
||||
public void onMessage(Game game, UUID player, PlayerMessage message) {
|
||||
if (getData().get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
|
||||
checkPrediction(game, predictMessage.getPrediction());
|
||||
transition(game, predictMessage.getPrediction());
|
||||
} else {
|
||||
super.onMessage(player, message);
|
||||
super.onMessage(game, player, message);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void checkPrediction(int prediction) {
|
||||
if (prediction < 0 || prediction > getRound() + 1) {
|
||||
throw new IllegalArgumentException("Prediction must be between 0 and " + (getRound() + 1) + ".");
|
||||
void checkPrediction(Game game, int prediction) {
|
||||
int round = get(ROUND);
|
||||
if (prediction < 0 || prediction > round + 1) {
|
||||
throw new IllegalArgumentException("Prediction must be between 0 and " + (round + 1) + ".");
|
||||
}
|
||||
|
||||
if (!getGame().getConfig().allowExactPredictions() && isLastPlayer()) {
|
||||
int sum = getPredictions().values().stream().mapToInt(i -> i).sum();
|
||||
if (sum + prediction == getRound() + 1) {
|
||||
if (!game.getConfig().allowExactPredictions() && isLastPlayer()) {
|
||||
int sum = get(PREDICTIONS).values().stream().mapToInt(i -> i).sum();
|
||||
if (sum + prediction == round + 1) {
|
||||
throw new IllegalArgumentException("Predictions must not add up.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void transition() {
|
||||
if (prediction == null) throw new AssertionError();
|
||||
private void transition(Game game, int prediction) {
|
||||
game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction));
|
||||
|
||||
GameData data = getData().withPrediction(getCurrentPlayer(), prediction)
|
||||
.withNextPlayer();
|
||||
// add prediction
|
||||
var predictions = new HashMap<>(get(PREDICTIONS));
|
||||
predictions.put(get(CURRENT_PLAYER), prediction);
|
||||
|
||||
GameData data = getData().with(
|
||||
PREDICTIONS, Map.copyOf(predictions),
|
||||
CURRENT_PLAYER, getNextPlayer()
|
||||
);
|
||||
|
||||
if (isLastPlayer()) {
|
||||
transition(new StartingTrick(
|
||||
getGame(),
|
||||
data.withTrick(0).withTricks(Collections.emptyMap())
|
||||
));
|
||||
data = data.with(
|
||||
TRICK, 0,
|
||||
TRICKS, Map.of()
|
||||
);
|
||||
transition(game, new StartingTrick(data));
|
||||
} else {
|
||||
transition(new Predicting(
|
||||
getGame(),
|
||||
data
|
||||
));
|
||||
transition(game, new Predicting(data));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLastPlayer() {
|
||||
return getDealer().equals(getCurrentPlayer());
|
||||
return getDealer().equals(get(CURRENT_PLAYER));
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.round;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameState;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public abstract class RoundState extends GameState {
|
||||
public RoundState(Game game, GameData data) {
|
||||
super(game, data.requireRound().requireScore());
|
||||
public RoundState(GameData data) {
|
||||
super(data.require(ROUND, SCORE));
|
||||
}
|
||||
|
||||
protected UUID getDealer() {
|
||||
return getPlayer(getRound() % getPlayerCount());
|
||||
return getPlayer(getData().get(ROUND) % getPlayerCount());
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
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.GameData;
|
||||
|
||||
public final class StartingRound extends RoundState {
|
||||
public StartingRound(Game game, GameData data) {
|
||||
super(game, data);
|
||||
public StartingRound(GameData data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
transition(new Dealing(getGame(), getData()));
|
||||
public void onEnter(Game game) {
|
||||
transition(game, new Dealing(getData()));
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +1,48 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.TrickMessage;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class FinishingTrick extends TrickState {
|
||||
private transient UUID winner;
|
||||
|
||||
public FinishingTrick(Game game, GameData data) {
|
||||
super(game, data.requireStack());
|
||||
assert assertions();
|
||||
public FinishingTrick(GameData data) {
|
||||
super(data.require(STACK));
|
||||
checkData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
this.winner = getWinner();
|
||||
public void onEnter(Game game) {
|
||||
var winner = getWinner();
|
||||
game.notify(new TrickMessage(winner, get(STACK).stream().map(Pair::second).toList()));
|
||||
|
||||
var tricks = new HashMap<>(getTricks());
|
||||
tricks.compute(winner, (k, t) -> t == null ? 1 : t + 1);
|
||||
GameData data = getData().withTricks(tricks);
|
||||
var tricks = new HashMap<>(get(TRICKS));
|
||||
tricks.merge(winner, 1, Integer::sum);
|
||||
|
||||
if (getTrick() < getRound()) {
|
||||
transition(new StartingTrick(
|
||||
getGame(),
|
||||
data.withCurrentPlayer(winner)
|
||||
.withNextTrick()
|
||||
));
|
||||
GameData data = getData().with(TRICKS, Map.copyOf(tricks));
|
||||
|
||||
if (get(TRICK) < get(ROUND)) {
|
||||
transition(game, new StartingTrick(data.with(
|
||||
CURRENT_PLAYER, winner,
|
||||
TRICK, get(TRICK) + 1
|
||||
)));
|
||||
} else {
|
||||
transition(new FinishingRound(getGame(), data));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit() {
|
||||
if (winner != null) {
|
||||
getGame().notify(new TrickMessage(winner, getStack().stream().map(Pair::second).toList()));
|
||||
transition(game, new FinishingRound(data));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
UUID getWinner() {
|
||||
var wizard = getStack().stream()
|
||||
var wizard = get(STACK).stream()
|
||||
.filter(pair -> pair.second().getSuit() == Card.Suit.WIZARD)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
@ -57,16 +50,15 @@ public final class FinishingTrick extends TrickState {
|
||||
return wizard.first();
|
||||
}
|
||||
|
||||
if (getStack().stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) {
|
||||
return getStack().get(0).first();
|
||||
if (get(STACK).stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) {
|
||||
return get(STACK).get(0).first();
|
||||
}
|
||||
|
||||
var trumpSuit = getTrumpSuit();
|
||||
var trumpSuit = get(TRUMP_SUIT);
|
||||
var suit = getTrickSuit();
|
||||
return getStack().stream()
|
||||
return get(STACK).stream()
|
||||
.max(
|
||||
Comparator.<Pair<UUID, Card>>comparingInt(pair -> pair.second()
|
||||
.getSuit() == trumpSuit ? 1 : 0)
|
||||
Comparator.<Pair<UUID, Card>>comparingInt(pair -> pair.second().getSuit() == trumpSuit ? 1 : 0)
|
||||
.thenComparing(pair -> pair.second().getSuit() == suit ? 1 : 0)
|
||||
.thenComparing(pair -> pair.second().getValue())
|
||||
)
|
||||
@ -74,16 +66,20 @@ public final class FinishingTrick extends TrickState {
|
||||
.first();
|
||||
}
|
||||
|
||||
private boolean assertions() {
|
||||
if (getStack().size() != getPlayerCount()) {
|
||||
throw new AssertionError("Unexpected stack size: " + getStack().size() + " (expected " + getPlayerCount() + ").");
|
||||
private static void checkData(GameData data) {
|
||||
// every player has played exactly one card
|
||||
int stackSize = data.get(STACK).size();
|
||||
int playerCount = data.get(PLAYERS).size();
|
||||
if (stackSize != playerCount) {
|
||||
throw new InvalidDataException("Unexpected stack size: " + stackSize + " (expected " + playerCount + ").");
|
||||
}
|
||||
int expectedSize = getRound() - getTrick();
|
||||
for (List<Card> hand : getHands().values()) {
|
||||
|
||||
// every player has round - trick cards left on his hand
|
||||
int expectedSize = data.get(ROUND) - data.get(TRICK);
|
||||
for (List<Card> hand : data.get(HANDS).values()) {
|
||||
if (hand.size() != expectedSize) {
|
||||
throw new AssertionError("Unexpected hand size: " + hand.size() + " (expected " + expectedSize + ").");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +1,49 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.CardMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PLAY_CARD;
|
||||
|
||||
public final class PlayingCard extends TrickState {
|
||||
private transient Card card;
|
||||
|
||||
public PlayingCard(Game game, GameData data) {
|
||||
super(game, data.requireStack());
|
||||
assert assertions();
|
||||
public PlayingCard(GameData data) {
|
||||
super(data.require(STACK));
|
||||
checkData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
getGame().notify(new UserInputMessage(getCurrentPlayer(), PLAY_CARD, getTimeout(true)));
|
||||
timeout();
|
||||
public void onEnter(Game game) {
|
||||
game.notify(new UserInputMessage(get(CURRENT_PLAYER), PLAY_CARD, getTimeout(game, true)));
|
||||
timeout(game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(UUID player, PlayerMessage message) {
|
||||
if (getCurrentPlayer().equals(player) && message instanceof PlayCardMessage cardMessage) {
|
||||
public void onMessage(Game game, UUID player, PlayerMessage message) {
|
||||
if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) {
|
||||
checkCard(cardMessage.getCard());
|
||||
this.card = cardMessage.getCard();
|
||||
transition();
|
||||
transition(game, cardMessage.getCard());
|
||||
} else {
|
||||
super.onMessage(player, message);
|
||||
super.onMessage(game, player, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout() {
|
||||
var hand = getHands().get(getCurrentPlayer());
|
||||
this.card = hand.stream().filter(c -> {
|
||||
public void onTimeout(Game game) {
|
||||
var hand = get(HANDS).get(get(CURRENT_PLAYER));
|
||||
var card = hand.stream().filter(c -> {
|
||||
try {
|
||||
checkCard(c);
|
||||
return true;
|
||||
@ -51,19 +51,12 @@ public final class PlayingCard extends TrickState {
|
||||
return false;
|
||||
}
|
||||
}).findAny().orElseThrow(() -> new AssertionError("Cannot play any card."));
|
||||
transition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit() {
|
||||
if (card != null) {
|
||||
getGame().notify(new CardMessage(getCurrentPlayer(), card));
|
||||
}
|
||||
transition(game, card);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void checkCard(Card card) {
|
||||
var hand = getHands().get(getCurrentPlayer());
|
||||
var hand = get(HANDS).get(get(CURRENT_PLAYER));
|
||||
if (!hand.contains(card)) {
|
||||
throw new IllegalArgumentException("You do not have this card on your hand.");
|
||||
}
|
||||
@ -76,34 +69,51 @@ public final class PlayingCard extends TrickState {
|
||||
}
|
||||
}
|
||||
|
||||
private void transition() {
|
||||
if (card == null) throw new AssertionError();
|
||||
private void transition(Game game, @NotNull Card card) {
|
||||
var currentPlayer = get(CURRENT_PLAYER);
|
||||
game.notify(new CardMessage(currentPlayer, card));
|
||||
|
||||
GameData data = getData().withCardPlayed(getCurrentPlayer(), card);
|
||||
// add card to stack
|
||||
var stack = get(STACK);
|
||||
var mutableStack = new ArrayList<Pair<UUID, Card>>(stack.size() + 1);
|
||||
mutableStack.addAll(stack);
|
||||
mutableStack.add(Pair.of(currentPlayer, card));
|
||||
|
||||
var summary = data.getHands().values().stream()
|
||||
// remove card from hand
|
||||
var hands = get(HANDS);
|
||||
var mutableHands = new HashMap<>(hands);
|
||||
var hand = hands.get(currentPlayer);
|
||||
var mutableHand = new ArrayList<>(hand);
|
||||
mutableHand.remove(card);
|
||||
mutableHands.put(currentPlayer, List.copyOf(mutableHand));
|
||||
|
||||
GameData data = getData().with(
|
||||
STACK, List.copyOf(mutableStack),
|
||||
HANDS, Map.copyOf(mutableHands)
|
||||
);
|
||||
|
||||
var summary = data.get(HANDS).values().stream()
|
||||
.mapToInt(Collection::size)
|
||||
.summaryStatistics();
|
||||
|
||||
if (summary.getMax() == summary.getMin()) {
|
||||
transition(new FinishingTrick(getGame(), data));
|
||||
if (summary.getMax() == summary.getMin()) { // everybody has the same amount of cards
|
||||
transition(game, new FinishingTrick(data));
|
||||
} else {
|
||||
transition(new PlayingCard(getGame(), data.withNextPlayer()));
|
||||
transition(game, new PlayingCard(data.with(CURRENT_PLAYER, getNextPlayer())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean assertions() {
|
||||
int expectedSize = getRound() - getTrick();
|
||||
for (List<Card> hand : getHands().values()) {
|
||||
private static void checkData(GameData data) {
|
||||
int expectedSize = data.get(ROUND) - data.get(TRICK);
|
||||
for (List<Card> hand : data.get(HANDS).values()) {
|
||||
if (hand.size() != expectedSize && hand.size() != expectedSize + 1) {
|
||||
throw new AssertionError("Unexpected hand size: " + hand.size() + " (expected " + expectedSize + " or " + (expectedSize + 1) + ").");
|
||||
throw new InvalidDataException("Unexpected hand size: " + hand.size() + " (expected " + expectedSize + " or " + (expectedSize + 1) + ").");
|
||||
}
|
||||
}
|
||||
int size = getHands().get(getCurrentPlayer()).size();
|
||||
int size = data.get(HANDS).get(data.get(CURRENT_PLAYER)).size();
|
||||
if (size != expectedSize + 1) {
|
||||
throw new AssertionError("Unexpected current player's hand size: " + size + " (expected " + (expectedSize + 1) + ")");
|
||||
throw new InvalidDataException("Unexpected current player's hand size: " + size + " (expected " + (expectedSize + 1) + ")");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,19 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class StartingTrick extends TrickState {
|
||||
public StartingTrick(Game game, GameData data) {
|
||||
super(game, data);
|
||||
public StartingTrick(GameData data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter() {
|
||||
transition(new PlayingCard(
|
||||
getGame(),
|
||||
getData().withStack(Collections.emptyList())
|
||||
));
|
||||
public void onEnter(Game game) {
|
||||
transition(game, new PlayingCard(getData().with(STACK, List.of())));
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,22 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.RoundState;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public abstract class TrickState extends RoundState {
|
||||
public TrickState(Game game, GameData data) {
|
||||
public TrickState(GameData data) {
|
||||
super(
|
||||
game,
|
||||
data.requireAllHands()
|
||||
.requireAllPredictions()
|
||||
.requireTrumpSuit()
|
||||
.requireTrick()
|
||||
.requireTricks()
|
||||
.requireCurrentPlayer()
|
||||
data.requireEach(PLAYERS, HANDS)
|
||||
.requireEach(PLAYERS, PREDICTIONS)
|
||||
.require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER)
|
||||
);
|
||||
}
|
||||
|
||||
protected Card.Suit getTrickSuit() {
|
||||
for (var pair : getStack()) {
|
||||
for (var pair : get(STACK)) {
|
||||
Card.Suit suit = pair.second().getSuit();
|
||||
if (suit == Card.Suit.WIZARD) {
|
||||
return Card.Suit.NONE;
|
||||
|
@ -1,7 +1,24 @@
|
||||
package eu.jonahbauer.wizard.core.util;
|
||||
|
||||
public record Pair<F,S>(F first, S second) {
|
||||
import java.util.Map;
|
||||
|
||||
public record Pair<F,S>(F first, S second) implements Map.Entry<F,S> {
|
||||
public static <F,S> Pair<F,S> of(F first, S second) {
|
||||
return new Pair<>(first, second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public F getKey() {
|
||||
return first();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S getValue() {
|
||||
return second();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S setValue(S value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@ -37,9 +38,9 @@ public class FinishingRoundTest {
|
||||
|
||||
FinishingRound state = mock(FinishingRound.class);
|
||||
when(state.getPoints()).thenCallRealMethod();
|
||||
when(state.getPredictions()).thenReturn(predictions);
|
||||
when(state.getTricks()).thenReturn(tricks);
|
||||
when(state.getPlayers()).thenReturn(players);
|
||||
when(state.get(PREDICTIONS)).thenReturn(predictions);
|
||||
when(state.get(TRICKS)).thenReturn(tricks);
|
||||
when(state.get(PLAYERS)).thenReturn(players);
|
||||
|
||||
assertEquals(Map.of(
|
||||
player0, 20,
|
||||
|
@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ -22,18 +23,17 @@ public class PredictingTest {
|
||||
when(game.getConfig()).thenReturn(config);
|
||||
|
||||
Predicting state = mock(Predicting.class);
|
||||
doCallRealMethod().when(state).checkPrediction(anyInt());
|
||||
when(state.getGame()).thenReturn(game);
|
||||
when(state.getPredictions()).thenReturn(predictions);
|
||||
doCallRealMethod().when(state).checkPrediction(any(), anyInt());
|
||||
when(state.get(PREDICTIONS)).thenReturn(predictions);
|
||||
when(state.getDealer()).thenReturn(new UUID(0,0));
|
||||
when(state.getCurrentPlayer()).thenReturn(new UUID(0,1));
|
||||
when(state.getRound()).thenReturn(10);
|
||||
when(state.get(CURRENT_PLAYER)).thenReturn(new UUID(0,1));
|
||||
when(state.get(ROUND)).thenReturn(10);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(-1));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(12));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 12));
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
state.checkPrediction(i);
|
||||
state.checkPrediction(game, i);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,18 +49,17 @@ public class PredictingTest {
|
||||
when(game.getConfig()).thenReturn(config);
|
||||
|
||||
Predicting state = mock(Predicting.class);
|
||||
doCallRealMethod().when(state).checkPrediction(anyInt());
|
||||
when(state.getGame()).thenReturn(game);
|
||||
when(state.getPredictions()).thenReturn(predictions);
|
||||
doCallRealMethod().when(state).checkPrediction(any(), anyInt());
|
||||
when(state.get(PREDICTIONS)).thenReturn(predictions);
|
||||
when(state.getDealer()).thenReturn(player);
|
||||
when(state.getCurrentPlayer()).thenReturn(player);
|
||||
when(state.getRound()).thenReturn(10);
|
||||
when(state.get(CURRENT_PLAYER)).thenReturn(player);
|
||||
when(state.get(ROUND)).thenReturn(10);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(1));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 1));
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (i == 1) continue;
|
||||
state.checkPrediction(i);
|
||||
state.checkPrediction(game, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.trick.FinishingTrick;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -8,11 +8,23 @@ import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.STACK;
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.TRUMP_SUIT;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class FinishingTrickTest {
|
||||
private void performTest(List<Pair<UUID, Card>> stack, Card.Suit trumpSuit, Card.Suit trickSuit) {
|
||||
FinishingTrick state = mock(FinishingTrick.class);
|
||||
when(state.getWinner()).thenCallRealMethod();
|
||||
when(state.get(STACK)).thenReturn(stack);
|
||||
when(state.get(TRUMP_SUIT)).thenReturn(trumpSuit);
|
||||
when(state.getTrickSuit()).thenReturn(trickSuit);
|
||||
|
||||
assertNotNull(state.getWinner());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWinner_ReturnsFirstWizard() {
|
||||
List<Pair<UUID, Card>> stack = List.of(
|
||||
@ -23,13 +35,7 @@ public class FinishingTrickTest {
|
||||
Pair.of(null, Card.GREEN_11)
|
||||
);
|
||||
|
||||
FinishingTrick state = mock(FinishingTrick.class);
|
||||
when(state.getWinner()).thenCallRealMethod();
|
||||
when(state.getStack()).thenReturn(stack);
|
||||
when(state.getTrickSuit()).thenReturn(Card.Suit.RED);
|
||||
when(state.getTrumpSuit()).thenReturn(Card.Suit.YELLOW);
|
||||
|
||||
assertNotNull(state.getWinner());
|
||||
performTest(stack, Card.Suit.YELLOW, Card.Suit.RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -42,13 +48,7 @@ public class FinishingTrickTest {
|
||||
Pair.of(null, Card.GREEN_11)
|
||||
);
|
||||
|
||||
FinishingTrick state = mock(FinishingTrick.class);
|
||||
when(state.getWinner()).thenCallRealMethod();
|
||||
when(state.getStack()).thenReturn(stack);
|
||||
when(state.getTrickSuit()).thenReturn(Card.Suit.RED);
|
||||
when(state.getTrumpSuit()).thenReturn(Card.Suit.YELLOW);
|
||||
|
||||
assertNotNull(state.getWinner());
|
||||
performTest(stack, Card.Suit.YELLOW, Card.Suit.RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -61,13 +61,7 @@ public class FinishingTrickTest {
|
||||
Pair.of(null, Card.GREEN_11)
|
||||
);
|
||||
|
||||
FinishingTrick state = mock(FinishingTrick.class);
|
||||
when(state.getWinner()).thenCallRealMethod();
|
||||
when(state.getStack()).thenReturn(stack);
|
||||
when(state.getTrickSuit()).thenReturn(Card.Suit.RED);
|
||||
when(state.getTrumpSuit()).thenReturn(Card.Suit.BLUE);
|
||||
|
||||
assertNotNull(state.getWinner());
|
||||
performTest(stack, Card.Suit.BLUE, Card.Suit.RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -79,12 +73,6 @@ public class FinishingTrickTest {
|
||||
Pair.of(null, Card.YELLOW_JESTER)
|
||||
);
|
||||
|
||||
FinishingTrick state = mock(FinishingTrick.class);
|
||||
when(state.getWinner()).thenCallRealMethod();
|
||||
when(state.getStack()).thenReturn(stack);
|
||||
when(state.getTrickSuit()).thenReturn(Card.Suit.NONE);
|
||||
when(state.getTrumpSuit()).thenReturn(Card.Suit.NONE);
|
||||
|
||||
assertNotNull(state.getWinner());
|
||||
performTest(stack, Card.Suit.NONE, Card.Suit.NONE);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.trick.PlayingCard;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
|
||||
import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
|
||||
@ -11,6 +11,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ -22,9 +23,9 @@ public class PlayingCardTest {
|
||||
|
||||
PlayingCard state = mock(PlayingCard.class);
|
||||
doCallRealMethod().when(state).checkCard(any());
|
||||
when(state.getHands()).thenReturn(Map.of(player, hand));
|
||||
when(state.getStack()).thenReturn(List.of());
|
||||
when(state.getCurrentPlayer()).thenReturn(player);
|
||||
when(state.get(HANDS)).thenReturn(Map.of(player, hand));
|
||||
when(state.get(STACK)).thenReturn(List.of());
|
||||
when(state.get(CURRENT_PLAYER)).thenReturn(player);
|
||||
|
||||
for (Card value : Card.values()) {
|
||||
if (hand.contains(value)) continue;
|
||||
@ -42,8 +43,8 @@ public class PlayingCardTest {
|
||||
PlayingCard state = mock(PlayingCard.class);
|
||||
doCallRealMethod().when(state).checkCard(any());
|
||||
when(state.getTrickSuit()).thenReturn(Card.Suit.YELLOW);
|
||||
when(state.getHands()).thenReturn(Map.of(player, hand));
|
||||
when(state.getCurrentPlayer()).thenReturn(player);
|
||||
when(state.get(HANDS)).thenReturn(Map.of(player, hand));
|
||||
when(state.get(CURRENT_PLAYER)).thenReturn(player);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.BLUE_1));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.RED_1));
|
||||
@ -58,24 +59,28 @@ public class PlayingCardTest {
|
||||
UUID player = new UUID(0, 0);
|
||||
UUID player2 = new UUID(0, 1);
|
||||
|
||||
PlayingCard state = mock(PlayingCard.class);
|
||||
doCallRealMethod().when(state).onMessage(any(), any());
|
||||
when(state.getCurrentPlayer()).thenReturn(player);
|
||||
Game game = mock(Game.class);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PlayCardMessage(Card.BLUE_WIZARD)));
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PredictMessage(1)));
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PickTrumpMessage(Card.Suit.BLUE)));
|
||||
PlayingCard state = mock(PlayingCard.class);
|
||||
doCallRealMethod().when(state).onMessage(any(), any(), any());
|
||||
when(state.get(CURRENT_PLAYER)).thenReturn(player);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PlayCardMessage(Card.BLUE_WIZARD)));
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PredictMessage(1)));
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PickTrumpMessage(Card.Suit.BLUE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onMessage_ThrowsIllegalState_IfNotPlayCard() {
|
||||
UUID player = new UUID(0, 0);
|
||||
|
||||
PlayingCard state = mock(PlayingCard.class);
|
||||
doCallRealMethod().when(state).onMessage(any(), any());
|
||||
when(state.getCurrentPlayer()).thenReturn(player);
|
||||
Game game = mock(Game.class);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(player, new PredictMessage(1)));
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(player, new PickTrumpMessage(Card.Suit.BLUE)));
|
||||
PlayingCard state = mock(PlayingCard.class);
|
||||
doCallRealMethod().when(state).onMessage(any(), any(), any());
|
||||
when(state.get(CURRENT_PLAYER)).thenReturn(player);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(game, player, new PredictMessage(1)));
|
||||
assertThrows(IllegalStateException.class, () -> state.onMessage(game, player, new PickTrumpMessage(Card.Suit.BLUE)));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.trick.TrickState;
|
||||
import eu.jonahbauer.wizard.core.model.Card;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -8,6 +7,7 @@ import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.STACK;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@ -19,7 +19,7 @@ public class TrickStateTest {
|
||||
|
||||
TrickState trickState = mock(TrickState.class);
|
||||
when(trickState.getTrickSuit()).thenCallRealMethod();
|
||||
when(trickState.getStack()).thenReturn(stack);
|
||||
when(trickState.get(STACK)).thenReturn(stack);
|
||||
|
||||
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
|
||||
}
|
||||
@ -36,7 +36,7 @@ public class TrickStateTest {
|
||||
|
||||
TrickState trickState = mock(TrickState.class);
|
||||
when(trickState.getTrickSuit()).thenCallRealMethod();
|
||||
when(trickState.getStack()).thenReturn(stack);
|
||||
when(trickState.get(STACK)).thenReturn(stack);
|
||||
|
||||
assertEquals(trickState.getTrickSuit(), Card.Suit.RED);
|
||||
}
|
||||
@ -53,7 +53,7 @@ public class TrickStateTest {
|
||||
|
||||
TrickState trickState = mock(TrickState.class);
|
||||
when(trickState.getTrickSuit()).thenCallRealMethod();
|
||||
when(trickState.getStack()).thenReturn(stack);
|
||||
when(trickState.get(STACK)).thenReturn(stack);
|
||||
|
||||
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
|
||||
}
|
||||
@ -70,7 +70,7 @@ public class TrickStateTest {
|
||||
|
||||
TrickState trickState = mock(TrickState.class);
|
||||
when(trickState.getTrickSuit()).thenCallRealMethod();
|
||||
when(trickState.getStack()).thenReturn(stack);
|
||||
when(trickState.get(STACK)).thenReturn(stack);
|
||||
|
||||
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
|
||||
|
||||
@ -83,7 +83,7 @@ public class TrickStateTest {
|
||||
);
|
||||
TrickState trickState2 = mock(TrickState.class);
|
||||
when(trickState2.getTrickSuit()).thenCallRealMethod();
|
||||
when(trickState2.getStack()).thenReturn(stack2);
|
||||
when(trickState2.get(STACK)).thenReturn(stack2);
|
||||
|
||||
assertEquals(trickState2.getTrickSuit(), Card.Suit.YELLOW);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user