Issue #5: Grundspiel implementieren

main
Jonah Bauer 3 years ago
parent c7c7b12ee1
commit ee6b7d7c2d

@ -19,6 +19,7 @@
<java.version>17</java.version> <java.version>17</java.version>
<log4j.version>2.14.1</log4j.version> <log4j.version>2.14.1</log4j.version>
<lombok.version>1.18.22</lombok.version> <lombok.version>1.18.22</lombok.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies> <dependencies>
@ -34,7 +35,13 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.8</version>
</dependency>
<!-- Logging -->
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId> <artifactId>log4j-api</artifactId>
@ -50,6 +57,32 @@
<artifactId>log4j-slf4j-impl</artifactId> <artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version> <version>${log4j.version}</version>
</dependency> </dependency>
<!-- Tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -74,6 +107,11 @@
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version> <version>3.3.0</version>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
</build> </build>

@ -0,0 +1,273 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.gson.typeadapters;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {@code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used. <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {@code
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*
* <h3>Serialization and deserialization</h3>
* In order to serialize and deserialize a polymorphic object,
* you must specify the base type explicitly.
* <pre> {@code
* Diamond diamond = new Diamond();
* String json = gson.toJson(diamond, Shape.class);
* }</pre>
* And then:
* <pre> {@code
* Shape shape = gson.fromJson(json, Shape.class);
* }</pre>
*/
@SuppressWarnings("ALL")
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
private final boolean maintainType;
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
this.maintainType = maintainType;
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
* {@code maintainType} flag decide if the type will be stored in pojo or not.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
}
/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label}
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
@Override
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<>();
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
labelToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
return new TypeAdapter<R>() {
@Override public R read(JsonReader in) throws IOException {
JsonElement jsonElement = jsonElementAdapter.read(in);
JsonElement labelJsonElement;
if (maintainType) {
labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
} else {
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
}
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
@Override public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (maintainType) {
jsonElementAdapter.write(out, jsonObject);
return;
}
JsonObject clone = new JsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
jsonElementAdapter.write(out, clone);
}
}.nullSafe();
}
}

