bugfixes and improvements

* fixed inconsistent player order
* minor visual adjustments
* refactoring
main
Jonah Bauer 3 years ago
parent 851c3a3451
commit 2484dec68a

@ -4,26 +4,29 @@ import lombok.experimental.UtilityClass;
@UtilityClass
public class AnimationTimings {
public static final float JUGGLE = 0.25f;
private static final float FACTOR = 1;
public static final float STACK_EXPAND = 0.25f;
public static final float STACK_COLLAPSE = 0.25f;
public static final float JUGGLE = 0.25f * FACTOR;
public static final float STACK_FINISH_MOVE = 0.25f;
public static final float STACK_FINISH_ROTATE = 0.1f;
public static final float STACK_FINISH_FADE = 0.5f;
public static final float STACK_HOLD = 0.5f;
public static final float STACK_EXPAND = 0.25f * FACTOR;
public static final float STACK_COLLAPSE = 0.25f * FACTOR;
public static final float PAD_OF_TRUTH_EXPAND = 0.25f;
public static final float PAD_OF_TRUTH_COLLAPSE = 0.25f;
public static final float STACK_FINISH_MOVE = 0.25f * FACTOR;
public static final float STACK_FINISH_ROTATE = 0.1f * FACTOR;
public static final float STACK_FINISH_FADE = 0.5f * FACTOR;
public static final float STACK_HOLD = 0.5f * FACTOR;
public static final float HAND_LAYOUT = 0.15f;
public static final float WEREWOLF_SWAP = 0.25f * FACTOR;
public static final float OVERLAY_HOLD = 3f;
public static final float OVERLAY_FADE = 0.1f;
public static final float PAD_OF_TRUTH_EXPAND = 0.25f * FACTOR;
public static final float PAD_OF_TRUTH_COLLAPSE = 0.25f * FACTOR;
public static final float OVERLAY_TRUMP = .3f;
public static final float HAND_LAYOUT = 0.15f * FACTOR;
public static final float MESSAGE_HOLD = 1.5f;
public static final float MESSAGE_FADE = 0.5f;
public static final float OVERLAY_HOLD = 3f * FACTOR;
public static final float OVERLAY_FADE = 0.1f * FACTOR;
public static final float OVERLAY_SHARED_ELEMENT = .3f * FACTOR;
public static final float MESSAGE_HOLD = 1.5f * FACTOR;
public static final float MESSAGE_FADE = 0.5f * FACTOR;
}

