- Jubiläumsedition implementiert (#12)

- Tests verbessert
main
Jonah Bauer 3 years ago
parent 9f067fae1a
commit b98d25599b

@ -13,6 +13,7 @@
<module>wizard-core</module> <module>wizard-core</module>
<module>wizard-client</module> <module>wizard-client</module>
<module>wizard-server</module> <module>wizard-server</module>
<module>wizard-common</module>
</modules> </modules>
<properties> <properties>

@ -19,8 +19,8 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>eu.jonahbauer</groupId> <groupId>${project.groupId}</groupId>
<artifactId>wizard-core</artifactId> <artifactId>wizard-common</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wizard</artifactId>
<groupId>eu.jonahbauer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wizard-common</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>

@ -1,6 +1,7 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -12,6 +13,7 @@ import java.util.UUID;
*/ */
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class CardMessage extends ObserverMessage { public final class CardMessage extends ObserverMessage {
/** /**
* The UUID of the player. * The UUID of the player.

@ -1,6 +1,7 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -11,6 +12,7 @@ import java.util.UUID;
* A {@link HandMessage} is sent when the player receives information about hit own or another player's hand cards. * A {@link HandMessage} is sent when the player receives information about hit own or another player's hand cards.
*/ */
@Getter @Getter
@EqualsAndHashCode(callSuper = true)
public final class HandMessage extends ObserverMessage { public final class HandMessage extends ObserverMessage {
/** /**
* The UUID of player whose hand cards are sent. * The UUID of player whose hand cards are sent.

@ -0,0 +1,7 @@
package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public final class JugglingMessage extends ObserverMessage {
}

@ -1,10 +1,12 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import eu.jonahbauer.wizard.core.util.SealedClassTypeAdapterFactory; import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory;
import lombok.EqualsAndHashCode;
public abstract sealed class ObserverMessage permits CardMessage, HandMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage { @EqualsAndHashCode
public abstract sealed class ObserverMessage permits CardMessage, HandMessage, JugglingMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage {
public static final Gson GSON = new GsonBuilder() public static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message")) .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message"))
.create(); .create();

@ -1,5 +1,6 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -12,6 +13,7 @@ import java.util.UUID;
*/ */
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PredictionMessage extends ObserverMessage { public final class PredictionMessage extends ObserverMessage {
/** /**
* The UUID of the player who made a prediction. * The UUID of the player who made a prediction.

@ -1,5 +1,6 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -11,6 +12,7 @@ import java.util.UUID;
* gained by each player in this round or the final result. * gained by each player in this round or the final result.
*/ */
@Getter @Getter
@EqualsAndHashCode(callSuper = true)
public final class ScoreMessage extends ObserverMessage { public final class ScoreMessage extends ObserverMessage {
/** /**
* The number of points for each player. * The number of points for each player.

@ -0,0 +1,18 @@
package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* A {@link StateMessage} is sent whenever the game changes its internal state.
*/
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class StateMessage extends ObserverMessage {
/**
* The name of the new state in snake_case.
*/
private final String state;
}

@ -1,6 +1,7 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import java.util.List; import java.util.List;
@ -11,6 +12,7 @@ import java.util.UUID;
* cards played. * cards played.
*/ */
@Getter @Getter
@EqualsAndHashCode(callSuper = true)
public final class TrickMessage extends ObserverMessage { public final class TrickMessage extends ObserverMessage {
/** /**
* The UUID of the player who won the trick. * The UUID of the player who won the trick.

@ -1,6 +1,7 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -8,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
* A {@link TrumpMessage} is sent when the trump suit of the current round is (being) determined. * A {@link TrumpMessage} is sent when the trump suit of the current round is (being) determined.
*/ */
@Getter @Getter
@EqualsAndHashCode(callSuper = true)
public final class TrumpMessage extends ObserverMessage { public final class TrumpMessage extends ObserverMessage {
/** /**
* The {@link Card} that was revealed to determine the {@linkplain Card.Suit trump suit} or {@code null} no cards * The {@link Card} that was revealed to determine the {@linkplain Card.Suit trump suit} or {@code null} no cards
@ -20,7 +22,6 @@ public final class TrumpMessage extends ObserverMessage {
private final @Nullable Card.Suit suit; private final @Nullable Card.Suit suit;
public TrumpMessage(@Nullable Card card, @Nullable Card.Suit suit) { public TrumpMessage(@Nullable Card card, @Nullable Card.Suit suit) {
if (suit != null && !suit.isColor()) throw new IllegalArgumentException("The trump suit must be a color or null.");
if (card == null && suit == null) throw new IllegalArgumentException("Card and suit must not both be null"); if (card == null && suit == null) throw new IllegalArgumentException("Card and suit must not both be null");
this.card = card; this.card = card;

@ -1,8 +1,9 @@
package eu.jonahbauer.wizard.core.messages.observer; package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.core.messages.player.PredictMessage; import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -13,6 +14,7 @@ import java.util.UUID;
*/ */
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class UserInputMessage extends ObserverMessage { public final class UserInputMessage extends ObserverMessage {
/** /**
* The UUID of the player whose input is required. * The UUID of the player whose input is required.
@ -33,6 +35,11 @@ public final class UserInputMessage extends ObserverMessage {
* {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}. * {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}.
*/ */
MAKE_PREDICTION, MAKE_PREDICTION,
/**
* An action that indicates that a player should change his a prediction by ±1. A {@link UserInputMessage} with
* this {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}.
*/
CHANGE_PREDICTION,
/** /**
* An action that indicates that a player should play a card. A {@link UserInputMessage} with this * An action that indicates that a player should play a card. A {@link UserInputMessage} with this
* {@link UserInputMessage#getAction()} should be responded to with a {@link PlayCardMessage}. * {@link UserInputMessage#getAction()} should be responded to with a {@link PlayCardMessage}.

@ -1,11 +1,13 @@
package eu.jonahbauer.wizard.core.messages.player; package eu.jonahbauer.wizard.common.messages.player;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PickTrumpMessage extends PlayerMessage { public final class PickTrumpMessage extends PlayerMessage {
private final Card.Suit trumpSuit; private final Card.Suit trumpSuit;
} }

@ -1,11 +1,13 @@
package eu.jonahbauer.wizard.core.messages.player; package eu.jonahbauer.wizard.common.messages.player;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PlayCardMessage extends PlayerMessage { public final class PlayCardMessage extends PlayerMessage {
private final Card card; private final Card card;
} }

@ -1,9 +1,11 @@
package eu.jonahbauer.wizard.core.messages.player; package eu.jonahbauer.wizard.common.messages.player;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import eu.jonahbauer.wizard.core.util.SealedClassTypeAdapterFactory; import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public abstract sealed class PlayerMessage permits PickTrumpMessage, PlayCardMessage, PredictMessage { public abstract sealed class PlayerMessage permits PickTrumpMessage, PlayCardMessage, PredictMessage {
public static final Gson GSON = new GsonBuilder() public static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message")) .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message"))

@ -1,10 +1,12 @@
package eu.jonahbauer.wizard.core.messages.player; package eu.jonahbauer.wizard.common.messages.player;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PredictMessage extends PlayerMessage { public final class PredictMessage extends PlayerMessage {
private final int prediction; private final int prediction;
} }

@ -0,0 +1,31 @@
package eu.jonahbauer.wizard.common.model.card;
public enum Card {
HIDDEN,
BLUE_1, RED_1, GREEN_1, YELLOW_1,
BLUE_2, RED_2, GREEN_2, YELLOW_2,
BLUE_3, RED_3, GREEN_3, YELLOW_3,
BLUE_4, RED_4, GREEN_4, YELLOW_4,
BLUE_5, RED_5, GREEN_5, YELLOW_5,
BLUE_6, RED_6, GREEN_6, YELLOW_6,
BLUE_7, RED_7, GREEN_7, YELLOW_7,
BLUE_8, RED_8, GREEN_8, YELLOW_8,
BLUE_9, RED_9, GREEN_9, YELLOW_9,
BLUE_10, RED_10, GREEN_10, YELLOW_10,
BLUE_11, RED_11, GREEN_11, YELLOW_11,
BLUE_12, RED_12, GREEN_12, YELLOW_12,
BLUE_13, RED_13, GREEN_13, YELLOW_13,
BLUE_WIZARD, RED_WIZARD, GREEN_WIZARD, YELLOW_WIZARD,
BLUE_JESTER, RED_JESTER, GREEN_JESTER, YELLOW_JESTER,
CHANGELING, CHANGELING_WIZARD, CHANGELING_JESTER,
BOMB,
WEREWOLF,
DRAGON,
FAIRY,
CLOUD, CLOUD_BLUE, CLOUD_RED, CLOUD_GREEN, CLOUD_YELLOW,
JUGGLER, JUGGLER_BLUE, JUGGLER_RED, JUGGLER_GREEN, JUGGLER_YELLOW;
public enum Suit {
NONE, YELLOW, RED, GREEN, BLUE
}
}

@ -1,4 +1,4 @@
package eu.jonahbauer.wizard.core.util; package eu.jonahbauer.wizard.common.util;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapter;

@ -16,4 +16,11 @@
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
</properties> </properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>wizard-common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project> </project>

@ -1,12 +1,11 @@
package eu.jonahbauer.wizard.core; package eu.jonahbauer.wizard.core;
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import eu.jonahbauer.wizard.common.model.card.Card;
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.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.Configuration;
import eu.jonahbauer.wizard.core.model.Configurations; import eu.jonahbauer.wizard.core.model.Configurations;
@ -30,15 +29,13 @@ 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) ([a-z]+) (.*)"); 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) ([a-z]+) (.*)\""); System.err.println("Format is \"(\\d) ([a-z]+) (.*)\"");
continue; continue;
} }
String player = matcher.group(1); String player = matcher.group(1);
@ -55,15 +52,24 @@ public class CLI {
switch (command) { switch (command) {
case "predict" -> { case "predict" -> {
int prediction = Integer.parseInt(param); int prediction = Integer.parseInt(param);
game.onMessage(players.get(id), new PredictMessage(prediction)); game.onMessage(players.get(id), new PredictMessage(prediction))
.whenComplete((v, err) -> {
if (err != null) err.printStackTrace();
});
} }
case "play" -> { case "play" -> {
Card card = Card.valueOf(param); Card card = Card.valueOf(param);
game.onMessage(players.get(id), new PlayCardMessage(card)); game.onMessage(players.get(id), new PlayCardMessage(card))
.whenComplete((v, err) -> {
if (err != null) err.printStackTrace();
});
} }
case "trump" -> { case "trump" -> {
Card.Suit suit = Card.Suit.valueOf(param); Card.Suit suit = Card.Suit.valueOf(param);
game.onMessage(players.get(id), new PickTrumpMessage(suit)); game.onMessage(players.get(id), new PickTrumpMessage(suit))
.whenComplete((v, err) -> {
if (err != null) err.printStackTrace();
});
} }
default -> System.err.println("Unknown command: " + command); default -> System.err.println("Unknown command: " + command);
} }

@ -1,123 +1,155 @@
package eu.jonahbauer.wizard.core.machine; package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.core.machine.states.State; import eu.jonahbauer.wizard.core.machine.states.State;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NonBlocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.locks.Condition; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
public abstract class Context<S extends State<S,C>, C extends Context<S,C>> { public abstract class Context<S extends State<S,C>, C extends Context<S,C>> {
protected final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1,
0, TimeUnit.SECONDS,
new PriorityBlockingQueue<>(
11,
Comparator.comparingInt(r -> r instanceof PriorityRunnable prio ? prio.getPriority() : Integer.MAX_VALUE)
.thenComparingLong(r -> r instanceof PriorityRunnable prio ? prio.getTimestamp() : Long.MAX_VALUE)
),
r -> {
var t = new Thread(r);
t.setUncaughtExceptionHandler((t1, e) -> finish(e));
return t;
}
);
protected S state; 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 S state) { private final CompletableFuture<Void> future = new CompletableFuture<>();
lock.lock(); private final CompletableFuture<Void> finished = future.whenComplete((v, t) -> {
try { executor.shutdownNow();
if (finished) throw new IllegalStateException("Context has already finished."); scheduler.shutdownNow();
transition(null, state); });
} finally {
lock.unlock(); @NonBlocking
} protected CompletableFuture<Void> submit(Runnable runnable) {
var future = new CompletableFuture<Void>();
executor.execute(new PriorityRunnable(100, () -> {
try {
runnable.run();
future.complete(null);
} catch (Throwable t) {
future.completeExceptionally(t);
}
}));
return future;
} }
public void transition(S currentState, S newState) { @Blocking
lock.lock(); protected void start(@NotNull S state) {
try { CountDownLatch latch = new CountDownLatch(1);
if (state == currentState) { AtomicReference<RuntimeException> exception = new AtomicReference<>();
state = newState;
if (currentState != null) //noinspection unchecked executor.execute(new PriorityRunnable(0, () -> {
currentState.onExit((C) this); if (future.isDone()) {
onTransition(currentState, newState); exception.set(new IllegalStateException("Context has already finished."));
if (newState != null) //noinspection unchecked latch.countDown();
newState.onEnter((C) this);
} else { } else {
throw new IllegalStateException("Current state does not match."); latch.countDown();
doTransition(null, state);
} }
} catch (Throwable t) { }));
handleError(t);
} finally { while (true) {
lock.unlock(); try {
latch.await();
if (exception.get() != null) {
throw exception.get();
}
break;
} catch (InterruptedException ignored) {}
} }
} }
@NonBlocking
public void transition(S currentState, S newState) {
executor.execute(new PriorityRunnable(0, () -> doTransition(currentState, newState)));
}
@NonBlocking
public void finish() { public void finish() {
finish(null); finish(null);
} }
@NonBlocking
public void finish(Throwable exception) { public void finish(Throwable exception) {
lock.lock(); executor.execute(new PriorityRunnable(0, () -> doFinish(exception)));
try {
finished = true;
this.exception = exception;
finishCondition.signalAll();
transition(state, null);
scheduler.shutdown();
} finally {
lock.unlock();
}
} }
@NonBlocking
public void cancel() { public void cancel() {
finish(new CancellationException()); finish(new CancellationException());
} }
@SuppressWarnings("BooleanMethodIsAlwaysInverted") /*
public boolean isDone() { * internal methods that are called on the executor
return finished; */
private void doTransition(S currentState, S newState) {
if (state == currentState) {
state = newState;
if (currentState != null) //noinspection unchecked
currentState.onExit((C) this);
onTransition(currentState, newState);
if (newState != null) //noinspection unchecked
newState.onEnter((C) this);
}
}
private void doFinish(Throwable t) {
if (future.isDone()) return;
doTransition(state, null);
if (t != null) {
future.completeExceptionally(t);
} else {
future.complete(null);
}
} }
protected void onTransition(S from, S to) {}
@Blocking @Blocking
public void await() throws InterruptedException, ExecutionException, CancellationException { public void await() throws InterruptedException, ExecutionException, CancellationException {
lock.lock(); finished.get();
try {
while (!finished) {
finishCondition.await();
}
if (exception != null) {
if (exception instanceof CancellationException cancelled) {
throw cancelled;
} else {
throw new ExecutionException(exception);
}
}
} finally {
lock.unlock();
}
} }
public void timeout(@NotNull S currentState, long delay) { public void timeout(@NotNull S currentState, long delay) {
scheduler.schedule(() -> { scheduler.schedule(() -> {
lock.lock(); submit(() -> {
try {
if (state == currentState) { if (state == currentState) {
//noinspection unchecked //noinspection unchecked
state.onTimeout((C) this); state.onTimeout((C) this);
} }
} catch (Throwable t) { });
handleError(t);
} finally {
lock.unlock();
}
}, delay, TimeUnit.MILLISECONDS); }, delay, TimeUnit.MILLISECONDS);
} }
protected void handleError(Throwable t) { @Getter
lock.lock(); @RequiredArgsConstructor
try { private static class PriorityRunnable implements Runnable {
if (!isDone()) { private final int priority;
finish(t); private final long timestamp = System.nanoTime();
t.printStackTrace(); private final Runnable runnable;
}
} finally { @Override
lock.unlock(); public void run() {
runnable.run();
} }
} }
protected void onTransition(S from, S to) {}
} }

@ -4,75 +4,73 @@ 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;
import eu.jonahbauer.wizard.core.messages.observer.ObserverMessage; import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage;
import eu.jonahbauer.wizard.core.messages.observer.StateMessage; import eu.jonahbauer.wizard.common.messages.observer.StateMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Configuration; import eu.jonahbauer.wizard.core.model.Configuration;
import eu.jonahbauer.wizard.core.util.Util;
import lombok.Getter; import lombok.Getter;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS; import static eu.jonahbauer.wizard.core.machine.states.GameData.PLAYERS;
public final class Game extends Context<GameState, Game> { public final class Game extends Context<GameState, Game> {
@Getter
private final Random random;
@Getter @Getter
private final Configuration config; private final Configuration config;
private final Observer observer; private final Observer observer;
public Game(Configuration config, Observer observer) { public Game(Configuration config, Observer observer) {
this.random = new Random();
this.config = config;
this.observer = observer;
}
public Game(long seed, Configuration config, Observer observer) {
this.random = new Random(seed);
this.config = config; this.config = config;
this.observer = observer; this.observer = observer;
} }
public void start(List<UUID> players) { public void start(List<UUID> players) {
start(new Starting(new GameData().with(PLAYERS, List.copyOf(players)))); start(new Starting(GameData.EMPTY.with(PLAYERS, List.copyOf(players))));
} }
public void resume(GameState state) { public void resume(GameState state) {
start(state); start(state);
} }
public GameState stop() { public CompletableFuture<Void> onMessage(UUID player, PlayerMessage message) {
GameState state = this.state; return submit(() -> {
if (state != null) { if (state != null) {
finish(); state.onMessage(this, player, message);
return state; }
} });
return null;
}
public void onMessage(UUID player, PlayerMessage message) {
lock.lock();
try {
state.onMessage(this, player, message);
} catch (IllegalStateException | IllegalArgumentException e) {
throw e;
} catch (Throwable t) {
handleError(t);
} finally {
lock.unlock();
}
} }
@Override @Override
protected void onTransition(GameState from, GameState to) { protected void onTransition(GameState from, GameState to) {
notify(new StateMessage(to != null ? to.getClass() : null)); notify(new StateMessage(to != null ? Util.toSnakeCase(to.getClass().getSimpleName()) : "null"));
} }
public void notify(ObserverMessage message) { public void notify(ObserverMessage message) {
try { try {
observer.notify(message); observer.notify(message);
} catch (Exception e) { } catch (Throwable t) {
e.printStackTrace(); t.printStackTrace();
} }
} }
public void notify(UUID player, ObserverMessage message) { public void notify(UUID player, ObserverMessage message) {
try { try {
observer.notify(player, message); observer.notify(player, message);
} catch (Exception e) { } catch (Throwable t) {
e.printStackTrace(); t.printStackTrace();
} }
} }
} }

@ -1,6 +1,6 @@
package eu.jonahbauer.wizard.core.machine.states; package eu.jonahbauer.wizard.core.machine.states;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair; import eu.jonahbauer.wizard.core.util.Pair;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -14,10 +14,12 @@ import org.jetbrains.annotations.Unmodifiable;
import java.util.*; import java.util.*;
@Unmodifiable @Unmodifiable
@EqualsAndHashCode(of = "values") @EqualsAndHashCode(of = {"values", "present"})
public final class GameData { public final class GameData {
private static final int SIZE = 11; private static final int SIZE = 11;
public static final GameData EMPTY = new GameData();
public static final Key<List<UUID>> PLAYERS = new Key<>("players", 0); 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<Map<UUID, Integer>> SCORE = new Key<>("score", 1);
public static final Key<Integer> ROUND = new Key<>("round", 2); public static final Key<Integer> ROUND = new Key<>("round", 2);
@ -34,7 +36,7 @@ public final class GameData {
private final boolean[] present; private final boolean[] present;
private transient final boolean[] required = new boolean[SIZE]; private transient final boolean[] required = new boolean[SIZE];
public GameData() { private GameData() {
this.values = new Object[SIZE]; this.values = new Object[SIZE];
this.present = new boolean[SIZE]; this.present = new boolean[SIZE];
} }
@ -81,6 +83,37 @@ public final class GameData {
return new GameData(newValues, newPresent); return new GameData(newValues, newPresent);
} }
/**
* @see #with(Key, Object)
*/
public GameData with(@NotNull TypedValue<?>@NotNull... entries) {
boolean equal = true;
for (TypedValue<?> entry : entries) {
int index = entry.key().index();
if (!present[index] || !Objects.equals(values[index], entry.value())) {
equal = false;
break;
}
}
if (equal) {
return this;
}
Object[] newValues = Arrays.copyOf(this.values, SIZE);
boolean[] newPresent = Arrays.copyOf(this.present, SIZE);
for (TypedValue<?> entry : entries) {
int index = entry.key().index();
newValues[index] = entry.value();
newPresent[index] = true;
}
return new GameData(newValues, newPresent);
}
/**
* @see #with(Key, Object)
*/
public <T1, T2> GameData with(@NotNull Key<T1> key1, T1 value1, Key<T2> key2, T2 value2) { public <T1, T2> GameData with(@NotNull Key<T1> key1, T1 value1, Key<T2> key2, T2 value2) {
int index1 = key1.index(); int index1 = key1.index();
int index2 = key2.index(); int index2 = key2.index();
@ -101,6 +134,9 @@ public final class GameData {
return new GameData(newValues, newPresent); return new GameData(newValues, newPresent);
} }
/**
* @see #with(Key, Object)
*/
public <T1, T2, T3> GameData with(@NotNull Key<T1> key1, T1 value1, Key<T2> key2, T2 value2, Key<T3> key3, T3 value3) { 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 index1 = key1.index();
int index2 = key2.index(); int index2 = key2.index();
@ -177,7 +213,7 @@ public final class GameData {
var mapValue = get(map); var mapValue = get(map);
var listValue = get(list); var listValue = get(list);
for (K k : listValue) { for (K k : listValue) {
if (!mapValue.containsKey(k)) throw new NoSuchElementException(); if (!mapValue.containsKey(k)) throw new NoSuchElementException("Could not find required value: " + map.name() + "[" + k + "].");
} }
return this; return this;
} }
@ -216,4 +252,10 @@ public final class GameData {
return name(); return name();
} }
} }
public record TypedValue<T>(Key<T> key, T value) {
public static <T> TypedValue<T> entry(Key<T> key, T value) {
return new TypedValue<>(key, value);
}
}
} }

@ -1,7 +1,7 @@
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.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;

@ -3,7 +3,7 @@ package eu.jonahbauer.wizard.core.machine.states.game;
import eu.jonahbauer.wizard.core.machine.states.GameData; 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.states.GameState; import eu.jonahbauer.wizard.core.machine.states.GameState;
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage; import eu.jonahbauer.wizard.common.messages.observer.ScoreMessage;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*;

@ -1,12 +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.common.messages.observer.HandMessage;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.messages.observer.HandMessage; import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.deck.Deck;
import eu.jonahbauer.wizard.core.model.Deck;
import java.util.*; import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
@ -19,7 +22,7 @@ public final class Dealing extends RoundState {
@Override @Override
public void onEnter(Game game) { public void onEnter(Game game) {
Deck deck = new Deck(game.getConfig().cards()); Deck deck = new Deck(game.getConfig().cards());
deck.shuffle(); deck.shuffle(game.getRandom());
var hands = new HashMap<UUID, List<Card>>(); var hands = new HashMap<UUID, List<Card>>();
@ -32,7 +35,7 @@ public final class Dealing extends RoundState {
} }
for (UUID player : get(PLAYERS)) { for (UUID player : get(PLAYERS)) {
game.notify(player, new HandMessage(player, hands.get(player))); game.notify(player, new HandMessage(player, hands.get(player).stream().toList()));
} }
Card trumpCard = deck.draw(); Card trumpCard = deck.draw();

@ -1,34 +1,54 @@
package eu.jonahbauer.wizard.core.machine.states.round; package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.states.GameData; 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.messages.observer.TrumpMessage; import eu.jonahbauer.wizard.common.messages.observer.HandMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.messages.observer.TrumpMessage;
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.card.GameCards;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.Arrays; import java.util.*;
import java.util.Map;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*; 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.common.messages.observer.UserInputMessage.Action.PICK_TRUMP;
public final class DeterminingTrump extends RoundState { public final class DeterminingTrump extends RoundState {
private transient UUID player;
private transient boolean werewolf;
public DeterminingTrump(GameData data) { public DeterminingTrump(GameData data) {
super(data.require(TRUMP_CARD).requireEach(PLAYERS, HANDS)); super(data.require(TRUMP_CARD).requireEach(PLAYERS, HANDS));
} }
@Override @Override
public void onEnter(Game game) { public void onEnter(Game game) {
Card trumpCard = getData().get(TRUMP_CARD); Card trumpCard = get(TRUMP_CARD);
Card.Suit trumpSuit = trumpCard != null ? trumpCard.getTrumpSuit() : Card.Suit.NONE;
// handle werewolf
for (Map.Entry<UUID, List<Card>> entry : get(HANDS).entrySet()) {
var player = entry.getKey();
var hand = entry.getValue();
if (hand.contains(Card.WEREWOLF)) {
this.player = player;
this.werewolf = true;
game.notify(new TrumpMessage(trumpCard, null));
game.notify(new TrumpMessage(Card.WEREWOLF, null));
game.notify(new UserInputMessage(this.player, PICK_TRUMP, getTimeout(game, true)));
timeout(game);
return;
}
}
// default trump handling
Card.Suit trumpSuit = trumpCard != null ? GameCards.get(trumpCard).getTrumpSuit() : Card.Suit.NONE;
if (trumpSuit == null) { if (trumpSuit == null) {
this.player = getDealer();
game.notify(new TrumpMessage(trumpCard, null)); game.notify(new TrumpMessage(trumpCard, null));
game.notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(game, true))); game.notify(new UserInputMessage(this.player, PICK_TRUMP, getTimeout(game, true)));
timeout(game); timeout(game);
} else { } else {
transition(game, trumpSuit); transition(game, trumpSuit);
@ -38,12 +58,12 @@ public final class DeterminingTrump extends RoundState {
@Override @Override
public void onTimeout(Game game) { 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};
transition(game, suits[(int) (Math.random() * suits.length)]); transition(game, suits[game.getRandom().nextInt(suits.length)]);
} }
@Override @Override
public void onMessage(Game game, UUID player, PlayerMessage message) { public void onMessage(Game game, UUID player, PlayerMessage message) {
if (getDealer().equals(player) && message instanceof PickTrumpMessage trumpMessage) { if (this.player.equals(player) && message instanceof PickTrumpMessage trumpMessage) {
checkTrumpSuit(trumpMessage.getTrumpSuit()); checkTrumpSuit(trumpMessage.getTrumpSuit());
transition(game, trumpMessage.getTrumpSuit()); transition(game, trumpMessage.getTrumpSuit());
} else { } else {
@ -51,21 +71,30 @@ public final class DeterminingTrump extends RoundState {
} }
} }
@VisibleForTesting private void checkTrumpSuit(Card.Suit suit) {
void checkTrumpSuit(Card.Suit suit) { if (suit == Card.Suit.NONE) {
Card.Suit[] suits = new Card.Suit[]{Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW}; throw new IllegalArgumentException("Trump suit must not be " + Card.Suit.NONE + ".");
for (Card.Suit s : suits) {
if (s == suit) return;
} }
throw new IllegalArgumentException("Trump suit must be one of " + Arrays.toString(suits) + ".");
} }
private void transition(Game game, @NotNull Card.Suit trumpSuit) { private void transition(Game game, @NotNull Card.Suit trumpSuit) {
game.notify(new TrumpMessage(getData().get(TRUMP_CARD), trumpSuit)); GameData data = getData().with(
transition(game, new Predicting(getData().with(
TRUMP_SUIT, trumpSuit, TRUMP_SUIT, trumpSuit,
PREDICTIONS, Map.of(), PREDICTIONS, Map.of(),
CURRENT_PLAYER, getNextPlayer(getDealer()) CURRENT_PLAYER, getNextPlayer(getDealer())
))); );
if (werewolf) {
var mutableHands = new HashMap<>(get(HANDS));
var mutableHand = new ArrayList<>(mutableHands.get(player));
mutableHand.set(mutableHand.indexOf(Card.WEREWOLF), get(TRUMP_CARD));
mutableHands.put(player, List.copyOf(mutableHand));
data = data.with(HANDS, Map.copyOf(mutableHands));
game.notify(new TrumpMessage(Card.WEREWOLF, trumpSuit));
game.notify(player, new HandMessage(player, mutableHands.get(player)));
} else {
game.notify(new TrumpMessage(get(TRUMP_CARD), trumpSuit));
}
transition(game, new Predicting(data));
} }
} }

@ -2,10 +2,8 @@ package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.states.GameData; 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.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.common.messages.observer.ScoreMessage;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -17,7 +15,6 @@ public final class FinishingRound extends RoundState {
public FinishingRound(GameData data) { public FinishingRound(GameData data) {
super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE)); super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE));
checkData(data);
} }
@Override @Override
@ -37,8 +34,7 @@ public final class FinishingRound extends RoundState {
} }
} }
@VisibleForTesting private Map<UUID, Integer> getPoints() {
Map<UUID, Integer> getPoints() {
var points = new HashMap<UUID, Integer>(); var points = new HashMap<UUID, Integer>();
for (UUID player : get(PLAYERS)) { for (UUID player : get(PLAYERS)) {
int prediction = get(PREDICTIONS).get(player); int prediction = get(PREDICTIONS).get(player);
@ -52,12 +48,4 @@ 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 + ".");
}
}
} }

@ -3,19 +3,18 @@ package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.states.GameData; 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.states.trick.StartingTrick; import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
import eu.jonahbauer.wizard.core.messages.observer.PredictionMessage; import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.messages.player.PredictMessage; import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.HashMap; import java.util.HashMap;
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 eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.MAKE_PREDICTION; import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.MAKE_PREDICTION;
@Getter @Getter
public final class Predicting extends RoundState { public final class Predicting extends RoundState {
@ -25,7 +24,7 @@ public final class Predicting extends RoundState {
@Override @Override
public void onEnter(Game game) { public void onEnter(Game game) {
game.notify(new UserInputMessage(getData().get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true))); game.notify(new UserInputMessage(get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true)));
timeout(game); timeout(game);
} }
@ -46,7 +45,7 @@ public final class Predicting extends RoundState {
@Override @Override
public void onMessage(Game game, UUID player, PlayerMessage message) { public void onMessage(Game game, UUID player, PlayerMessage message) {
if (getData().get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) { if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
checkPrediction(game, predictMessage.getPrediction()); checkPrediction(game, predictMessage.getPrediction());
transition(game, predictMessage.getPrediction()); transition(game, predictMessage.getPrediction());
} else { } else {
@ -54,8 +53,7 @@ public final class Predicting extends RoundState {
} }
} }
@VisibleForTesting private void checkPrediction(Game game, int prediction) {
void checkPrediction(Game game, int prediction) {
int round = get(ROUND); int round = get(ROUND);
if (prediction < 0 || prediction > round + 1) { if (prediction < 0 || prediction > round + 1) {
throw new IllegalArgumentException("Prediction must be between 0 and " + (round + 1) + "."); throw new IllegalArgumentException("Prediction must be between 0 and " + (round + 1) + ".");

@ -13,6 +13,6 @@ public abstract class RoundState extends GameState {
} }
protected UUID getDealer() { protected UUID getDealer() {
return getPlayer(getData().get(ROUND) % getPlayerCount()); return getPlayer(get(ROUND) % getPlayerCount());
} }
} }

@ -0,0 +1,73 @@
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.states.round.FinishingRound;
import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
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.common.messages.observer.UserInputMessage.Action.*;
public final class ChangingPrediction extends TrickState {
private transient final int oldPrediction;
public ChangingPrediction(GameData data) {
super(data);
oldPrediction = get(PREDICTIONS).get(get(CURRENT_PLAYER));
}
@Override
public void onEnter(Game game) {
game.notify(new UserInputMessage(get(CURRENT_PLAYER), CHANGE_PREDICTION, getTimeout(game, true)));
timeout(game);
}
@Override
public void onMessage(Game game, UUID player, PlayerMessage message) {
if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
checkPrediction(predictMessage.getPrediction());
transition(game, predictMessage.getPrediction());
} else {
super.onMessage(game, player, message);
}
}
@Override
public void onTimeout(Game game) {
transition(game, oldPrediction + 1);
}
private void checkPrediction(int prediction) {
if (prediction < 0) {
throw new IllegalArgumentException("Prediction must be greater than or equal to 0.");
} else if (Math.abs(prediction - oldPrediction) != 1) {
throw new IllegalArgumentException("Prediction must differ from your old prediction by exactly one.");
}
}
private void transition(Game game, int prediction) {
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().with(
PREDICTIONS, Map.copyOf(predictions)
);
boolean hasNextTrick = get(TRICK) < get(ROUND);
if (hasNextTrick) {
transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1)));
} else {
transition(game, new FinishingRound(data));
}
}
}

@ -1,13 +1,15 @@
package eu.jonahbauer.wizard.core.machine.states.trick; package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.states.GameData; 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.states.InvalidDataException; 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.common.messages.observer.HandMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.common.messages.observer.JugglingMessage;
import eu.jonahbauer.wizard.common.messages.observer.TrickMessage;
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 java.util.*; import java.util.*;
@ -22,48 +24,52 @@ public final class FinishingTrick extends TrickState {
@Override @Override
public void onEnter(Game game) { public void onEnter(Game game) {
var winner = getWinner(); var stack = get(STACK);
game.notify(new TrickMessage(winner, get(STACK).stream().map(Pair::second).toList())); var cards = stack.stream().map(Pair::second).toList();
var tricks = new HashMap<>(get(TRICKS)); var winner = CardUtils.getWinner(stack, get(TRUMP_SUIT));
tricks.merge(winner, 1, Integer::sum); game.notify(new TrickMessage(winner, cards));
GameData data = getData().with(TRICKS, Map.copyOf(tricks)); GameData data = getData();
if (get(TRICK) < get(ROUND)) { boolean bomb = cards.contains(Card.BOMB);
transition(game, new StartingTrick(data.with( boolean juggler = cards.contains(Card.JUGGLER_BLUE)
CURRENT_PLAYER, winner, || cards.contains(Card.JUGGLER_RED)
TRICK, get(TRICK) + 1 || cards.contains(Card.JUGGLER_GREEN)
))); || cards.contains(Card.JUGGLER_YELLOW);
} else { boolean cloud = cards.contains(Card.CLOUD_BLUE)
transition(game, new FinishingRound(data)); || cards.contains(Card.CLOUD_RED)
} || cards.contains(Card.CLOUD_GREEN)
} || cards.contains(Card.CLOUD_YELLOW);
boolean hasNextTrick = get(TRICK) < get(ROUND);
@VisibleForTesting // juggle hands
UUID getWinner() { if (juggler && hasNextTrick) {
var wizard = get(STACK).stream() game.notify(new JugglingMessage());
.filter(pair -> pair.second().getSuit() == Card.Suit.WIZARD) var hands = get(HANDS);
.findFirst() Map<UUID, List<Card>> juggledHands = new HashMap<>();
.orElse(null); hands.forEach((player, hand) -> juggledHands.put(getNextPlayer(player), hand));
if (wizard != null) { data = data.with(HANDS, Map.copyOf(juggledHands));
return wizard.first(); juggledHands.forEach((player, hand) -> game.notify(player, new HandMessage(player, hand)));
} }
if (get(STACK).stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) { // trick is not counted when a bomb is present
return get(STACK).get(0).first(); if (!bomb) {
var tricks = new HashMap<>(get(TRICKS));
tricks.merge(winner, 1, Integer::sum);
data = data.with(TRICKS, Map.copyOf(tricks));
} }
var trumpSuit = get(TRUMP_SUIT); data = data.with(CURRENT_PLAYER, winner);
var suit = getTrickSuit();
return get(STACK).stream() if (cloud && !bomb) {
.max( // adjust prediction
Comparator.<Pair<UUID, Card>>comparingInt(pair -> pair.second().getSuit() == trumpSuit ? 1 : 0) transition(game, new ChangingPrediction(data));
.thenComparing(pair -> pair.second().getSuit() == suit ? 1 : 0) } else if (hasNextTrick) {
.thenComparing(pair -> pair.second().getValue()) transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1)));
) } else {
.orElseThrow(AssertionError::new) transition(game, new FinishingRound(data));
.first(); }
} }
private static void checkData(GameData data) { private static void checkData(GameData data) {

@ -1,21 +1,21 @@
package eu.jonahbauer.wizard.core.machine.states.trick; package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.states.GameData; 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.states.InvalidDataException; import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
import eu.jonahbauer.wizard.core.messages.observer.CardMessage; import eu.jonahbauer.wizard.common.messages.observer.CardMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Card; import eu.jonahbauer.wizard.core.model.card.GameCards;
import eu.jonahbauer.wizard.core.util.Pair; import eu.jonahbauer.wizard.core.util.Pair;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.*; import java.util.*;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*; 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.common.messages.observer.UserInputMessage.Action.PLAY_CARD;
public final class PlayingCard extends TrickState { public final class PlayingCard extends TrickState {
@ -33,7 +33,6 @@ public final class PlayingCard extends TrickState {
@Override @Override
public void onMessage(Game game, UUID player, PlayerMessage message) { public void onMessage(Game game, UUID player, PlayerMessage message) {
if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) { if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) {
checkCard(cardMessage.getCard());
transition(game, cardMessage.getCard()); transition(game, cardMessage.getCard());
} else { } else {
super.onMessage(game, player, message); super.onMessage(game, player, message);
@ -43,55 +42,43 @@ public final class PlayingCard extends TrickState {
@Override @Override
public void onTimeout(Game game) { public void onTimeout(Game game) {
var hand = get(HANDS).get(get(CURRENT_PLAYER)); var hand = get(HANDS).get(get(CURRENT_PLAYER));
var card = hand.stream().filter(c -> { var stack = get(STACK);
try {
checkCard(c);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}).findAny().orElseThrow(() -> new AssertionError("Cannot play any card."));
transition(game, card);
}
@VisibleForTesting var card = GameCards.values().stream()
void checkCard(Card card) { .filter(c -> c.isPlayable(hand, stack))
var hand = get(HANDS).get(get(CURRENT_PLAYER)); .findAny()
if (!hand.contains(card)) { .orElseThrow(() -> new AssertionError("Cannot play any card."));
throw new IllegalArgumentException("You do not have this card on your hand.");
}
Card.Suit suit = getTrickSuit(); transition(game, card.getCard());
if (card.getSuit().isColor() && suit != null && card.getSuit() != suit) {
if (hand.stream().anyMatch(c -> c.getSuit() == suit)) {
throw new IllegalArgumentException("Must follow suit.");
}
}
} }
private void transition(Game game, @NotNull Card card) { private void transition(Game game, @NotNull Card card) {
var currentPlayer = get(CURRENT_PLAYER); var currentPlayer = get(CURRENT_PLAYER);
game.notify(new CardMessage(currentPlayer, card));
// add card to stack // create mutable stack
var stack = get(STACK); var stack = get(STACK);
var mutableStack = new ArrayList<Pair<UUID, Card>>(stack.size() + 1); var mutableStack = new ArrayList<Pair<UUID, Card>>(stack.size() + 1);
mutableStack.addAll(stack); mutableStack.addAll(stack);
mutableStack.add(Pair.of(currentPlayer, card));
// remove card from hand // create mutable hand
var hands = get(HANDS); var hands = get(HANDS);
var mutableHands = new HashMap<>(hands); var mutableHands = new HashMap<>(hands);
var hand = hands.get(currentPlayer); var hand = hands.get(currentPlayer);
var mutableHand = new ArrayList<>(hand); var mutableHand = new ArrayList<>(hand);
mutableHand.remove(card);
mutableHands.put(currentPlayer, List.copyOf(mutableHand));
GameCards.get(card).play(currentPlayer, mutableHand, mutableStack);
// when card was played successfully
game.notify(new CardMessage(currentPlayer, card));
// apply modifications
mutableHands.put(currentPlayer, List.copyOf(mutableHand));
GameData data = getData().with( GameData data = getData().with(
STACK, List.copyOf(mutableStack), STACK, List.copyOf(mutableStack),
HANDS, Map.copyOf(mutableHands) HANDS, Map.copyOf(mutableHands)
); );
// check whether the trick is finished
var summary = data.get(HANDS).values().stream() var summary = data.get(HANDS).values().stream()
.mapToInt(Collection::size) .mapToInt(Collection::size)
.summaryStatistics(); .summaryStatistics();

@ -2,7 +2,6 @@ package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.states.GameData; import eu.jonahbauer.wizard.core.machine.states.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 static eu.jonahbauer.wizard.core.machine.states.GameData.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
@ -14,16 +13,4 @@ public abstract class TrickState extends RoundState {
.require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER) .require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER)
); );
} }
protected Card.Suit getTrickSuit() {
for (var pair : get(STACK)) {
Card.Suit suit = pair.second().getSuit();
if (suit == Card.Suit.WIZARD) {
return Card.Suit.NONE;
} else if (suit.isColor()) {
return suit;
}
}
return Card.Suit.NONE;
}
} }

@ -1,6 +1,6 @@
package eu.jonahbauer.wizard.core.messages; package eu.jonahbauer.wizard.core.messages;
import eu.jonahbauer.wizard.core.messages.observer.ObserverMessage; import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage;
import java.util.UUID; import java.util.UUID;

@ -1,26 +0,0 @@
package eu.jonahbauer.wizard.core.messages.observer;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.states.GameState;
import lombok.Getter;
import java.util.Locale;
/**
* A {@link StateMessage} is sent whenever the {@link Game} changes its internal {@link GameState}.
*/
@Getter
public final class StateMessage extends ObserverMessage {
/**
* The name of the new state in snake_case.
*/
private final String state;
public StateMessage(Class<? extends GameState> state) {
if (state == null) {
this.state = "null";
} else {
this.state = state.getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT);
}
}
}

@ -1,98 +0,0 @@
package eu.jonahbauer.wizard.core.model;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Getter
public enum Card {
HIDDEN(0, Suit.NONE, Suit.NONE),
RED_JESTER(0, Suit.JESTER, Suit.NONE),
RED_1(1, Suit.RED, Suit.RED),
RED_2(2, Suit.RED, Suit.RED),
RED_3(3, Suit.RED, Suit.RED),
RED_4(4, Suit.RED, Suit.RED),
RED_5(5, Suit.RED, Suit.RED),
RED_6(6, Suit.RED, Suit.RED),
RED_7(7, Suit.RED, Suit.RED),
RED_8(8, Suit.RED, Suit.RED),
RED_9(9, Suit.RED, Suit.RED),
RED_10(10, Suit.RED, Suit.RED),
RED_11(11, Suit.RED, Suit.RED),
RED_12(12, Suit.RED, Suit.RED),
RED_13(13, Suit.RED, Suit.RED),
RED_WIZARD(14, Suit.WIZARD, null),
YELLOW_JESTER(0, Suit.JESTER, Suit.NONE),
YELLOW_1(1, Suit.YELLOW, Suit.YELLOW),
YELLOW_2(2, Suit.YELLOW, Suit.YELLOW),
YELLOW_3(3, Suit.YELLOW, Suit.YELLOW),
YELLOW_4(4, Suit.YELLOW, Suit.YELLOW),
YELLOW_5(5, Suit.YELLOW, Suit.YELLOW),
YELLOW_6(6, Suit.YELLOW, Suit.YELLOW),
YELLOW_7(7, Suit.YELLOW, Suit.YELLOW),
YELLOW_8(8, Suit.YELLOW, Suit.YELLOW),
YELLOW_9(9, Suit.YELLOW, Suit.YELLOW),
YELLOW_10(10, Suit.YELLOW, Suit.YELLOW),
YELLOW_11(11, Suit.YELLOW, Suit.YELLOW),
YELLOW_12(12, Suit.YELLOW, Suit.YELLOW),
YELLOW_13(13, Suit.YELLOW, Suit.YELLOW),
YELLOW_WIZARD(14, Suit.WIZARD, null),
GREEN_JESTER(0, Suit.JESTER, Suit.NONE),
GREEN_1(1, Suit.GREEN, Suit.GREEN),
GREEN_2(2, Suit.GREEN, Suit.GREEN),
GREEN_3(3, Suit.GREEN, Suit.GREEN),
GREEN_4(4, Suit.GREEN, Suit.GREEN),
GREEN_5(5, Suit.GREEN, Suit.GREEN),
GREEN_6(6, Suit.GREEN, Suit.GREEN),
GREEN_7(7, Suit.GREEN, Suit.GREEN),
GREEN_8(8, Suit.GREEN, Suit.GREEN),
GREEN_9(9, Suit.GREEN, Suit.GREEN),
GREEN_10(10, Suit.GREEN, Suit.GREEN),
GREEN_11(11, Suit.GREEN, Suit.GREEN),
GREEN_12(12, Suit.GREEN, Suit.GREEN),
GREEN_13(13, Suit.GREEN, Suit.GREEN),
GREEN_WIZARD(14, Suit.WIZARD, null),
BLUE_JESTER(0, Suit.JESTER, Suit.NONE),
BLUE_1(1, Suit.BLUE, Suit.BLUE),
BLUE_2(2, Suit.BLUE, Suit.BLUE),
BLUE_3(3, Suit.BLUE, Suit.BLUE),
BLUE_4(4, Suit.BLUE, Suit.BLUE),
BLUE_5(5, Suit.BLUE, Suit.BLUE),
BLUE_6(6, Suit.BLUE, Suit.BLUE),
BLUE_7(7, Suit.BLUE, Suit.BLUE),
BLUE_8(8, Suit.BLUE, Suit.BLUE),
BLUE_9(9, Suit.BLUE, Suit.BLUE),
BLUE_10(10, Suit.BLUE, Suit.BLUE),
BLUE_11(11, Suit.BLUE, Suit.BLUE),
BLUE_12(12, Suit.BLUE, Suit.BLUE),
BLUE_13(13, Suit.BLUE, Suit.BLUE),
BLUE_WIZARD(14, Suit.WIZARD, null);
private final int value;
private final @NotNull Suit suit;
private final @Nullable Suit trumpSuit;
Card(int value, @NotNull Suit suit, @Nullable Suit trumpSuit) {
if (trumpSuit != null && !trumpSuit.isColor()) throw new IllegalArgumentException();
this.value = value;
this.suit = suit;
this.trumpSuit = trumpSuit;
}
@Getter
public enum Suit {
NONE(true),
YELLOW(true),
RED(true),
GREEN(true),
BLUE(true),
JESTER(false),
WIZARD(false);
private final boolean color;
Suit(boolean color) {
this.color = color;
}
}
}

@ -1,26 +0,0 @@
package eu.jonahbauer.wizard.core.model;
import lombok.experimental.UtilityClass;
import java.util.Set;
@UtilityClass
public class Cards {
public static final Set<Card> DEFAULT = Set.of(
Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER, Card.BLUE_JESTER,
Card.RED_1, Card.GREEN_1, Card.YELLOW_1, Card.BLUE_1,
Card.RED_2, Card.GREEN_2, Card.YELLOW_2, Card.BLUE_2,
Card.RED_3, Card.GREEN_3, Card.YELLOW_3, Card.BLUE_3,
Card.RED_4, Card.GREEN_4, Card.YELLOW_4, Card.BLUE_4,
Card.RED_5, Card.GREEN_5, Card.YELLOW_5, Card.BLUE_5,
Card.RED_6, Card.GREEN_6, Card.YELLOW_6, Card.BLUE_6,
Card.RED_7, Card.GREEN_7, Card.YELLOW_7, Card.BLUE_7,
Card.RED_8, Card.GREEN_8, Card.YELLOW_8, Card.BLUE_8,
Card.RED_9, Card.GREEN_9, Card.YELLOW_9, Card.BLUE_9,
Card.RED_10, Card.GREEN_10, Card.YELLOW_10, Card.BLUE_10,
Card.RED_11, Card.GREEN_11, Card.YELLOW_11, Card.BLUE_11,
Card.RED_12, Card.GREEN_12, Card.YELLOW_12, Card.BLUE_12,
Card.RED_13, Card.GREEN_13, Card.YELLOW_13, Card.BLUE_13,
Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD, Card.BLUE_WIZARD
);
}

@ -1,5 +1,6 @@
package eu.jonahbauer.wizard.core.model; package eu.jonahbauer.wizard.core.model;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Value; import lombok.Value;

@ -1,5 +1,6 @@
package eu.jonahbauer.wizard.core.model; package eu.jonahbauer.wizard.core.model;
import eu.jonahbauer.wizard.core.model.deck.Decks;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -13,15 +14,45 @@ import java.util.NoSuchElementException;
public class Configurations { public class Configurations {
private static final Map<String, Configuration> CONFIGURATIONS = new HashMap<>(); private static final Map<String, Configuration> CONFIGURATIONS = new HashMap<>();
public static final Configuration DEFAULT = register("default", new Configuration( public static final Configuration DEFAULT = register("DEFAULT", new Configuration(
Cards.DEFAULT, Decks.DEFAULT,
true, true,
10 * 60 * 1000 10 * 60 * 1000
)); ));
public static final Configuration DEFAULT_PM1 = register("DEFAULT_PM1", new Configuration(
Decks.DEFAULT,
false,
10 * 60 * 1000
));
public static final Configuration ANNIVERSARY_2016 = register("ANNIVERSARY_2016", new Configuration(
Decks.ANNIVERSARY_2016,
true,
10 * 60 * 1000
));
public static final Configuration ANNIVERSARY_2016_PM1 = register("ANNIVERSARY_2016_PM1", new Configuration(
Decks.ANNIVERSARY_2016,
false,
10 * 60 * 1000
));
public static final Configuration ANNIVERSARY_2021 = register("ANNIVERSARY_2021", new Configuration(
Decks.ANNIVERSARY_2021,
true,
10 * 60 * 1000
));
public static final Configuration ANNIVERSARY_2021_PM1 = register("ANNIVERSARY_2021_PM1", new Configuration(
Decks.ANNIVERSARY_2021,
false,
10 * 60 * 1000
));
@Contract("_,_ -> param2") @Contract("_,_ -> param2")
private static Configuration register(@NotNull String name, @NotNull Configuration configuration) { private static Configuration register(@NotNull String name, @NotNull Configuration configuration) {
if (CONFIGURATIONS.putIfAbsent(name.toLowerCase(Locale.ROOT), configuration) != null) { if (CONFIGURATIONS.putIfAbsent(name.toUpperCase(Locale.ROOT), configuration) != null) {
throw new IllegalArgumentException("Name already taken."); throw new IllegalArgumentException("Name already taken.");
} }
return configuration; return configuration;
@ -29,7 +60,7 @@ public class Configurations {
@NotNull @NotNull
public static Configuration get(@NotNull String name) { public static Configuration get(@NotNull String name) {
var out = CONFIGURATIONS.get(name.toLowerCase(Locale.ROOT)); var out = CONFIGURATIONS.get(name.toUpperCase(Locale.ROOT));
if (out == null) throw new NoSuchElementException("Configuration with name '" + name + "' does not exist."); if (out == null) throw new NoSuchElementException("Configuration with name '" + name + "' does not exist.");
return out; return out;
} }

@ -0,0 +1,75 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import lombok.experimental.UtilityClass;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
@UtilityClass
public class CardUtils {
public Card.Suit getTrickSuit(List<Pair<UUID, Card>> stack) {
for (var pair : stack) {
Card.Suit suit = GameCards.get(pair.second()).getTrickSuit();
if (suit != null) {
return suit;
}
}
return Card.Suit.NONE;
}
public UUID getWinner(List<Pair<UUID, Card>> stack, Card.Suit trumpSuit) {
var suit = getTrickSuit(stack);
Pair<UUID, Card> fairy = null, dragon = null;
for (Pair<UUID, Card> pair : stack) {
Card card = pair.second();
if (card == Card.FAIRY) {
fairy = pair;
} else if (card == Card.DRAGON) {
dragon = pair;
}
}
if (fairy != null && dragon != null) {
return fairy.first();
} else if (dragon != null) {
return dragon.first();
}
Comparator<Pair<UUID, Card>> comparator = Comparator.comparingDouble(pair -> {
GameCard card = GameCards.get(pair.second());
if (card instanceof WizardCard) {
return 1000;
} else if (card instanceof JesterCard) {
return 0;
} else if (card == GameCards.BOMB) {
return -1;
} else if (card == GameCards.DRAGON) {
return 2000;
} else if (card == GameCards.FAIRY) {
return -1;
} else if (card instanceof ColoredCard coloredCard) {
if (coloredCard.getSuit() == trumpSuit) {
return 200 + coloredCard.getValue();
} else if (coloredCard.getSuit() == suit) {
return 100 + coloredCard.getValue();
} else {
return coloredCard.getValue();
}
} else {
throw new IllegalArgumentException("Dont know how to handle " + pair.second() + ".");
}
});
return stack.stream()
.max(comparator)
.orElseThrow(() -> new RuntimeException("Could not determine trick winner."))
.first();
}
}

@ -0,0 +1,24 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import java.util.List;
import java.util.UUID;
public class ChangelingJesterCard extends JesterCard implements Subcard {
public ChangelingJesterCard(Card card) {
super(card);
}
@Override
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> stack) {
GameCards.CHANGELING.checkHand(hand);
hand.remove(Card.CHANGELING);
stack.add(Pair.of(player, getCard()));
}
public boolean isPlayable(List<Card> hand, List<Pair<UUID, Card>> stack) {
return hand.contains(Card.CHANGELING);
}
}

@ -0,0 +1,25 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import java.util.List;
import java.util.UUID;
public class ChangelingWizardCard extends WizardCard implements Subcard {
public ChangelingWizardCard(Card card) {
super(card);
}
@Override
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> stack) {
GameCards.CHANGELING.checkHand(hand);
hand.remove(Card.CHANGELING);
stack.add(Pair.of(player, getCard()));
}
@Override
public boolean isPlayable(List<Card> hand, List<Pair<UUID, Card>> stack) {
return hand.contains(Card.CHANGELING);
}
}

@ -0,0 +1,56 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
@Getter
public class ColoredCard extends GameCard {
private final double value;
private final Card.Suit suit;
public ColoredCard(Card card, double value, Card.Suit suit) {
super(card);
this.value = value;
this.suit = suit;
}
@Override
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> stack) {
Card.Suit suit = CardUtils.getTrickSuit(stack);
if (this.suit != suit && canFollowSuit(hand, suit)) {
throw new IllegalArgumentException("Must follow suit.");
}
super.play(player, hand, stack);
}
@Override
public Card.Suit getTrumpSuit() {
return getSuit();
}
@Override
public Card.Suit getTrickSuit() {
return getSuit();
}
@Override
public boolean isPlayable(List<Card> hand, List<Pair<UUID, Card>> stack) {
Card.Suit suit = CardUtils.getTrickSuit(stack);
if (this.suit != suit && canFollowSuit(hand, suit)) {
return false;
}
return super.isPlayable(hand, stack);
}
private boolean canFollowSuit(List<Card> hand, Card.Suit suit) {
return hand.stream()
.map(GameCards::get)
.anyMatch(card -> card instanceof ColoredCard coloredCard && coloredCard.getSuit() == suit);
}
}

@ -0,0 +1,30 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
@Getter
public class ColoredSubcard extends ColoredCard implements Subcard {
private final GameCard parent;
public ColoredSubcard(Card card, GameCard parent, double value, Card.Suit suit) {
super(card, value, suit);
this.parent = parent;
}
@Override
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> stack) {
parent.checkHand(hand);
hand.remove(parent.getCard());
stack.add(Pair.of(player, getCard()));
}
@Override
public boolean isPlayable(List<Card> hand, List<Pair<UUID, Card>> stack) {
return hand.contains(parent.getCard());
}
}

@ -0,0 +1,44 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Contract;
import java.util.List;
import java.util.UUID;
@Getter
@RequiredArgsConstructor
public abstract class GameCard {
private final Card card;
public abstract Card.Suit getTrumpSuit();
public abstract Card.Suit getTrickSuit();
/**
* Standard implementation of playing a card.
* <br>
* Removes the card from the hand checking that the hand contains it and then adds a new entry to the stack.
* @param player the player playing this card
* @param hand the player's hand cards
* @param stack the stack
*/
@Contract(mutates = "param2, param3")
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> stack) {
checkHand(hand);
hand.remove(getCard());
stack.add(Pair.of(player, getCard()));
}
public boolean isPlayable(List<Card> hand, List<Pair<UUID, Card>> stack) {
return hand.contains(getCard());
}
protected void checkHand(List<Card> hand) {
if (!hand.contains(getCard())) {
throw new IllegalArgumentException("You do not have this card on your hand.");
}
}
}

@ -0,0 +1,123 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@UtilityClass
@SuppressWarnings("unused")
public class GameCards {
private static final Map<Card, GameCard> CARDS = new HashMap<>();
public final GameCard HIDDEN = register(new NonPlayableCard(Card.HIDDEN, null));
public final GameCard BLUE_1 = register(new ColoredCard(Card.BLUE_1, 1, Card.Suit.BLUE));
public final GameCard BLUE_2 = register(new ColoredCard(Card.BLUE_2, 2, Card.Suit.BLUE));
public final GameCard BLUE_3 = register(new ColoredCard(Card.BLUE_3, 3, Card.Suit.BLUE));
public final GameCard BLUE_4 = register(new ColoredCard(Card.BLUE_4, 4, Card.Suit.BLUE));
public final GameCard BLUE_5 = register(new ColoredCard(Card.BLUE_5, 5, Card.Suit.BLUE));
public final GameCard BLUE_6 = register(new ColoredCard(Card.BLUE_6, 6, Card.Suit.BLUE));
public final GameCard BLUE_7 = register(new ColoredCard(Card.BLUE_7, 7, Card.Suit.BLUE));
public final GameCard BLUE_8 = register(new ColoredCard(Card.BLUE_8, 8, Card.Suit.BLUE));
public final GameCard BLUE_9 = register(new ColoredCard(Card.BLUE_9, 9, Card.Suit.BLUE));
public final GameCard BLUE_10 = register(new ColoredCard(Card.BLUE_10, 10, Card.Suit.BLUE));
public final GameCard BLUE_11 = register(new ColoredCard(Card.BLUE_11, 11, Card.Suit.BLUE));
public final GameCard BLUE_12 = register(new ColoredCard(Card.BLUE_12, 12, Card.Suit.BLUE));
public final GameCard BLUE_13 = register(new ColoredCard(Card.BLUE_13, 13, Card.Suit.BLUE));
public final GameCard RED_1 = register(new ColoredCard(Card.RED_1, 1, Card.Suit.RED));
public final GameCard RED_2 = register(new ColoredCard(Card.RED_2, 2, Card.Suit.RED));
public final GameCard RED_3 = register(new ColoredCard(Card.RED_3, 3, Card.Suit.RED));
public final GameCard RED_4 = register(new ColoredCard(Card.RED_4, 4, Card.Suit.RED));
public final GameCard RED_5 = register(new ColoredCard(Card.RED_5, 5, Card.Suit.RED));
public final GameCard RED_6 = register(new ColoredCard(Card.RED_6, 6, Card.Suit.RED));
public final GameCard RED_7 = register(new ColoredCard(Card.RED_7, 7, Card.Suit.RED));
public final GameCard RED_8 = register(new ColoredCard(Card.RED_8, 8, Card.Suit.RED));
public final GameCard RED_9 = register(new ColoredCard(Card.RED_9, 9, Card.Suit.RED));
public final GameCard RED_10 = register(new ColoredCard(Card.RED_10, 10, Card.Suit.RED));
public final GameCard RED_11 = register(new ColoredCard(Card.RED_11, 11, Card.Suit.RED));
public final GameCard RED_12 = register(new ColoredCard(Card.RED_12, 12, Card.Suit.RED));
public final GameCard RED_13 = register(new ColoredCard(Card.RED_13, 13, Card.Suit.RED));
public final GameCard GREEN_1 = register(new ColoredCard(Card.GREEN_1, 1, Card.Suit.GREEN));
public final GameCard GREEN_2 = register(new ColoredCard(Card.GREEN_2, 2, Card.Suit.GREEN));
public final GameCard GREEN_3 = register(new ColoredCard(Card.GREEN_3, 3, Card.Suit.GREEN));
public final GameCard GREEN_4 = register(new ColoredCard(Card.GREEN_4, 4, Card.Suit.GREEN));
public final GameCard GREEN_5 = register(new ColoredCard(Card.GREEN_5, 5, Card.Suit.GREEN));
public final GameCard GREEN_6 = register(new ColoredCard(Card.GREEN_6, 6, Card.Suit.GREEN));
public final GameCard GREEN_7 = register(new ColoredCard(Card.GREEN_7, 7, Card.Suit.GREEN));
public final GameCard GREEN_8 = register(new ColoredCard(Card.GREEN_8, 8, Card.Suit.GREEN));
public final GameCard GREEN_9 = register(new ColoredCard(Card.GREEN_9, 9, Card.Suit.GREEN));
public final GameCard GREEN_10 = register(new ColoredCard(Card.GREEN_10, 10, Card.Suit.GREEN));
public final GameCard GREEN_11 = register(new ColoredCard(Card.GREEN_11, 11, Card.Suit.GREEN));
public final GameCard GREEN_12 = register(new ColoredCard(Card.GREEN_12, 12, Card.Suit.GREEN));
public final GameCard GREEN_13 = register(new ColoredCard(Card.GREEN_13, 13, Card.Suit.GREEN));
public final GameCard YELLOW_1 = register(new ColoredCard(Card.YELLOW_1, 1, Card.Suit.YELLOW));
public final GameCard YELLOW_2 = register(new ColoredCard(Card.YELLOW_2, 2, Card.Suit.YELLOW));
public final GameCard YELLOW_3 = register(new ColoredCard(Card.YELLOW_3, 3, Card.Suit.YELLOW));
public final GameCard YELLOW_4 = register(new ColoredCard(Card.YELLOW_4, 4, Card.Suit.YELLOW));
public final GameCard YELLOW_5 = register(new ColoredCard(Card.YELLOW_5, 5, Card.Suit.YELLOW));
public final GameCard YELLOW_6 = register(new ColoredCard(Card.YELLOW_6, 6, Card.Suit.YELLOW));
public final GameCard YELLOW_7 = register(new ColoredCard(Card.YELLOW_7, 7, Card.Suit.YELLOW));
public final GameCard YELLOW_8 = register(new ColoredCard(Card.YELLOW_8, 8, Card.Suit.YELLOW));
public final GameCard YELLOW_9 = register(new ColoredCard(Card.YELLOW_9, 9, Card.Suit.YELLOW));
public final GameCard YELLOW_10 = register(new ColoredCard(Card.YELLOW_10, 10, Card.Suit.YELLOW));
public final GameCard YELLOW_11 = register(new ColoredCard(Card.YELLOW_11, 11, Card.Suit.YELLOW));
public final GameCard YELLOW_12 = register(new ColoredCard(Card.YELLOW_12, 12, Card.Suit.YELLOW));
public final GameCard YELLOW_13 = register(new ColoredCard(Card.YELLOW_13, 13, Card.Suit.YELLOW));
public final GameCard BLUE_WIZARD = register(new WizardCard(Card.BLUE_WIZARD));
public final GameCard RED_WIZARD = register(new WizardCard(Card.RED_WIZARD));
public final GameCard GREEN_WIZARD = register(new WizardCard(Card.GREEN_WIZARD));
public final GameCard YELLOW_WIZARD = register(new WizardCard(Card.YELLOW_WIZARD));
public final GameCard BLUE_JESTER = register(new JesterCard(Card.BLUE_JESTER));
public final GameCard RED_JESTER = register(new JesterCard(Card.RED_JESTER));
public final GameCard GREEN_JESTER = register(new JesterCard(Card.GREEN_JESTER));
public final GameCard YELLOW_JESTER = register(new JesterCard(Card.YELLOW_JESTER));
public final GameCard CHANGELING = register(new NonPlayableCard(Card.CHANGELING, null));
public final GameCard CHANGELING_WIZARD = register(new ChangelingWizardCard(Card.CHANGELING_WIZARD));
public final GameCard CHANGELING_JESTER = register(new ChangelingJesterCard(Card.CHANGELING_JESTER));
public final GameCard BOMB = register(new SimpleCard(Card.BOMB, Card.Suit.NONE, null));
public final GameCard WEREWOLF = register(new NonPlayableCard(Card.WEREWOLF, null));
public final GameCard DRAGON = register(new SimpleCard(Card.DRAGON, null, Card.Suit.NONE));
public final GameCard FAIRY = register(new SimpleCard(Card.FAIRY, Card.Suit.NONE, null));
public final GameCard CLOUD = register(new NonPlayableCard(Card.CLOUD, null));
public final GameCard CLOUD_BLUE = register(new ColoredSubcard(Card.CLOUD_BLUE, CLOUD, 9.75, Card.Suit.BLUE));
public final GameCard CLOUD_RED = register(new ColoredSubcard(Card.CLOUD_RED, CLOUD, 9.75, Card.Suit.RED));
public final GameCard CLOUD_GREEN = register(new ColoredSubcard(Card.CLOUD_GREEN, CLOUD, 9.75, Card.Suit.GREEN));
public final GameCard CLOUD_YELLOW = register(new ColoredSubcard(Card.CLOUD_YELLOW, CLOUD, 9.75, Card.Suit.YELLOW));
public final GameCard JUGGLER = register(new NonPlayableCard(Card.JUGGLER, null));
public final GameCard JUGGLER_BLUE = register(new ColoredSubcard(Card.JUGGLER_BLUE, JUGGLER, 7.5, Card.Suit.BLUE));
public final GameCard JUGGLER_RED = register(new ColoredSubcard(Card.JUGGLER_RED, JUGGLER, 7.5, Card.Suit.RED));
public final GameCard JUGGLER_GREEN = register(new ColoredSubcard(Card.JUGGLER_GREEN, JUGGLER, 7.5, Card.Suit.GREEN));
public final GameCard JUGGLER_YELLOW = register(new ColoredSubcard(Card.JUGGLER_YELLOW, JUGGLER, 7.5, Card.Suit.YELLOW));
@Contract("_ -> param1")
private static GameCard register(@NotNull GameCard card) {
if (CARDS.containsKey(card.getCard())) {
throw new IllegalArgumentException("Name already taken.");
}
CARDS.put(card.getCard(), card);
return card;
}
@NotNull
public static GameCard get(@NotNull Card name) {
return CARDS.get(name);
}
public static Collection<GameCard> values() {
return Collections.unmodifiableCollection(CARDS.values());
}
}

@ -0,0 +1,9 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
public class JesterCard extends SimpleCard {
public JesterCard(Card card) {
super(card, Card.Suit.NONE, null);
}
}

@ -0,0 +1,38 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
@Getter
public final class NonPlayableCard extends GameCard {
private final Card.Suit suit;
public NonPlayableCard(Card card, Card.Suit suit) {
super(card);
this.suit = suit;
}
@Override
public void play(UUID player, List<Card> hand, List<Pair<UUID, Card>> stack) {
throw new IllegalArgumentException("This card cannot be played.");
}
@Override
public Card.Suit getTrumpSuit() {
return getSuit();
}
@Override
public Card.Suit getTrickSuit() {
throw new UnsupportedOperationException();
}
@Override
public boolean isPlayable(List<Card> hand, List<Pair<UUID, Card>> stack) {
return false;
}
}

@ -0,0 +1,16 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.Getter;
@Getter
public class SimpleCard extends GameCard {
private final Card.Suit trumpSuit;
private final Card.Suit trickSuit;
public SimpleCard(Card card, Card.Suit trumpSuit, Card.Suit trickSuit) {
super(card);
this.trumpSuit = trumpSuit;
this.trickSuit = trickSuit;
}
}

@ -0,0 +1,14 @@
package eu.jonahbauer.wizard.core.model.card;
/**
* Marker interface for subcards. A subcard is an internal variant of another card. It contains additional information
* that would otherwise need to be provided in a seperate message.
* <br>
* A subcard may not be on any player's hand.
* <br><br>
* For example: when a player has the {@link GameCards#CLOUD} on his hand, he may not play the card directly but can only
* play one of its subcards {@link GameCards#CLOUD_BLUE}, {@link GameCards#CLOUD_GREEN}, {@link GameCards#CLOUD_RED} or
* {@link GameCards#CLOUD_YELLOW}.
*/
public interface Subcard {
}

@ -0,0 +1,9 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
public class WizardCard extends SimpleCard {
public WizardCard(Card card) {
super(card, null, Card.Suit.NONE);
}
}

@ -1,5 +1,6 @@
package eu.jonahbauer.wizard.core.model; package eu.jonahbauer.wizard.core.model.deck;
import eu.jonahbauer.wizard.common.model.card.Card;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
@ -13,8 +14,8 @@ public final class Deck {
this.cards.addAll(cards); this.cards.addAll(cards);
} }
public void shuffle() { public void shuffle(Random random) {
Collections.shuffle(cards); Collections.shuffle(cards, random);
next = 0; next = 0;
} }

@ -0,0 +1,65 @@
package eu.jonahbauer.wizard.core.model.deck;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.experimental.UtilityClass;
import java.util.*;
@UtilityClass
public class Decks {
public static final Set<Card> DEFAULT = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(
Card.BLUE_1, Card.RED_1, Card.GREEN_1, Card.YELLOW_1,
Card.BLUE_2, Card.RED_2, Card.GREEN_2, Card.YELLOW_2,
Card.BLUE_3, Card.RED_3, Card.GREEN_3, Card.YELLOW_3,
Card.BLUE_4, Card.RED_4, Card.GREEN_4, Card.YELLOW_4,
Card.BLUE_5, Card.RED_5, Card.GREEN_5, Card.YELLOW_5,
Card.BLUE_6, Card.RED_6, Card.GREEN_6, Card.YELLOW_6,
Card.BLUE_7, Card.RED_7, Card.GREEN_7, Card.YELLOW_7,
Card.BLUE_8, Card.RED_8, Card.GREEN_8, Card.YELLOW_8,
Card.BLUE_9, Card.RED_9, Card.GREEN_9, Card.YELLOW_9,
Card.BLUE_10, Card.RED_10, Card.GREEN_10, Card.YELLOW_10,
Card.BLUE_11, Card.RED_11, Card.GREEN_11, Card.YELLOW_11,
Card.BLUE_12, Card.RED_12, Card.GREEN_12, Card.YELLOW_12,
Card.BLUE_13, Card.RED_13, Card.GREEN_13, Card.YELLOW_13,
Card.BLUE_JESTER, Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER,
Card.BLUE_WIZARD, Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD
)));
public static final Set<Card> ANNIVERSARY_2016 = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(
Card.BLUE_1, Card.RED_1, Card.GREEN_1, Card.YELLOW_1,
Card.BLUE_2, Card.RED_2, Card.GREEN_2, Card.YELLOW_2,
Card.BLUE_3, Card.RED_3, Card.GREEN_3, Card.YELLOW_3,
Card.BLUE_4, Card.RED_4, Card.GREEN_4, Card.YELLOW_4,
Card.BLUE_5, Card.RED_5, Card.GREEN_5, Card.YELLOW_5,
Card.BLUE_6, Card.RED_6, Card.GREEN_6, Card.YELLOW_6,
Card.BLUE_7, Card.RED_7, Card.GREEN_7, Card.YELLOW_7,
Card.BLUE_8, Card.RED_8, Card.GREEN_8, Card.YELLOW_8,
Card.BLUE_9, Card.RED_9, Card.GREEN_9, Card.YELLOW_9,
Card.BLUE_10, Card.RED_10, Card.GREEN_10, Card.YELLOW_10,
Card.BLUE_11, Card.RED_11, Card.GREEN_11, Card.YELLOW_11,
Card.BLUE_12, Card.RED_12, Card.GREEN_12, Card.YELLOW_12,
Card.BLUE_13, Card.RED_13, Card.GREEN_13, Card.YELLOW_13,
Card.BLUE_JESTER, Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER,
Card.BLUE_WIZARD, Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD,
Card.WEREWOLF, Card.BOMB, Card.DRAGON, Card.FAIRY, Card.JUGGLER, Card.CLOUD
)));
public static final Set<Card> ANNIVERSARY_2021 = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(
Card.BLUE_1, Card.RED_1, Card.GREEN_1, Card.YELLOW_1,
Card.BLUE_2, Card.RED_2, Card.GREEN_2, Card.YELLOW_2,
Card.BLUE_3, Card.RED_3, Card.GREEN_3, Card.YELLOW_3,
Card.BLUE_4, Card.RED_4, Card.GREEN_4, Card.YELLOW_4,
Card.BLUE_5, Card.RED_5, Card.GREEN_5, Card.YELLOW_5,
Card.BLUE_6, Card.RED_6, Card.GREEN_6, Card.YELLOW_6,
Card.BLUE_7, Card.RED_7, Card.GREEN_7, Card.YELLOW_7,
Card.BLUE_8, Card.RED_8, Card.GREEN_8, Card.YELLOW_8,
Card.BLUE_9, Card.RED_9, Card.GREEN_9, Card.YELLOW_9,
Card.BLUE_10, Card.RED_10, Card.GREEN_10, Card.YELLOW_10,
Card.BLUE_11, Card.RED_11, Card.GREEN_11, Card.YELLOW_11,
Card.BLUE_12, Card.RED_12, Card.GREEN_12, Card.YELLOW_12,
Card.BLUE_13, Card.RED_13, Card.GREEN_13, Card.YELLOW_13,
Card.BLUE_JESTER, Card.RED_JESTER, Card.GREEN_JESTER, Card.YELLOW_JESTER,
Card.BLUE_WIZARD, Card.RED_WIZARD, Card.GREEN_WIZARD, Card.YELLOW_WIZARD,
Card.WEREWOLF, Card.BOMB, Card.DRAGON, Card.FAIRY, Card.JUGGLER, Card.CLOUD, Card.CHANGELING
)));
}