@ -0,0 +1,68 @@
package eu.jonahbauer.wizard.core;
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 eu.jonahbauer.wizard.core.model.Configurations;
import java.util.List;
import java.util.Scanner;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CLI {
public static void main(String[] args) {
Game game = new Game(Configurations.DEFAULT, (player, msg) -> System.out.println(msg));
var players = List.of(
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID()
);
game.start(players);
Scanner scanner = new Scanner(System.in);
Pattern pattern = Pattern.compile("(\\d) (predict|play|trump) (.*)");
while (scanner.hasNextLine()) {
try {
Matcher matcher = pattern.matcher(scanner.nextLine());
if (!matcher.find()) {
System.err.println("Format is \"(\\\\d) (predict|play|trump) (.*)\"");
continue;
}
String player = matcher.group(1);
String command = matcher.group(2);
String param = matcher.group(3);
int id = Integer.parseInt(player);
if (id > players.size()) {
System.err.println("ID must be between 0 and " + (players.size() - 1));
continue;
}
switch (command) {
case "predict" -> {
int prediction = Integer.parseInt(param);
game.onMessage(players.get(id), new PredictMessage(prediction));
}
case "play" -> {
Card card = Card.valueOf(param);
game.onMessage(players.get(id), new PlayCardMessage(card));
}
case "trump" -> {
Card.Suit suit = Card.Suit.valueOf(param);
game.onMessage(players.get(id), new PickTrumpMessage(suit));
}
default -> System.err.println("Unknown command: " + command);
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}
}

@ -0,0 +1,107 @@
package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.core.machine.states.State;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public abstract class Context<T extends State> {
protected final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
protected T state;
protected final ReentrantLock lock = new ReentrantLock();
private final Condition finishCondition = lock.newCondition();
private boolean finished;
private Throwable exception;
protected void start(@NotNull T state) {
lock.lock();
try {
if (finished) throw new IllegalStateException("Context has already finished.");
transition(null, state);
} finally {
lock.unlock();
}
}
public void transition(T currentState, T newState) {
lock.lock();
try {
if (state == currentState) {
state = newState;
if (currentState != null) currentState.onExit();
onTransition(currentState, newState);
if (newState != null) newState.onEnter();
} else {
throw new IllegalStateException("Current state does not match.");
}
} catch (Throwable t) {
handleError(t);
} finally {
lock.unlock();
}
}
public void finish() {
finish(null);
}
public void finish(Throwable exception) {
lock.lock();
try {
finished = true;
this.exception = exception;
finishCondition.signalAll();
transition(state, null);
scheduler.shutdown();
} finally {
lock.unlock();
}
}
public void cancel() {
finish(new CancellationException());
}
@Blocking
public void await() throws InterruptedException, ExecutionException, CancellationException {
lock.lock();
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 T currentState, long delay) {
scheduler.schedule(() -> {
lock.lock();
try {
if (state == currentState) {
state.onTimeout();
}
} catch (Throwable t) {
handleError(t);
} finally {
lock.unlock();
}
}, delay, TimeUnit.MILLISECONDS);
}
protected void handleError(Throwable t) {
finish(t);
}
protected void onTransition(T from, T to) {}
}

@ -0,0 +1,66 @@
package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.core.machine.states.GameState;
import eu.jonahbauer.wizard.core.machine.states.game.Starting;
import eu.jonahbauer.wizard.core.messages.Observer;
import eu.jonahbauer.wizard.core.messages.observer.ObserverMessage;
import eu.jonahbauer.wizard.core.messages.observer.StateMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Configuration;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
public final class Game extends Context<GameState> {
@Getter
private final Configuration config;
private final Observer observer;
public Game(Configuration config, Observer observer) {
this.config = config;
this.observer = observer;
}
public void start(List<UUID> players) {
start(new Starting(this, GameData.builder().players(players).build()));
}
public void resume(GameState state) {
start(state);
}
public void onMessage(UUID player, PlayerMessage message) {
lock.lock();
try {
state.onMessage(player, message);
} catch (IllegalStateException | IllegalArgumentException e) {
throw e;
} catch (Throwable t) {
handleError(t);
} finally {
lock.unlock();
}
}
@Override
protected void onTransition(GameState from, GameState to) {
notify(new StateMessage(to != null ? to.getClass() : null));
}
public void notify(ObserverMessage message) {
try {
observer.notify(message);
} catch (Exception e) {
e.printStackTrace();
}
}
public void notify(UUID player, ObserverMessage message) {
try {
observer.notify(player, message);
} catch (Exception e) {
e.printStackTrace();
}
}
}

@ -0,0 +1,334 @@
package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import lombok.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
@With
@Getter
@EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings({"unused", "UnusedReturnValue", "OptionalUsedAsFieldOrParameterType", "OptionalAssignedToNull"})
public final class GameData {
private final @Unmodifiable List<UUID> players;
private final @Unmodifiable Map<UUID, Integer> score;
private final Integer round;
private final @Unmodifiable Map<UUID, @Unmodifiable List<Card>> hands;
private final Optional<Card> trumpCard;
private final Card.Suit trumpSuit;
private final @Unmodifiable Map<UUID, Integer> predictions;
private final @Unmodifiable Map<UUID, Integer> tricks;
private final Integer trick;
private final @Unmodifiable List<Pair<UUID, Card>> stack;
private final UUID currentPlayer;
private @EqualsAndHashCode.Exclude transient boolean requiredPlayers;
private @EqualsAndHashCode.Exclude transient boolean requiredRound;
private @EqualsAndHashCode.Exclude transient boolean requiredTrick;
private @EqualsAndHashCode.Exclude transient boolean requiredHands;
private @EqualsAndHashCode.Exclude transient boolean requiredTrumpCard;
private @EqualsAndHashCode.Exclude transient boolean requiredTrumpSuit;
private @EqualsAndHashCode.Exclude transient boolean requiredStack;
private @EqualsAndHashCode.Exclude transient boolean requiredPredictions;
private @EqualsAndHashCode.Exclude transient boolean requiredTricks;
private @EqualsAndHashCode.Exclude transient boolean requiredScore;
private @EqualsAndHashCode.Exclude transient boolean requiredCurrentPlayer;
@Builder(toBuilder = true)
private GameData(@Unmodifiable List<UUID> players,
@Unmodifiable Map<UUID, Integer> score,
Integer round,
@Unmodifiable Map<UUID, @Unmodifiable List<Card>> hands,
Optional<Card> trumpCard,
Card.Suit trumpSuit,
@Unmodifiable Map<UUID, Integer> predictions,
@Unmodifiable Map<UUID, Integer> tricks,
Integer trick,
@Unmodifiable List<Pair<UUID, Card>> stack,
UUID currentPlayer)
{
this.players = players != null ? List.copyOf(players) : null;
this.score = score != null ? Map.copyOf(score) : null;
this.round = round;
this.hands = hands != null ? Map.copyOf(hands) : null;
this.trumpCard = trumpCard;
this.trumpSuit = trumpSuit;
this.predictions = predictions != null ? Map.copyOf(predictions) : null;
this.tricks = tricks != null ? Map.copyOf(tricks) : null;
this.trick = trick;
this.stack = stack != null ? List.copyOf(stack) : null;
this.currentPlayer = currentPlayer;
}
//<editor-fold desc="Getters" defaultstate="collapsed">
public List<UUID> getPlayers() {
if (players == null) throw new UnsupportedOperationException();
return players;
}
public int getRound() {
if (round == null) throw new UnsupportedOperationException();
return round;
}
public int getTrick() {
if (trick == null) throw new UnsupportedOperationException();
return trick;
}
public Map<UUID, List<Card>> getHands() {
if (hands == null) throw new UnsupportedOperationException();
return hands;
}
public Card getTrumpCard() {
if (trumpCard == null) throw new UnsupportedOperationException();
return trumpCard.orElse(null);
}
public Card.Suit getTrumpSuit() {
if (trumpSuit == null) throw new UnsupportedOperationException();
return trumpSuit;
}
public List<Pair<UUID, Card>> getStack() {
if (stack == null) throw new UnsupportedOperationException();
return stack;
}
public Map<UUID, Integer> getPredictions() {
if (predictions == null) throw new UnsupportedOperationException();
return predictions;
}
public Map<UUID, Integer> getTricks() {
if (tricks == null) throw new UnsupportedOperationException();
return tricks;
}
public Map<UUID, Integer> getScore() {
if (score == null) throw new UnsupportedOperationException();
return score;
}
public UUID getCurrentPlayer() {
if (currentPlayer == null) throw new UnsupportedOperationException();
return currentPlayer;
}
public UUID getPlayer(int index) {
return getPlayers().get(index);
}
public int getPlayerCount() {
return getPlayers().size();
}
//</editor-fold>
//<editor-fold desc="Requirements" defaultstate="collapsed">
public GameData requirePlayers() {
if (players == null) throw new AssertionError();
requiredPlayers = true;
return this;
}
public GameData requireRound() {
if (round == null) throw new AssertionError();
requiredRound = true;
return this;
}
public GameData requireTrick() {
if (trick == null) throw new AssertionError();
requiredTrick = true;
return this;
}
public GameData requireHands() {
if (hands == null) throw new AssertionError();
requiredHands = true;
return this;
}
public GameData requireAllHands() {
requirePlayers();
requireHands();
for (UUID uuid : players) {
if (!hands.containsKey(uuid)) {
throw new AssertionError();
}
}
return this;
}
public GameData requireTrumpCard() {
if (trumpCard == null) throw new AssertionError();
requiredTrumpCard = true;
return this;
}
public GameData requireTrumpSuit() {
if (trumpSuit == null) throw new AssertionError();
requiredTrumpSuit = true;
return this;
}
public GameData requireStack() {
if (stack == null) throw new AssertionError();
requiredStack = true;
return this;
}
public GameData requirePredictions() {
if (predictions == null) throw new AssertionError();
requiredPredictions = true;
return this;
}
public GameData requireAllPredictions() {
requirePlayers();
requirePredictions();
for (UUID uuid : players) {
if (!predictions.containsKey(uuid)) {
throw new AssertionError();
}
}
return this;
}
public GameData requireTricks() {
if (tricks == null) throw new AssertionError();
requiredTricks = true;
return this;
}
public GameData requireScore() {
if (score == null) throw new AssertionError();
requiredScore = true;
return this;
}
public GameData requireCurrentPlayer() {
if (currentPlayer == null) throw new AssertionError();
requiredCurrentPlayer = true;
return this;
}
public GameData onlyRequired() {
var builder = toBuilder();
if (!requiredPlayers) builder.players(null);
if (!requiredRound) builder.round(null);
if (!requiredTrick) builder.trick(null);
if (!requiredHands) builder.hands(null);
if (!requiredTrumpCard) builder.trumpCard(null);
if (!requiredTrumpSuit) builder.trumpSuit(null);
if (!requiredStack) builder.stack(null);
if (!requiredPredictions) builder.predictions(null);
if (!requiredTricks) builder.tricks(null);
if (!requiredScore) builder.score(null);
if (!requiredCurrentPlayer) builder.currentPlayer(null);
requiredPlayers = false;
requiredRound = false;
requiredTrick = false;
requiredHands = false;
requiredTrumpCard = false;
requiredTrumpSuit = false;
requiredStack = false;
requiredPredictions = false;
requiredTricks = false;
requiredScore = false;
requiredCurrentPlayer = false;
return builder.build();
}
//</editor-fold>
//<editor-fold desc="Wither" defaultstate="collapsed">
public GameData withPrediction(UUID player, int prediction) {
Map<UUID, Integer> predictions = new HashMap<>(getPredictions());
predictions.put(player, prediction);
return withPredictions(predictions);
}
public GameData withNextPlayer() {
int index = getPlayers().indexOf(getCurrentPlayer());
UUID next;
if (index == -1) {
throw new IllegalArgumentException();
} else if (index == getPlayerCount() - 1) {
next = getPlayers().get(0);
} else {
next = getPlayers().get(index + 1);
}
return withCurrentPlayer(next);
}
public GameData withNextTrick() {
int trick = getTrick();
return withTrick(trick + 1);
}
public GameData withNextRound() {
int round = getRound();
return withRound(round + 1);
}
public GameData withCardPlayed(UUID player, Card card) {
List<Card> hand = new ArrayList<>(getHands().get(player));
hand.remove(card);
Map<UUID, List<Card>> hands = new HashMap<>(getHands());
hands.put(player, hand);
List<Pair<UUID, Card>> stack = new ArrayList<>(getStack());
stack.add(Pair.of(player, card));
return withStack(stack).withHands(hands);
}
//</editor-fold>
public String toString() {
StringBuilder builder = new StringBuilder("(");
if (players != null) builder.append("players=").append(players).append(", ");
if (round != null) builder.append("round=").append(round).append(", ");
if (trick != null) builder.append("trick=").append(trick).append(", ");
if (hands != null) builder.append("hands=").append(hands).append(", ");
if (trumpCard != null) builder.append("trumpCard=").append(trumpCard).append(", ");
if (trumpSuit != null) builder.append("trumpSuit=").append(trumpSuit).append(", ");
if (stack != null) builder.append("stack=").append(stack).append(", ");
if (predictions != null) builder.append("predictions=").append(predictions).append(", ");
if (tricks != null) builder.append("tricks=").append(tricks).append(", ");
if (currentPlayer != null) builder.append("currentPlayer=").append(currentPlayer).append(", ");
if (builder.length() > 1) builder.setLength(builder.length() - 2);
builder.append(")");
return builder.toString();
}
// An interface for @lombok.experimental.Delegate
public interface Getters {
@NotNull List<UUID> getPlayers();
@NotNull Map<UUID, Integer> getScore();
int getRound();
@NotNull Map<UUID, List<Card>> getHands();
@Nullable Card getTrumpCard();
@NotNull Card.Suit getTrumpSuit();
@NotNull Map<UUID, Integer> getPredictions();
@NotNull Map<UUID, Integer> getTricks();
int getTrick();
@NotNull List<Pair<UUID, Card>> getStack();
@NotNull UUID getCurrentPlayer();
@NotNull UUID getPlayer(int index);
int getPlayerCount();
}
}

@ -0,0 +1,45 @@
package eu.jonahbauer.wizard.core.machine.states;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import lombok.Getter;
import lombok.experimental.Delegate;
import org.jetbrains.annotations.Unmodifiable;
import java.util.UUID;
@Unmodifiable
public abstract class GameState implements State {
@Getter
private final Game game;
@Getter
@Delegate(types = GameData.Getters.class)
private final GameData data;
public GameState(Game game, GameData data) {
this.game = game;
this.data = data.requirePlayers().onlyRequired();
}
protected final void transition(GameState state) {
getGame().transition(this, state);
}
protected final void timeout() {
getGame().timeout(this, getTimeout(false));
}
protected final long getTimeout(boolean absolute) {
return (absolute ? System.currentTimeMillis() : 0) + getGame().getConfig().timeout();
}
public void onMessage(UUID player, PlayerMessage message) {
throw new IllegalStateException("You cannot do that right now.");
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}

@ -0,0 +1,7 @@
package eu.jonahbauer.wizard.core.machine.states;
public interface State {
default void onEnter() {}
default void onTimeout() {}
default void onExit() {}
}

@ -0,0 +1,19 @@
package eu.jonahbauer.wizard.core.machine.states.game;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.GameState;
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage;
public final class Finishing extends GameState {
public Finishing(Game game, GameData data) {
super(game, data.requireScore());
}
@Override
public void onEnter() {
getGame().notify(new ScoreMessage(getScore()));
getGame().finish();
}
}

@ -0,0 +1,20 @@
package eu.jonahbauer.wizard.core.machine.states.game;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.GameState;
import eu.jonahbauer.wizard.core.machine.states.round.StartingRound;
import java.util.Collections;
public final class Starting extends GameState {
public Starting(Game game, GameData data) {
super(game, data);
}
@Override
public void onEnter() {
transition(new StartingRound(getGame(), getData().withRound(0).withScore(Collections.emptyMap())));
}
}

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

@ -0,0 +1,87 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.messages.observer.TrumpMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Card;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PICK_TRUMP;
/**
*
*/
public final class DeterminingTrump extends RoundState {
private transient Card.Suit trumpSuit;
public DeterminingTrump(Game game, GameData data) {
super(game, data.requireAllHands().requireTrumpCard());
}
@Override
public void onEnter() {
Card trumpCard = getTrumpCard();
Card.Suit trumpSuit = trumpCard != null ? trumpCard.getTrumpSuit() : Card.Suit.NONE;
if (trumpSuit == null) {
getGame().notify(new TrumpMessage(getTrumpCard(), null));
getGame().notify(new UserInputMessage(getDealer(), PICK_TRUMP, getTimeout(true)));
timeout();
} else {
this.trumpSuit = trumpSuit;
transition();
}
}
@Override
public void onTimeout() {
Card.Suit[] suits = new Card.Suit[] {Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW};
this.trumpSuit = suits[(int)(Math.random() * suits.length)];
transition();
}
@Override
public void onExit() {
if (trumpSuit != null) {
getGame().notify(new TrumpMessage(getTrumpCard(), trumpSuit));
}
}
@Override
public void onMessage(UUID player, PlayerMessage message) {
if (getDealer().equals(player) && message instanceof PickTrumpMessage trumpMessage) {
checkTrumpSuit(trumpMessage.getTrumpSuit());
this.trumpSuit = trumpMessage.getTrumpSuit();
transition();
} else {
super.onMessage(player, message);
}
}
@VisibleForTesting
void checkTrumpSuit(Card.Suit suit) {
Card.Suit[] suits = new Card.Suit[] {Card.Suit.BLUE, Card.Suit.GREEN, Card.Suit.RED, Card.Suit.YELLOW};
for (Card.Suit s : suits) {
if (s == suit) return;
}
throw new IllegalArgumentException("Trump suit must be one of " + Arrays.toString(suits) + ".");
}
private void transition() {
if (trumpSuit == null) throw new AssertionError();
transition(new Predicting(
getGame(),
getData().withTrumpSuit(trumpSuit)
.withPredictions(Collections.emptyMap())
.withCurrentPlayer(getDealer())
.withNextPlayer()
));
}
}

@ -0,0 +1,63 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.game.Finishing;
import eu.jonahbauer.wizard.core.messages.observer.ScoreMessage;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public final class FinishingRound extends RoundState {
private transient Map<UUID, Integer> points;
public FinishingRound(Game game, GameData data) {
super(game, data.requireAllPredictions().requireTricks().requireScore());
int tricks = data.getTricks().values().stream().mapToInt(i -> i).sum();
if (tricks != data.getRound() + 1) {
throw new AssertionError("Unexpected number of tricks in round " + data.getRound() + ": " + tricks + ".");
}
}
@Override
public void onEnter() {
points = getPoints();
var score = new HashMap<>(getScore());
points.forEach((uuid, p) -> score.compute(uuid, (u, total) -> total == null ? p : total + p));
GameData data = getData().withScore(score);
if (60 / getPlayerCount() == getRound() + 1) {
transition(new Finishing(getGame(), data));
} else {
transition(new StartingRound(getGame(), data.withNextRound()));
}
}
@Override
public void onExit() {
if (points != null) {
getGame().notify(new ScoreMessage(points));
}
}
@VisibleForTesting
Map<UUID, Integer> getPoints() {
var points = new HashMap<UUID, Integer>();
for (UUID player : getPlayers()) {
int prediction = getPredictions().get(player);
int tricks = getTricks().getOrDefault(player, 0);
if (tricks == prediction) {
points.put(player, 20 + 10 * tricks);
} else {
points.put(player, -10 * Math.abs(tricks - prediction));
}
}
return points;
}
}

@ -0,0 +1,103 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
import eu.jonahbauer.wizard.core.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.messages.player.PredictMessage;
import lombok.Getter;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.Collections;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.MAKE_PREDICTION;
@Getter
public final class Predicting extends RoundState {
private transient Integer prediction;
public Predicting(Game game, GameData data) {
super(game, data.requireAllHands().requirePredictions().requireTrumpSuit().requireCurrentPlayer());
}
@Override
public void onEnter() {
getGame().notify(new UserInputMessage(getCurrentPlayer(), MAKE_PREDICTION, getTimeout(true)));
timeout();
}
@Override
public void onTimeout() {
try {
checkPrediction(0);
this.prediction = 0;
transition();
} catch (IllegalArgumentException e) {
try {
checkPrediction(1);
this.prediction = 1;
transition();
} catch (IllegalArgumentException e2) {
throw new AssertionError(e2);
}
}
}
@Override
public void onExit() {
if (prediction != null) {
getGame().notify(new PredictionMessage(getCurrentPlayer(), prediction));
}
}
@Override
public void onMessage(UUID player, PlayerMessage message) {
if (getCurrentPlayer().equals(player) && message instanceof PredictMessage predictMessage) {
checkPrediction(predictMessage.getPrediction());
this.prediction = predictMessage.getPrediction();
transition();
} else {
super.onMessage(player, message);
}
}
@VisibleForTesting
void checkPrediction(int prediction) {
if (prediction < 0 || prediction > getRound() + 1) {
throw new IllegalArgumentException("Prediction must be between 0 and " + (getRound() + 1) + ".");
}
if (!getGame().getConfig().allowExactPredictions() && isLastPlayer()) {
int sum = getPredictions().values().stream().mapToInt(i -> i).sum();
if (sum + prediction == getRound() + 1) {
throw new IllegalArgumentException("Predictions must not add up.");
}
}
}
private void transition() {
if (prediction == null) throw new AssertionError();
GameData data = getData().withPrediction(getCurrentPlayer(), prediction)
.withNextPlayer();
if (isLastPlayer()) {
transition(new StartingTrick(
getGame(),
data.withTrick(0).withTricks(Collections.emptyMap())
));
} else {
transition(new Predicting(
getGame(),
data
));
}
}
private boolean isLastPlayer() {
return getDealer().equals(getCurrentPlayer());
}
}

@ -0,0 +1,17 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.GameState;
import java.util.UUID;
public abstract class RoundState extends GameState {
public RoundState(Game game, GameData data) {
super(game, data.requireRound().requireScore());
}
protected UUID getDealer() {
return getPlayer(getRound() % getPlayerCount());
}
}

@ -0,0 +1,15 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
public final class StartingRound extends RoundState {
public StartingRound(Game game, GameData data) {
super(game, data);
}
@Override
public void onEnter() {
transition(new Dealing(getGame(), getData()));
}
}

@ -0,0 +1,89 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound;
import eu.jonahbauer.wizard.core.messages.observer.TrickMessage;
import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
public final class FinishingTrick extends TrickState {
private transient UUID winner;
public FinishingTrick(Game game, GameData data) {
super(game, data.requireStack());
assert assertions();
}
@Override
public void onEnter() {
this.winner = getWinner();
var tricks = new HashMap<>(getTricks());
tricks.compute(winner, (k, t) -> t == null ? 1 : t + 1);
GameData data = getData().withTricks(tricks);
if (getTrick() < getRound()) {
transition(new StartingTrick(
getGame(),
data.withCurrentPlayer(winner)
.withNextTrick()
));
} else {
transition(new FinishingRound(getGame(), data));
}
}
@Override
public void onExit() {
if (winner != null) {
getGame().notify(new TrickMessage(winner, getStack().stream().map(Pair::second).toList()));
}
}
@VisibleForTesting
UUID getWinner() {
var wizard = getStack().stream()
.filter(pair -> pair.second().getSuit() == Card.Suit.WIZARD)
.findFirst()
.orElse(null);
if (wizard != null) {
return wizard.first();
}
if (getStack().stream().allMatch(pair -> pair.second().getSuit() == Card.Suit.JESTER)) {
return getStack().get(0).first();
}
var trumpSuit = getTrumpSuit();
var suit = getTrickSuit();
return getStack().stream()
.max(
Comparator.<Pair<UUID, Card>>comparingInt(pair -> pair.second()
.getSuit() == trumpSuit ? 1 : 0)
.thenComparing(pair -> pair.second().getSuit() == suit ? 1 : 0)
.thenComparing(pair -> pair.second().getValue())
)
.orElseThrow(AssertionError::new)
.first();
}
private boolean assertions() {
if (getStack().size() != getPlayerCount()) {
throw new AssertionError("Unexpected stack size: " + getStack().size() + " (expected " + getPlayerCount() + ").");
}
int expectedSize = getRound() - getTrick();
for (List<Card> hand : getHands().values()) {
if (hand.size() != expectedSize) {
throw new AssertionError("Unexpected hand size: " + hand.size() + " (expected " + expectedSize + ").");
}
}
return true;
}
}

@ -0,0 +1,109 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.messages.observer.CardMessage;
import eu.jonahbauer.wizard.core.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.core.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.Card;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import static eu.jonahbauer.wizard.core.messages.observer.UserInputMessage.Action.PLAY_CARD;
public final class PlayingCard extends TrickState {
private transient Card card;
public PlayingCard(Game game, GameData data) {
super(game, data.requireStack());
assert assertions();
}
@Override
public void onEnter() {
getGame().notify(new UserInputMessage(getCurrentPlayer(), PLAY_CARD, getTimeout(true)));
timeout();
}
@Override
public void onMessage(UUID player, PlayerMessage message) {
if (getCurrentPlayer().equals(player) && message instanceof PlayCardMessage cardMessage) {
checkCard(cardMessage.getCard());
this.card = cardMessage.getCard();
transition();
} else {
super.onMessage(player, message);
}
}
@Override
public void onTimeout() {
var hand = getHands().get(getCurrentPlayer());
this.card = hand.stream().filter(c -> {
try {
checkCard(c);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}).findAny().orElseThrow(() -> new AssertionError("Cannot play any card."));
transition();
}
@Override
public void onExit() {
if (card != null) {
getGame().notify(new CardMessage(getCurrentPlayer(), card));
}
}
@VisibleForTesting
void checkCard(Card card) {
var hand = getHands().get(getCurrentPlayer());
if (!hand.contains(card)) {
throw new IllegalArgumentException("You do not have this card on your hand.");
}
Card.Suit suit = getTrickSuit();
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() {
if (card == null) throw new AssertionError();
GameData data = getData().withCardPlayed(getCurrentPlayer(), card);
var summary = data.getHands().values().stream()
.mapToInt(Collection::size)
.summaryStatistics();
if (summary.getMax() == summary.getMin()) {
transition(new FinishingTrick(getGame(), data));
} else {
transition(new PlayingCard(getGame(), data.withNextPlayer()));
}
}
private boolean assertions() {
int expectedSize = getRound() - getTrick();
for (List<Card> hand : getHands().values()) {
if (hand.size() != expectedSize && hand.size() != expectedSize + 1) {
throw new AssertionError("Unexpected hand size: " + hand.size() + " (expected " + expectedSize + " or " + (expectedSize + 1) + ").");
}
}
int size = getHands().get(getCurrentPlayer()).size();
if (size != expectedSize + 1) {
throw new AssertionError("Unexpected current player's hand size: " + size + " (expected " + (expectedSize + 1) + ")");
}
return true;
}
}

@ -0,0 +1,20 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import java.util.Collections;
public final class StartingTrick extends TrickState {
public StartingTrick(Game game, GameData data) {
super(game, data);
}
@Override
public void onEnter() {
transition(new PlayingCard(
getGame(),
getData().withStack(Collections.emptyList())
));
}
}

@ -0,0 +1,32 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameData;
import eu.jonahbauer.wizard.core.machine.states.round.RoundState;
import eu.jonahbauer.wizard.core.model.Card;
public abstract class TrickState extends RoundState {
public TrickState(Game game, GameData data) {
super(
game,
data.requireAllHands()
.requireAllPredictions()
.requireTrumpSuit()
.requireTrick()
.requireTricks()
.requireCurrentPlayer()
);
}
protected Card.Suit getTrickSuit() {
for (var pair : getStack()) {
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;
}
}

@ -0,0 +1,12 @@
package eu.jonahbauer.wizard.core.messages;
import eu.jonahbauer.wizard.core.messages.observer.ObserverMessage;
import java.util.UUID;
public interface Observer {
default void notify(ObserverMessage message) {
notify(null, message);
}
void notify(UUID player, ObserverMessage message);
}

@ -0,0 +1,24 @@
package eu.jonahbauer.wizard.core.messages.observer;
import eu.jonahbauer.wizard.core.model.Card;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* A {@link CardMessage} is sent whenever a player plays a card.
*/
@Getter
@RequiredArgsConstructor
public final class CardMessage extends ObserverMessage {
/**
* The UUID of the player.
*/
private final @NotNull UUID player;
/**
* The card played.
*/
private final @NotNull Card card;
}

@ -0,0 +1,29 @@
package eu.jonahbauer.wizard.core.messages.observer;
import eu.jonahbauer.wizard.core.model.Card;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.UUID;
/**
* A {@link HandMessage} is sent when the player receives information about hit own or another player's hand cards.
*/
@Getter
public final class HandMessage extends ObserverMessage {
/**
* The UUID of player whose hand cards are sent.
*/
private final @NotNull UUID player;
/**
* A list of all the hand cards. May consist only of {@link Card#HIDDEN} if the cars are not visible to the player
* receiving this message
*/
private final @NotNull List<@NotNull Card> hand;
public HandMessage(@NotNull UUID player, @NotNull List<@NotNull Card> hand) {
this.player = player;
this.hand = List.copyOf(hand);
}
}

@ -0,0 +1,16 @@
package eu.jonahbauer.wizard.core.messages.observer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import eu.jonahbauer.wizard.core.util.SealedClassTypeAdapterFactory;
public abstract sealed class ObserverMessage permits CardMessage, HandMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message"))
.create();
@Override
public String toString() {
return GSON.toJson(this, ObserverMessage.class);
}
}

@ -0,0 +1,24 @@
package eu.jonahbauer.wizard.core.messages.observer;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Range;
import java.util.UUID;
/**
* A {@link PredictionMessage} is sent after a player makes (or changes) his prediction.
*/
@Getter
@RequiredArgsConstructor
public final class PredictionMessage extends ObserverMessage {
/**
* The UUID of the player who made a prediction.
*/
private final @NotNull UUID player;
/**
* The prediction.
*/
private final @Range(from = 0, to = Integer.MAX_VALUE) int prediction;
}

@ -0,0 +1,23 @@
package eu.jonahbauer.wizard.core.messages.observer;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
/**
* A {@link ScoreMessage} is sent at the end of each round and at the end of the game. It contains the number of points
* gained by each player in this round or the final result.
*/
@Getter
public final class ScoreMessage extends ObserverMessage {
/**
* The number of points for each player.
*/
private final @NotNull Map<@NotNull UUID, @NotNull Integer> points;
public ScoreMessage(@NotNull Map<@NotNull UUID, @NotNull Integer> points) {
this.points = Map.copyOf(points);
}
}

@ -0,0 +1,26 @@
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);
}
}
}

@ -0,0 +1,28 @@
package eu.jonahbauer.wizard.core.messages.observer;
import eu.jonahbauer.wizard.core.model.Card;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
/**
* A {@link TrickMessage} is sent when a trick is completed. It contains the player who won the trick and a list of the
* cards played.
*/
@Getter
public final class TrickMessage extends ObserverMessage {
/**
* The UUID of the player who won the trick.
*/
private final UUID player;
/**
* The cards played.
*/
private final List<Card> cards;
public TrickMessage(UUID player, List<Card> cards) {
this.player = player;
this.cards = List.copyOf(cards);
}
}

@ -0,0 +1,29 @@
package eu.jonahbauer.wizard.core.messages.observer;
import eu.jonahbauer.wizard.core.model.Card;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
/**
* A {@link TrumpMessage} is sent when the trump suit of the current round is (being) determined.
*/
@Getter
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
* were left.
*/
private final @Nullable Card card;
/**
* The trump suit or {@code null} is the dealer has yet to decide.
*/
private final @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");
this.card = card;
this.suit = suit;
}
}

