From 0d1f51d868c4630d8f65e63f72f99e0d36f6aa68 Mon Sep 17 00:00:00 2001 From: jbb01 <32650546+jbb01@users.noreply.github.com> Date: Wed, 26 Oct 2022 22:35:50 +0200 Subject: [PATCH] implement dedicated classes for each combination type --- .../jonahbauer/tichu/common/model/Card.java | 8 +- .../tichu/common/model/Combination.java | 190 ------------------ .../tichu/common/model/CombinationType.java | 180 ++--------------- .../jonahbauer/tichu/common/model/Stack.java | 51 +++-- .../combinations/AbstractCombination.java | 62 ++++++ .../tichu/common/model/combinations/Bomb.java | 15 ++ .../model/combinations/BombSequence.java | 37 ++++ .../common/model/combinations/BombTuple.java | 47 +++++ .../model/combinations/Combination.java | 99 +++++++++ .../common/model/combinations/FullHouse.java | 63 ++++++ .../tichu/common/model/combinations/Pair.java | 31 +++ .../model/combinations/PairSequence.java | 24 +++ .../common/model/combinations/Sequence.java | 48 +++++ .../common/model/combinations/Single.java | 70 +++++++ .../model/combinations/SingleSequence.java | 24 +++ .../common/model/combinations/Triple.java | 32 +++ .../common/model/combinations/Tuple.java | 46 +++++ .../IncompatibleCombinationException.java | 2 +- .../TooLowCombinationException.java | 2 +- tichu-common/src/main/java/module-info.java | 1 + .../tichu/common/model/CombinationTest.java | 102 ++++++---- 21 files changed, 719 insertions(+), 415 deletions(-) delete mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Combination.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/AbstractCombination.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Bomb.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombSequence.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombTuple.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Combination.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/FullHouse.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Pair.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/PairSequence.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Sequence.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Single.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/SingleSequence.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Triple.java create mode 100644 tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Tuple.java diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Card.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Card.java index 7646e6c..5c6fae5 100644 --- a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Card.java +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Card.java @@ -37,7 +37,7 @@ public enum Card { private static final Map> CARDS_BY_VALUE = Arrays.stream(values()) .filter(Card::isNormal) .collect(Collectors.collectingAndThen( - Collectors.groupingBy(Card::getValue, Collectors.toUnmodifiableSet()), + Collectors.groupingBy(Card::value, Collectors.toUnmodifiableSet()), Collections::unmodifiableMap )); @@ -76,7 +76,7 @@ public enum Card { * * @return the value of this card */ - public @Range(from = 1, to = 15) Integer getValue() { + public @Range(from = 1, to = 15) Integer value() { return value; } @@ -85,7 +85,7 @@ public enum Card { * {@linkplain #isSpecial() special cards}. * @return the suit of this card */ - public Suit getSuit() { + public Suit suit() { return suit; } @@ -107,7 +107,7 @@ public enum Card { /** * Returns an unmodifiable set of all {@linkplain #isNormal() normal cards} with the specified - * {@linkplain #getValue() value}. + * {@linkplain #value() value}. * @param value the card value between {@code 2} and {@code 14} (inclusive) * @return a set of cards */ diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Combination.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Combination.java deleted file mode 100644 index f380490..0000000 --- a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Combination.java +++ /dev/null @@ -1,190 +0,0 @@ -package eu.jonahbauer.tichu.common.model; - -import eu.jonahbauer.tichu.common.model.exceptions.InvalidCombinationException; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; - -import java.util.List; -import java.util.Objects; - -/** - * A combination of cards. - * @param type the type of the combination - * @param cards the cards composing this combination - */ -@SuppressWarnings("unused") -public record Combination( - @NotNull CombinationType type, - @NotNull @Unmodifiable List<@NotNull Card> cards -) { - /** - * Creates a new combination. - * @param type the type of the new combination - * @param cards the cards composing the new combination - * @throws InvalidCombinationException if the cards do not compose a valid combination of the given type - * @throws NullPointerException if {@code type}, {@code cards} or any element of {@code cards} is {@code null} - */ - public Combination(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards) { - this.type = Objects.requireNonNull(type, "type must not be null"); - this.cards = List.copyOf(Objects.requireNonNull(cards, "cards must not be null")); // List#copyOf performs null-checks - if (!type.check(this.cards)) { - throw new InvalidCombinationException(this.type, this.cards); - } - } - - /** - * Returns the card at the specified position. This is equivalent to {@code cards().get(index)} - * @param index index of the card to return - * @return the card at the specified position - * @see #cards() - */ - public @NotNull Card card(int index) { - return cards.get(index); - } - - @Override - public @NotNull String toString() { - return type + cards.toString(); - } - - // - /** - * Creates a new {@link CombinationType#SINGLE}-Combination. - * @param card the card - * @return a new combination - * @see CombinationType#SINGLE - */ - @Contract("_ -> new") - public static @NotNull Combination single(@NotNull Card card) { - return new Combination(CombinationType.SINGLE, List.of(card)); - } - - /** - * Creates a new {@link CombinationType#PAIR}-Combination. - * @param card0 the first card - * @param card1 the second card - * @return a new combination - * @see CombinationType#PAIR - */ - @Contract("_, _ -> new") - public static @NotNull Combination pair(@NotNull Card card0, @NotNull Card card1) { - return new Combination(CombinationType.PAIR, List.of(card0, card1)); - } - - /** - * Creates a new {@link CombinationType#TRIPLE}-Combination. - * @param card0 the first card - * @param card1 the second card - * @param card2 the third card - * @return a new combination - * @see CombinationType#TRIPLE - */ - @Contract("_, _, _ -> new") - public static @NotNull Combination triple(@NotNull Card card0, @NotNull Card card1, @NotNull Card card2) { - return new Combination(CombinationType.TRIPLE, List.of(card0, card1, card2)); - } - - /** - * Creates a new {@link CombinationType#FULL_HOUSE}-Combination. - * @param tripleCard0 the first card of the triple - * @param tripleCard1 the second card of the triple - * @param tripleCard2 the third card of the triple - * @param pairCard0 the first card of the pair - * @param pairCard1 the second card of the pair - * @return a new combination - * @see CombinationType#FULL_HOUSE - */ - @Contract("_, _, _, _, _ -> new") - public static @NotNull Combination fullHouse( - @NotNull Card tripleCard0, @NotNull Card tripleCard1, @NotNull Card tripleCard2, - @NotNull Card pairCard0, @NotNull Card pairCard1 - ) { - return new Combination(CombinationType.FULL_HOUSE, List.of( - tripleCard0, tripleCard1, tripleCard2, pairCard0, pairCard1 - )); - } - - /** - * Creates a new {@link CombinationType#SEQUENCE}-Combination. - * @param cards the sequence of cards (in correct order) - * @return a new combination - * @see CombinationType#SEQUENCE - */ - @Contract("_ -> new") - public static @NotNull Combination sequence(@NotNull Card @NotNull... cards) { - return new Combination(CombinationType.SEQUENCE, List.of(cards)); - } - - /** - * Creates a new {@link CombinationType#SEQUENCE}-Combination. - * @param cards the sequence of cards (in correct order) - * @return a new combination - * @see CombinationType#SEQUENCE - */ - @Contract("_ -> new") - public static @NotNull Combination sequence(@NotNull List<@NotNull Card> cards) { - return new Combination(CombinationType.SEQUENCE, cards); - } - - /** - * Creates a new {@link CombinationType#PAIR_SEQUENCE}-Combination. - * @param cards the sequence of cards (in correct order) - * @return a new combination - * @see CombinationType#PAIR_SEQUENCE - */ - @Contract("_ -> new") - public static @NotNull Combination pairSequence(@NotNull Card @NotNull... cards) { - return new Combination(CombinationType.PAIR_SEQUENCE, List.of(cards)); - } - - /** - * Creates a new {@link CombinationType#PAIR_SEQUENCE}-Combination. - * @param cards the sequence of cards (in correct order) - * @return a new combination - * @see CombinationType#PAIR_SEQUENCE - */ - @Contract("_ -> new") - public static @NotNull Combination pairSequence(@NotNull List<@NotNull Card> cards) { - return new Combination(CombinationType.PAIR_SEQUENCE, cards); - } - - /** - * Creates a new {@link CombinationType#BOMB}-Combination. - * @param card0 the first card - * @param card1 the second card - * @param card2 the third card - * @param card3 the fourth card - * @return a new combination - * @see CombinationType#BOMB - */ - @Contract("_, _, _, _ -> new") - public static @NotNull Combination bomb( - @NotNull Card card0, @NotNull Card card1, @NotNull Card card2, @NotNull Card card3 - ) { - return new Combination(CombinationType.BOMB, List.of(card0, card1, card2, card3)); - } - - /** - * Creates a new {@link CombinationType#BOMB_SEQUENCE}-Combination. - * @param cards the sequence of cards (in correct order) - * @return a new combination - * @see CombinationType#BOMB_SEQUENCE - */ - @Contract("_ -> new") - public static @NotNull Combination bombSequence(@NotNull Card @NotNull... cards) { - return new Combination(CombinationType.BOMB_SEQUENCE, List.of(cards)); - } - - /** - * Creates a new {@link CombinationType#BOMB_SEQUENCE}-Combination. - * @param cards the sequence of cards (in correct order) - * @return a new combination - * @see CombinationType#BOMB_SEQUENCE - */ - @Contract("_ -> new") - public static @NotNull Combination bombSequence(@NotNull List<@NotNull Card> cards) { - return new Combination(CombinationType.BOMB_SEQUENCE, cards); - } - // -} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/CombinationType.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/CombinationType.java index f38014e..bf25490 100644 --- a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/CombinationType.java +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/CombinationType.java @@ -1,7 +1,6 @@ package eu.jonahbauer.tichu.common.model; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Range; import java.util.*; @@ -20,34 +19,9 @@ public enum CombinationType { public boolean check(@NotNull List<@NotNull Card> cards) { return checkBasics(cards, 1, SPECIAL_CARDS); } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - if (stack.isEmpty()) return true; // you can play any card onto an empty stack - if (stack.size() == 1 && stack.top().card(0) == Card.HOUND) return false; // you cannot play any card onto a hound - - var card = cards.get(0); - return switch (card) { - case HOUND, MAHJONG -> false; - case DRAGON -> true; - case PHOENIX -> stack.top().card(0) != Card.DRAGON; - default -> { - var top = stack.top().card(0); - if (top != Card.PHOENIX) { - yield card.getValue() > top.getValue(); - } else if (stack.size() == 1) { - // every normal card has a value of at least 2, which is higher that the phoenix 1½ - yield true; - } else { - yield card.getValue() > stack.top(-1).card(0).getValue(); - } - } - }; - } }, /** - * A pair, i.e. two {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#getValue() value} or the + * A pair, i.e. two {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#value() value} or the * {@link Card#PHOENIX} and a normal card. */ PAIR { @@ -59,16 +33,9 @@ public enum CombinationType { return checkBasics(cards, 2, SPECIAL_CARDS) && isSameValue(cards, INDICES); } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || getSameValue(cards, INDICES) > getSameValue(stack.top().cards(), INDICES); - } }, /** - * A triple, i.e. three {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#getValue() value} + * A triple, i.e. three {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#value() value} * or the {@linkplain Card#PHOENIX} and two normal cards of the same value. */ TRIPLE { @@ -80,13 +47,6 @@ public enum CombinationType { return checkBasics(cards, 3, SPECIAL_CARDS) && isSameValue(cards, INDICES); } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || getSameValue(cards, INDICES) > getSameValue(stack.top().cards(), INDICES); - } }, /** * A full house, i.e. five cards of which three form a {@link #TRIPLE} and the other two form a {@link #PAIR}. @@ -105,13 +65,6 @@ public enum CombinationType { && isSameValue(cards, INDICES_TRIPLE) && isSameValue(cards, INDICES_PAIR); } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || getSameValue(cards, INDICES_TRIPLE) > getSameValue(stack.top().cards(), INDICES_TRIPLE); - } }, /** * A sequence, i.e. at least five cards with increasing values. The first card in the sequence may be a @@ -124,8 +77,8 @@ public enum CombinationType { private int getStartValue(@NotNull List<@NotNull Card> cards) { return cards.get(0) == Card.PHOENIX - ? cards.get(1).getValue() - 1 - : cards.get(0).getValue(); + ? cards.get(1).value() - 1 + : cards.get(0).value(); } @Override @@ -141,27 +94,13 @@ public enum CombinationType { if (start + i > 14) { return false; } - } else if (card.getValue() != start + i) { + } else if (card.value() != start + i) { return false; } } return true; } - - @Override - public boolean isCompatible(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert check(cards); - return stack.isEmpty() - || stack.top().type() == this && stack.top().cards().size() == cards.size(); - } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || getStartValue(cards) > getStartValue(stack.top().cards()); - } }, /** * A sequence of pairs, i.e. at least two pairs with increasing values. @@ -173,8 +112,8 @@ public enum CombinationType { private int getStartValue(@NotNull List<@NotNull Card> cards) { return cards.get(0) == Card.PHOENIX - ? cards.get(1).getValue() - : cards.get(0).getValue(); + ? cards.get(1).value() + : cards.get(0).value(); } @Override @@ -187,58 +126,29 @@ public enum CombinationType { for (int i = 0; i < cards.size(); i++) { var card = cards.get(i); - if (card != Card.PHOENIX && card.getValue() != start + i / 2) { + if (card != Card.PHOENIX && card.value() != start + i / 2) { return false; } } return true; } - - @Override - public boolean isCompatible(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert check(cards); - return stack.isEmpty() - || stack.top().type() == this && stack.top().cards().size() == cards.size(); - } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || getStartValue(cards) > getStartValue(stack.top().cards()); - } }, /** - * A bomb, i.e. four {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#getValue() value}. + * A bomb, i.e. four {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#value() value}. */ BOMB { private static final int[] INDICES = new int[] {0, 1, 2, 3}; private static final Set SPECIAL_CARDS = Collections.emptySet(); - private static final Combination COMBINATION_HOUND = new Combination(SINGLE, List.of(Card.HOUND)); @Override public boolean check(@NotNull List<@NotNull Card> cards) { return checkBasics(cards, 4, SPECIAL_CARDS) && isSameValue(cards, INDICES); } - - @Override - public boolean isCompatible(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert check(cards); - return stack.isEmpty() || !COMBINATION_HOUND.equals(stack.top()); - } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || stack.top().type() != BOMB && stack.top().type() != BOMB_SEQUENCE - || stack.top().type() == BOMB && getSameValue(cards, INDICES) > getSameValue(stack.top().cards(), INDICES); - } }, /** - * A bomb sequence, i.e. a {@linkplain Card#getSuit() monochrome} {@link #SEQUENCE} that does not contain any + * A bomb sequence, i.e. a {@linkplain Card#suit() monochrome} {@link #SEQUENCE} that does not contain any * {@linkplain Card#isSpecial() special cards}. * @apiNote Although not strictly necessary, for consistency with {@link #SEQUENCE} a list of cards representing a * bomb sequence must be sorted by ascending value. @@ -246,43 +156,27 @@ public enum CombinationType { */ BOMB_SEQUENCE { private static final Set SPECIAL_CARDS = Collections.emptySet(); - private static final Combination COMBINATION_HOUND = new Combination(SINGLE, List.of(Card.HOUND)); private int getStartValue(@NotNull List<@NotNull Card> cards) { - return cards.get(0).getValue(); + return cards.get(0).value(); } @Override public boolean check(@NotNull List<@NotNull Card> cards) { if (!checkBasics(cards, null, SPECIAL_CARDS)) return false; if (cards.size() < 5) return false; - if (cards.stream().map(Card::getSuit).distinct().count() != 1) return false; + if (cards.stream().map(Card::suit).distinct().count() != 1) return false; int start = getStartValue(cards); for (int i = 1; i < cards.size(); i++) { var card = cards.get(i); - if (card.getValue() != start + i) { + if (card.value() != start + i) { return false; } } return true; } - - @Override - public boolean isCompatible(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert check(cards); - return stack.isEmpty() || !COMBINATION_HOUND.equals(stack.top()); - } - - @Override - public boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert isCompatible(stack, cards); - return stack.isEmpty() - || stack.top().type() != BOMB_SEQUENCE - || cards.size() > stack.top().cards().size() - || getStartValue(cards) > getStartValue(stack.top().cards()); - } }; /** @@ -293,32 +187,6 @@ public enum CombinationType { */ public abstract boolean check(@NotNull List<@NotNull Card> cards); - /** - *

Checks whether the {@code cards} are compatible with the {@code stack}, i.e. the {@code cards} can - * theoretically be played on top of the {@code stack}. For example a {@link #SEQUENCE} is compatible with a given - * stack only if the stack is empty, or it consists of sequences of the same length. - *

However, this method does not check whether the {@code cards} are actually higher than the top of the - * {@code stack}. If the {@code cards} are not a {@linkplain #check(List) valid} combination of this type, the - * behaviour of this method is undefined and an exception may be thrown. - * @param stack the current stack of cards - * @param cards the cards to be played - * @return {@code true} iff the {@code cards} are compatible with the {@code stack} - */ - public boolean isCompatible(@NotNull Stack stack, @NotNull List<@NotNull Card> cards) { - assert check(cards); - return stack.isEmpty() || stack.top().type() == this; - } - - /** - *

Checks whether the {@code cards} are higher than the combination currently on top of the {@code stack}. - *

If the {@code cards} are not {@linkplain #isCompatible(Stack, List) compatible} with the {@code stack}, - * the behaviour of this method is undefined and an exception may be thrown. - * @param stack the current stack of cards - * @param cards the cards to be played - * @return {@code true} iff the {@code cards} can be played on top of the {@code stack} - */ - public abstract boolean isHigher(@NotNull Stack stack, @NotNull List<@NotNull Card> cards); - // /** * Performs basic checks on a list of cards, i.e. this method checks @@ -377,31 +245,13 @@ public enum CombinationType { if (card.isSpecial()) continue; if (value == null) { - value = card.getValue(); - } else if (!value.equals(card.getValue())) { + value = card.value(); + } else if (!value.equals(card.value())) { return false; } } return true; } - - /** - * Assuming that all the {@linkplain Card#isNormal() normal cards} at the specified indices have the same value - * returns that value. The return value is unspecified if the assumption is violated. - * @param cards a list of cards - * @param indices an array of indices of cards in the list - * @see #isSameValue(List, int...) - * @throws NoSuchElementException if there is no normal card among the specified indices - */ - private static @Range(from = 2, to = 14) int getSameValue(@NotNull List<@NotNull Card> cards, int @NotNull... indices) { - for (int index : indices) { - Card card = cards.get(index); - if (card.isSpecial()) continue; - - return card.getValue(); - } - throw new NoSuchElementException(); - } // } diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Stack.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Stack.java index 6c84950..cabfefe 100644 --- a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Stack.java +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/Stack.java @@ -1,5 +1,6 @@ package eu.jonahbauer.tichu.common.model; +import eu.jonahbauer.tichu.common.model.combinations.Combination; import eu.jonahbauer.tichu.common.model.exceptions.IncompatibleCombinationException; import eu.jonahbauer.tichu.common.model.exceptions.TooLowCombinationException; import lombok.EqualsAndHashCode; @@ -16,10 +17,30 @@ public final class Stack { private final @Unmodifiable @NotNull List<@NotNull Combination> combinations; + /** + * Builds a stack of the given combinations. + * @param combinations an array of combinations from bottom to top + * @return a stack of the given combinations. + * @throws IncompatibleCombinationException if any one of the combinations + * {@link Combination#isCompatibleWith(Stack) is incompatible with} the stack of all the combinations before it + * @throws TooLowCombinationException if any one of the combinations + * {@linkplain Combination#isHigherThan(Stack) is lower than} the stack of alle the combinations before it + * @throws NullPointerException if the array or any of its elements are {@code null} + */ public static @NotNull Stack of(@NotNull Combination @NotNull... combinations) { - return of(Arrays.asList(combinations)); + return of(List.of(combinations)); } + /** + * Builds a stack of the given combinations. + * @param combinations a list of combinations from bottom to top + * @return a stack of the given combinations. + * @throws IncompatibleCombinationException if any one of the combinations + * {@link Combination#isCompatibleWith(Stack) is incompatible with} the stack of all the combinations before it + * @throws TooLowCombinationException if any one of the combinations + * {@linkplain Combination#isHigherThan(Stack) is lower than} the stack of alle the combinations before it + * @throws NullPointerException if the list or any of its elements are {@code null} + */ public static @NotNull Stack of(@NotNull List<@NotNull Combination> combinations) { Stack out = new Stack(List.copyOf(combinations)); // List#copyOf performs null-checks @@ -27,14 +48,20 @@ public final class Stack { Stack lower = out.substack(0, i); Combination top = out.get(i); - if (!top.type().isHigher(lower, top.cards())) { - throw new IllegalArgumentException("Invalid stack: %s cannot be played on top of %s".formatted(top, lower)); + if (!top.isCompatibleWith(lower)) { + throw new IncompatibleCombinationException(lower, top); + } else if (!top.isHigherThan(lower)) { + throw new TooLowCombinationException(lower, top); } } return out; } + /** + * Returns an empty stack. + * @return an empty stack. + */ public static @NotNull Stack empty() { return EMPTY; } @@ -101,35 +128,35 @@ public final class Stack { } /** - * Checks whether the {@code combination} is {@linkplain CombinationType#isCompatible(Stack, List) compatible} with + * Checks whether the {@code combination} {@linkplain Combination#isCompatibleWith(Stack) is compatible with} * this stack. * @param combination a combination * @return {@code true} iff the {@code combination} is compatible - * @see CombinationType#isCompatible(Stack, List) + * @see Combination#isCompatibleWith(Stack) * @see #isHigher(Combination) * @see #isCompatibleAndHigher(Combination) */ @Contract(pure = true) public boolean isCompatible(@NotNull Combination combination) { Objects.requireNonNull(combination, "combination must not be null"); - return combination.type().isCompatible(this, combination.cards()); + return combination.isCompatibleWith(this); } /** - * Checks whether a {@linkplain #isCompatible(Combination) compatible} {@code combination} is - * {@linkplain CombinationType#isHigher(Stack, List) higher} than the {@linkplain #top() topmost} combination + * Checks whether a {@linkplain #isCompatible(Combination) compatible} {@code combination} + * {@linkplain Combination#isHigherThan(Stack) is higher than} the {@linkplain #top() topmost} combination * on this stack. * The behaviour of this method is undefined if the {@code combination} is incompatible and an exception may be thrown. * @param combination a combination * @return {@code true} iff the combination is higher than the topmost combination on this stack - * @see CombinationType#isHigher(Stack, List) + * @see Combination#isHigherThan(Stack) * @see #isCompatible(Combination) * @see #isCompatibleAndHigher(Combination) */ @Contract(pure = true) public boolean isHigher(@NotNull Combination combination) { Objects.requireNonNull(combination, "combination must not be null"); - return combination.type().isHigher(this, combination.cards()); + return combination.isHigherThan(this); } /** @@ -138,14 +165,14 @@ public final class Stack { * than the {@linkplain #top() topmost} combination on this stack. * @param combination a combination * @return {@code true} iff the combination is compatible with and higher than this stack + * @see Combination#isCompatibleAndHigher(Stack) * @see #isHigher(Combination) * @see #isCompatible(Combination) */ @Contract(pure = true) public boolean isCompatibleAndHigher(@NotNull Combination combination) { Objects.requireNonNull(combination, "combination must not be null"); - return combination.type().isCompatible(this, combination.cards()) - && combination.type().isHigher(this, combination.cards()); + return combination.isCompatibleAndHigher(this); } /** diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/AbstractCombination.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/AbstractCombination.java new file mode 100644 index 0000000..8b63e9a --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/AbstractCombination.java @@ -0,0 +1,62 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import eu.jonahbauer.tichu.common.model.exceptions.InvalidCombinationException; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.List; +import java.util.Objects; + +@EqualsAndHashCode +abstract sealed class AbstractCombination implements Combination permits Tuple, Sequence, FullHouse, Single { + private final @NotNull CombinationType type; + private final @Unmodifiable @NotNull List<@NotNull Card> cards; + + AbstractCombination(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards) { + this.type = Objects.requireNonNull(type, "type must not be null"); + this.cards = List.copyOf(Objects.requireNonNull(cards, "cards must not be null")); + + if (!this.type.check(this.cards)) { + throw new InvalidCombinationException(type, cards); + } + } + + @Override + public @NotNull CombinationType type() { + return type; + } + + @Override + public @Unmodifiable @NotNull List<@NotNull Card> cards() { + return cards; + } + + @Override + public final @NotNull Card card(int index) { + return cards.get(index); + } + + @Override + public final int size() { + return cards.size(); + } + + @Override + public boolean isCompatibleWith(@NotNull Stack stack) { + return stack.isEmpty() || stack.top().type() == type() && stack.top().size() == size(); + } + + @Override + public boolean isCompatibleAndHigher(@NotNull Stack stack) { + return isCompatibleWith(stack) && isHigherThan(stack); + } + + @Override + public String toString() { + return type + cards.toString(); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Bomb.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Bomb.java new file mode 100644 index 0000000..8f0612b --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Bomb.java @@ -0,0 +1,15 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.NotNull; + +public sealed interface Bomb extends Combination permits BombTuple, BombSequence{ + @Override + default boolean isCompatibleWith(@NotNull Stack stack) { + return stack.isEmpty() + || stack.top().type() != CombinationType.SINGLE + || stack.top().card(0) != Card.HOUND; + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombSequence.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombSequence.java new file mode 100644 index 0000000..96c7a45 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombSequence.java @@ -0,0 +1,37 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static eu.jonahbauer.tichu.common.model.CombinationType.BOMB_SEQUENCE; + +/** + * A combination of type {@link CombinationType#BOMB_SEQUENCE} + */ +public final class BombSequence extends Sequence implements Bomb { + public static BombSequence of(@NotNull List<@NotNull Card> cards) { + return new BombSequence(cards); + } + + BombSequence(@NotNull List<@NotNull Card> cards) { + super(BOMB_SEQUENCE, cards, 1); + } + + @Override + public boolean isCompatibleWith(@NotNull Stack stack) { + return Bomb.super.isCompatibleWith(stack); + } + + @Override + public boolean isHigherThan(@NotNull Stack stack) { + assert isCompatibleWith(stack); + return stack.isEmpty() + || stack.top().type() != BOMB_SEQUENCE + || size() > stack.top().size() + || start() > ((BombSequence) stack.top()).start(); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombTuple.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombTuple.java new file mode 100644 index 0000000..133dda9 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/BombTuple.java @@ -0,0 +1,47 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +import static eu.jonahbauer.tichu.common.model.CombinationType.BOMB; +import static eu.jonahbauer.tichu.common.model.CombinationType.BOMB_SEQUENCE; + +/** + * A combination of type {@link CombinationType#BOMB} + */ +public final class BombTuple extends Tuple implements Bomb { + public static BombTuple of(@NotNull List<@NotNull Card> cards) { + return new BombTuple(cards); + } + + public static BombTuple of(@NotNull Card card1, @NotNull Card card2, @NotNull Card card3, @NotNull Card card4) { + return new BombTuple(List.of( + Objects.requireNonNull(card1, "card1 must not be null"), + Objects.requireNonNull(card2, "card2 must not be null"), + Objects.requireNonNull(card3, "card3 must not be null"), + Objects.requireNonNull(card4, "card4 must not be null") + )); + } + + BombTuple(@NotNull List<@NotNull Card> cards) { + super(BOMB, cards); + } + + @Override + public boolean isCompatibleWith(@NotNull Stack stack) { + return Bomb.super.isCompatibleWith(stack); + } + + @Override + public boolean isHigherThan(@NotNull Stack stack) { + assert isCompatibleWith(stack); + return stack.isEmpty() + || stack.top().type() != BOMB && stack.top().type() != BOMB_SEQUENCE + || stack.top().type() == BOMB && value() > ((BombTuple) stack.top()).value(); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Combination.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Combination.java new file mode 100644 index 0000000..3d0a519 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Combination.java @@ -0,0 +1,99 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.Serializable; +import java.util.List; + +public sealed interface Combination extends Serializable permits AbstractCombination, Bomb { + /** + * Returns the type of this combination. + * @return the type of this combination. + */ + @NotNull CombinationType type(); + + /** + * Returns the cards composing this combination. + * @return the cards composing this combination. + */ + @Unmodifiable @NotNull List<@NotNull Card> cards(); + + /** + * Returns the card at the specified index. This is shorthand for {@link #cards()}{@code .}{@link List#get(int) get(index)}. + * @param index the index of the card to return + * @return the card at the specified index. + * @throws IndexOutOfBoundsException if the index is out of bounds ({@code index < 0 || index >= size()}) + */ + @NotNull Card card(@Range(from = 0, to = 13) int index); + + /** + * Returns the size of this combination, i.e. the number of cards it consists of. This is shorthand for + * {@link #cards()}{@code .}{@link List#size() size()}. + * @return the size of this combination. + */ + @Range(from = 1, to = 14) int size(); + + /** + * Checks whether this combination is compatible with the given stack. The definition of + * compatible depends on the type of this combination: + *

    + *
  • + * a {@link Bomb} is compatible with any stack that does not have a {@linkplain Single single} + * {@link Card#HOUND} on top. + *
  • + *
  • + * any other combination is compatible with an empty stack and all stacks that have a combination + * of the same {@link #type()} and {@link #size()} on top. + *
  • + *
+ * @param stack a stack of combinations + * @return {@code true}, iff this combination is compatible with the given stack. + */ + boolean isCompatibleWith(@NotNull Stack stack); + + /** + * Checks whether this combination is higher than the given stack, i.e. it could be played on top of + * that stack. This method does not, however, ensure that this combination can be played on top of that + * stack in every circumstance, since mahjong-wishes and right to play are not accounted for. + * + *

This method assumes that this combination {@linkplain #isCompatibleWith(Stack) is compatible with} the + * given stack, otherwise the result is undefined and the method may throw an exception. + * @param stack a stack of combinations + * @return {@code true}, iff this combination is higher than the given stack. + */ + boolean isHigherThan(@NotNull Stack stack); + + /** + * Checks whether this combination is {@linkplain #isCompatibleWith(Stack) compatible with} and + * {@linkplain #isHigherThan(Stack) higher than} the given stack. + * @param stack a stack of combinations + * @return {@code true}, iff this combination is compatible with and higher than the given stack. + */ + boolean isCompatibleAndHigher(@NotNull Stack stack); + + /** + * Creates a new combination of the given type consisting of the given cards. + * @param type a combination type + * @param cards a list of cards forming a {@linkplain CombinationType#check(List) valid} combination + * @return a new combination + */ + @Contract("_, _ -> new") + static Combination of(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards) { + return switch (type) { + case SINGLE -> Single.of(cards); + case PAIR -> Pair.of(cards); + case TRIPLE -> Triple.of(cards); + case FULL_HOUSE -> FullHouse.of(cards); + case SEQUENCE -> SingleSequence.of(cards); + case PAIR_SEQUENCE -> PairSequence.of(cards); + case BOMB -> BombTuple.of(cards); + case BOMB_SEQUENCE -> BombSequence.of(cards); + }; + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/FullHouse.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/FullHouse.java new file mode 100644 index 0000000..97e4339 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/FullHouse.java @@ -0,0 +1,63 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/** + * A combination of type {@link CombinationType#FULL_HOUSE} + */ +public final class FullHouse extends AbstractCombination { + @Contract("_ -> new") + public static @NotNull FullHouse of(@NotNull List<@NotNull Card> cards) { + return new FullHouse(cards); + } + + @Contract("_, _, _, _, _ -> new") + public static @NotNull FullHouse of( + @NotNull Card triple1, @NotNull Card triple2, @NotNull Card triple3, + @NotNull Card pair1, @NotNull Card pair2 + ) { + return new FullHouse(List.of( + Objects.requireNonNull(triple1, "triple1 must not be null"), + Objects.requireNonNull(triple2, "triple2 must not be null"), + Objects.requireNonNull(triple3, "triple3 must not be null"), + Objects.requireNonNull(pair1, "pair1 must not be null"), + Objects.requireNonNull(pair2, "pair2 must not be null") + )); + } + + private final int triple; + private final int pair; + + FullHouse(@NotNull List<@NotNull Card> cards) { + super(CombinationType.FULL_HOUSE, cards); + this.triple = Tuple.getSameValue(cards.subList(0, 3)); + this.pair = Tuple.getSameValue(cards.subList(3, 5)); + } + + /** + * The {@linkplain Card#value() value} of the triple. + */ + public int triple() { + return triple; + } + + /** + * The {@linkplain Card#value() value} of the pair. + */ + public int pair() { + return pair; + } + + @Override + public boolean isHigherThan(@NotNull Stack stack) { + assert isCompatibleWith(stack); + return stack.isEmpty() || triple() > ((FullHouse) stack.top()).triple(); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Pair.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Pair.java new file mode 100644 index 0000000..5d11f14 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Pair.java @@ -0,0 +1,31 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/** + * A combination of type {@link CombinationType#PAIR} + */ +public final class Pair extends Tuple { + @Contract("_ -> new") + public static @NotNull Pair of(@NotNull List<@NotNull Card> cards) { + return new Pair(cards); + } + + @Contract("_, _ -> new") + public static @NotNull Pair of(@NotNull Card card1, @NotNull Card card2) { + return new Pair(List.of( + Objects.requireNonNull(card1, "card1 must not be null"), + Objects.requireNonNull(card2, "card2 must not be null") + )); + } + + Pair(@NotNull List<@NotNull Card> cards) { + super(CombinationType.PAIR, cards); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/PairSequence.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/PairSequence.java new file mode 100644 index 0000000..5b24ecd --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/PairSequence.java @@ -0,0 +1,24 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * A combination of type {@link CombinationType#PAIR_SEQUENCE} + */ +public final class PairSequence extends Sequence { + public static PairSequence of(@NotNull List<@NotNull Card> cards) { + return new PairSequence(cards); + } + + public static PairSequence of(@NotNull Card @NotNull... cards) { + return new PairSequence(List.of(cards)); + } + + PairSequence(@NotNull List<@NotNull Card> cards) { + super(CombinationType.PAIR_SEQUENCE, cards, 2); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Sequence.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Sequence.java new file mode 100644 index 0000000..c899bc8 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Sequence.java @@ -0,0 +1,48 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.NoSuchElementException; + +public abstract sealed class Sequence extends AbstractCombination permits SingleSequence, PairSequence, BombSequence { + private final int start; + private final int length; + + Sequence(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards, int n) { + super(type, cards); + this.length = cards.size() / n; + this.start = getStartValue(cards, n); + } + + /** + * The {@linkplain Card#value() value} of the lowest card in this sequence. + */ + public int start() { + return start; + } + + /** + * The length of this sequence. + */ + public int length() { + return length; + } + + @Override + public boolean isHigherThan(@NotNull Stack stack) { + assert isCompatibleWith(stack); + return stack.isEmpty() || start() > ((Sequence) stack.top()).start(); + } + + static int getStartValue(@NotNull List<@NotNull Card> cards, int n) { + for (int i = 0, size = cards.size(); i < size; i++) { + Integer value = cards.get(i).value(); + if (value != null) return value - (i / n); + } + throw new NoSuchElementException(); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Single.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Single.java new file mode 100644 index 0000000..23679e7 --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Single.java @@ -0,0 +1,70 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/** + * A combination of type {@link CombinationType#SINGLE} + */ +public final class Single extends AbstractCombination { + @Contract("_ -> new") + public static @NotNull Single of(@NotNull List<@NotNull Card> cards) { + return new Single(cards); + } + + @Contract("_ -> new") + public static @NotNull Single of(@NotNull Card card) { + return new Single(List.of(Objects.requireNonNull(card, "card must not be null"))); + } + + Single(@NotNull List<@NotNull Card> cards) { + super(CombinationType.SINGLE, cards); + } + + /** + * Returns the single card this combination consists of. This is shorthand for {@link #card(int) card(0)}. + * @return the single card this combination consists of. + */ + public @NotNull Card card() { + return card(0); + } + + /** + * Returns the {@linkplain Card#value() value} of the {@linkplain #card() card}. + * @return the value of the card. + */ + public Integer value() { + return card().value(); + } + + @Override + public boolean isHigherThan(@NotNull Stack stack) { + assert isCompatibleWith(stack); + if (stack.isEmpty()) return true; // you can play any card onto an empty stack + + var top = (Single) stack.top(); + if (top.card() == Card.HOUND) return false; // you cannot play any card onto a hound + + return switch (card()) { + case HOUND, MAHJONG -> false; + case DRAGON -> true; + case PHOENIX -> top.card() != Card.DRAGON; + default -> { + if (top.card() != Card.PHOENIX) { + yield value() > top.value(); + } else if (stack.size() == 1) { + // every normal card has a value of at least 2, which is higher that the phoenix 1½ + yield true; + } else { + yield value() > ((Single) stack.top(-1)).value(); + } + } + }; + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/SingleSequence.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/SingleSequence.java new file mode 100644 index 0000000..ce3471a --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/SingleSequence.java @@ -0,0 +1,24 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * A combination of type {@link CombinationType#SEQUENCE} + */ +public final class SingleSequence extends Sequence { + public static SingleSequence of(@NotNull List<@NotNull Card> cards) { + return new SingleSequence(cards); + } + + public static SingleSequence of(@NotNull Card @NotNull... cards) { + return new SingleSequence(List.of(cards)); + } + + SingleSequence(@NotNull List<@NotNull Card> cards) { + super(CombinationType.SEQUENCE, cards, 1); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Triple.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Triple.java new file mode 100644 index 0000000..536286b --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Triple.java @@ -0,0 +1,32 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/** + * A combination of type {@link CombinationType#TRIPLE} + */ +public final class Triple extends Tuple { + @Contract("_ -> new") + public static @NotNull Triple of(@NotNull List<@NotNull Card> cards) { + return new Triple(cards); + } + + @Contract("_, _, _ -> new") + public static @NotNull Triple of(@NotNull Card card1, @NotNull Card card2, @NotNull Card card3) { + return new Triple(List.of( + Objects.requireNonNull(card1, "card1 must not be null"), + Objects.requireNonNull(card2, "card2 must not be null"), + Objects.requireNonNull(card3, "card3 must not be null") + )); + } + + Triple(@NotNull List<@NotNull Card> cards) { + super(CombinationType.TRIPLE, cards); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Tuple.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Tuple.java new file mode 100644 index 0000000..7893b7b --- /dev/null +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/combinations/Tuple.java @@ -0,0 +1,46 @@ +package eu.jonahbauer.tichu.common.model.combinations; + +import eu.jonahbauer.tichu.common.model.Card; +import eu.jonahbauer.tichu.common.model.CombinationType; +import eu.jonahbauer.tichu.common.model.Stack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.NoSuchElementException; + +public abstract sealed class Tuple extends AbstractCombination permits Pair, Triple, BombTuple { + private final int value; + + Tuple(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards) { + super(type, cards); + this.value = getSameValue(cards); + } + + /** + * The {@linkplain Card#value() value} of the cards in this tuple. + */ + public int value() { + return value; + } + + @Override + public boolean isHigherThan(@NotNull Stack stack) { + assert isCompatibleWith(stack); + return stack.isEmpty() || value() > ((Tuple) stack.top()).value(); + } + + /** + * Assuming that all the {@linkplain Card#isNormal() normal cards} at the specified indices have the same value + * returns that value. The return value is unspecified if the assumption is violated. + * @param cards a list of cards + * @throws NoSuchElementException if there is no normal card among the specified indices + */ + static int getSameValue(@NotNull List<@NotNull Card> cards) { + for (var card : cards) { + if (card.isSpecial()) continue; + + return card.value(); + } + throw new NoSuchElementException(); + } +} diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/IncompatibleCombinationException.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/IncompatibleCombinationException.java index ae50055..f49bd2f 100644 --- a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/IncompatibleCombinationException.java +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/IncompatibleCombinationException.java @@ -1,7 +1,7 @@ package eu.jonahbauer.tichu.common.model.exceptions; -import eu.jonahbauer.tichu.common.model.Combination; import eu.jonahbauer.tichu.common.model.Stack; +import eu.jonahbauer.tichu.common.model.combinations.Combination; import lombok.Getter; import org.jetbrains.annotations.NotNull; diff --git a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/TooLowCombinationException.java b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/TooLowCombinationException.java index cd29f1c..b905159 100644 --- a/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/TooLowCombinationException.java +++ b/tichu-common/src/main/java/eu/jonahbauer/tichu/common/model/exceptions/TooLowCombinationException.java @@ -1,7 +1,7 @@ package eu.jonahbauer.tichu.common.model.exceptions; -import eu.jonahbauer.tichu.common.model.Combination; import eu.jonahbauer.tichu.common.model.Stack; +import eu.jonahbauer.tichu.common.model.combinations.Combination; import lombok.Getter; import org.jetbrains.annotations.NotNull; diff --git a/tichu-common/src/main/java/module-info.java b/tichu-common/src/main/java/module-info.java index e7832da..5db5117 100644 --- a/tichu-common/src/main/java/module-info.java +++ b/tichu-common/src/main/java/module-info.java @@ -1,6 +1,7 @@ module eu.jonahbauer.tichu.common { exports eu.jonahbauer.tichu.common.model; exports eu.jonahbauer.tichu.common.model.exceptions; + exports eu.jonahbauer.tichu.common.model.combinations; requires static lombok; requires static org.jetbrains.annotations; diff --git a/tichu-common/src/test/java/eu/jonahbauer/tichu/common/model/CombinationTest.java b/tichu-common/src/test/java/eu/jonahbauer/tichu/common/model/CombinationTest.java index cc6dbd0..58d5c32 100644 --- a/tichu-common/src/test/java/eu/jonahbauer/tichu/common/model/CombinationTest.java +++ b/tichu-common/src/test/java/eu/jonahbauer/tichu/common/model/CombinationTest.java @@ -1,5 +1,6 @@ package eu.jonahbauer.tichu.common.model; +import eu.jonahbauer.tichu.common.model.combinations.*; import eu.jonahbauer.tichu.common.model.exceptions.IncompatibleCombinationException; import eu.jonahbauer.tichu.common.model.exceptions.InvalidCombinationException; import eu.jonahbauer.tichu.common.model.exceptions.TooLowCombinationException; @@ -28,7 +29,7 @@ public class CombinationTest { .map(suit -> IntStream.rangeClosed(2, 14) .mapToObj(Card::getCardsByValue) .flatMap(cards -> cards.stream() - .filter(card -> card.getSuit() == suit) + .filter(card -> card.suit() == suit) ) .map(List::of) .toList() @@ -191,13 +192,13 @@ public class CombinationTest { VALID_COMBINATIONS.get(type).forEach(combinations -> { Stack stack = Stack.empty(); for (int i = 0; i < combinations.size(); i++) { - stack = stack.put(new Combination(type, combinations.get(i))); + stack = stack.put(Combination.of(type, combinations.get(i))); for (int j = 0; j < i; j++) { - assertIsNotHigher(stack, type, combinations.get(j)); + assertIsNotHigher(stack, Combination.of(type, combinations.get(j))); } for (int j = i + 1; j < combinations.size(); j++) { - assertIsHigher(stack, type, combinations.get(j)); + assertIsHigher(stack, Combination.of(type, combinations.get(j))); } } }); @@ -209,30 +210,32 @@ public class CombinationTest { .flatMap(entry -> entry.getValue().stream() .flatMap(List::stream) .filter(not(cards -> cards.size() == 1 && cards.get(0) == HOUND)) // hound cannot be bombed - .map(cards -> new Combination(entry.getKey(), cards)) + .map(cards -> Combination.of(entry.getKey(), cards)) ) .map(Stack::of) .toList(); - Stack hound = Stack.of(Combination.single(HOUND)); + Stack hound = Stack.of(Single.of(HOUND)); VALID_COMBINATIONS.get(BOMB).stream() .flatMap(List::stream) + .map(BombTuple::of) .forEach(bomb -> { - stacks.forEach(stack -> assertIsCompatible(stack, BOMB, bomb)); - assertIsIncompatible(hound, BOMB, bomb); + stacks.forEach(stack -> assertIsCompatible(stack, bomb)); + assertIsIncompatible(hound, bomb); }); VALID_COMBINATIONS.get(BOMB_SEQUENCE).stream() .flatMap(List::stream) + .map(BombSequence::of) .forEach(bomb -> { - stacks.forEach(stack -> assertIsCompatible(stack, BOMB_SEQUENCE, bomb)); - assertIsIncompatible(hound, BOMB_SEQUENCE, bomb); + stacks.forEach(stack -> assertIsCompatible(stack, bomb)); + assertIsIncompatible(hound, bomb); }); } private static void assertValidCombination(CombinationType type, List cards) { try { - new Combination(type, cards); + Combination.of(type, cards); } catch (InvalidCombinationException e) { fail("%s should be a valid %s.".formatted(cards, type), e); } @@ -245,7 +248,7 @@ public class CombinationTest { private static void assertInvalidCombination(CombinationType type, List cards) { assertThrows(InvalidCombinationException.class, () -> { - new Combination(type, cards); + Combination.of(type, cards); }, "%s should not be a valid %s.".formatted(cards, type)); assertFalse( @@ -255,79 +258,94 @@ public class CombinationTest { } private static void assertIsCompatible(CombinationType type, List stack, List cards) { - assertIsCompatible(Stack.of(new Combination(type, stack)), type, cards); + assertIsCompatible(Stack.of(Combination.of(type, stack)), Combination.of(type, cards)); } - private static void assertIsCompatible(Stack stack, CombinationType type, List cards) { + private static void assertIsCompatible(Stack stack, Combination combination) { assertTrue( - type.isCompatible(stack, cards), - "%s should be compatible with %s".formatted(cards, stack) + combination.isCompatibleWith(stack), + "%s should be compatible with %s".formatted(combination, stack) ); assertTrue( - stack.isCompatible(new Combination(type, cards)), - "%s should be compatible with %s".formatted(cards, stack) + stack.isCompatible(combination), + "%s should be compatible with %s".formatted(combination, stack) ); } - private static void assertIsIncompatible(Stack stack, CombinationType type, List cards) { + private static void assertIsIncompatible(Stack stack, Combination combination) { assertFalse( - type.isCompatible(stack, cards), - "%s should be incompatible with %s".formatted(cards, stack) + stack.isCompatible(combination), + "%s should be incompatible with %s".formatted(combination, stack) ); assertFalse( - stack.isCompatible(new Combination(type, cards)), - "%s should be incompatible with %s".formatted(cards, stack) + stack.isCompatibleAndHigher(combination), + "%s should be incompatible with %s".formatted(combination, stack) ); assertFalse( - stack.isCompatibleAndHigher(new Combination(type, cards)), - "%s should be incompatible with %s".formatted(cards, stack) + combination.isCompatibleWith(stack), + "%s should be incompatible with %s".formatted(combination, stack) + ); + + assertFalse( + combination.isCompatibleAndHigher(stack), + "%s should be incompatible with %s".formatted(combination, stack) ); assertThrows(IncompatibleCombinationException.class, () -> { //noinspection ResultOfMethodCallIgnored - stack.put(new Combination(type, cards)); - }, "%s should be incompatible with %s".formatted(cards, stack)); + stack.put(combination); + }, "%s should be incompatible with %s".formatted(combination, stack)); } - private static void assertIsHigher(Stack stack, CombinationType type, List cards) { + private static void assertIsHigher(Stack stack, Combination combination) { assertTrue( - type.isHigher(stack, cards), - "%s should be higher than %s".formatted(cards, stack) + combination.isHigherThan(stack), + "%s should be higher than %s".formatted(combination, stack) ); assertTrue( - stack.isHigher(new Combination(type, cards)), - "%s should be higher than %s".formatted(cards, stack) + stack.isHigher(combination), + "%s should be higher than %s".formatted(combination, stack) ); assertTrue( - stack.isCompatibleAndHigher(new Combination(type, cards)), - "%s should be higher than %s".formatted(cards, stack) + combination.isCompatibleAndHigher(stack), + "%s should be higher than %s".formatted(combination, stack) + ); + + assertTrue( + stack.isCompatibleAndHigher(combination), + "%s should be higher than %s".formatted(combination, stack) ); } - private static void assertIsNotHigher(Stack stack, CombinationType type, List cards) { + private static void assertIsNotHigher(Stack stack, Combination combination) { + assertFalse( + combination.isHigherThan(stack), + "%s should not be higher than %s".formatted(combination, stack) + ); + assertFalse( - type.isHigher(stack, cards), - "%s should not be higher than %s".formatted(cards, stack) + stack.isHigher(combination), + "%s should not be higher than %s".formatted(combination, stack) ); assertFalse( - stack.isHigher(new Combination(type, cards)), - "%s should not be higher than %s".formatted(cards, stack) + combination.isCompatibleAndHigher(stack), + "%s should not be higher than %s".formatted(combination, stack) ); assertFalse( - stack.isCompatibleAndHigher(new Combination(type, cards)), - "%s should not be higher than %s".formatted(cards, stack) + stack.isCompatibleAndHigher(combination), + "%s should not be higher than %s".formatted(combination, stack) ); assertThrows(TooLowCombinationException.class, () -> { //noinspection ResultOfMethodCallIgnored - stack.put(new Combination(type, cards)); + stack.put(combination); }); } }