@ -0,0 +1,12 @@
package eu.jonahbauer.wizard.core.util;
import lombok.experimental.UtilityClass;
import java.util.Locale;
@UtilityClass
public class Util {
public static String toSnakeCase(String str) {
return str.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT);
}
}

@ -1,16 +1,57 @@
package eu.jonahbauer.wizard.core.machine; package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.core.model.Configurations; import eu.jonahbauer.wizard.core.model.Configurations;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public class GameTest { public class GameTest {
@Test @RepeatedTest(3)
public void run() throws InterruptedException, ExecutionException { public void runDefault(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException {
Game game = new Game(Configurations.DEFAULT.withTimeout(0), (player, msg) -> System.out.println(msg)); Game game = new Game(
repetitionInfo.getCurrentRepetition(),
Configurations.DEFAULT.withTimeout(0),
(player, msg) -> System.out.println(msg)
);
var players = List.of(
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID()
);
game.start(players);
game.await();
}
@RepeatedTest(3)
public void runAnniversary2016(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException {
Game game = new Game(
repetitionInfo.getCurrentRepetition(),
Configurations.ANNIVERSARY_2016.withTimeout(0),
(player, msg) -> System.out.println(msg)
);
var players = List.of(
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID()
);
game.start(players);
game.await();
}
@RepeatedTest(3)
public void runAnniversary2021(RepetitionInfo repetitionInfo) throws InterruptedException, ExecutionException {
Game game = new Game(
repetitionInfo.getCurrentRepetition(),
Configurations.ANNIVERSARY_2021.withTimeout(0),
(player, msg) -> System.out.println(msg)
);
var players = List.of( var players = List.of(
UUID.randomUUID(), UUID.randomUUID(),
UUID.randomUUID(), UUID.randomUUID(),

@ -0,0 +1,30 @@
package eu.jonahbauer.wizard.core.machine;
import lombok.experimental.UtilityClass;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@UtilityClass
public class GameTestUtils {
public static Map<UUID, Integer> emptyToIntMap(List<UUID> players) {
return players.stream().collect(Collectors.toUnmodifiableMap(player -> player, player -> 0));
}
public static Stubber doFinish() {
return Mockito.doAnswer(finish());
}
public static <T> Answer<T> finish() {
return invocation -> {
Game game = (Game) invocation.getMock();
game.finish();
return null;
};
}
}

@ -0,0 +1,145 @@
package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.messages.Observer;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.junit.jupiter.api.Assertions;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.*;
/**
* A queue of {@link PlayerMessage}s that will automatically be sent to a {@link Game} when
* {@linkplain UserInputMessage input is required}. The {@code PlayerMessage}s are ordered and it is asserted that
* the incoming {@code UserInputMessage}s match the given {@code PlayerMessage}s.
*/
public class MessageQueue implements Observer {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final LinkedList<QueuedMessage> messages = new LinkedList<>();
@Setter
private Game game;
public MessageQueue add(UUID player, UserInputMessage.Action action, PlayerMessage message) {
messages.add(new QueuedMessage(player, action, message));
return this;
}
public MessageQueue addCard(UUID player, Card card) {
return add(player, UserInputMessage.Action.PLAY_CARD, new PlayCardMessage(card));
}
public MessageQueue addPrediction(UUID player, int prediction) {
return add(player, UserInputMessage.Action.MAKE_PREDICTION, new PredictMessage(prediction));
}
public MessageQueue addChangePrediction(UUID player, int prediction) {
return add(player, UserInputMessage.Action.CHANGE_PREDICTION, new PredictMessage(prediction));
}
public MessageQueue addPickTrump(UUID player, Card.Suit trumpSuit) {
return add(player, UserInputMessage.Action.PICK_TRUMP, new PickTrumpMessage(trumpSuit));
}
public MessageQueue addCards(List<UUID> players, Map<UUID, List<Card>> cards, int startPlayer) {
Map<UUID, Integer> nextCards = new HashMap<>();
int size = players.size();
boolean changed = true;
while (changed) {
changed = false;
for (int i = 0; i < size; i++) {
var player = players.get((i + startPlayer) % size);
var hand = cards.get(player);
int next = nextCards.getOrDefault(player, 0);
if (next < hand.size()) {
nextCards.merge(player, 1, Integer::sum);
var card = hand.get(next);
addCard(player, card);
changed = true;
}
}
}
return this;
}
public MessageQueue assertThrows(Class<? extends Exception> exception) {
messages.getLast().setException(exception);
return this;
}
public void doNotify(ObserverMessage om) {
try {
System.out.println(om);
if (om instanceof UserInputMessage message) {
UUID player = message.getPlayer();
while (true) {
assertFalse(messages.isEmpty(), "User input is required but none is provided.");
var queued = messages.poll();
var queuedPlayer = queued.getPlayer();
var queuedAction = queued.getAction();
var queuedMessage = queued.getMessage();
var exception = queued.getException();
if (exception == null) {
assertEquals(queuedPlayer, player);
assertEquals(queuedAction, message.getAction());
}
System.out.println(queuedPlayer + ": " + queuedMessage);
if (exception != null) {
var executionException = Assertions.assertThrows(
ExecutionException.class,
() -> game.onMessage(queuedPlayer, queuedMessage).get(),
"Excepted exception for message " + queuedMessage + " from player " + queuedPlayer + "."
);
assertInstanceOf(
exception,
executionException.getCause(),
"Excepted exception for message " + queuedMessage + " from player " + queuedPlayer + "."
);
} else {
Assertions.assertDoesNotThrow(() -> game.onMessage(queuedPlayer, queuedMessage).get());
return;
}
}
}
} catch (Throwable t) {
game.finish(t);
}
}
@Override
public void notify(ObserverMessage message) {
executor.execute(() -> doNotify(message));
}
@Override
public void notify(UUID player, ObserverMessage message) {
}
@Getter
@Setter
@RequiredArgsConstructor
private static class QueuedMessage {
private final UUID player;
private final UserInputMessage.Action action;
private final PlayerMessage message;
private Class<? extends Exception> exception;
}
}

@ -0,0 +1,39 @@
package eu.jonahbauer.wizard.core.machine.states;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GameStateTest {
@Test
public void getNextPlayer_IsPermutation() {
List<UUID> players = List.of(
new UUID(0, 1),
new UUID(0, 2),
new UUID(0, 3),
new UUID(0, 4),
new UUID(0, 5),
new UUID(0, 6)
);
GameState gameState = mock(GameState.class);
when(gameState.getNextPlayer(any())).thenCallRealMethod();
when(gameState.get(GameData.PLAYERS)).thenReturn(players);
Set<UUID> a = new HashSet<>(players);
Set<UUID> b = new HashSet<>();
a.forEach(player -> b.add(gameState.getNextPlayer(player)));
assertEquals(a, b);
}
}

@ -0,0 +1,163 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.common.messages.observer.HandMessage;
import eu.jonahbauer.wizard.common.messages.observer.StateMessage;
import eu.jonahbauer.wizard.common.messages.observer.TrumpMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.MessageQueue;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.model.Configuration;
import eu.jonahbauer.wizard.core.model.Configurations;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
public class DeterminingTrumpTest {
private final UUID[] players = new UUID[] {
new UUID(0, 0),
new UUID(0, 1),
new UUID(0, 2),
new UUID(0, 3)
};
@SneakyThrows
@SuppressWarnings("SameParameterValue")
private Game performTest(Configuration configuration, int round, Map<UUID, List<Card>> hands, Card trumpCard, MessageQueue queue) {
Game game = spy(new Game(configuration, queue));
doFinish().when(game).transition(any(), any(Predicting.class));
queue.setGame(game);
var playerList = List.of(players);
GameData data = GameData.EMPTY.with(
entry(PLAYERS, playerList),
entry(ROUND, round),
entry(SCORE, Map.of()),
entry(TRUMP_CARD, trumpCard),
entry(HANDS, hands)
);
game.resume(new DeterminingTrump(data));
game.await();
verify(game, never()).transition(any(), isNull());
return game;
}
@Test
public void run_Simple() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.CLOUD),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue();
Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.YELLOW_1, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // determining trump
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.YELLOW_1 && trump.getSuit() == Card.Suit.YELLOW));
order.verify(game).transition(any(), any(Predicting.class)); // round is finished
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_Simple2() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.CLOUD),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue();
Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.GREEN_JESTER, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // determining trump
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.GREEN_JESTER && trump.getSuit() == Card.Suit.NONE));
order.verify(game).transition(any(), any(Predicting.class));
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithUserInput() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.CLOUD),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addPickTrump(players[0], Card.Suit.GREEN);
Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.BLUE_WIZARD, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // determining trump
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.BLUE_WIZARD && trump.getSuit() == null));
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.BLUE_WIZARD && trump.getSuit() == Card.Suit.GREEN));
order.verify(game).transition(any(), any(Predicting.class));
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithWerewolf() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.CLOUD),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.WEREWOLF)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addPickTrump(players[3], Card.Suit.YELLOW);
Game game = performTest(Configurations.ANNIVERSARY_2021, 0, hands, Card.GREEN_1, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // determining trump
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.GREEN_1 && trump.getSuit() == null));
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.WEREWOLF && trump.getSuit() == null));
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(argThat(msg -> msg instanceof TrumpMessage trump && trump.getCard() == Card.WEREWOLF && trump.getSuit() == Card.Suit.YELLOW));
order.verify(game).notify(eq(players[3]), any(HandMessage.class)); // swap trump card and werewolf
order.verify(game).transition(any(), any(Predicting.class));
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
}