@ -0,0 +1,47 @@
package 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 lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.UUID;
/**
* A {@link UserInputMessage} is sent when user input is required.
*/
@Getter
@RequiredArgsConstructor
public final class UserInputMessage extends ObserverMessage {
/**
* The UUID of the player whose input is required.
*/
private final UUID player;
/**
* The type of input that is required.
*/
private final Action action;
/**
* A timeout in {@link System#currentTimeMillis() UNIX time} after which a default action is taken.
*/
private final long timeout;
public enum Action {
/**
* An action that indicates that a player should make a prediction. A {@link UserInputMessage} with this
* {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}.
*/
MAKE_PREDICTION,
/**
* 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}.
*/
PLAY_CARD,
/**
* An action that indicates that a player should pick a trump suit. A {@link UserInputMessage} with this
* {@link UserInputMessage#getAction()} should be responded to with a {@link PickTrumpMessage}.
*/
PICK_TRUMP
}
}

@ -0,0 +1,11 @@
package eu.jonahbauer.wizard.core.messages.player;
import eu.jonahbauer.wizard.core.model.Card;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public final class PickTrumpMessage extends PlayerMessage {
private final Card.Suit trumpSuit;
}

@ -0,0 +1,11 @@
package eu.jonahbauer.wizard.core.messages.player;
import eu.jonahbauer.wizard.core.model.Card;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public final class PlayCardMessage extends PlayerMessage {
private final Card card;
}

