implement dedicated classes for each combination type
This commit is contained in:
parent
ae7890375e
commit
0d1f51d868
@ -37,7 +37,7 @@ public enum Card {
|
||||
private static final Map<Integer, Set<Card>> 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 {
|
||||
* </ul>
|
||||
* @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
|
||||
*/
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
//<editor-fold desc="Named Constructors" defaultstate="collapsed">
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
@ -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<Card> 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<Card> 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);
|
||||
|
||||
/**
|
||||
* <p>Checks whether the {@code cards} are <i>compatible</i> 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.
|
||||
* <p>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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether the {@code cards} are higher than the combination currently on top of the {@code stack}.
|
||||
* <p>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);
|
||||
|
||||
//<editor-fold desc="Utility Methods" defaultstate="collapsed">
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 <em>compatible</em> with the given stack. The definition of
|
||||
* <em>compatible</em> depends on the type of this combination:
|
||||
* <ul>
|
||||
* <li>
|
||||
* a {@link Bomb} is compatible with any stack that does not have a {@linkplain Single single}
|
||||
* {@link Card#HOUND} on top.
|
||||
* </li>
|
||||
* <li>
|
||||
* 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.
|
||||
* </li>
|
||||
* </ul>
|
||||
* @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 <em>higher</em> than the given stack, i.e. it could be played on top of
|
||||
* that stack. This method does not, however, ensure that this combination <em>can</em> be played on top of that
|
||||
* stack in every circumstance, since mahjong-wishes and right to play are not accounted for.
|
||||
*
|
||||
* <p>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);
|
||||
};
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<Card> 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<Card> 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<Card> stack, List<Card> 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<Card> 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<Card> 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<Card> 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<Card> cards) {
|
||||
private static void assertIsNotHigher(Stack stack, Combination combination) {
|
||||
assertFalse(
|
||||
type.isHigher(stack, cards),
|
||||
"%s should not be higher than %s".formatted(cards, stack)
|
||||
combination.isHigherThan(stack),
|
||||
"%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)
|
||||
stack.isHigher(combination),
|
||||
"%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)
|
||||
combination.isCompatibleAndHigher(stack),
|
||||
"%s should not be higher than %s".formatted(combination, stack)
|
||||
);
|
||||
|
||||
assertFalse(
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user