Issue #5: Grundspiel implementieren
parent
c7c7b12ee1
commit
ee6b7d7c2d
@ -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…
Reference in New Issue