@ -0,0 +1,16 @@
package eu.jonahbauer.wizard.core.messages.player;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import eu.jonahbauer.wizard.core.util.SealedClassTypeAdapterFactory;
public abstract sealed class PlayerMessage permits PickTrumpMessage, PlayCardMessage, PredictMessage {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message"))
.create();
@Override
public String toString() {
return GSON.toJson(this);
}
}

@ -0,0 +1,10 @@
package eu.jonahbauer.wizard.core.messages.player;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public final class PredictMessage extends PlayerMessage {
private final int prediction;
}

@ -0,0 +1,98 @@
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;
}
}
}

@ -0,0 +1,26 @@
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
);
}

@ -0,0 +1,18 @@
package eu.jonahbauer.wizard.core.model;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.With;
import lombok.experimental.Accessors;
import java.util.Set;
@Value
@Accessors(fluent = true)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public class Configuration {
Set<Card> cards;
boolean allowExactPredictions;
@With long timeout;
}

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

@ -0,0 +1,45 @@
package eu.jonahbauer.wizard.core.model;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
public final class Deck {
private final ArrayList<Card> cards = new ArrayList<>();
private int next = 0;
public Deck(Collection<Card> cards) {
this.cards.addAll(cards);
}
public void shuffle() {
Collections.shuffle(cards);
next = 0;
}
@Unmodifiable
public List<Card> draw(int n) {
if (remaining() < n) {
throw new NoSuchElementException("Not enough cards left.");
}
return List.copyOf(cards.subList(next, next += n));
}
@Nullable
public Card draw() {
if (next >= cards.size()) {
return null;
}
return cards.get(next++);
}
public int size() {
return this.cards.size();
}
public int remaining() {
return size() - next;
}
}