@ -1,51 +0,0 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import org.junit.jupiter.api.Test;
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;
public class FinishingRoundTest {
@Test
public void getPoints() {
UUID player0 = new UUID(0, 0);
UUID player1 = new UUID(0, 1);
UUID player2 = new UUID(0, 2);
List<UUID> players = List.of(
player0,
player1,
player2
);
Map<UUID, Integer> predictions = Map.of(
player0, 0,
player1, 3,
player2, 3
);
Map<UUID, Integer> tricks = Map.of(
player0, 0,
player1, 3,
player2, 5
);
FinishingRound state = mock(FinishingRound.class);
when(state.getPoints()).thenCallRealMethod();
when(state.get(PREDICTIONS)).thenReturn(predictions);
when(state.get(TRICKS)).thenReturn(tricks);
when(state.get(PLAYERS)).thenReturn(players);
assertEquals(Map.of(
player0, 20,
player1, 50,
player2, -20
), state.getPoints());
}
}

@ -1,65 +1,170 @@
package eu.jonahbauer.wizard.core.machine.states.round; package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.common.messages.observer.StateMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameTestUtils;
import eu.jonahbauer.wizard.core.machine.MessageQueue;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
import eu.jonahbauer.wizard.core.model.Configuration; import eu.jonahbauer.wizard.core.model.Configuration;
import eu.jonahbauer.wizard.core.model.Configurations;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import java.util.Collections;
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.GameTestUtils.doFinish;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*; import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static org.junit.jupiter.api.Assertions.assertThrows; import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public class PredictingTest { public class PredictingTest {
@Test private final UUID[] players = new UUID[] {
public void checkPrediction_ThrowsIllegalArgument_IfOutOfBounds() { new UUID(0, 0),
Map<UUID, Integer> predictions = Map.of(); new UUID(0, 1),
new UUID(0, 2),
new UUID(0, 3)
};
@SneakyThrows
@SuppressWarnings("SameParameterValue")
private Game performTest(Configuration configuration, int round, MessageQueue queue) {
Map<UUID, List<Card>> hands = Map.of(
players[0], Collections.nCopies(round + 1, Card.HIDDEN),
players[1], Collections.nCopies(round + 1, Card.HIDDEN),
players[2], Collections.nCopies(round + 1, Card.HIDDEN),
players[3], Collections.nCopies(round + 1, Card.HIDDEN)
);
Configuration config = mock(Configuration.class); Game game = spy(new Game(configuration, queue));
when(config.allowExactPredictions()).thenReturn(true); doFinish().when(game).transition(any(), any(StartingTrick.class));
queue.setGame(game);
Game game = mock(Game.class); var playerList = List.of(players);
when(game.getConfig()).thenReturn(config);
Predicting state = mock(Predicting.class); GameData data = GameData.EMPTY.with(
doCallRealMethod().when(state).checkPrediction(any(), anyInt()); entry(PLAYERS, playerList),
when(state.get(PREDICTIONS)).thenReturn(predictions); entry(ROUND, round),
when(state.getDealer()).thenReturn(new UUID(0,0)); entry(SCORE, Map.of()),
when(state.get(CURRENT_PLAYER)).thenReturn(new UUID(0,1)); entry(TRUMP_SUIT, Card.Suit.NONE),
when(state.get(ROUND)).thenReturn(10); entry(PREDICTIONS, GameTestUtils.emptyToIntMap(playerList)),
entry(HANDS, hands),
entry(CURRENT_PLAYER, players[(round + 1) % players.length])
);
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, -1)); game.resume(new Predicting(data));
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 12)); game.await();
for (int i = 0; i < 12; i++) { verify(game, never()).transition(any(), isNull());
state.checkPrediction(game, i); return game;
}
} }
@Test @Test
public void checkPrediction_ThrowsIllegalArgument_IfAddsUp() { public void predicting_Simple() {
UUID player = new UUID(0,0); // play cards in given order
Map<UUID, Integer> predictions = Map.of(new UUID(0,1), 10); MessageQueue queue = new MessageQueue()
.addPrediction(players[0], 4)
.addPrediction(players[1], 3)
.addPrediction(players[2], 3)
.addPrediction(players[3], 0);
Configuration config = mock(Configuration.class); Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue);
when(config.allowExactPredictions()).thenReturn(false);
Game game = mock(Game.class); // validate messages
when(game.getConfig()).thenReturn(config); InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // predicting
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void predicting_WithWrongInput() {
// play cards in given order
MessageQueue queue = new MessageQueue()
.addPrediction(players[0], -1).assertThrows(IllegalArgumentException.class)
.addPrediction(players[0], 6).assertThrows(IllegalArgumentException.class)
.addPrediction(players[0], 4)
.addPrediction(players[2], 3).assertThrows(IllegalStateException.class)
.addPrediction(players[1], 3)
.addPrediction(players[2], 5).assertThrows(IllegalArgumentException.class)
.addPrediction(players[2], 3)
.addCard(players[3], Card.GREEN_WIZARD).assertThrows(IllegalStateException.class)
.addPickTrump(players[3], Card.Suit.GREEN).assertThrows(IllegalStateException.class)
.addPrediction(players[3], 0);
Predicting state = mock(Predicting.class); Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue);
doCallRealMethod().when(state).checkPrediction(any(), anyInt());
when(state.get(PREDICTIONS)).thenReturn(predictions); // validate messages
when(state.getDealer()).thenReturn(player); InOrder order = inOrder(game);
when(state.get(CURRENT_PLAYER)).thenReturn(player); order.verify(game).notify(any(StateMessage.class)); // predicting
when(state.get(ROUND)).thenReturn(10); order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void predicting_AddUp() {
// play cards in given order
MessageQueue queue = new MessageQueue()
.addPrediction(players[0], 1)
.addPrediction(players[1], 1)
.addPrediction(players[2], 1)
.addPrediction(players[3], 1).assertThrows(IllegalArgumentException.class)
.addPrediction(players[3], 0);
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(game, 1)); Game game = performTest(Configurations.ANNIVERSARY_2021_PM1, 3, queue);
for (int i = 0; i < 12; i++) { // validate messages
if (i == 1) continue; InOrder order = inOrder(game);
state.checkPrediction(game, i); order.verify(game).notify(any(StateMessage.class)); // predicting
} order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(Predicting.class)); // next player
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
} }
} }

