- refactored GameData

- removed reference to Context in State
main
Jonah Bauer 3 years ago
parent 3ff2fdfeda
commit 9f067fae1a

@ -1,10 +1,13 @@
package eu.jonahbauer.wizard.core; package eu.jonahbauer.wizard.core;
import eu.jonahbauer.wizard.core.machine.Game; 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.PickTrumpMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.core.messages.player.PredictMessage; import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.model.Configuration;
import eu.jonahbauer.wizard.core.model.Configurations; import eu.jonahbauer.wizard.core.model.Configurations;
import java.util.List; import java.util.List;
@ -15,7 +18,9 @@ import java.util.regex.Pattern;
public class CLI { public class CLI {
public static void main(String[] args) { 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( var players = List.of(
UUID.randomUUID(), UUID.randomUUID(),
UUID.randomUUID(), UUID.randomUUID(),
@ -25,13 +30,15 @@ public class CLI {
game.start(players); game.start(players);
GameState state = null;
Scanner scanner = new Scanner(System.in); Scanner scanner = new Scanner(System.in);
Pattern pattern = Pattern.compile("(\\d) (predict|play|trump) (.*)"); Pattern pattern = Pattern.compile("(\\d) ([a-z]+) (.*)");
while (scanner.hasNextLine()) { while (scanner.hasNextLine()) {
try { try {
Matcher matcher = pattern.matcher(scanner.nextLine()); Matcher matcher = pattern.matcher(scanner.nextLine());
if (!matcher.find()) { if (!matcher.find()) {
System.err.println("Format is \"(\\\\d) (predict|play|trump) (.*)\""); System.err.println("Format is \"(\\\\d) ([a-z]+) (.*)\"");
continue; continue;
} }
String player = matcher.group(1); String player = matcher.group(1);

@ -8,15 +8,15 @@ import java.util.concurrent.*;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; 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 final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
protected T state; protected S state;
protected final ReentrantLock lock = new ReentrantLock(); protected final ReentrantLock lock = new ReentrantLock();
private final Condition finishCondition = lock.newCondition(); private final Condition finishCondition = lock.newCondition();
private boolean finished; private boolean finished;
private Throwable exception; private Throwable exception;
protected void start(@NotNull T state) { protected void start(@NotNull S state) {
lock.lock(); lock.lock();
try { try {
if (finished) throw new IllegalStateException("Context has already finished."); 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(); lock.lock();
try { try {
if (state == currentState) { if (state == currentState) {
state = newState; state = newState;
if (currentState != null) currentState.onExit(); if (currentState != null) //noinspection unchecked
currentState.onExit((C) this);
onTransition(currentState, newState); onTransition(currentState, newState);
if (newState != null) newState.onEnter(); if (newState != null) //noinspection unchecked
newState.onEnter((C) this);
} else { } else {
throw new IllegalStateException("Current state does not match."); throw new IllegalStateException("Current state does not match.");
} }
@ -65,6 +67,11 @@ public abstract class Context<T extends State> {
finish(new CancellationException()); finish(new CancellationException());
} }
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isDone() {
return finished;
}
@Blocking @Blocking
public void await() throws InterruptedException, ExecutionException, CancellationException { public void await() throws InterruptedException, ExecutionException, CancellationException {
lock.lock(); 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(() -> { scheduler.schedule(() -> {
lock.lock(); lock.lock();
try { try {
if (state == currentState) { if (state == currentState) {
state.onTimeout(); //noinspection unchecked
state.onTimeout((C) this);
} }
} catch (Throwable t) { } catch (Throwable t) {
handleError(t); handleError(t);
@ -100,8 +108,16 @@ public abstract class Context<T extends State> {
} }
protected void handleError(Throwable t) { protected void handleError(Throwable t) {
lock.lock();
try {
if (!isDone()) {
finish(t); 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; 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.GameState;
import eu.jonahbauer.wizard.core.machine.states.game.Starting; import eu.jonahbauer.wizard.core.machine.states.game.Starting;
import eu.jonahbauer.wizard.core.messages.Observer; import eu.jonahbauer.wizard.core.messages.Observer;
@ -12,7 +13,9 @@ import lombok.Getter;
import java.util.List; import java.util.List;
import java.util.UUID; 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 @Getter
private final Configuration config; private final Configuration config;
private final Observer observer; private final Observer observer;
@ -23,17 +26,26 @@ public final class Game extends Context<GameState> {
} }
public void start(List<UUID> players) { 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) { public void resume(GameState state) {
start(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) { public void onMessage(UUID player, PlayerMessage message) {
lock.lock(); lock.lock();
try { try {
state.onMessage(player, message); state.onMessage(this, player, message);
} catch (IllegalStateException | IllegalArgumentException e) { } catch (IllegalStateException | IllegalArgumentException e) {
throw e; throw e;
} catch (Throwable t) { } 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; package eu.jonahbauer.wizard.core.machine.states;
import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import lombok.Getter; import lombok.Getter;
import lombok.experimental.Delegate;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
import java.util.UUID; 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 @Unmodifiable
public abstract class GameState implements State { public abstract class GameState implements State<GameState, Game> {
@Getter
private final Game game;
@Getter @Getter
@Delegate(types = GameData.Getters.class)
private final GameData data; private final GameData data;
public GameState(Game game, GameData data) { public GameState(GameData data) {
this.game = game; this.data = data.require(PLAYERS).clean();
this.data = data.requirePlayers().onlyRequired();
} }
protected final void transition(GameState state) { protected final void transition(Game game, GameState state) {
getGame().transition(this, state); game.transition(this, state);
} }
protected final void timeout() { protected final void timeout(Game game) {
getGame().timeout(this, getTimeout(false)); game.timeout(this, getTimeout(game, false));
} }
protected final long getTimeout(boolean absolute) { protected final long getTimeout(Game game, boolean absolute) {
return (absolute ? System.currentTimeMillis() : 0) + getGame().getConfig().timeout(); 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."); throw new IllegalStateException("You cannot do that right now.");
} }
@ -42,4 +39,33 @@ public abstract class GameState implements State {
public String toString() { public String toString() {
return getClass().getSimpleName(); 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; package eu.jonahbauer.wizard.core.machine.states;
public interface State { import eu.jonahbauer.wizard.core.machine.Context;
default void onEnter() {}
default void onTimeout() {} public interface State<S extends State<S,C>, C extends Context<S,C>> {
default void onExit() {} 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; 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.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.GameState; import eu.jonahbauer.wizard.core.machine.states.GameState;
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage; import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
public final class Finishing extends GameState { public final class Finishing extends GameState {
public Finishing(Game game, GameData data) { public Finishing(GameData data) {
super(game, data.requireScore()); super(data.require(SCORE));
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
getGame().notify(new ScoreMessage(getScore())); game.notify(new ScoreMessage(get(SCORE)));
getGame().finish(); game.finish();
} }
} }

@ -1,20 +1,25 @@
package eu.jonahbauer.wizard.core.machine.states.game; 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.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.GameState; import eu.jonahbauer.wizard.core.machine.states.GameState;
import eu.jonahbauer.wizard.core.machine.states.round.StartingRound; 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 final class Starting extends GameState {
public Starting(Game game, GameData data) { public Starting(GameData data) {
super(game, data); super(data);
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
transition(new StartingRound(getGame(), getData().withRound(0).withScore(Collections.emptyMap()))); transition(game, new StartingRound(getData().with(
ROUND, 0,
SCORE, Map.of()
)));
} }
} }

@ -1,50 +1,46 @@
package eu.jonahbauer.wizard.core.machine.states.round; 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.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.messages.observer.HandMessage; import eu.jonahbauer.wizard.core.messages.observer.HandMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.model.Deck; import eu.jonahbauer.wizard.core.model.Deck;
import java.util.*; import java.util.*;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
public final class Dealing extends RoundState { public final class Dealing extends RoundState {
private transient Map<UUID, List<Card>> hands;
public Dealing(Game game, GameData data) { public Dealing(GameData data) {
super(game, data); super(data);
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
Deck deck = new Deck(getGame().getConfig().cards()); Deck deck = new Deck(game.getConfig().cards());
deck.shuffle(); deck.shuffle();
hands = new HashMap<>(); var hands = new HashMap<UUID, List<Card>>();
int dealer = getRound(); int round = get(ROUND);
int playerCount = getPlayerCount(); int playerCount = getPlayerCount();
int cardCount = getRound() + 1; int cardCount = round + 1;
for (int i = 1; i <= playerCount; i++) { for (int i = 1; i <= playerCount; i++) {
int player = (dealer + i) % playerCount; int player = (round + i) % playerCount;
hands.put(getPlayer(player), deck.draw(cardCount)); 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)
));
} }
@Override Card trumpCard = deck.draw();
public void onExit() { transition(game, new DeterminingTrump(
if (hands != null) { getData().with(
for (UUID player : getPlayers()) { HANDS, Map.copyOf(hands),
getGame().notify(player, new HandMessage(player, hands.get(player))); TRUMP_CARD, trumpCard
} )
} ));
} }
} }

@ -1,66 +1,53 @@
package eu.jonahbauer.wizard.core.machine.states.round; 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.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.messages.observer.TrumpMessage; import eu.jonahbauer.wizard.core.messages.observer.TrumpMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.Card;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting; import org.jetbrains.annotations.VisibleForTesting;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Map;
import java.util.UUID; 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; import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PICK_TRUMP;
/**
*
*/
public final class DeterminingTrump extends RoundState { public final class DeterminingTrump extends RoundState {
private transient Card.Suit trumpSuit; public DeterminingTrump(GameData data) {
super(data.require(TRUMP_CARD).requireEach(PLAYERS, HANDS));
public DeterminingTrump(Game game, GameData data) {
super(game, data.requireAllHands().requireTrumpCard());
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
Card trumpCard = getTrumpCard(); Card trumpCard = getData().get(TRUMP_CARD);
Card.Suit trumpSuit = trumpCard != null ? trumpCard.getTrumpSuit() : Card.Suit.NONE; Card.Suit trumpSuit = trumpCard != null ? trumpCard.getTrumpSuit() : Card.Suit.NONE;
if (trumpSuit == null) { if (trumpSuit == null) {
getGame().notify(new TrumpMessage(getTrumpCard(), null)); game.notify(new TrumpMessage(trumpCard, null));
getGame().notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(true))); game.notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(game, true)));
timeout(); timeout(game);
} else { } else {
this.trumpSuit = trumpSuit; transition(game, trumpSuit);
transition();
} }
} }
@Override @Override
public void onTimeout() { public void onTimeout(Game game) {
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};
this.trumpSuit = suits[(int)(Math.random() * suits.length)]; transition(game, suits[(int) (Math.random() * suits.length)]);
transition();
}
@Override
public void onExit() {
if (trumpSuit != null) {
getGame().notify(new TrumpMessage(getTrumpCard(), trumpSuit));
}
} }
@Override @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) { if (getDealer().equals(player) && message instanceof PickTrumpMessage trumpMessage) {
checkTrumpSuit(trumpMessage.getTrumpSuit()); checkTrumpSuit(trumpMessage.getTrumpSuit());
this.trumpSuit = trumpMessage.getTrumpSuit(); transition(game, trumpMessage.getTrumpSuit());
transition();
} else { } else {
super.onMessage(player, message); super.onMessage(game, player, message);
} }
} }
@ -73,15 +60,12 @@ public final class DeterminingTrump extends RoundState {
throw new IllegalArgumentException("Trump suit must be one of " + Arrays.toString(suits) + "."); throw new IllegalArgumentException("Trump suit must be one of " + Arrays.toString(suits) + ".");
} }
private void transition() { private void transition(Game game, @NotNull Card.Suit trumpSuit) {
if (trumpSuit == null) throw new AssertionError(); game.notify(new TrumpMessage(getData().get(TRUMP_CARD), trumpSuit));
transition(game, new Predicting(getData().with(
transition(new Predicting( TRUMP_SUIT, trumpSuit,
getGame(), PREDICTIONS, Map.of(),
getData().withTrumpSuit(trumpSuit) CURRENT_PLAYER, getNextPlayer(getDealer())
.withPredictions(Collections.emptyMap()) )));
.withCurrentPlayer(getDealer())
.withNextPlayer()
));
} }
} }

@ -1,7 +1,8 @@
package eu.jonahbauer.wizard.core.machine.states.round; 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.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.machine.states.game.Finishing;
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage; import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage;
import org.jetbrains.annotations.VisibleForTesting; import org.jetbrains.annotations.VisibleForTesting;
@ -10,47 +11,38 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
public final class FinishingRound extends RoundState { import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
private transient Map<UUID, Integer> points;
public FinishingRound(Game game, GameData data) { public final class FinishingRound extends RoundState {
super(game, data.requireAllPredictions().requireTricks().requireScore());
int tricks = data.getTricks().values().stream().mapToInt(i -> i).sum(); public FinishingRound(GameData data) {
if (tricks != data.getRound() + 1) { super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE));
throw new AssertionError("Unexpected number of tricks in round " + data.getRound() + ": " + tricks + "."); checkData(data);
}
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
points = getPoints(); var points = getPoints();
game.notify(new ScoreMessage(points));
var score = new HashMap<>(getScore()); var score = new HashMap<>(get(SCORE));
points.forEach((uuid, p) -> score.compute(uuid, (u, total) -> total == null ? p : total + p)); 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) { if (60 / getPlayerCount() == get(ROUND) + 1) {
transition(new Finishing(getGame(), data)); transition(game, new Finishing(data));
} else { } else {
transition(new StartingRound(getGame(), data.withNextRound())); transition(game, new StartingRound(data.with(ROUND, get(ROUND) + 1)));
}
}
@Override
public void onExit() {
if (points != null) {
getGame().notify(new ScoreMessage(points));
} }
} }
@VisibleForTesting @VisibleForTesting
Map<UUID, Integer> getPoints() { Map<UUID, Integer> getPoints() {
var points = new HashMap<UUID, Integer>(); var points = new HashMap<UUID, Integer>();
for (UUID player : getPlayers()) { for (UUID player : get(PLAYERS)) {
int prediction = getPredictions().get(player); int prediction = get(PREDICTIONS).get(player);
int tricks = getTricks().getOrDefault(player, 0); int tricks = get(TRICKS).getOrDefault(player, 0);
if (tricks == prediction) { if (tricks == prediction) {
points.put(player, 20 + 10 * tricks); points.put(player, 20 + 10 * tricks);
@ -60,4 +52,12 @@ public final class FinishingRound extends RoundState {
} }
return points; 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; 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.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick; import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
import eu.jonahbauer.wizard.core.messages.observer.PredictionMessage; import eu.jonahbauer.wizard.core.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
@ -10,36 +10,34 @@ import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.VisibleForTesting; import org.jetbrains.annotations.VisibleForTesting;
import java.util.Collections; import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.MAKE_PREDICTION; import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.MAKE_PREDICTION;
@Getter @Getter
public final class Predicting extends RoundState { public final class Predicting extends RoundState {
private transient Integer prediction; public Predicting(GameData data) {
super(data.requireEach(PLAYERS, HANDS).require(PREDICTIONS, TRUMP_SUIT, CURRENT_PLAYER));
public Predicting(Game game, GameData data) {
super(game, data.requireAllHands().requirePredictions().requireTrumpSuit().requireCurrentPlayer());
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
getGame().notify(new UserInputMessage(getCurrentPlayer(), MAKE_PREDICTION, getTimeout(true))); game.notify(new UserInputMessage(getData().get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true)));
timeout(); timeout(game);
} }
@Override @Override
public void onTimeout() { public void onTimeout(Game game) {
try { try {
checkPrediction(0); checkPrediction(game, 0);
this.prediction = 0; transition(game, 0);
transition();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
try { try {
checkPrediction(1); checkPrediction(game, 1);
this.prediction = 1; transition(game, 1);
transition();
} catch (IllegalArgumentException e2) { } catch (IllegalArgumentException e2) {
throw new AssertionError(e2); throw new AssertionError(e2);
} }
@ -47,57 +45,54 @@ public final class Predicting extends RoundState {
} }
@Override @Override
public void onExit() { public void onMessage(Game game, UUID player, PlayerMessage message) {
if (prediction != null) { if (getData().get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
getGame().notify(new PredictionMessage(getCurrentPlayer(), prediction)); checkPrediction(game, predictMessage.getPrediction());
} transition(game, predictMessage.getPrediction());
}
@Override
public void onMessage(UUID player, PlayerMessage message) {
if (getCurrentPlayer().equals(player) && message instanceof PredictMessage predictMessage) {
checkPrediction(predictMessage.getPrediction());
this.prediction = predictMessage.getPrediction();
transition();
} else { } else {
super.onMessage(player, message); super.onMessage(game, player, message);
} }
} }
@VisibleForTesting @VisibleForTesting
void checkPrediction(int prediction) { void checkPrediction(Game game, int prediction) {
if (prediction < 0 || prediction > getRound() + 1) { int round = get(ROUND);
throw new IllegalArgumentException("Prediction must be between 0 and " + (getRound() + 1) + "."); if (prediction < 0 || prediction > round + 1) {
throw new IllegalArgumentException("Prediction must be between 0 and " + (round + 1) + ".");
} }
if (!getGame().getConfig().allowExactPredictions() && isLastPlayer()) { if (!game.getConfig().allowExactPredictions() && isLastPlayer()) {
int sum = getPredictions().values().stream().mapToInt(i -> i).sum(); int sum = get(PREDICTIONS).values().stream().mapToInt(i -> i).sum();
if (sum + prediction == getRound() + 1) { if (sum + prediction == round + 1) {
throw new IllegalArgumentException("Predictions must not add up."); throw new IllegalArgumentException("Predictions must not add up.");
} }
} }
} }
private void transition() { private void transition(Game game, int prediction) {
if (prediction == null) throw new AssertionError(); game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction));
// add prediction
var predictions = new HashMap<>(get(PREDICTIONS));
predictions.put(get(CURRENT_PLAYER), prediction);
GameData data = getData().withPrediction(getCurrentPlayer(), prediction) GameData data = getData().with(
.withNextPlayer(); PREDICTIONS, Map.copyOf(predictions),
CURRENT_PLAYER, getNextPlayer()
);
if (isLastPlayer()) { if (isLastPlayer()) {
transition(new StartingTrick( data = data.with(
getGame(), TRICK, 0,
data.withTrick(0).withTricks(Collections.emptyMap()) TRICKS, Map.of()
)); );
transition(game, new StartingTrick(data));
} else { } else {
transition(new Predicting( transition(game, new Predicting(data));
getGame(),
data
));
} }
} }
private boolean isLastPlayer() { 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; package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.GameState; import eu.jonahbauer.wizard.core.machine.states.GameState;
import java.util.UUID; import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
public abstract class RoundState extends GameState { public abstract class RoundState extends GameState {
public RoundState(Game game, GameData data) { public RoundState(GameData data) {
super(game, data.requireRound().requireScore()); super(data.require(ROUND, SCORE));
} }
protected UUID getDealer() { 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; 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.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
public final class StartingRound extends RoundState { public final class StartingRound extends RoundState {
public StartingRound(Game game, GameData data) { public StartingRound(GameData data) {
super(game, data); super(data);
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
transition(new Dealing(getGame(), getData())); transition(game, new Dealing(getData()));
} }
} }

@ -1,55 +1,48 @@
package eu.jonahbauer.wizard.core.machine.states.trick; 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.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.machine.states.round.FinishingRound;
import eu.jonahbauer.wizard.core.messages.observer.TrickMessage; import eu.jonahbauer.wizard.core.messages.observer.TrickMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.util.Pair; import eu.jonahbauer.wizard.core.util.Pair;
import org.jetbrains.annotations.VisibleForTesting; import org.jetbrains.annotations.VisibleForTesting;
import java.util.Comparator; import java.util.*;
import java.util.HashMap;
import java.util.List; import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import java.util.UUID;
public final class FinishingTrick extends TrickState { public final class FinishingTrick extends TrickState {
private transient UUID winner;
public FinishingTrick(Game game, GameData data) { public FinishingTrick(GameData data) {
super(game, data.requireStack()); super(data.require(STACK));
assert assertions(); checkData(data);
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
this.winner = getWinner(); var winner = getWinner();
game.notify(new TrickMessage(winner, get(STACK).stream().map(Pair::second).toList()));
var tricks = new HashMap<>(getTricks()); var tricks = new HashMap<>(get(TRICKS));
tricks.compute(winner, (k, t) -> t == null ? 1 : t + 1); tricks.merge(winner, 1, Integer::sum);
GameData data = getData().withTricks(tricks);
if (getTrick() < getRound()) { GameData data = getData().with(TRICKS, Map.copyOf(tricks));
transition(new StartingTrick(
getGame(),
data.withCurrentPlayer(winner)
.withNextTrick()
));
} else {
transition(new FinishingRound(getGame(), data));
}
}
@Override if (get(TRICK) < get(ROUND)) {
public void onExit() { transition(game, new StartingTrick(data.with(
if (winner != null) { CURRENT_PLAYER, winner,
getGame().notify(new TrickMessage(winner, getStack().stream().map(Pair::second).toList())); TRICK, get(TRICK) + 1
)));
} else {
transition(game, new FinishingRound(data));
} }
} }
@VisibleForTesting @VisibleForTesting
UUID getWinner() { UUID getWinner() {
var wizard = getStack().stream() var wizard = get(STACK).stream()
.filter(pair -> pair.second().getSuit() == Card.Suit.WIZARD) .filter(pair -> pair.second().getSuit() == Card.Suit.WIZARD)
.findFirst() .findFirst()
.orElse(null); .orElse(null);
@ -57,16 +50,15 @@ public final class FinishingTrick extends TrickState {
return wizard.first(); return wizard.first();
} }
if (getStack().stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) { if (get(STACK).stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) {
return getStack().get(0).first(); return get(STACK).get(0).first();
} }
var trumpSuit = getTrumpSuit(); var trumpSuit = get(TRUMP_SUIT);
var suit = getTrickSuit(); var suit = getTrickSuit();
return getStack().stream() return get(STACK).stream()
.max( .max(
Comparator.<Pair<UUID, Card>>comparingInt(pair -> pair.second() Comparator.<Pair<UUID, Card>>comparingInt(pair -> pair.second().getSuit() == trumpSuit ? 1 : 0)
.getSuit() == trumpSuit ? 1 : 0)
.thenComparing(pair -> pair.second().getSuit() == suit ? 1 : 0) .thenComparing(pair -> pair.second().getSuit() == suit ? 1 : 0)
.thenComparing(pair -> pair.second().getValue()) .thenComparing(pair -> pair.second().getValue())
) )
@ -74,16 +66,20 @@ public final class FinishingTrick extends TrickState {
.first(); .first();
} }
private boolean assertions() { private static void checkData(GameData data) {
if (getStack().size() != getPlayerCount()) { // every player has played exactly one card
throw new AssertionError("Unexpected stack size: " + getStack().size() + " (expected " + getPlayerCount() + ")."); 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) { if (hand.size() != expectedSize) {
throw new AssertionError("Unexpected hand size: " + hand.size() + " (expected " + 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; 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.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.CardMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Card; 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 org.jetbrains.annotations.VisibleForTesting;
import java.util.Collection; import java.util.*;
import java.util.List;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PLAY_CARD; import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PLAY_CARD;
public final class PlayingCard extends TrickState { public final class PlayingCard extends TrickState {
private transient Card card;
public PlayingCard(Game game, GameData data) { public PlayingCard(GameData data) {
super(game, data.requireStack()); super(data.require(STACK));
assert assertions(); checkData(data);
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
getGame().notify(new UserInputMessage(getCurrentPlayer(), PLAY_CARD, getTimeout(true))); game.notify(new UserInputMessage(get(CURRENT_PLAYER), PLAY_CARD, getTimeout(game, true)));
timeout(); timeout(game);
} }
@Override @Override
public void onMessage(UUID player, PlayerMessage message) { public void onMessage(Game game, UUID player, PlayerMessage message) {
if (getCurrentPlayer().equals(player) && message instanceof PlayCardMessage cardMessage) { if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) {
checkCard(cardMessage.getCard()); checkCard(cardMessage.getCard());
this.card = cardMessage.getCard(); transition(game, cardMessage.getCard());
transition();
} else { } else {
super.onMessage(player, message); super.onMessage(game, player, message);
} }
} }
@Override @Override
public void onTimeout() { public void onTimeout(Game game) {
var hand = getHands().get(getCurrentPlayer()); var hand = get(HANDS).get(get(CURRENT_PLAYER));
this.card = hand.stream().filter(c -> { var card = hand.stream().filter(c -> {
try { try {
checkCard(c); checkCard(c);
return true; return true;
@ -51,19 +51,12 @@ public final class PlayingCard extends TrickState {
return false; return false;
} }
}).findAny().orElseThrow(() -> new AssertionError("Cannot play any card.")); }).findAny().orElseThrow(() -> new AssertionError("Cannot play any card."));
transition(); transition(game, card);
}
@Override
public void onExit() {
if (card != null) {
getGame().notify(new CardMessage(getCurrentPlayer(), card));
}
} }
@VisibleForTesting @VisibleForTesting
void checkCard(Card card) { void checkCard(Card card) {
var hand = getHands().get(getCurrentPlayer()); var hand = get(HANDS).get(get(CURRENT_PLAYER));
if (!hand.contains(card)) { if (!hand.contains(card)) {
throw new IllegalArgumentException("You do not have this card on your hand."); 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() { private void transition(Game game, @NotNull Card card) {
if (card == null) throw new AssertionError(); 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) .mapToInt(Collection::size)
.summaryStatistics(); .summaryStatistics();
if (summary.getMax() == summary.getMin()) { if (summary.getMax() == summary.getMin()) { // everybody has the same amount of cards
transition(new FinishingTrick(getGame(), data)); transition(game, new FinishingTrick(data));
} else { } else {
transition(new PlayingCard(getGame(), data.withNextPlayer())); transition(game, new PlayingCard(data.with(CURRENT_PLAYER, getNextPlayer())));
} }
} }
private boolean assertions() { private static void checkData(GameData data) {
int expectedSize = getRound() - getTrick(); int expectedSize = data.get(ROUND) - data.get(TRICK);
for (List<Card> hand : getHands().values()) { for (List<Card> hand : data.get(HANDS).values()) {
if (hand.size() != expectedSize && hand.size() != expectedSize + 1) { 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) { 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; 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.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 final class StartingTrick extends TrickState {
public StartingTrick(Game game, GameData data) { public StartingTrick(GameData data) {
super(game, data); super(data);
} }
@Override @Override
public void onEnter() { public void onEnter(Game game) {
transition(new PlayingCard( transition(game, new PlayingCard(getData().with(STACK, List.of())));
getGame(),
getData().withStack(Collections.emptyList())
));
} }
} }

@ -1,25 +1,22 @@
package eu.jonahbauer.wizard.core.machine.states.trick; 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.GameData;
import eu.jonahbauer.wizard.core.machine.states.round.RoundState; import eu.jonahbauer.wizard.core.machine.states.round.RoundState;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.Card;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
public abstract class TrickState extends RoundState { public abstract class TrickState extends RoundState {
public TrickState(Game game, GameData data) { public TrickState(GameData data) {
super( super(
game, data.requireEach(PLAYERS, HANDS)
data.requireAllHands() .requireEach(PLAYERS, PREDICTIONS)
.requireAllPredictions() .require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER)
.requireTrumpSuit()
.requireTrick()
.requireTricks()
.requireCurrentPlayer()
); );
} }
protected Card.Suit getTrickSuit() { protected Card.Suit getTrickSuit() {
for (var pair : getStack()) { for (var pair : get(STACK)) {
Card.Suit suit = pair.second().getSuit(); Card.Suit suit = pair.second().getSuit();
if (suit == Card.Suit.WIZARD) { if (suit == Card.Suit.WIZARD) {
return Card.Suit.NONE; return Card.Suit.NONE;

@ -1,7 +1,24 @@
package eu.jonahbauer.wizard.core.util; 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) { public static <F,S> Pair<F,S> of(F first, S second) {
return new Pair<>(first, 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.Map;
import java.util.UUID; import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -37,9 +38,9 @@ public class FinishingRoundTest {
FinishingRound state = mock(FinishingRound.class); FinishingRound state = mock(FinishingRound.class);
when(state.getPoints()).thenCallRealMethod(); when(state.getPoints()).thenCallRealMethod();
when(state.getPredictions()).thenReturn(predictions); when(state.get(PREDICTIONS)).thenReturn(predictions);
when(state.getTricks()).thenReturn(tricks); when(state.get(TRICKS)).thenReturn(tricks);
when(state.getPlayers()).thenReturn(players); when(state.get(PLAYERS)).thenReturn(players);
assertEquals(Map.of( assertEquals(Map.of(
player0, 20, player0, 20,

@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -22,18 +23,17 @@ public class PredictingTest {
when(game.getConfig()).thenReturn(config); when(game.getConfig()).thenReturn(config);
Predicting state = mock(Predicting.class); Predicting state = mock(Predicting.class);
doCallRealMethod().when(state).checkPrediction(anyInt()); doCallRealMethod().when(state).checkPrediction(any(), anyInt());
when(state.getGame()).thenReturn(game); when(state.get(PREDICTIONS)).thenReturn(predictions);
when(state.getPredictions()).thenReturn(predictions);
when(state.getDealer()).thenReturn(new UUID(0,0)); when(state.getDealer()).thenReturn(new UUID(0,0));
when(state.getCurrentPlayer()).thenReturn(new UUID(0,1)); when(state.get(CURRENT_PLAYER)).thenReturn(new UUID(0,1));
when(state.getRound()).thenReturn(10); when(state.get(ROUND)).thenReturn(10);
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(-1)); assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, -1));
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(12)); assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 12));
for (int i = 0; i < 12; i++) { 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); when(game.getConfig()).thenReturn(config);
Predicting state = mock(Predicting.class); Predicting state = mock(Predicting.class);
doCallRealMethod().when(state).checkPrediction(anyInt()); doCallRealMethod().when(state).checkPrediction(any(), anyInt());
when(state.getGame()).thenReturn(game); when(state.get(PREDICTIONS)).thenReturn(predictions);
when(state.getPredictions()).thenReturn(predictions);
when(state.getDealer()).thenReturn(player); when(state.getDealer()).thenReturn(player);
when(state.getCurrentPlayer()).thenReturn(player); when(state.get(CURRENT_PLAYER)).thenReturn(player);
when(state.getRound()).thenReturn(10); 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++) { for (int i = 0; i < 12; i++) {
if (i == 1) continue; if (i == 1) continue;
state.checkPrediction(i); state.checkPrediction(game, i);
} }
} }
} }

@ -1,6 +1,6 @@
package eu.jonahbauer.wizard.core.machine.states.trick; 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.model.Card;
import eu.jonahbauer.wizard.core.util.Pair; import eu.jonahbauer.wizard.core.util.Pair;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -8,11 +8,23 @@ import org.junit.jupiter.api.Test;
import java.util.List; import java.util.List;
import java.util.UUID; 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.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class FinishingTrickTest { 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 @Test
public void getWinner_ReturnsFirstWizard() { public void getWinner_ReturnsFirstWizard() {
List<Pair<UUID, Card>> stack = List.of( List<Pair<UUID, Card>> stack = List.of(
@ -23,13 +35,7 @@ public class FinishingTrickTest {
Pair.of(null, Card.GREEN_11) Pair.of(null, Card.GREEN_11)
); );
FinishingTrick state = mock(FinishingTrick.class); performTest(stack, Card.Suit.YELLOW, Card.Suit.RED);
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());
} }
@Test @Test
@ -42,13 +48,7 @@ public class FinishingTrickTest {
Pair.of(null, Card.GREEN_11) Pair.of(null, Card.GREEN_11)
); );
FinishingTrick state = mock(FinishingTrick.class); performTest(stack, Card.Suit.YELLOW, Card.Suit.RED);
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());
} }
@Test @Test
@ -61,13 +61,7 @@ public class FinishingTrickTest {
Pair.of(null, Card.GREEN_11) Pair.of(null, Card.GREEN_11)
); );
FinishingTrick state = mock(FinishingTrick.class); performTest(stack, Card.Suit.BLUE, Card.Suit.RED);
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());
} }
@Test @Test
@ -79,12 +73,6 @@ public class FinishingTrickTest {
Pair.of(null, Card.YELLOW_JESTER) Pair.of(null, Card.YELLOW_JESTER)
); );
FinishingTrick state = mock(FinishingTrick.class); performTest(stack, Card.Suit.NONE, Card.Suit.NONE);
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());
} }
} }

@ -1,6 +1,6 @@
package eu.jonahbauer.wizard.core.machine.states.trick; 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.PickTrumpMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.core.messages.player.PredictMessage; import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
@ -11,6 +11,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -22,9 +23,9 @@ public class PlayingCardTest {
PlayingCard state = mock(PlayingCard.class); PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).checkCard(any()); doCallRealMethod().when(state).checkCard(any());
when(state.getHands()).thenReturn(Map.of(player, hand)); when(state.get(HANDS)).thenReturn(Map.of(player, hand));
when(state.getStack()).thenReturn(List.of()); when(state.get(STACK)).thenReturn(List.of());
when(state.getCurrentPlayer()).thenReturn(player); when(state.get(CURRENT_PLAYER)).thenReturn(player);
for (Card value : Card.values()) { for (Card value : Card.values()) {
if (hand.contains(value)) continue; if (hand.contains(value)) continue;
@ -42,8 +43,8 @@ public class PlayingCardTest {
PlayingCard state = mock(PlayingCard.class); PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).checkCard(any()); doCallRealMethod().when(state).checkCard(any());
when(state.getTrickSuit()).thenReturn(Card.Suit.YELLOW); when(state.getTrickSuit()).thenReturn(Card.Suit.YELLOW);
when(state.getHands()).thenReturn(Map.of(player, hand)); when(state.get(HANDS)).thenReturn(Map.of(player, hand));
when(state.getCurrentPlayer()).thenReturn(player); when(state.get(CURRENT_PLAYER)).thenReturn(player);
assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.BLUE_1)); assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.BLUE_1));
assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.RED_1)); assertThrows(IllegalArgumentException.class, () -> state.checkCard(Card.RED_1));
@ -58,24 +59,28 @@ public class PlayingCardTest {
UUID player = new UUID(0, 0); UUID player = new UUID(0, 0);
UUID player2 = new UUID(0, 1); UUID player2 = new UUID(0, 1);
Game game = mock(Game.class);
PlayingCard state = mock(PlayingCard.class); PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).onMessage(any(), any()); doCallRealMethod().when(state).onMessage(any(), any(), any());
when(state.getCurrentPlayer()).thenReturn(player); when(state.get(CURRENT_PLAYER)).thenReturn(player);
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PlayCardMessage(Card.BLUE_WIZARD))); assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PlayCardMessage(Card.BLUE_WIZARD)));
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PredictMessage(1))); assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PredictMessage(1)));
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PickTrumpMessage(Card.Suit.BLUE))); assertThrows(IllegalStateException.class, () -> state.onMessage(game, player2, new PickTrumpMessage(Card.Suit.BLUE)));
} }
@Test @Test
public void onMessage_ThrowsIllegalState_IfNotPlayCard() { public void onMessage_ThrowsIllegalState_IfNotPlayCard() {
UUID player = new UUID(0, 0); UUID player = new UUID(0, 0);
Game game = mock(Game.class);
PlayingCard state = mock(PlayingCard.class); PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).onMessage(any(), any()); doCallRealMethod().when(state).onMessage(any(), any(), any());
when(state.getCurrentPlayer()).thenReturn(player); when(state.get(CURRENT_PLAYER)).thenReturn(player);
assertThrows(IllegalStateException.class, () -> state.onMessage(player, new PredictMessage(1))); assertThrows(IllegalStateException.class, () -> state.onMessage(game, player, new PredictMessage(1)));
assertThrows(IllegalStateException.class, () -> state.onMessage(player, new PickTrumpMessage(Card.Suit.BLUE))); assertThrows(IllegalStateException.class, () -> state.onMessage(game, player, new PickTrumpMessage(Card.Suit.BLUE)));
} }
} }

@ -1,6 +1,5 @@
package eu.jonahbauer.wizard.core.machine.states.trick; 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.model.Card;
import eu.jonahbauer.wizard.core.util.Pair; import eu.jonahbauer.wizard.core.util.Pair;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -8,6 +7,7 @@ import org.junit.jupiter.api.Test;
import java.util.List; import java.util.List;
import java.util.UUID; 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.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -19,7 +19,7 @@ public class TrickStateTest {
TrickState trickState = mock(TrickState.class); TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod(); when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.getStack()).thenReturn(stack); when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE); assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
} }
@ -36,7 +36,7 @@ public class TrickStateTest {
TrickState trickState = mock(TrickState.class); TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod(); when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.getStack()).thenReturn(stack); when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.RED); assertEquals(trickState.getTrickSuit(), Card.Suit.RED);
} }
@ -53,7 +53,7 @@ public class TrickStateTest {
TrickState trickState = mock(TrickState.class); TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod(); when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.getStack()).thenReturn(stack); when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE); assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
} }
@ -70,7 +70,7 @@ public class TrickStateTest {
TrickState trickState = mock(TrickState.class); TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod(); when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.getStack()).thenReturn(stack); when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE); assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
@ -83,7 +83,7 @@ public class TrickStateTest {
); );
TrickState trickState2 = mock(TrickState.class); TrickState trickState2 = mock(TrickState.class);
when(trickState2.getTrickSuit()).thenCallRealMethod(); when(trickState2.getTrickSuit()).thenCallRealMethod();
when(trickState2.getStack()).thenReturn(stack2); when(trickState2.get(STACK)).thenReturn(stack2);
assertEquals(trickState2.getTrickSuit(), Card.Suit.YELLOW); assertEquals(trickState2.getTrickSuit(), Card.Suit.YELLOW);
} }

Loading…
Cancel
Save