@ -0,0 +1,7 @@
package eu.jonahbauer.wizard.core.util;
public record Pair<F,S>(F first, S second) {
public static <F,S> Pair<F,S> of(F first, S second) {
return new Pair<>(first, second);
}
}

@ -0,0 +1,44 @@
package eu.jonahbauer.wizard.core.util;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
public final class SealedClassTypeAdapterFactory<T> implements TypeAdapterFactory {
private final RuntimeTypeAdapterFactory<T> factory;
public static <T> SealedClassTypeAdapterFactory<T> of(Class<T> clazz) {
return new SealedClassTypeAdapterFactory<>(clazz, null);
}
public static <T> SealedClassTypeAdapterFactory<T> of(Class<T> clazz, @Nullable String suffix) {
return new SealedClassTypeAdapterFactory<>(clazz, suffix);
}
private SealedClassTypeAdapterFactory(Class<T> clazz, @Nullable String suffix) {
factory = RuntimeTypeAdapterFactory.of(clazz);
for (Class<?> subclass : clazz.getPermittedSubclasses()) {
String name = subclass.getSimpleName();
// remove suffix
if (suffix != null) {
if (name.endsWith(suffix)) name = name.substring(0, name.length() - suffix.length());
}
// transform camelCast to snake_case
name = name.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT);
factory.registerSubtype(subclass.asSubclass(clazz), name);
}
}
@Override
public <S> TypeAdapter<S> create(Gson gson, TypeToken<S> type) {
return factory.create(gson, type);
}
}