@ -0,0 +1,241 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.MessageQueue;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.model.Configuration;
import eu.jonahbauer.wizard.core.model.Configurations;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
public class RoundTest {
private final UUID[] players = new UUID[] {
new UUID(0, 0),
new UUID(0, 1),
new UUID(0, 2),
new UUID(0, 3)
};
@SneakyThrows
private Game performTest(long seed, Configuration configuration, int round, MessageQueue queue) {
Game game = spy(new Game(seed, configuration, queue));
doFinish().when(game).transition(any(), any(StartingRound.class));
queue.setGame(game);
var playerList = List.of(players);
GameData data = GameData.EMPTY.with(
entry(PLAYERS, playerList),
entry(ROUND, round),
entry(SCORE, Map.of())
);
game.resume(new StartingRound(data));
game.await();
verify(game, never()).transition(any(), isNull());
return game;
}
@Test
public void run_Simple() throws ExecutionException, InterruptedException {
MessageQueue queue = new MessageQueue()
.addPrediction(players[3], 3)
.addPrediction(players[0], 0)
.addPrediction(players[1], 3)
.addPrediction(players[2], 1)
// trick 0
.addCard(players[3], Card.RED_7)
.addCard(players[0], Card.GREEN_WIZARD)
.addCard(players[1], Card.GREEN_10)
.addCard(players[2], Card.RED_1)
// trick 1
.addCard(players[0], Card.RED_9)
.addCard(players[1], Card.YELLOW_WIZARD)
.addCard(players[2], Card.RED_3)
.addCard(players[3], Card.RED_10)
// trick 2
.addCard(players[1], Card.YELLOW_JESTER)
.addCard(players[2], Card.GREEN_12)
.addCard(players[3], Card.GREEN_5)
.addCard(players[0], Card.BLUE_7)
// trick 3
.addCard(players[2], Card.GREEN_1)
.addCard(players[3], Card.YELLOW_10)
.addCard(players[0], Card.BLUE_5)
.addCard(players[1], Card.GREEN_2)
// trick 4
.addCard(players[1], Card.GREEN_7)
.addCard(players[2], Card.GREEN_3)
.addCard(players[3], Card.YELLOW_5)
.addCard(players[0], Card.RED_13)
// trick 5
.addCard(players[1], Card.YELLOW_8)
.addCard(players[2], Card.BLUE_9)
.addCard(players[3], Card.YELLOW_9)
.addCard(players[0], Card.GREEN_JESTER)
// trick 5
.addCard(players[3], Card.YELLOW_2)
.addCard(players[0], Card.BLUE_2)
.addCard(players[1], Card.BLUE_10)
.addCard(players[2], Card.BLUE_6);
int round = 6;
Game game = performTest(0L, Configurations.DEFAULT, round, queue);
game.await();
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting_round
order.verify(game).notify(any(StateMessage.class)); // dealing
order.verify(game, atLeast(4)).notify(any(), any(HandMessage.class)); // hands
order.verify(game).notify(any(StateMessage.class)); // determining_trump
order.verify(game).notify(any(TrumpMessage.class)); // trump
for (int i = 0; i < players.length; i++) {
order.verify(game).notify(any(StateMessage.class)); // predicting
order.verify(game).notify(any(UserInputMessage.class)); // user input
order.verify(game).notify(any(PredictionMessage.class)); // user input
}
for (int i = 0; i < round + 1; i++) {
order.verify(game).notify(any(StateMessage.class)); // starting trick
for (int j = 0; j < players.length; j++) {
order.verify(game).notify(any(StateMessage.class)); // playing_card
order.verify(game).notify(any(UserInputMessage.class)); // user input
order.verify(game).notify(any(CardMessage.class)); // user input
}
order.verify(game).notify(any(StateMessage.class)); // finishing_trick
order.verify(game).notify(any(TrickMessage.class)); // trick
}
order.verify(game).notify(any(StateMessage.class)); // finishing_round
order.verify(game).notify(argThat(message ->
message instanceof ScoreMessage score
&& score.getPoints().get(players[0]) == -10
&& score.getPoints().get(players[1]) == 50
&& score.getPoints().get(players[2]) == 30
&& score.getPoints().get(players[3]) == -10
)); // score
order.verify(game).transition(any(), any(StartingRound.class)); // next round
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_Anniversary() throws ExecutionException, InterruptedException {
MessageQueue queue = new MessageQueue()
.addPickTrump(players[2], Card.Suit.YELLOW)
.addPrediction(players[3], 2)
.addPrediction(players[0], 2)
.addPrediction(players[1], 2)
.addPrediction(players[2], 1).assertThrows(IllegalArgumentException.class)
.addPrediction(players[2], 3)
// trick 0
.addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class)
.addCard(players[3], Card.BLUE_2)
.addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
.addCard(players[0], Card.YELLOW_8)
.addCard(players[1], Card.BLUE_9)
.addCard(players[2], Card.GREEN_WIZARD)
// trick 1
.addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
.addCard(players[2], Card.YELLOW_4)
.addCard(players[3], Card.YELLOW_3)
.addCard(players[0], Card.YELLOW_WIZARD)
.addCard(players[1], Card.BOMB)
// trick 2
.addCard(players[0], Card.RED_3)
.addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
.addCard(players[1], Card.RED_12)
.addCard(players[2], Card.RED_2)
.addCard(players[3], Card.DRAGON)
// trick 3
.addCard(players[3], Card.BLUE_13)
.addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class)
.addCard(players[0], Card.CLOUD_YELLOW)
.addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
.addCard(players[1], Card.YELLOW_13)
.addCard(players[2], Card.BLUE_1)
.addChangePrediction(players[1], 0).assertThrows(IllegalArgumentException.class)
.addChangePrediction(players[1], 2).assertThrows(IllegalArgumentException.class)
.addChangePrediction(players[1], 4).assertThrows(IllegalArgumentException.class)
.addChangePrediction(players[1], 1)
// trick 4
.addCard(players[1], Card.RED_7)
.addCard(players[2], Card.YELLOW_11)
.addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class)
.addCard(players[3], Card.RED_5)
.addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class)
.addCard(players[0], Card.CHANGELING_WIZARD)
// trick 5
.addCard(players[0], Card.GREEN_7)
.addCard(players[1], Card.FAIRY)
.addCard(players[2], Card.YELLOW_7)
.addCard(players[3], Card.BLUE_6)
// trick 6
.addCard(players[2], Card.BLUE_4)
.addCard(players[3], Card.BLUE_11)
.addCard(players[0], Card.GREEN_1)
.addCard(players[1], Card.GREEN_11);
int round = 6;
Game game = performTest(227L, Configurations.ANNIVERSARY_2021_PM1, round, queue);
game.await();
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting round
order.verify(game).notify(any(StateMessage.class)); // dealing
order.verify(game, times(4)).notify(any(), any(HandMessage.class)); // hands
order.verify(game).notify(any(StateMessage.class)); // determining trump
order.verify(game, times(2)).notify(any(TrumpMessage.class)); // werewolf
order.verify(game).notify(any(UserInputMessage.class)); // user input
order.verify(game).notify(any(TrumpMessage.class)); // user input
order.verify(game).notify(any(), any(HandMessage.class)); // update hand
for (int i = 0; i < players.length; i++) {
order.verify(game).notify(any(StateMessage.class));
order.verify(game).notify(any(UserInputMessage.class));
order.verify(game).notify(any(PredictionMessage.class));
}
for (int i = 0; i < round + 1; i++) {
order.verify(game).notify(any(StateMessage.class)); // starting trick
for (int j = 0; j < players.length; j++) {
order.verify(game).notify(any(StateMessage.class)); // playing_card
order.verify(game).notify(any(UserInputMessage.class)); // user input
order.verify(game).notify(any(CardMessage.class)); // user input
}
order.verify(game).notify(any(StateMessage.class)); // finishing_trick
order.verify(game).notify(any(TrickMessage.class)); // trick
if (i == 3) {
order.verify(game).notify(any(StateMessage.class)); // change prediction
order.verify(game).notify(any(UserInputMessage.class));
order.verify(game).notify(any(PredictionMessage.class));
}
}
order.verify(game).notify(any(StateMessage.class)); // finishing_round
order.verify(game).notify(argThat(message ->
message instanceof ScoreMessage score
&& score.getPoints().get(players[0]) == -10
&& score.getPoints().get(players[1]) == 30
&& score.getPoints().get(players[2]) == -10
&& score.getPoints().get(players[3]) == 40
)); // score
order.verify(game).transition(any(), any(StartingRound.class)); // next round
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
}