@ -112,6 +112,7 @@ public class CardActor extends Actor {
setWidth(PREF_WIDTH);
setHeight(PREF_HEIGHT);
setColor(0.8f, 0.8f, 0.8f, 1.0f);
setOrigin(PREF_WIDTH / 2, PREF_HEIGHT / 2);
}
public CardActor(Card card, TextureAtlas atlas) {

@ -41,8 +41,6 @@ public class CardsGroup extends WidgetGroup {
private CardActor dragTarget;
private float dragStartX;
@Getter
private SelectMode selectMode = SelectMode.NONE;
@Setter
@Getter
private Card selected;
@ -130,8 +128,7 @@ public class CardsGroup extends WidgetGroup {
for (var child : getChildren()) {
if (child instanceof CardActor card) {
float height = card.getHeight();
if (selectMode == SelectMode.NONE && (child == dragTarget || dragTarget == null && child == target)
|| selectMode == SelectMode.SINGLE && card.getCard() == selected) {
if ((child == dragTarget || dragTarget == null && child == target) || card.getCard() == selected) {
height += speed * delta;
} else {
height -= speed * delta;
@ -207,19 +204,8 @@ public class CardsGroup extends WidgetGroup {
}
public CardActor remove(Card card) {
// find actor
CardActor actor = null;
var children = getChildren();
for (int i = 0; i < children.size; i++) {
if (children.get(i) instanceof CardActor cardActor && cardActor.getCard() == card) {
actor = cardActor;
break;
}
}
if (actor == null) {
throw new NoSuchElementException();
}
var actor = find(card);
if (actor == null) return null;
// adjust actor
actor.setY(getY() + actor.getHeight() - getHeight());
@ -237,20 +223,22 @@ public class CardsGroup extends WidgetGroup {
return actor;
}
public CardActor find(Card card) {
var children = getChildren();
for (int i = 0; i < children.size; i++) {
if (children.get(i) instanceof CardActor cardActor && cardActor.getCard() == card) {
return cardActor;
}
}
return null;
}
@Override
public float getMinHeight() {
return 0;
}
public void setSelectMode(SelectMode selectMode) {
this.selectMode = Objects.requireNonNull(selectMode);
}
public void setOnClickListener(Consumer<CardActor> onClickListener) {
this.onClickListener = onClickListener;
}
public enum SelectMode {
SINGLE, NONE
}
}

@ -13,16 +13,20 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;
import lombok.Setter;
public class PadOfTruth extends Table {
private static final float EXTENDED_WIDTH = 636;
private static final float EXTENDED_HEIGHT = 824;
private static final float COLLAPSED_SCALE = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT;
public static final float EXTENDED_WIDTH = 636;
public static final float EXTENDED_HEIGHT = 824;
public static final float COLLAPSED_SCALE = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT;
private final Label[] names = new Label[6];
private final Label[][] predictions = new Label[20][];
private final Label[][] scores = new Label[20][];
@Setter
private boolean enabled = true;
public PadOfTruth(Skin skin, Drawable background) {
super(skin);
setTouchable(Touchable.enabled);
@ -32,7 +36,6 @@ public class PadOfTruth extends Table {
setHeight(EXTENDED_HEIGHT);
setTransform(true);
setOrigin(0, 0);
setScale(COLLAPSED_SCALE);
addListener(new InputListener() {
@ -40,6 +43,7 @@ public class PadOfTruth extends Table {
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (!enabled) return;
if (fromActor != null && isAscendantOf(fromActor)) return;
if (action != null) removeAction(action);
action = Actions.scaleTo(1, 1, AnimationTimings.PAD_OF_TRUTH_EXPAND);
@ -48,6 +52,7 @@ public class PadOfTruth extends Table {
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
if (!enabled) return;
if (toActor != null && isAscendantOf(toActor)) return;
if (action != null) removeAction(action);
action = Actions.scaleTo(COLLAPSED_SCALE, COLLAPSED_SCALE, AnimationTimings.PAD_OF_TRUTH_COLLAPSE);

@ -1,4 +1,5 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay;
public interface InteractionOverlay {
void close();
}

@ -36,9 +36,10 @@ public class MakePredictionOverlay extends Overlay implements InteractionOverlay
var listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (isClosing()) return;
for (int i = 0; i < values.length; i++) {
if (actor == buttons[i]) {
screen.send(new PredictMessage(values[i]));
screen.onPredictionMade(values[i]);
break;
}
}

@ -11,6 +11,8 @@ import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import lombok.AccessLevel;
import lombok.Getter;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
@ -23,7 +25,9 @@ public abstract class Overlay extends Action {
private Container<?> root;
private boolean started;
private boolean finished;
@Getter(AccessLevel.PROTECTED)
private boolean closing;
private boolean closed;
public Overlay(GameScreen gameScreen, long timeout) {
this.screen = gameScreen;
@ -44,11 +48,12 @@ public abstract class Overlay extends Action {
show((Group) getActor());
}
if (System.currentTimeMillis() > timeout) {
finishInternal();
if (System.currentTimeMillis() > timeout && !closing) {
closing = true;
onClosing();
}
return finished;
return closed;
}
protected abstract Actor createContent();
@ -68,21 +73,20 @@ public abstract class Overlay extends Action {
parent.addActor(getRoot());
}
protected void finishInternal() {
if (!finished) {
finished = true;
protected final void onClosed() {
if (closed) return;
closed = true;
getRoot().remove();
}
}
public void finish() {
protected void onClosing() {
getRoot().addAction(sequence(
targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)),
run(this::finishInternal)
run(this::onClosed)
));
}
public void timeout() {
public void close() {
timeout = 0;
}
}

@ -8,7 +8,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.common.model.Card;
import java.util.EnumMap;
@ -44,10 +43,11 @@ public class PickTrumpOverlay extends Overlay implements InteractionOverlay {
cardGroup.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
if (isClosing()) return;
var target = event.getTarget();
for (Card.Suit suit : Card.Suit.values()) {
if (cards.get(suit) == target) {
screen.send(new PickTrumpMessage(suit));
screen.onSuitClicked(suit);
break;
}
}

@ -10,7 +10,6 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.model.Card;
import java.util.EnumMap;
@ -51,10 +50,11 @@ public class PlayColoredCardOverlay extends Overlay implements InteractionOverla
cardGroup.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
if (isClosing()) return;
var target = event.getTarget();
for (Card.Suit suit : Card.Suit.values()) {
if (actors.get(suit) == target) {
screen.send(new PlayCardMessage(cards.get(suit)));
screen.onCardClicked(cards.get(suit));
break;
}
}
@ -68,7 +68,7 @@ public class PlayColoredCardOverlay extends Overlay implements InteractionOverla
cancel.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
finishInternal();
close();
}
});
root.addActor(cancel);

@ -1,12 +1,10 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.actions.MyActions;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
@ -38,13 +36,7 @@ public class StartRoundOverlay extends Overlay {
var root = getRoot();
root.addAction(sequence(
delay(AnimationTimings.OVERLAY_HOLD),
targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)),
run(this::finishInternal)
run(this::close)
));
}
@Override
public void finish() {
MyActions.finish(getRoot().getActions().get(0));
}
}

@ -1,7 +1,6 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game.overlay;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.actions.ParallelAction;
@ -161,7 +160,7 @@ public class TrumpOverlay extends Overlay {
cardAnimation.addAction(sequence(
targeting(trumpCardActor, removeActorSilently()),
targeting(trumpCardActor, changeParent(parent)),
targeting(trumpCardActor, moveTo(10, 10, AnimationTimings.OVERLAY_TRUMP))
targeting(trumpCardActor, moveTo(10, 10, AnimationTimings.OVERLAY_SHARED_ELEMENT))
));
}
@ -171,9 +170,9 @@ public class TrumpOverlay extends Overlay {
targeting(trumpSuitActor, changeParent(parent)),
run(trumpCardActor::toFront),
parallel(
targeting(trumpSuitActor, rotateTo(-90, AnimationTimings.OVERLAY_TRUMP)),
targeting(trumpSuitActor, rotateTo(-90, AnimationTimings.OVERLAY_SHARED_ELEMENT)),
targeting(trumpSuitActor,
moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, AnimationTimings.OVERLAY_TRUMP)
moveTo(10, 10 + (trumpSuitActor.getHeight() + trumpSuitActor.getWidth()) / 2, AnimationTimings.OVERLAY_SHARED_ELEMENT)
)
)
));
@ -186,12 +185,14 @@ public class TrumpOverlay extends Overlay {
targeting(root, alpha(0.0f, AnimationTimings.OVERLAY_FADE, Interpolation.pow2Out)),
cardAnimation
),
run(this::finishInternal)
run(this::close)
));
}
@Override
public void finish() {
MyActions.finish(getRoot().getActions().get(0));
protected void onClosing() {
var actions = getRoot().getActions();
if (actions.size > 0) MyActions.finish(actions.get(0));
onClosed();
}
}

@ -22,22 +22,17 @@ import eu.jonahbauer.wizard.client.libgdx.actors.game.CardsGroup;
import eu.jonahbauer.wizard.client.libgdx.actors.game.PadOfTruth;
import eu.jonahbauer.wizard.client.libgdx.actors.game.overlay.*;
import eu.jonahbauer.wizard.client.libgdx.state.Game;
import eu.jonahbauer.wizard.client.libgdx.util.Triple;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.ContinueMessage;
import eu.jonahbauer.wizard.common.messages.player.JuggleMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Getter;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static eu.jonahbauer.wizard.client.libgdx.actions.MyActions.*;
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*;
public class GameScreen extends MenuScreen {
@Getter
@ -46,11 +41,9 @@ public class GameScreen extends MenuScreen {
private Label.LabelStyle labelStyleDefault;
private Label.LabelStyle labelStyleActive;
private final Game state;
private final List<UUID> players;
private Triple<UUID, UserInputMessage.Action, Long> activePlayer;
private final UUID self;
private final LinkedHashMap<UUID, String> players;
private final List<UUID> orderedPlayers;
private CardsGroup handCards;
private CardStack cardStack;
@ -70,18 +63,16 @@ public class GameScreen extends MenuScreen {
private final Map<UUID, Seat> seats = new HashMap<>();
private final Map<UUID, Label> nameLabels = new HashMap<>();
private final AtomicBoolean sending = new AtomicBoolean();
private final AtomicBoolean pendingSync = new AtomicBoolean();
private boolean juggling;
private Card juggledCard;
public GameScreen(WizardGame game) {
public GameScreen(WizardGame game, @NotNull UUID self, @NotNull LinkedHashMap<UUID, String> players) {
super(game);
this.state = (Game) game.getClient().getState();
this.players = new ArrayList<>(state.getPlayers().keySet());
this.self = self;
this.players = players;
this.orderedPlayers = players.keySet().stream().toList();
}
//<editor-fold desc="Layout">
@Override
public void show() {
super.show();
@ -92,10 +83,9 @@ public class GameScreen extends MenuScreen {
labelStyleActive.fontColor = Color.RED;
seat();
prepareLabels();
handCards = new CardsGroup(Collections.emptyList(), atlas);
handCards.setOnClickListener(this::onCardClicked);
handCards.setOnClickListener(actor -> onCardClicked(actor.getCard()));
var container = new Container<>(handCards);
container.setPosition(360, 75);
container.setSize(1200, CardActor.PREF_HEIGHT);
@ -107,13 +97,13 @@ public class GameScreen extends MenuScreen {
messages.setTouchable(Touchable.disabled);
padOfTruth = new PadOfTruth(game.data.skin, new TextureRegionDrawable(atlas.findRegion(GameAtlas.PAD_OF_TRUTH)));
padOfTruth.setPosition(1910 - 636f, 10);
padOfTruth.setOrigin(636f, 0);
padOfTruth.setPosition(WizardGame.WIDTH - 10 - PadOfTruth.EXTENDED_WIDTH, 10);
padOfTruth.setOrigin(PadOfTruth.EXTENDED_WIDTH, 0);
cardStack = new CardStack();
cardStack.setHoverBounds(0.5f * WizardGame.WIDTH - 50, 0.5f * WizardGame.HEIGHT - 50, 100, 100);
setNames();
addLabels();
Gdx.input.setInputProcessor(game.data.stage);
game.data.stage.addActor(container);
@ -122,14 +112,53 @@ public class GameScreen extends MenuScreen {
game.data.stage.addActor(messages);
}
private void seat() {
var count = players.size();
Seat[] seats = switch (count) {
case 3 -> new Seat[] {Seat.BOTTOM, Seat.TOP_LEFT, Seat.TOP_RIGHT};
case 4 -> new Seat[] {Seat.BOTTOM, Seat.LEFT, Seat.TOP, Seat.RIGHT};
case 5 -> new Seat[] {Seat.BOTTOM, Seat.LEFT, Seat.TOP_LEFT, Seat.TOP_RIGHT, Seat.RIGHT};
case 6 -> new Seat[] {Seat.BOTTOM, Seat.LEFT, Seat.TOP_LEFT, Seat.TOP, Seat.TOP_RIGHT, Seat.RIGHT};
default -> throw new AssertionError();
};
int index = orderedPlayers.indexOf(self);
for (int i = 0; i < count; i++) {
var player = orderedPlayers.get((index + i) % count);
var seat = seats[i];
this.seats.put(player, seat);
}
}
private void addLabels() {
int i = 0;
for (var entry : players.entrySet()) {
UUID uuid = entry.getKey();
String name = entry.getValue();
padOfTruth.setName(i++, name);
if (isSelf(uuid)) continue;
var label = new Label("", game.data.skin);
var seat = seats.get(uuid);
label.setX(seat.getLabelX());
label.setY(seat.getLabelY());
label.setAlignment(seat.getLabelAlign());
label.setText(name);
nameLabels.put(uuid, label);
game.data.stage.addActor(label);
}
}
//</editor-fold>
@Override
protected void renderBackground(float delta) {
float scale = Math.max(
game.data.extendViewport.getWorldWidth() / WizardGame.WIDTH,
game.data.extendViewport.getWorldHeight() / WizardGame.HEIGHT
);
game.batch.setColor(1, 1, 1, 0.25f);
game.batch.draw(game.data.background, 0,0, scale * WizardGame.WIDTH, scale * WizardGame.HEIGHT);
game.batch.setColor(1, 1, 1, 0.25f);
game.batch.draw(
game.data.title,
(game.data.extendViewport.getWorldWidth() - game.data.title.getRegionWidth() * 0.75f) / 2f,
@ -158,7 +187,7 @@ public class GameScreen extends MenuScreen {
game.data.stage.addAction(currentAction);
}
} else if (pendingSync.getAndSet(false)) {
send(new ContinueMessage());
game.getClient().execute(Game.class, Game::sync);
}
}
@ -170,90 +199,25 @@ public class GameScreen extends MenuScreen {
pendingActions.add(run(runnable));
}
private void seat() {
var count = players.size();
Seat[] seats = switch (count) {
case 3 -> new Seat[] {Seat.TOP_LEFT, Seat.TOP_RIGHT};
case 4 -> new Seat[] {Seat.LEFT, Seat.TOP, Seat.RIGHT};
case 5 -> new Seat[] {Seat.LEFT, Seat.TOP_LEFT, Seat.TOP_RIGHT, Seat.RIGHT};
case 6 -> new Seat[] {Seat.LEFT, Seat.TOP_LEFT, Seat.TOP, Seat.TOP_RIGHT, Seat.RIGHT};
default -> throw new AssertionError();
};
int index = players.indexOf(state.getSelf());
for (int i = 1; i < count; i++) {
var player = players.get((index + i) % count);
var seat = seats[i - 1];
this.seats.put(player, seat);
}
this.seats.put(state.getSelf(), Seat.BOTTOM);
}
private void prepareLabels() {
for (UUID player : players) {
if (isSelf(player)) continue;
var label = new Label("", game.data.skin);
var seat = seats.get(player);
label.setX(seat.getLabelX());
label.setY(seat.getLabelY());
label.setAlignment(seat.getLabelAlign());
this.nameLabels.put(player, label);
game.data.stage.addActor(label);
}
}
private void setNames() {
for (int i = 0; i < players.size(); i++) {
var player = players.get(i);
var name = state.getPlayers().get(player);
padOfTruth.setName(i, name);
if (!isSelf(player)) {
nameLabels.get(player).setText(name);
}
}
private boolean isSelf(UUID uuid) {
return self.equals(uuid);
}
private void onCardClicked(CardActor actor) {
var card = actor.getCard();
if (checkAction(PLAY_CARD)) {
var timeout = activePlayer.third();
if (card == Card.CLOUD) {
execute(new PlayColoredCardOverlay(this,
timeout,
Card.CLOUD,
Card.CLOUD_RED,
Card.CLOUD_GREEN,
Card.CLOUD_BLUE,
Card.CLOUD_YELLOW
));
} else if (card == Card.JUGGLER) {
execute(new PlayColoredCardOverlay(this,
timeout,
Card.JUGGLER,
Card.JUGGLER_RED,
Card.JUGGLER_GREEN,
Card.JUGGLER_BLUE,
Card.JUGGLER_YELLOW
));
} else {
send(new PlayCardMessage(card));
}
} else if (checkAction(JUGGLE_CARD)) {
juggledCard = card;
send(new JuggleMessage(card));
} else {
addMessage("You cannot do that right now.");
}
public void onCardClicked(Card card) {
game.getClient().execute(Game.class, (s, c) -> s.onCardClicked(c, card));
}
private boolean checkAction(UserInputMessage.Action action) {
return activePlayer != null && (activePlayer.first() == null || isSelf(activePlayer.first())) && activePlayer.second() == action;
public void onSuitClicked(Card.Suit suit) {
game.getClient().execute(Game.class, (s, c) -> s.onSuitClicked(c, suit));
}
private boolean isSelf(UUID uuid) {
return state.getSelf().equals(uuid);
public void onPredictionMade(int prediction) {
game.getClient().execute(Game.class, (s, c) -> s.onPredictionMade(c, prediction));
}
/**
* Resets all round-scoped and shows an {@linkplain StartRoundOverlay overlay}.
*/
public void startRound(int round) {
execute(parallel(
run(() -> {
@ -277,24 +241,8 @@ public class GameScreen extends MenuScreen {
execute(() -> cardStack.clearChildren());
}
/**
* Indicates that the next call to {@link #setHand(UUID, List)} should be animated.
*/
public void setJuggling(boolean juggling) {
execute(() -> {
this.juggling = juggling;
if (juggling) {
handCards.setSelectMode(CardsGroup.SelectMode.SINGLE);
} else {
handCards.setSelectMode(CardsGroup.SelectMode.NONE);
juggledCard = null;
handCards.setSelected(null);
}
});
}
public void finishTrick(UUID player, List<Card> cards) {
var seat = seats.get(player);
public void finishTrick(UUID player) {
var seat = seats.getOrDefault(player, Seat.FALLBACK);
var action = parallel();
execute(sequence(
@ -321,15 +269,17 @@ public class GameScreen extends MenuScreen {
));
}
public void setHand(UUID player, List<Card> cards) {
/**
* Updates the given players hand cards.
*/
public void setHand(UUID player, List<Card> cards, boolean juggle) {
if (isSelf(player)) {
var sequence = sequence();
sequence.addAction(run(() -> {
var changes = handCards.update(cards);
// animate card changes
if (juggling) {
setJuggling(false);
if (juggle) {
sequence.addAction(animateJuggle(changes.first(), changes.second()));
}
}));
@ -337,62 +287,39 @@ public class GameScreen extends MenuScreen {
}
}
public void setTrump(Card trumpCard, Card.Suit trumpSuit) {
if (trumpCardActor == null) {
trumpCardActor = new CardActor(Card.HIDDEN, atlas);
}
if (trumpSuitActor == null) {
trumpSuitActor = new CardActor(Card.HIDDEN, atlas);
}
String player = null;
if (activePlayer != null && activePlayer.second() == PICK_TRUMP) {
player = state.getPlayers().get(activePlayer.first());
clearActivePlayer();
}
execute(new TrumpOverlay(this, player, trumpCard, trumpSuit));
}
public void addPrediction(int round, UUID player, int prediction) {
boolean changed = false;
if (activePlayer != null && activePlayer.first().equals(player) && (activePlayer.second() == CHANGE_PREDICTION || activePlayer.second() == MAKE_PREDICTION)) {
changed = activePlayer.second() == CHANGE_PREDICTION;
clearActivePlayer();
}
/**
* Adds the prediction for the given round and player to the {@linkplain #padOfTruth pad of truth} and
* shows a corresponding message.
*/
public void addPrediction(int round, UUID player, int prediction, boolean changed) {
if (isSelf(player)) {
addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.self", prediction));
} else {
var name = state.getPlayers().get(player);
var name = players.get(player);
addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.other", name, prediction));
}
execute(() -> padOfTruth.setPrediction(players.indexOf(player), round, prediction));
execute(() -> padOfTruth.setPrediction(orderedPlayers.indexOf(player), round, prediction));
}
/**
* Removes the card from the players hand and puts it into the {@linkplain #cardStack stack}.
*/
public void playCard(UUID player, Card card) {
if (activePlayer != null && activePlayer.first().equals(player) && activePlayer.second() == PLAY_CARD) {
clearActivePlayer();
}
if (isSelf(player)) {
addMessage(game.messages.get("game.action.play_card.self"));
} else {
var name = state.getPlayers().get(player);
var name = players.get(player);
addMessage(game.messages.format("game.action.play_card.other", name));
}
Seat seat = seats.get(player);
Seat seat = seats.getOrDefault(player, Seat.FALLBACK);
var sequence = sequence();
sequence.addAction(run(() -> {
CardActor actor = null;
if (isSelf(player)) {
actor = handCards.remove(card);
actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2);
}
if (actor == null) {
@ -406,55 +333,35 @@ public class GameScreen extends MenuScreen {
execute(sequence);
}
/**
* Adds the scores for a round to the corresponding row of the {@linkplain #padOfTruth pad of truth}.
*/
public void addScores(int round, Map<UUID, Integer> scores) {
execute(() -> {
for (int i = 0; i < players.size(); i++) {
UUID player = players.get(i);
for (int i = 0; i < orderedPlayers.size(); i++) {
UUID player = orderedPlayers.get(i);
padOfTruth.setScore(i, round, scores.get(player));
}
});
}
/**
* Removes all visual changes done by {@link #setActivePlayer(UUID, UserInputMessage.Action, long)}.
*/
public void clearActivePlayer() {
setActivePlayer(null, null, 0);
}
/**
* Highlights the given players label and sets the {@linkplain #setPersistentMessage(String) persistent message}
* accordingly.
*/
public void setActivePlayer(UUID player, UserInputMessage.Action action, long timeout) {
if (action == SYNC) throw new IllegalArgumentException();
// reset label color
if (activePlayer != null && nameLabels.containsKey(activePlayer.first())) {
var label = nameLabels.get(activePlayer.first());
execute(() -> label.setStyle(labelStyleDefault));
}
if (player == null && action == null) {
activePlayer = null;
setPersistentMessage(null);
return;
}
activePlayer = Triple.of(player, action, timeout);
// set label color
if (nameLabels.containsKey(player)) {
var label = nameLabels.get(player);
execute(() -> label.setStyle(labelStyleActive));
}
execute(() -> nameLabels.forEach((p, l) -> l.setStyle(p.equals(player) ? labelStyleActive : labelStyleDefault)));
boolean isSelf = state.getSelf().equals(player);
if (isSelf || player == null) {
// show interface
if (isSelf(player) || player == null && action == null) {
setPersistentMessage(null);
switch (action) {
case PICK_TRUMP -> execute(new PickTrumpOverlay(this, timeout, false));
case MAKE_PREDICTION -> execute(new MakePredictionOverlay(this, timeout, state.getRound()));
case CHANGE_PREDICTION -> execute(new ChangePredictionOverlay(this, timeout, state.getRound(), state.getPredictions().get(state.getSelf())));
case PLAY_CARD -> setPersistentMessage(game.messages.get("game.message.play_card.self"));
}
}
if (!isSelf) {
// show message
} else {
var key = switch (action) {
case CHANGE_PREDICTION -> "game.message.change_prediction.";
case JUGGLE_CARD -> "game.message.juggle_card.";
@ -464,15 +371,141 @@ public class GameScreen extends MenuScreen {
default -> throw new AssertionError();
};
if (player == null) {
if (player != null) {
setPersistentMessage(game.messages.format(key + "other", players.get(player)));
} else {
setPersistentMessage(game.messages.get(key + "all"));
}
}
}
/**
* Visually highlights the given card on the players hand.
*/
public void setSelectedCard(@Nullable Card card) {
handCards.setSelected(card);
}
/**
* Swaps the current {@linkplain #trumpCardActor trump card} (if present) with a {@linkplain Card#WEREWOLF werewolf}
* card on the given players hand.
*/
public void swapTrumpCard(UUID player) {
var seat = seats.getOrDefault(player, Seat.FALLBACK);
var sequence = sequence();
sequence.addAction(run(() -> {
if (trumpCardActor == null || !trumpCardActor.hasParent()) return;
if (isSelf(player)) {
var handCard = handCards.find(Card.WEREWOLF);
if (handCard == null) return;
handCard.setCard(trumpCardActor.getCard());
trumpCardActor.setCard(Card.WEREWOLF);
float localHandX = handCard.getX(), localHandY = handCard.getY();
float stageTrumpX = 10, stageTrumpY = 10;
float localTrumpX, localTrumpY;
float stageHandX, stageHandY;
var pos = new Vector2(handCard.getX(), handCard.getY());
handCards.localToStageCoordinates(pos);
stageHandX = pos.x;
stageHandY = pos.y;
pos.set(stageTrumpX, stageTrumpY);
handCards.stageToLocalCoordinates(pos);
localTrumpX = pos.x;
localTrumpY = pos.y;
trumpCardActor.setPosition(stageHandX, stageHandY);
trumpCardActor.setRotation(0);
handCard.setPosition(localTrumpX, localTrumpY);
handCard.setRotation(0);
sequence.addAction(parallel(
targeting(trumpCardActor, moveTo(stageTrumpX, stageTrumpY, AnimationTimings.WEREWOLF_SWAP)),
targeting(handCard, moveTo(localHandX, localHandY, AnimationTimings.WEREWOLF_SWAP))
));
} else {
var name = state.getPlayers().get(player);
setPersistentMessage(game.messages.format(key + "other", name));
var handCard = new CardActor(trumpCardActor.getCard(), atlas);
handCard.setPosition(10, 10);
trumpCardActor.setCard(Card.WEREWOLF);
game.data.stage.addActor(handCard);
sequence.addAction(seat.moveToHand(trumpCardActor, 0));
sequence.addAction(parallel(
targeting(trumpCardActor, moveTo(10,10, AnimationTimings.WEREWOLF_SWAP)),
targeting(trumpCardActor, rotateTo(0, AnimationTimings.WEREWOLF_SWAP)),
seat.moveToHand(handCard, AnimationTimings.WEREWOLF_SWAP)
));
sequence.addAction(removeActor(handCard));
}
}));
execute(sequence);
}
//<editor-fold desc="Overlays" defaultstate="collapsed">
public void showTrumpOverlay(UUID player, Card trumpCard, Card.Suit trumpSuit) {
if (trumpCardActor == null) {
trumpCardActor = new CardActor(Card.HIDDEN, atlas);
}
if (trumpSuitActor == null) {
trumpSuitActor = new CardActor(Card.HIDDEN, atlas);
}
execute(new TrumpOverlay(this, players.get(player), trumpCard, trumpSuit));
}
public void showColoredCardOverlay(Card card, long timeout) {
if (card == Card.JUGGLER) {
execute(new PlayColoredCardOverlay(this, timeout, card, Card.JUGGLER_RED, Card.JUGGLER_GREEN, Card.JUGGLER_BLUE, Card.JUGGLER_YELLOW));
} else if (card == Card.CLOUD) {
execute(new PlayColoredCardOverlay(this, timeout, card, Card.CLOUD_RED, Card.CLOUD_GREEN, Card.CLOUD_BLUE, Card.CLOUD_YELLOW));
} else {
throw new IllegalArgumentException();
}
}
public InteractionOverlay showPickTrumpOverlay(long timeout, boolean allowNone) {
var overlay = new PickTrumpOverlay(this, timeout, allowNone);
execute(overlay);
return overlay;
}
public void showScoreOverlay() {
execute(sequence(
run(() -> padOfTruth.setEnabled(false)),
parallel(
targeting(padOfTruth, scaleTo(1, 1, AnimationTimings.OVERLAY_SHARED_ELEMENT)),
targeting(padOfTruth, moveTo((WizardGame.WIDTH - padOfTruth.getWidth()) / 2, (WizardGame.HEIGHT - padOfTruth.getHeight()) / 2, AnimationTimings.OVERLAY_SHARED_ELEMENT))
),
delay(AnimationTimings.OVERLAY_HOLD),
parallel(
targeting(padOfTruth, scaleTo(PadOfTruth.COLLAPSED_SCALE, PadOfTruth.COLLAPSED_SCALE, AnimationTimings.OVERLAY_SHARED_ELEMENT)),
targeting(padOfTruth, moveTo(WizardGame.WIDTH - 10 - PadOfTruth.EXTENDED_WIDTH, 10))
),
run(() -> padOfTruth.setEnabled(true))
));
}
public InteractionOverlay showMakePredictionOverlay(int round, long timeout) {
var overlay = new MakePredictionOverlay(this, timeout, round);
execute(overlay);
return overlay;
}
public InteractionOverlay showChangePredictionOverlay(int round, int oldPrediction, long timeout) {
var overlay = new ChangePredictionOverlay(this, timeout, round, oldPrediction);
execute(overlay);
return overlay;
}
//</editor-fold>
//<editor-fold desc="Messages" defaultstate="collapsed">
public void addMessage(@Nls String text) {
addMessage(text, false);
}
@ -512,44 +545,33 @@ public class GameScreen extends MenuScreen {
}
if (text != null) {
persistentMessage = new Label(text, getData().skin);
persistentMessage = new Label(text, game.data.skin);
messages.addActor(persistentMessage);
}
});
}
public void send(PlayerMessage message) {
if (!sending.getAndSet(true) || (message instanceof ContinueMessage)) {
game.getClient().send(new InteractionMessage(message));
} else {
addMessage("Please slow down.", true);
}
}
//</editor-fold>
public void ready(boolean success) {
if (!pendingSync.get()) {
sending.set(false);
}
if (success && currentAction instanceof Overlay overlay && overlay instanceof InteractionOverlay) {
overlay.finish();
}
if (checkAction(JUGGLE_CARD) && juggledCard != null) {
handCards.setSelected(juggledCard);
juggledCard = null;
}
if (success) closeInteractionOverlay();
}
public void timeout() {
addMessage(game.messages.get("game.message.timeout"));
ready(true);
closeInteractionOverlay();
clearActivePlayer();
}
public void sync() {
pendingSync.set(true);
sending.set(true);
}
public void closeInteractionOverlay() {
execute(() -> {
if (currentAction instanceof Overlay overlay && overlay instanceof InteractionOverlay) {
overlay.close();
}
});
}
@Override
@ -579,8 +601,8 @@ public class GameScreen extends MenuScreen {
var animation = parallel();
removed.forEach(actor -> {
getData().stage.addActor(actor);
animation.addAction(targeting(actor, left.moveToHand(AnimationTimings.JUGGLE)));
game.data.stage.addActor(actor);
animation.addAction(left.moveToHand(actor, AnimationTimings.JUGGLE));
});
added.forEach(actor -> {
@ -600,7 +622,6 @@ public class GameScreen extends MenuScreen {
// apply start values
var startPos = new Vector2(right.getHandX(), right.getHandY());
handCards.stageToLocalCoordinates(startPos);
actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2);
animation.addAction(targeting(actor, moveTo(startPos.x, startPos.y)));
animation.addAction(targeting(actor, rotateTo(right.getHandAngle())));
@ -617,6 +638,7 @@ public class GameScreen extends MenuScreen {
@Getter
public enum Seat {
FALLBACK(WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT * 0.5f, 0, 0, 0, WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT * 0.5f),
BOTTOM(WizardGame.WIDTH * 0.5f, 0, 0, 0, Align.bottom, WizardGame.WIDTH * 0.5f, 300),
LEFT(0, WizardGame.HEIGHT * 0.5f, 50, WizardGame.HEIGHT * 0.5f + 110f, Align.bottomLeft, 117.5f, WizardGame.HEIGHT * 0.5f),
TOP_LEFT(WizardGame.WIDTH * 0.25f, WizardGame.HEIGHT, WizardGame.WIDTH * 0.25f, WizardGame.HEIGHT - 50, Align.top, WizardGame.WIDTH * 0.25f, WizardGame.HEIGHT - 200),
@ -655,14 +677,13 @@ public class GameScreen extends MenuScreen {
var actor = new CardActor(card, atlas);
actor.setPosition(getHandX() - actor.getWidth() / 2, getHandY() - actor.getHeight() / 2);
actor.setRotation(getHandAngle());
actor.setOrigin(actor.getWidth() / 2, actor.getHeight() / 2);
return actor;
}
public Action moveToHand(float duration) {
public Action moveToHand(CardActor actor, float duration) {
return parallel(
moveTo(handX, handY, duration),
rotateTo(handAngle, duration)
targeting(actor, moveTo(handX, handY, duration)),
targeting(actor, rotateTo(handAngle, duration))
);
}
}

@ -1,23 +1,32 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.actors.game.overlay.InteractionOverlay;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.client.libgdx.util.Pair;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.messages.player.*;
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Data;
import lombok.Getter;
import lombok.experimental.Accessors;
import lombok.extern.log4j.Log4j2;
import java.lang.ref.WeakReference;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*;
@Log4j2
@Getter
@ -27,7 +36,7 @@ public final class Game extends BaseState {
private final String sessionName;
private final String secret;
private final Map<UUID, String> players;
private final LinkedHashMap<UUID, String> players;
private final Map<UUID, Integer> scores = new HashMap<>();
private int round = -1;
@ -37,13 +46,20 @@ public final class Game extends BaseState {
private int trick = -1;
private final List<Pair<UUID, Card>> stack = new ArrayList<>();
private Interaction currentInteraction;
private int pendingClearActivePlayer = 0;
private Card trumpCard;
private Card.Suit trumpSuit;
private GameScreen gameScreen;
private final AtomicBoolean sending = new AtomicBoolean(false);
private boolean juggling;
private Card juggleCard;
private boolean werewolf;
public Game(UUID self, UUID session, String sessionName, String secret, Map<UUID, String> players) {
public Game(UUID self, UUID session, String sessionName, String secret, LinkedHashMap<UUID, String> players) {
this.self = self;
this.session = session;
this.sessionName = sessionName;
@ -53,20 +69,71 @@ public final class Game extends BaseState {
@Override
public Optional<ClientState> onEnter(Client client) {
gameScreen = new GameScreen(client.getGame());
gameScreen = new GameScreen(client.getGame(), self, players);
client.getGame().setScreen(gameScreen);
return super.onEnter(client);
}
//<editor-fold desc="onMessage">
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
log(message);
try {
if (message instanceof GameMessage game) {
var observerMessage = game.getObserverMessage();
if (observerMessage instanceof StateMessage state) {
switch (state.getState()) {
case "starting_round" -> {
case "starting_round" -> onStartRound();
case "starting_trick" -> onStartTrick();
case "juggling" -> onJuggle();
case "finished" -> {
onFinished();
return returnToSession();
}
case "error" -> {
onError();
return returnToSession();
}
}
} else if (observerMessage instanceof HandMessage hand) {
onHandMessage(hand.getPlayer(), hand.getHand());
} else if (observerMessage instanceof PredictionMessage prediction) {
onPredictionMessage(prediction.getPlayer(), prediction.getPrediction());
} else if (observerMessage instanceof TrumpMessage trump) {
onTrumpMessage(trump.getCard(), trump.getSuit());
} else if (observerMessage instanceof TrickMessage trick) {
onTrickMessage(trick.getPlayer(), trick.getCards());
} else if (observerMessage instanceof CardMessage card) {
onCardMessage(card.getPlayer(), card.getCard());
} else if (observerMessage instanceof ScoreMessage score) {
onScoreMessage(score.getPoints());
} else if (observerMessage instanceof UserInputMessage input) {
onUserInputMessage(input.getPlayer(), input.getAction(), input.getTimeout());
} else if (observerMessage instanceof TimeoutMessage) {
onTimeoutMessage();
} else {
log.fatal("Unknown observer message type {}.", observerMessage.getClass());
// TODO user feedback
return Optional.of(new Menu());
}
return Optional.empty();
} else if (message instanceof NackMessage nack) {
return onNackMessage(client, message, nack);
} else if (message instanceof AckMessage) {
onAckMessage();
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
} finally {
if (pendingClearActivePlayer > 0 && --pendingClearActivePlayer == 0) {
finishInteraction();
}
}
}
private void onStartRound() {
log.info("Round {} is starting...", round + 1);
round ++;
predictions.clear();
tricks.clear();
@ -76,80 +143,232 @@ public final class Game extends BaseState {
trick = -1;
gameScreen.startRound(round);
}
case "starting_trick" -> {
private void onStartTrick() {
log.info("Trick {} is starting...", trick + 1);
trick ++;
stack.clear();
finishInteraction();
gameScreen.startTrick();
}
case "finished", "error" -> {
return returnToSession();
private void onJuggle() {
juggling = true;
juggleCard = null;
}
case "juggling" -> gameScreen.setJuggling(true);
private void onFinished() {
log.info("The game has finished.");
}
} else if (observerMessage instanceof HandMessage hand) {
hands.put(hand.getPlayer(), hand.getHand());
gameScreen.setHand(hand.getPlayer(), hand.getHand());
} else if (observerMessage instanceof PredictionMessage prediction) {
predictions.put(prediction.getPlayer(), prediction.getPrediction());
gameScreen.addPrediction(round, prediction.getPlayer(), prediction.getPrediction());
} else if (observerMessage instanceof TrumpMessage trump) {
trumpCard = trump.getCard();
trumpSuit = trump.getSuit();
gameScreen.setTrump(trumpCard, trumpSuit);
} else if (observerMessage instanceof TrickMessage trick) {
private void onError() {
log.error("The game has finished with an error.");
}
private void onHandMessage(UUID player, List<Card> hand) {
checkPlayer(player);
log.info("{} hand cards are: {}", nameOf(player, true, true), hand);
if (juggling) checkActivePlayer(player, JUGGLE_CARD);
finishInteraction();
hands.put(player, hand);
gameScreen.setSelectedCard(null);
gameScreen.setHand(player, hand, juggling);
juggling = false;
}
private void onPredictionMessage(UUID player, int prediction) {
checkPlayer(player);
checkActivePlayer(player, MAKE_PREDICTION, CHANGE_PREDICTION);
log.info("{} predicted: {}", nameOf(player, true, false), prediction);
boolean changed = currentInteraction != null && currentInteraction.action() == CHANGE_PREDICTION;
finishInteraction();
predictions.put(player, prediction);
gameScreen.addPrediction(round, player, prediction, changed);
}
private void onTrumpMessage(Card trumpCard, Card.Suit trumpSuit) {
if (trumpCard == null) {
log.info("There is no trump in this round.");
} else {
log.info("The trump suit is {} ({}).", trumpSuit != null ? trumpSuit : "yet to be determined.", trumpCard);
}
finishInteraction();
this.trumpCard = trumpCard;
this.trumpSuit = trumpSuit;
if (trumpCard == Card.WEREWOLF && trumpSuit == null) {
werewolf = true;
} else {
werewolf = false;
gameScreen.showTrumpOverlay(null, trumpCard, trumpSuit);
}
}
private void onTrickMessage(UUID player, List<Card> cards) {
checkPlayer(player);
log.info("This trick {} goes to {}.", cards, nameOf(player));
this.stack.clear();
this.tricks.computeIfAbsent(trick.getPlayer(), player -> new ArrayList<>())
.add(trick.getCards());
gameScreen.finishTrick(trick.getPlayer(), trick.getCards());
} else if (observerMessage instanceof CardMessage card) {
this.stack.add(Pair.of(card.getPlayer(), card.getCard()));
this.tricks.computeIfAbsent(player, p -> new ArrayList<>())
.add(cards);
gameScreen.finishTrick(player);
}
private void onCardMessage(UUID player, Card card) {
checkPlayer(player);
checkActivePlayer(player, PLAY_CARD);
log.info("{} played {}.", nameOf(player, true, false), card);
finishInteraction();
this.stack.add(Pair.of(player, card));
var handCard = switch (card.getCard()) {
var handCard = switch (card) {
case CHANGELING_JESTER, CHANGELING_WIZARD -> Card.CHANGELING;
case JUGGLER_BLUE, JUGGLER_GREEN, JUGGLER_RED, JUGGLER_YELLOW -> Card.JUGGLER;
case CLOUD_BLUE, CLOUD_GREEN, CLOUD_RED, CLOUD_YELLOW -> Card.CLOUD;
default -> card.getCard();
default -> card;
};
var hand = this.hands.get(card.getPlayer());
var hand = this.hands.get(player);
if (hand != null) {
hand.remove(handCard);
}
gameScreen.playCard(card.getPlayer(), handCard);
} else if (observerMessage instanceof ScoreMessage score) {
score.getPoints().forEach((player, points) -> scores.merge(player, points, Integer::sum));
gameScreen.addScores(round, score.getPoints());
} else if (observerMessage instanceof UserInputMessage input) {
if (input.getAction() == UserInputMessage.Action.SYNC) {
gameScreen.playCard(player, handCard);
}
private void onScoreMessage(Map<UUID, Integer> points) {
log.info("The scores are as follows: " + points);
points.forEach((player, p) -> scores.merge(player, p, Integer::sum));
gameScreen.addScores(round, points);
gameScreen.showScoreOverlay();
}
private void onUserInputMessage(UUID player, UserInputMessage.Action action, long timeout) {
checkPlayer(player);
log.info(
"Waiting for input {} from {}. (times out at {})",
action,
nameOf(player),
LocalDateTime.ofInstant(Instant.ofEpochMilli(timeout), ZoneId.systemDefault())
);
if (action == UserInputMessage.Action.SYNC) {
gameScreen.sync();
} else {
gameScreen.setActivePlayer(input.getPlayer(), input.getAction(), input.getTimeout());
currentInteraction = new Interaction(player, action, timeout);
gameScreen.setActivePlayer(player, action, timeout);
if (werewolf && action == PICK_TRUMP) {
gameScreen.swapTrumpCard(player);
}
} else if (observerMessage instanceof TimeoutMessage) {
if (isActive()) {
switch (action) {
case PICK_TRUMP -> currentInteraction.overlay(gameScreen.showPickTrumpOverlay(timeout, werewolf));
case MAKE_PREDICTION -> currentInteraction.overlay(gameScreen.showMakePredictionOverlay(round, timeout));
case CHANGE_PREDICTION -> currentInteraction.overlay(gameScreen.showChangePredictionOverlay(round, predictions.get(player), timeout));
case PLAY_CARD -> gameScreen.setPersistentMessage(gameScreen.getMessages().get("game.message.play_card.self"));
}
}
if (werewolf && action == PICK_TRUMP) {
werewolf = false;
}
}
}
private void onTimeoutMessage() {
log.info("The previous interaction timed out.");
delayedFinishInteraction();
gameScreen.timeout();
} else {
// TODO user feedback
return Optional.of(new Menu());
}
return Optional.empty();
} else if (message instanceof NackMessage nack) {
private Optional<ClientState> onNackMessage(Client client, ServerMessage message, NackMessage nack) {
sending.set(false);
if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) {
juggleCard = null;
}
int code = nack.getCode();
if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
log.error(nack.getMessage());
gameScreen.addMessage(nack.getMessage(), true);
gameScreen.ready(false);
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
} else if (message instanceof AckMessage) {
}
private void onAckMessage() {
log.info("OK");
sending.set(false);
if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) {
gameScreen.setSelectedCard(juggleCard);
juggleCard = null;
}
gameScreen.ready(true);
}
//</editor-fold>
//<editor-fold desc="Screen Callbacks" defaultstate="collapsed">
public Optional<ClientState> onCardClicked(Client client, Card card) {
if (isActive()) {
if (currentInteraction.action() == PLAY_CARD) {
if (card == Card.CLOUD || card == Card.JUGGLER) {
gameScreen.showColoredCardOverlay(card, currentInteraction.timeout());
} else {
send(client, new PlayCardMessage(card));
}
return Optional.empty();
} else if (currentInteraction.action() == JUGGLE_CARD) {
if (send(client, new JuggleMessage(juggleCard))) {
juggleCard = card;
}
return Optional.empty();
}
}
gameScreen.addMessage("You cannot do that right now.", true);
return Optional.empty();
}
public Optional<ClientState> onSuitClicked(Client client, Card.Suit suit) {
if (isActive() && currentInteraction.action() == PICK_TRUMP) {
send(client, new PickTrumpMessage(suit));
} else {
return unexpectedMessage(client, message);
gameScreen.addMessage("You cannot do that right now.", true);
}
return Optional.empty();
}
public Optional<ClientState> onPredictionMade(Client client, int prediction) {
if (isActive()) {
if (currentInteraction.action() == MAKE_PREDICTION || currentInteraction.action() == CHANGE_PREDICTION) {
send(client, new PredictMessage(prediction));
return Optional.empty();
}
}
gameScreen.addMessage("You cannot do that right now.", true);
return Optional.empty();
}
public Optional<ClientState> sync(Client client) {
send(client, new ContinueMessage());
return Optional.empty();
}
//</editor-fold>
private Optional<ClientState> returnToSession() {
return Optional.of(new Session(
new SessionData(session, sessionName, -1, null),
@ -160,88 +379,115 @@ public final class Game extends BaseState {
));
}
private void log(ServerMessage message) {
if (message instanceof GameMessage gameMessage) {
var observerMessage = gameMessage.getObserverMessage();
if (observerMessage instanceof StateMessage state) {
switch (state.getState()) {
case "starting_round" -> log.info("Round {} is starting...", round + 1);
case "starting_trick" -> log.info("Trick {} is starting...", trick + 1);
case "finished" -> log.info("The game has finished.");
case "error" -> log.info("The game has finished with an error.");
}
} else if (observerMessage instanceof HandMessage hand) {
if (hand.getPlayer().equals(self)) {
log.info("Your hand cards are: {}", hand.getHand());
/**
* Sends a message. Only one message may be sent at a time until without receiving an answer.
* @return {@code true} iff the message was sent
*/
private boolean send(Client client, PlayerMessage message) {
if (message instanceof ContinueMessage || !sending.getAndSet(true)) {
client.send(new InteractionMessage(message));
return true;
} else {
log.info("{}'s hand cards are: {}", nameOf(hand.getPlayer()), hand.getHand());
gameScreen.addMessage("Please slow down.", true);
return false;
}
} else if (observerMessage instanceof PredictionMessage prediction) {
if (prediction.getPlayer().equals(self)) {
log.info("You predicted: {}%n", prediction.getPrediction());
} else {
log.info("{} predicted: {}%n", nameOf(prediction.getPlayer()), prediction.getPrediction());
}
} else if (observerMessage instanceof TrumpMessage trump) {
trumpCard = trump.getCard();
trumpSuit = trump.getSuit();
if (trumpCard == null) {
log.info("There is no trump in this round.");
} else {
log.info("The trump suit is {} ({}).", trumpSuit, trumpCard);
/**
* Checks whether some action from the player is expected.
*/
private boolean isActive() {
return currentInteraction != null && (currentInteraction.player() == null || self.equals(currentInteraction.player()));
}
} else if (observerMessage instanceof TrickMessage trick) {
log.info("This trick {} goes to {}.", trick.getCards(), nameOf(trick.getPlayer()));
} else if (observerMessage instanceof CardMessage card) {
if (card.getPlayer().equals(self)) {
log.info("You played {}.", card.getCard());
} else {
log.info("{} played {}.", nameOf(card.getPlayer()), card.getCard());
/**
* Close the interaction overlay associated with the current interaction and reset the current interaction.
*/
private void finishInteraction() {
if (currentInteraction != null) {
var overlay = currentInteraction.overlay();
if (overlay != null) overlay.close();
}
} else if (observerMessage instanceof ScoreMessage score) {
log.info("The scores are as follows: " + score.getPoints());
} else if (observerMessage instanceof UserInputMessage input) {
if (input.getAction() != UserInputMessage.Action.SYNC) {
if (self.equals(input.getPlayer())) {
log.info("It is your turn to {}. You have time until {}.", switch (input.getAction()) {
case CHANGE_PREDICTION -> "change your prediction";
case JUGGLE_CARD -> "juggle a card";
case PLAY_CARD -> "play a card";
case PICK_TRUMP -> "pick the trump suit";
case MAKE_PREDICTION -> "make a prediction";
default -> throw new AssertionError();
}, LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault()));
} else {
log.info(
"Waiting for input {} from {}. (times out at {})",
input.getAction(),
nameOf(input.getPlayer()),
LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault())
);
currentInteraction = null;
gameScreen.clearActivePlayer();
}
/**
* UI-wise equivalent to {@link #finishInteraction()} but information about the interaction is kept until after the
* next message.
*/
private void delayedFinishInteraction() {
pendingClearActivePlayer = 2;
if (currentInteraction != null) {
var overlay = currentInteraction.overlay();
if (overlay != null) overlay.close();
}
} else if (observerMessage instanceof TimeoutMessage) {
log.info("The previous interaction timed out.");
} else {
log.fatal("Unknown observer message type {}.", observerMessage.getClass());
gameScreen.clearActivePlayer();
}
} else if (message instanceof NackMessage nack) {
int code = nack.getCode();
if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
log.error(nack.getMessage());
} else {
log.fatal("Unexpected message: {}", message);
//<editor-fold desc="Logging" defaultState="collapsed">
/**
* Checks whether the given player is known and logs errors.
*/
private void checkPlayer(UUID player) {
if (player != null && !players.containsKey(player)) {
log.error("Unknown player {}.", player);
}
} else if (message instanceof AckMessage) {
log.info("OK");
}
/**
* Checks whether one of the given actions is currently expected from the given player and logs errors.
*/
private void checkActivePlayer(UUID player, UserInputMessage.Action...actions) {
if (currentInteraction != null && (currentInteraction.player() == null || currentInteraction.player().equals(player))) {
for (var action : actions) {
if (currentInteraction.action() == action) {
return;
}
}
}
log.warn("Received message does not match the previous user input message.");
}
/**
* Returns the name of the given player.
*/
private String nameOf(UUID player) {
return nameOf(player, false, false);
}
/**
* Returns the name of the given player, optionally with a capitalized first letter and/or a possessive suffix ("'s")
*/
private String nameOf(UUID player, boolean capitalize, boolean possessive) {
if (player == null) {
return "all players";
return (capitalize ? "A" : "a") + "ll players" + (possessive ? "'" : "");
} else if (self.equals(player)) {
return (capitalize ? "Y" : "y") + "ou" + (possessive ? "r" : "");
} else {
return players.get(player);
return players.get(player) + (possessive ? "'s" : "");
}
}
//</editor-fold>
@Data
@Accessors(fluent = true)
public static final class Interaction {
private final UUID player;
private final UserInputMessage.Action action;
private final long timeout;
private WeakReference<InteractionOverlay> overlay;
public void overlay(InteractionOverlay overlay) {
this.overlay = new WeakReference<>(overlay);
}
public InteractionOverlay overlay() {
if (overlay == null) return null;
else return overlay.get();
}
}
}

@ -12,7 +12,6 @@ import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import java.util.*;
import java.util.stream.Collectors;
@Log4j2
@Getter
@ -74,10 +73,9 @@ public final class Session extends BaseState {
return Optional.empty();
} else if (message instanceof StartingGameMessage) {
return Optional.of(new Game(
self, session, sessionName, secret,
players.stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName))
));
var players = new LinkedHashMap<UUID, String>();
this.players.forEach(player -> players.put(player.getUuid(), player.getName()));
return Optional.of(new Game(self, session, sessionName, secret, players));
} else if (sending && message instanceof NackMessage nack) {
// TODO display error
log.error(nack.getMessage());

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

@ -1,6 +1,7 @@
package eu.jonahbauer.wizard.core.machine;
import eu.jonahbauer.wizard.common.machine.TimeoutState;
import eu.jonahbauer.wizard.common.messages.player.ContinueMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.states.SyncState;
@ -57,6 +58,8 @@ public abstract class GameState implements TimeoutState<GameState, Game> {
//</editor-fold>
public Optional<GameState> onMessage(Game game, UUID player, PlayerMessage message) {
if (message instanceof ContinueMessage) return Optional.empty();
throw new IllegalStateException("You cannot do that right now.");
}

@ -35,7 +35,7 @@ public class Session implements Observer {
private final Configuration configuration;
@Getter(AccessLevel.NONE)
private final Map<UUID, SessionPlayer> players = new HashMap<>();
private final Map<UUID, SessionPlayer> players = new LinkedHashMap<>();
private Game game;
private final List<Pair<UUID, ServerMessage>> messages = new ArrayList<>();

Loading…
Cancel
Save