@ -0,0 +1,24 @@
package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.core.model.Configurations;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
public class GameTest {
@Test
public void run() throws InterruptedException, ExecutionException {
Game game = new Game(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();
}
}

@ -0,0 +1,50 @@
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 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.getPredictions()).thenReturn(predictions);
when(state.getTricks()).thenReturn(tricks);
when(state.getPlayers()).thenReturn(players);
assertEquals(Map.of(
player0, 20,
player1, 50,
player2, -20
), state.getPoints());
}
}

@ -0,0 +1,66 @@
package eu.jonahbauer.wizard.core.machine.states.round;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.model.Configuration;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
public class PredictingTest {
@Test
public void checkPrediction_ThrowsIllegalArgument_IfOutOfBounds() {
Map<UUID, Integer> predictions = Map.of();
Configuration config = mock(Configuration.class);
when(config.allowExactPredictions()).thenReturn(true);
Game game = mock(Game.class);
when(game.getConfig()).thenReturn(config);
Predicting state = mock(Predicting.class);
doCallRealMethod().when(state).checkPrediction(anyInt());
when(state.getGame()).thenReturn(game);
when(state.getPredictions()).thenReturn(predictions);
when(state.getDealer()).thenReturn(new UUID(0,0));
when(state.getCurrentPlayer()).thenReturn(new UUID(0,1));
when(state.getRound()).thenReturn(10);
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(-1));
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(12));
for (int i = 0; i < 12; i++) {
state.checkPrediction(i);
}
}
@Test
public void checkPrediction_ThrowsIllegalArgument_IfAddsUp() {
UUID player = new UUID(0,0);
Map<UUID, Integer> predictions = Map.of(new UUID(0,1), 10);
Configuration config = mock(Configuration.class);
when(config.allowExactPredictions()).thenReturn(false);
Game game = mock(Game.class);
when(game.getConfig()).thenReturn(config);
Predicting state = mock(Predicting.class);
doCallRealMethod().when(state).checkPrediction(anyInt());
when(state.getGame()).thenReturn(game);
when(state.getPredictions()).thenReturn(predictions);
when(state.getDealer()).thenReturn(player);
when(state.getCurrentPlayer()).thenReturn(player);
when(state.getRound()).thenReturn(10);
assertThrows(IllegalArgumentException.class, () -> state.checkPrediction(1));
for (int i = 0; i < 12; i++) {
if (i == 1) continue;
state.checkPrediction(i);
}
}
}