@ -1,78 +0,0 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
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;
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(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(UUID.randomUUID(), Card.BLUE_WIZARD),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11)
);
performTest(stack, Card.Suit.YELLOW, Card.Suit.RED);
}
@Test
public void getWinner_ReturnsHighestTrump_IfNoWizard() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.YELLOW_13),
Pair.of(null, Card.GREEN_11)
);
performTest(stack, Card.Suit.YELLOW, Card.Suit.RED);
}
@Test
public void getWinner_ReturnsHighestTrickSuit_IfNeitherWizardNorTrump() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.RED_1),
Pair.of(null, Card.YELLOW_13),
Pair.of(null, Card.GREEN_11)
);
performTest(stack, Card.Suit.BLUE, Card.Suit.RED);
}
@Test
public void getWinner_ReturnsFirstJester_IfOnlyJester() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(UUID.randomUUID(), Card.GREEN_JESTER),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.RED_JESTER),
Pair.of(null, Card.YELLOW_JESTER)
);
performTest(stack, Card.Suit.NONE, Card.Suit.NONE);
}
}

@ -1,86 +0,0 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
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;
import eu.jonahbauer.wizard.core.model.Card;
import org.junit.jupiter.api.Test;
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.*;
public class PlayingCardTest {
@Test
public void checkCard_ThrowsIllegalArgument_IfCardNotInHand() {
List<Card> hand = List.of(Card.BLUE_1, Card.RED_1, Card.YELLOW_1, Card.GREEN_JESTER, Card.BLUE_WIZARD);
UUID player = new UUID(0, 0);
PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).checkCard(any());
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;
assertThrows(IllegalArgumentException.class, () -> state.checkCard(value));
}
verify(state, times(0)).getData();
}
@Test
public void checkCard_ThrowsIllegalArgument_IfNotFollowingSuit() {
List<Card> hand = List.of(Card.BLUE_1, Card.RED_1, Card.YELLOW_1, Card.GREEN_JESTER, Card.BLUE_WIZARD);
UUID player = new UUID(0, 0);
PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).checkCard(any());
when(state.getTrickSuit()).thenReturn(Card.Suit.YELLOW);
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));
state.checkCard(Card.YELLOW_1);
state.checkCard(Card.GREEN_JESTER);
state.checkCard(Card.BLUE_WIZARD);
}
@Test
public void onMessage_ThrowsIllegalState_IfNotCurrentPlayer() {
UUID player = new UUID(0, 0);
UUID player2 = new UUID(0, 1);
Game game = mock(Game.class);
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);
Game game = mock(Game.class);
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,90 +0,0 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.util.Pair;
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;
public class TrickStateTest {
@Test
public void getTrickSuit_ReturnsNone_IfEmpty() {
List<Pair<UUID, Card>> stack = List.of();
TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
}
@Test
public void getTrickSuit_ReturnsColor() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11)
);
TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.RED);
}
@Test
public void getTickSuit_ReturnsNone_IfFirstCardIsWizard() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11)
);
TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
}
@Test
public void getTrickSuit_IgnoresJesters() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.GREEN_11)
);
TrickState trickState = mock(TrickState.class);
when(trickState.getTrickSuit()).thenCallRealMethod();
when(trickState.get(STACK)).thenReturn(stack);
assertEquals(trickState.getTrickSuit(), Card.Suit.NONE);
List<Pair<UUID, Card>> stack2 = List.of(
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.RED_1),
Pair.of(null, Card.GREEN_11)
);
TrickState trickState2 = mock(TrickState.class);
when(trickState2.getTrickSuit()).thenCallRealMethod();
when(trickState2.get(STACK)).thenReturn(stack2);
assertEquals(trickState2.getTrickSuit(), Card.Suit.YELLOW);
}
}

