Fehler in Spiellogik von Jongleur und Wolke behoben (#12)
This commit is contained in:
@@ -12,26 +12,28 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Unmodifiable
|
||||
@EqualsAndHashCode(of = {"values", "present"})
|
||||
public final class GameData {
|
||||
private static final int SIZE = 11;
|
||||
private static final int SIZE = 12;
|
||||
|
||||
public static final GameData EMPTY = new GameData();
|
||||
|
||||
public static final Key<List<UUID>> PLAYERS = new Key<>("players", 0);
|
||||
public static final Key<Map<UUID, Integer>> SCORE = new Key<>("score", 1);
|
||||
public static final Key<Integer> ROUND = new Key<>("round", 2);
|
||||
public static final Key<Map<UUID, List<Card>>> HANDS = new Key<>("hands", 3);
|
||||
public static final Key<Card> TRUMP_CARD = new Key<>("trumpCard", 4);
|
||||
public static final Key<Card.Suit> TRUMP_SUIT = new Key<>("trumpSuit", 5);
|
||||
public static final Key<Map<UUID, Integer>> PREDICTIONS = new Key<>("predictions", 6);
|
||||
public static final Key<Map<UUID, Integer>> TRICKS = new Key<>("tricks", 7);
|
||||
public static final Key<Integer> TRICK = new Key<>("trick", 8);
|
||||
public static final Key<List<Pair<UUID, Card>>> STACK = new Key<>("stack", 9);
|
||||
public static final Key<UUID> CURRENT_PLAYER = new Key<>("currentPlayer", 10);
|
||||
|
||||
public static final Key<List<UUID>> PLAYERS = new Key<>("players", 0, null);
|
||||
public static final Key<Map<UUID, Integer>> SCORE = new Key<>("score", 1, Map::of);
|
||||
public static final Key<Integer> ROUND = new Key<>("round", 2, () -> 0);
|
||||
public static final Key<Map<UUID, List<Card>>> HANDS = new Key<>("hands", 3, null);
|
||||
public static final Key<Card> TRUMP_CARD = new Key<>("trumpCard", 4, null);
|
||||
public static final Key<Card.Suit> TRUMP_SUIT = new Key<>("trumpSuit", 5, null);
|
||||
public static final Key<Map<UUID, Integer>> PREDICTIONS = new Key<>("predictions", 6, Map::of);
|
||||
public static final Key<Map<UUID, Integer>> TRICKS = new Key<>("tricks", 7, Map::of);
|
||||
public static final Key<Integer> TRICK = new Key<>("trick", 8, () -> 0);
|
||||
public static final Key<List<Pair<UUID, Card>>> STACK = new Key<>("stack", 9, List::of);
|
||||
public static final Key<UUID> CURRENT_PLAYER = new Key<>("currentPlayer", 10, null);
|
||||
public static final Key<UUID> CLOUDED_PLAYER = new Key<>("cloudedPlayer", 11, null);
|
||||
|
||||
private final Object[] values;
|
||||
private final boolean[] present;
|
||||
private transient final boolean[] required = new boolean[SIZE];
|
||||
@@ -47,18 +49,35 @@ public final class GameData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped or {@code null} if this map contains no mapping for the
|
||||
* key.
|
||||
* Returns the value to which the specified key is mapped, the default value for the specified key, or throws
|
||||
* a {@link NoSuchElementException} if this map neither contains a mapping for the key nor does the key have a
|
||||
* default value.
|
||||
* @param key the key whose associated value is to be returned
|
||||
* @param <T> the value type
|
||||
* @return the value to which the specified key is mapped, or null if this map contains no mapping for the key
|
||||
*/
|
||||
public <T> T get(@NotNull Key<T> key) {
|
||||
int index = key.index();
|
||||
if (!present[index]) throw new NoSuchElementException();
|
||||
if (present[index]) {
|
||||
//noinspection unchecked
|
||||
return (T) values[index];
|
||||
} else if (key.defaultValue() != null) {
|
||||
present[index] = true;
|
||||
values[index] = key.defaultValue().get();
|
||||
//noinspection unchecked
|
||||
return (T) values[index];
|
||||
} else {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection unchecked
|
||||
return (T) values[index];
|
||||
/**
|
||||
* Returns {@code true} if this map contains a mapping for the specified key.
|
||||
* @param key key whose presence in this map is to be tested
|
||||
* @return {@code true} if this map contains a mapping for the specified key
|
||||
*/
|
||||
public boolean has(@NotNull Key<?> key) {
|
||||
return present[key.index()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,13 +191,21 @@ public final class GameData {
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public GameData require(@NotNull Key<?> key) {
|
||||
if (!present[key.index()]) {
|
||||
throw new NoSuchElementException("Could not find required value '" + key + "'.");
|
||||
if (!present[key.index()] && key.defaultValue() == null) {
|
||||
throw new InvalidDataException("Could not find required value '" + key + "'.");
|
||||
}
|
||||
required[key.index()] = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #clean()
|
||||
*/
|
||||
public GameData keep(@NotNull Key<?> key) {
|
||||
required[key.index()] = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code this} if this map contains a mapping for each of the specified keys or throws a
|
||||
* {@link NoSuchElementException} otherwise.
|
||||
@@ -213,13 +240,13 @@ public final class GameData {
|
||||
var mapValue = get(map);
|
||||
var listValue = get(list);
|
||||
for (K k : listValue) {
|
||||
if (!mapValue.containsKey(k)) throw new NoSuchElementException("Could not find required value: " + map.name() + "[" + k + "].");
|
||||
if (!mapValue.containsKey(k)) throw new InvalidDataException("Could not find required value: " + map.name() + "[" + k + "].");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retains only the mappings that have been required since object creation.
|
||||
* Retains only the mappings that have been required or kept since object creation.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public GameData clean() {
|
||||
@@ -246,6 +273,7 @@ public final class GameData {
|
||||
public static class Key<T> {
|
||||
String name;
|
||||
int index;
|
||||
Supplier<T> defaultValue;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@@ -1,14 +1,10 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.game;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameState;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.StartingRound;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class Starting extends GameState {
|
||||
|
||||
public Starting(GameData data) {
|
||||
@@ -17,9 +13,6 @@ public final class Starting extends GameState {
|
||||
|
||||
@Override
|
||||
public void onEnter(Game game) {
|
||||
transition(game, new StartingRound(getData().with(
|
||||
ROUND, 0,
|
||||
SCORE, Map.of()
|
||||
)));
|
||||
transition(game, new StartingRound(getData()));
|
||||
}
|
||||
}
|
||||
|
@@ -1,37 +1,38 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
package eu.jonahbauer.wizard.core.machine.states.round;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound;
|
||||
import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.CHANGE_PREDICTION;
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*;
|
||||
|
||||
public final class ChangingPrediction extends TrickState {
|
||||
public final class ChangingPrediction extends RoundState {
|
||||
private transient final int oldPrediction;
|
||||
|
||||
public ChangingPrediction(GameData data) {
|
||||
super(data);
|
||||
oldPrediction = get(PREDICTIONS).get(get(CURRENT_PLAYER));
|
||||
super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE, CLOUDED_PLAYER));
|
||||
checkData(data);
|
||||
oldPrediction = get(PREDICTIONS).get(get(CLOUDED_PLAYER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter(Game game) {
|
||||
game.notify(new UserInputMessage(get(CURRENT_PLAYER), CHANGE_PREDICTION, getTimeout(game, true)));
|
||||
game.notify(new UserInputMessage(get(CLOUDED_PLAYER), CHANGE_PREDICTION, getTimeout(game, true)));
|
||||
timeout(game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Game game, UUID player, PlayerMessage message) {
|
||||
if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
|
||||
if (get(CLOUDED_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
|
||||
checkPrediction(predictMessage.getPrediction());
|
||||
transition(game, predictMessage.getPrediction());
|
||||
} else {
|
||||
@@ -53,21 +54,22 @@ public final class ChangingPrediction extends TrickState {
|
||||
}
|
||||
|
||||
private void transition(Game game, int prediction) {
|
||||
game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction));
|
||||
game.notify(new PredictionMessage(get(CLOUDED_PLAYER), prediction));
|
||||
|
||||
// add prediction
|
||||
var predictions = new HashMap<>(get(PREDICTIONS));
|
||||
predictions.put(get(CURRENT_PLAYER), prediction);
|
||||
predictions.put(get(CLOUDED_PLAYER), prediction);
|
||||
|
||||
GameData data = getData().with(
|
||||
PREDICTIONS, Map.copyOf(predictions)
|
||||
);
|
||||
|
||||
boolean hasNextTrick = get(TRICK) < get(ROUND);
|
||||
if (hasNextTrick) {
|
||||
transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1)));
|
||||
} else {
|
||||
transition(game, new FinishingRound(data));
|
||||
transition(game, new FinishingRound(data));
|
||||
}
|
||||
|
||||
private void checkData(GameData data) {
|
||||
if (data.get(CLOUDED_PLAYER) == null) {
|
||||
throw new InvalidDataException("Clouded player is null.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -80,7 +80,6 @@ public final class DeterminingTrump extends RoundState {
|
||||
private void transition(Game game, @NotNull Card.Suit trumpSuit) {
|
||||
GameData data = getData().with(
|
||||
TRUMP_SUIT, trumpSuit,
|
||||
PREDICTIONS, Map.of(),
|
||||
CURRENT_PLAYER, getNextPlayer(getDealer())
|
||||
);
|
||||
|
||||
|
@@ -80,10 +80,6 @@ public final class Predicting extends RoundState {
|
||||
);
|
||||
|
||||
if (isLastPlayer()) {
|
||||
data = data.with(
|
||||
TRICK, 0,
|
||||
TRICKS, Map.of()
|
||||
);
|
||||
transition(game, new StartingTrick(data));
|
||||
} else {
|
||||
transition(game, new Predicting(data));
|
||||
|
@@ -4,9 +4,8 @@ import eu.jonahbauer.wizard.common.model.card.Card;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.ChangingPrediction;
|
||||
import eu.jonahbauer.wizard.core.machine.states.round.FinishingRound;
|
||||
import eu.jonahbauer.wizard.common.messages.observer.HandMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.observer.JugglingMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.observer.TrickMessage;
|
||||
import eu.jonahbauer.wizard.core.model.card.*;
|
||||
import eu.jonahbauer.wizard.core.util.Pair;
|
||||
@@ -43,32 +42,30 @@ public final class FinishingTrick extends TrickState {
|
||||
|| cards.contains(Card.CLOUD_YELLOW);
|
||||
boolean hasNextTrick = get(TRICK) < get(ROUND);
|
||||
|
||||
// juggle hands
|
||||
if (juggler && hasNextTrick) {
|
||||
game.notify(new JugglingMessage());
|
||||
var hands = get(HANDS);
|
||||
Map<UUID, List<Card>> juggledHands = new HashMap<>();
|
||||
hands.forEach((player, hand) -> juggledHands.put(getNextPlayer(player), hand));
|
||||
data = data.with(HANDS, Map.copyOf(juggledHands));
|
||||
juggledHands.forEach((player, hand) -> game.notify(player, new HandMessage(player, hand)));
|
||||
}
|
||||
|
||||
// trick is not counted when a bomb is present
|
||||
if (!bomb) {
|
||||
// trick is not counted when a bomb is present
|
||||
var tricks = new HashMap<>(get(TRICKS));
|
||||
tricks.merge(winner, 1, Integer::sum);
|
||||
data = data.with(TRICKS, Map.copyOf(tricks));
|
||||
|
||||
// mark "clouded player"
|
||||
if (cloud) {
|
||||
data = data.with(CLOUDED_PLAYER, winner);
|
||||
}
|
||||
}
|
||||
|
||||
data = data.with(CURRENT_PLAYER, winner);
|
||||
|
||||
if (cloud && !bomb) {
|
||||
// adjust prediction
|
||||
transition(game, new ChangingPrediction(data));
|
||||
} else if (hasNextTrick) {
|
||||
transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1)));
|
||||
if (!hasNextTrick) {
|
||||
if (data.has(CLOUDED_PLAYER)) {
|
||||
transition(game, new ChangingPrediction(data));
|
||||
} else {
|
||||
transition(game, new FinishingRound(data));
|
||||
}
|
||||
} else if (juggler) {
|
||||
transition(game, new Juggling(data));
|
||||
} else {
|
||||
transition(game, new FinishingRound(data));
|
||||
transition(game, new StartingTrick(data.with(TRICK, get(TRICK) + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,82 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.common.messages.observer.HandMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.player.JuggleMessage;
|
||||
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
|
||||
import eu.jonahbauer.wizard.common.model.card.Card;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*;
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
|
||||
public final class Juggling extends TrickState {
|
||||
private final transient Map<UUID, Card> juggledCards = new ConcurrentHashMap<>();
|
||||
|
||||
public Juggling(GameData data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnter(Game game) {
|
||||
game.notify(new UserInputMessage(null, JUGGLE_CARD, getTimeout(game, true)));
|
||||
timeout(game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(Game game) {
|
||||
for (UUID player : get(PLAYERS)) {
|
||||
juggledCards.computeIfAbsent(player, p -> {
|
||||
var hand = get(HANDS).get(p);
|
||||
return hand.get(game.getRandom().nextInt(hand.size()));
|
||||
});
|
||||
}
|
||||
juggle(game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Game game, UUID player, PlayerMessage message) {
|
||||
if (get(PLAYERS).contains(player) && message instanceof JuggleMessage juggleMessage) {
|
||||
Card card = juggleMessage.getCard();
|
||||
|
||||
if (!get(HANDS).get(player).contains(card)) {
|
||||
throw new IllegalArgumentException("You do not have this card on your hand.");
|
||||
}
|
||||
|
||||
juggledCards.put(player, card);
|
||||
if (juggledCards.size() == get(PLAYERS).size()) {
|
||||
juggle(game);
|
||||
}
|
||||
} else {
|
||||
super.onMessage(game, player, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void juggle(Game game) {
|
||||
Map<UUID, Card> newCards = new HashMap<>();
|
||||
juggledCards.forEach((player, card) -> newCards.put(getNextPlayer(player), card));
|
||||
|
||||
var mutableHands = new HashMap<>(get(HANDS));
|
||||
|
||||
for (UUID player : get(PLAYERS)) {
|
||||
var mutableHand = new ArrayList<>(mutableHands.get(player));
|
||||
var oldCard = juggledCards.get(player);
|
||||
var newCard = newCards.get(player);
|
||||
mutableHand.set(mutableHand.indexOf(oldCard), newCard);
|
||||
var immutableHand = List.copyOf(mutableHand);
|
||||
mutableHands.put(player, immutableHand);
|
||||
game.notify(player, new HandMessage(player, immutableHand));
|
||||
}
|
||||
|
||||
var data = getData().with(
|
||||
HANDS, Map.copyOf(mutableHands),
|
||||
TRICK, get(TRICK) + 1
|
||||
);
|
||||
|
||||
transition(game, new StartingTrick(data));
|
||||
}
|
||||
}
|
@@ -1,11 +1,7 @@
|
||||
package eu.jonahbauer.wizard.core.machine.states.trick;
|
||||
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
import eu.jonahbauer.wizard.core.machine.Game;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
|
||||
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||
|
||||
public final class StartingTrick extends TrickState {
|
||||
public StartingTrick(GameData data) {
|
||||
@@ -14,6 +10,6 @@ public final class StartingTrick extends TrickState {
|
||||
|
||||
@Override
|
||||
public void onEnter(Game game) {
|
||||
transition(game, new PlayingCard(getData().with(STACK, List.of())));
|
||||
transition(game, new PlayingCard(getData()));
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ public abstract class TrickState extends RoundState {
|
||||
data.requireEach(PLAYERS, HANDS)
|
||||
.requireEach(PLAYERS, PREDICTIONS)
|
||||
.require(TRUMP_SUIT, TRICK, TRICKS, CURRENT_PLAYER)
|
||||
.keep(CLOUDED_PLAYER)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user