@ -0,0 +1,90 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.states.trick.FinishingTrick;
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 org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FinishingTrickTest {
@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)
);
FinishingTrick state = mock(FinishingTrick.class);
when(state.getWinner()).thenCallRealMethod();
when(state.getStack()).thenReturn(stack);
when(state.getTrickSuit()).thenReturn(Card.Suit.RED);
when(state.getTrumpSuit()).thenReturn(Card.Suit.YELLOW);
assertNotNull(state.getWinner());
}
@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)
);
FinishingTrick state = mock(FinishingTrick.class);
when(state.getWinner()).thenCallRealMethod();
when(state.getStack()).thenReturn(stack);
when(state.getTrickSuit()).thenReturn(Card.Suit.RED);
when(state.getTrumpSuit()).thenReturn(Card.Suit.YELLOW);
assertNotNull(state.getWinner());
}
@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)
);
FinishingTrick state = mock(FinishingTrick.class);
when(state.getWinner()).thenCallRealMethod();
when(state.getStack()).thenReturn(stack);
when(state.getTrickSuit()).thenReturn(Card.Suit.RED);
when(state.getTrumpSuit()).thenReturn(Card.Suit.BLUE);
assertNotNull(state.getWinner());
}
@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)
);
FinishingTrick state = mock(FinishingTrick.class);
when(state.getWinner()).thenCallRealMethod();
when(state.getStack()).thenReturn(stack);
when(state.getTrickSuit()).thenReturn(Card.Suit.NONE);
when(state.getTrumpSuit()).thenReturn(Card.Suit.NONE);
assertNotNull(state.getWinner());
}
}