@ -0,0 +1,299 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameTestUtils;
import eu.jonahbauer.wizard.core.machine.MessageQueue;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound;
import eu.jonahbauer.wizard.core.model.Configuration;
import eu.jonahbauer.wizard.core.model.Configurations;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.machine.GameTestUtils.doFinish;
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.core.machine.states.GameData.TypedValue.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
public class TrickTest {
private final UUID[] players = new UUID[] {
new UUID(0, 0),
new UUID(0, 1),
new UUID(0, 2),
new UUID(0, 3)
};
@SneakyThrows
@SuppressWarnings("SameParameterValue")
private Game performTest(Configuration configuration, int round, int trick, Map<UUID, List<Card>> hands, Card.Suit trump, MessageQueue queue) {
Game game = spy(new Game(configuration, queue));
doFinish().when(game).transition(any(), any(StartingTrick.class));
doFinish().when(game).transition(any(), any(FinishingRound.class));
queue.setGame(game);
var playerList = List.of(players);
GameData data = GameData.EMPTY.with(
entry(PLAYERS, playerList),
entry(ROUND, round),
entry(SCORE, Map.of()),
entry(HANDS, hands),
entry(PREDICTIONS, GameTestUtils.emptyToIntMap(playerList)),
entry(TRICKS, GameTestUtils.emptyToIntMap(playerList)),
entry(TRICK, trick),
entry(TRUMP_SUIT, trump),
entry(CURRENT_PLAYER, players[0])
);
game.resume(new StartingTrick(data));
game.await();
return game;
}
@Test
public void run_Simple() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.YELLOW_1),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addCards(List.of(players), hands, 0);
Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting trick
for (int i = 0; i < players.length; i++) {
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
}
order.verify(game).notify(any(StateMessage.class)); // finishing trick
order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner
order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithWrongInput() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.YELLOW_1),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addCard(players[0], Card.RED_1)
.addCard(players[2], Card.GREEN_1).assertThrows(IllegalStateException.class)
.addCard(players[1], Card.GREEN_1).assertThrows(IllegalArgumentException.class)
.addCard(players[1], Card.YELLOW_1)
.addCard(players[2], Card.GREEN_1)
.addPrediction(players[3], 1).assertThrows(IllegalStateException.class)
.addCard(players[3], Card.BLUE_1);
Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // finishing trick
order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner
order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithCloud() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1),
players[1], List.of(Card.CLOUD),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addCard(players[0], Card.RED_1)
.addCard(players[1], Card.CLOUD_BLUE)
.addCard(players[2], Card.GREEN_1)
.addCard(players[3], Card.BLUE_1)
.addChangePrediction(players[2], 1);
Game game = performTest(Configurations.ANNIVERSARY_2021, 0, 0, hands, Card.Suit.GREEN, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting trick
for (int i = 0; i < players.length; i++) {
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
}
order.verify(game).notify(any(StateMessage.class)); // finishing trick
order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[2])); // trick with correct winner
order.verify(game).notify(any(StateMessage.class)); // changing prediction
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(PredictionMessage.class)); // user response
order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithJuggler() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.RED_1, Card.GREEN_12),
players[1], List.of(Card.JUGGLER, Card.YELLOW_3),
players[2], List.of(Card.GREEN_1, Card.BLUE_4),
players[3], List.of(Card.BLUE_1, Card.RED_5)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addCard(players[0], Card.RED_1)
.addCard(players[1], Card.JUGGLER_RED)
.addCard(players[2], Card.GREEN_1)
.addCard(players[3], Card.RED_5);
Game game = performTest(Configurations.ANNIVERSARY_2021, 1, 0, hands, Card.Suit.YELLOW, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // finishing trick
order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[1])); // trick with correct winner
order.verify(game).notify(any(JugglingMessage.class));
order.verify(game, times(4)).notify(any(), any(HandMessage.class));
order.verify(game).transition(any(), any(StartingTrick.class)); // there is another trick
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithChangeling() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.CHANGELING),
players[1], List.of(Card.RED_1),
players[2], List.of(Card.GREEN_1),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addCard(players[0], Card.CHANGELING_JESTER)
.addCard(players[1], Card.RED_1)
.addCard(players[2], Card.GREEN_1)
.addCard(players[3], Card.BLUE_1);
Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // finishing trick
order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner
order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
@Test
public void run_WithChangeling2() {
Map<UUID, List<Card>> hands = Map.of(
players[0], List.of(Card.CHANGELING),
players[1], List.of(Card.RED_1),
players[2], List.of(Card.GREEN_WIZARD),
players[3], List.of(Card.BLUE_1)
);
// play cards in given order
MessageQueue queue = new MessageQueue()
.addCard(players[0], Card.CHANGELING_WIZARD)
.addCard(players[1], Card.RED_1)
.addCard(players[2], Card.GREEN_WIZARD)
.addCard(players[3], Card.BLUE_1);
Game game = performTest(Configurations.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);
// validate messages
InOrder order = inOrder(game);
order.verify(game).notify(any(StateMessage.class)); // starting trick
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // playing card
order.verify(game).notify(any(UserInputMessage.class)); // user input request
order.verify(game).notify(any(CardMessage.class)); // user response
order.verify(game).notify(any(StateMessage.class)); // finishing trick
order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[0])); // trick with correct winner
order.verify(game).transition(any(), any(FinishingRound.class)); // round is finished
order.verify(game).notify(any(StateMessage.class)); // finish
order.verify(game, never()).notify(any());
order.verify(game, never()).notify(any(), any());
}
}