@ -0,0 +1,81 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.states.trick.PlayingCard;
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 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.getHands()).thenReturn(Map.of(player, hand));
when(state.getStack()).thenReturn(List.of());
when(state.getCurrentPlayer()).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.getHands()).thenReturn(Map.of(player, hand));
when(state.getCurrentPlayer()).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);
PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).onMessage(any(), any());
when(state.getCurrentPlayer()).thenReturn(player);
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PlayCardMessage(Card.BLUE_WIZARD)));
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PredictMessage(1)));
assertThrows(IllegalStateException.class, () -> state.onMessage(player2, new PickTrumpMessage(Card.Suit.BLUE)));
}
@Test
public void onMessage_ThrowsIllegalState_IfNotPlayCard() {
UUID player = new UUID(0, 0);
PlayingCard state = mock(PlayingCard.class);
doCallRealMethod().when(state).onMessage(any(), any());
when(state.getCurrentPlayer()).thenReturn(player);
assertThrows(IllegalStateException.class, () -> state.onMessage(player, new PredictMessage(1)));
assertThrows(IllegalStateException.class, () -> state.onMessage(player, new PickTrumpMessage(Card.Suit.BLUE)));
}
}

@ -0,0 +1,90 @@
package eu.jonahbauer.wizard.core.machine.states.trick;
import eu.jonahbauer.wizard.core.machine.states.trick.TrickState;
import eu.jonahbauer.wizard.core.model.Card;
import eu.jonahbauer.wizard.core.util.Pair;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.UUID;
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.getStack()).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.getStack()).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.getStack()).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.getStack()).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.getStack()).thenReturn(stack2);
assertEquals(trickState2.getTrickSuit(), Card.Suit.YELLOW);
}
}
Loading…
Cancel
Save