@ -0,0 +1,219 @@
package eu.jonahbauer.wizard.core.model.card;
import eu.jonahbauer.wizard.common.model.card.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class CardUtilsTest {
private void performWinnerTest(List<Pair<UUID, Card>> stack, Card.Suit trumpSuit) {
assertNotNull(CardUtils.getWinner(stack, trumpSuit));
}
@Test
public void getWinner_ReturnsFirstWizard() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(UUID.randomUUID(), Card.BLUE_WIZARD),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.YELLOW);
}
@Test
public void getWinner_ReturnsDragon() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(UUID.randomUUID(), Card.DRAGON),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11)
);
performWinnerTest(stack, Card.Suit.YELLOW);
}
@Test
public void getWinner_ReturnsFairy_IfDragon() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(UUID.randomUUID(), Card.FAIRY),
Pair.of(null, Card.DRAGON),
Pair.of(null, Card.GREEN_11)
);
performWinnerTest(stack, Card.Suit.YELLOW);
}
@Test
public void getWinner_ReturnsHighestTrump_IfNoWizard() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.YELLOW_13),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.YELLOW);
}
@Test
public void getWinner_ReturnsHighestSpecialTrump_IfNoWizard() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.JUGGLER_YELLOW),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.YELLOW);
}
@Test
public void getWinner_ReturnsHighestSpecialTrump_IfNoWizard2() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.CLOUD_YELLOW),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.YELLOW);
}
@Test
public void getWinner_ReturnsHighestTrickSuit_IfNeitherWizardNorTrump() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_5),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.RED_13),
Pair.of(null, Card.YELLOW_13),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.RED_7),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.BLUE);
}
@Test
public void getWinner_ReturnsHighestSpecialTrickSuit_IfNeitherWizardNorTrump() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_5),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.CLOUD_RED),
Pair.of(null, Card.YELLOW_13),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.RED_7),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.BLUE);
}
@Test
public void getWinner_ReturnsHighestSpecialTrickSuit_IfNeitherWizardNorTrump2() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_5),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(UUID.randomUUID(), Card.JUGGLER_RED),
Pair.of(null, Card.YELLOW_13),
Pair.of(null, Card.GREEN_11),
Pair.of(null, Card.RED_7),
Pair.of(null, Card.FAIRY)
);
performWinnerTest(stack, Card.Suit.BLUE);
}
@Test
public void getWinner_ReturnsFirstJester_IfOnlyJester() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(UUID.randomUUID(), Card.GREEN_JESTER),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.RED_JESTER),
Pair.of(null, Card.FAIRY),
Pair.of(null, Card.YELLOW_JESTER)
);
performWinnerTest(stack, Card.Suit.RED);
}
@Test
public void getWinner_ReturnsJester_IfOnlyFairyAndBomb() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.FAIRY),
Pair.of(null, Card.BOMB),
Pair.of(UUID.randomUUID(), Card.RED_JESTER)
);
performWinnerTest(stack, Card.Suit.RED);
}
@Test
public void getTrickSuit_ReturnsNone_IfEmpty() {
List<Pair<UUID, Card>> stack = List.of();
assertEquals(Card.Suit.NONE, CardUtils.getTrickSuit(stack));
}
@Test
public void getTrickSuit_ReturnsColor() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11)
);
assertEquals(Card.Suit.RED, CardUtils.getTrickSuit(stack));
}
@Test
public void getTickSuit_ReturnsNone_IfFirstCardIsWizard() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.GREEN_11)
);
assertEquals(Card.Suit.NONE, CardUtils.getTrickSuit(stack));
}
@Test
public void getTrickSuit_IgnoresJesters() {
List<Pair<UUID, Card>> stack = List.of(
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.RED_1),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.GREEN_11)
);
assertEquals(Card.Suit.NONE, CardUtils.getTrickSuit(stack));
List<Pair<UUID, Card>> stack2 = List.of(
Pair.of(null, Card.BLUE_JESTER),
Pair.of(null, Card.YELLOW_1),
Pair.of(null, Card.BLUE_WIZARD),
Pair.of(null, Card.RED_1),
Pair.of(null, Card.GREEN_11)
);
assertEquals(Card.Suit.YELLOW, CardUtils.getTrickSuit(stack2));
}
}

@ -19,7 +19,7 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>eu.jonahbauer</groupId> <groupId>${project.groupId}</groupId>
<artifactId>wizard-core</artifactId> <artifactId>wizard-core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>

Loading…
Cancel
Save