implement card, combination and stack
parent
9cd6d2babe
commit
ae7890375e
@ -1,19 +1,71 @@
|
||||
plugins {
|
||||
id("java")
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("org.owasp:dependency-check-gradle:6.5.0.1")
|
||||
}
|
||||
}
|
||||
|
||||
group = "eu.jonahbauer"
|
||||
version = "1.0-SNAPSHOT"
|
||||
subprojects {
|
||||
apply(plugin = "java-library")
|
||||
apply(plugin = "org.owasp.dependencycheck")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
group = "eu.jonahbauer"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
|
||||
}
|
||||
val implementation by configurations
|
||||
val testImplementation by configurations
|
||||
val compileOnly by configurations
|
||||
val annotationProcessor by configurations
|
||||
val runtimeOnly by configurations
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains:annotations:23.0.0")
|
||||
|
||||
implementation("org.apache.logging.log4j:log4j-api:2.19.0")
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-core:2.19.0")
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.19.0")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.1")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.1")
|
||||
|
||||
compileOnly("org.projectlombok:lombok:1.18.24")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.24")
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
withType<Jar> {
|
||||
onlyIf {
|
||||
!project.the<SourceSetContainer>()["main"].allSource.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
named<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
named("check") {
|
||||
dependsOn("dependencyCheckAnalyze")
|
||||
}
|
||||
}
|
||||
|
||||
configure<JavaPluginExtension> {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
tasks.getByName<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
configure<org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension> {
|
||||
format = org.owasp.dependencycheck.reporting.ReportGenerator.Format.JUNIT
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
rootProject.name = "tichu"
|
||||
|
||||
include(":tichu-common")
|
@ -1,7 +0,0 @@
|
||||
package eu.jonahbauer;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello world!");
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum Card {
|
||||
HOUND(0, null),
|
||||
MAHJONG(1, null),
|
||||
PHOENIX(null, null),
|
||||
DRAGON(15, null),
|
||||
GREEN_2(2, Suit.GREEN), RED_2(2, Suit.RED), BLACK_2(2, Suit.BLACK), BLUE_2(2, Suit.BLUE),
|
||||
GREEN_3(3, Suit.GREEN), RED_3(3, Suit.RED), BLACK_3(3, Suit.BLACK), BLUE_3(3, Suit.BLUE),
|
||||
GREEN_4(4, Suit.GREEN), RED_4(4, Suit.RED), BLACK_4(4, Suit.BLACK), BLUE_4(4, Suit.BLUE),
|
||||
GREEN_5(5, Suit.GREEN), RED_5(5, Suit.RED), BLACK_5(5, Suit.BLACK), BLUE_5(5, Suit.BLUE),
|
||||
GREEN_6(6, Suit.GREEN), RED_6(6, Suit.RED), BLACK_6(6, Suit.BLACK), BLUE_6(6, Suit.BLUE),
|
||||
GREEN_7(7, Suit.GREEN), RED_7(7, Suit.RED), BLACK_7(7, Suit.BLACK), BLUE_7(7, Suit.BLUE),
|
||||
GREEN_8(8, Suit.GREEN), RED_8(8, Suit.RED), BLACK_8(8, Suit.BLACK), BLUE_8(8, Suit.BLUE),
|
||||
GREEN_9(9, Suit.GREEN), RED_9(9, Suit.RED), BLACK_9(9, Suit.BLACK), BLUE_9(9, Suit.BLUE),
|
||||
GREEN_10(10, Suit.GREEN), RED_10(10, Suit.RED), BLACK_10(10, Suit.BLACK), BLUE_10(10, Suit.BLUE),
|
||||
GREEN_JACK(11, Suit.GREEN), RED_JACK(11, Suit.RED), BLACK_JACK(11, Suit.BLACK), BLUE_JACK(11, Suit.BLUE),
|
||||
GREEN_QUEEN(12, Suit.GREEN), RED_QUEEN(12, Suit.RED), BLACK_QUEEN(12, Suit.BLACK), BLUE_QUEEN(12, Suit.BLUE),
|
||||
GREEN_KING(13, Suit.GREEN), RED_KING(13, Suit.RED), BLACK_KING(13, Suit.BLACK), BLUE_KING(13, Suit.BLUE),
|
||||
GREEN_ACE(14, Suit.GREEN), RED_ACE(14, Suit.RED), BLACK_ACE(14, Suit.BLACK), BLUE_ACE(14, Suit.BLUE);
|
||||
|
||||
private static final Set<Card> NORMAL_CARDS = Arrays.stream(values())
|
||||
.filter(Card::isNormal)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
private static final Set<Card> SPECIAL_CARDS = Arrays.stream(values())
|
||||
.filter(Card::isSpecial)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
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()),
|
||||
Collections::unmodifiableMap
|
||||
));
|
||||
|
||||
|
||||
private final Integer value;
|
||||
private final Suit suit;
|
||||
|
||||
/**
|
||||
* Checks whether this card is a special card, i.e. one of {@link #MAHJONG}, {@link #PHOENIX}, {@link #DRAGON} or
|
||||
* {@link #HOUND}.
|
||||
* @return {@code true} iff this card is a special card.
|
||||
* @see #isNormal()
|
||||
*/
|
||||
public boolean isSpecial() {
|
||||
return suit == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this card is a normal card, i.e. none of {@link #MAHJONG}, {@link #PHOENIX}, {@link #DRAGON} or
|
||||
* {@link #HOUND}.
|
||||
* @return {@code true} iff this card is a normal card.
|
||||
* @see #isSpecial()
|
||||
*/
|
||||
public boolean isNormal() {
|
||||
return suit != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of this card. The value is
|
||||
* <ul>
|
||||
* <li>{@code 0} for {@link #HOUND}</li>
|
||||
* <li>{@code 1} for {@link #MAHJONG}</li>
|
||||
* <li>{@code null} for {@link #PHOENIX}</li>
|
||||
* <li>{@code 15} for {@link #DRAGON}</li>
|
||||
* <li>obvious for any {@linkplain #isNormal() normal card}</li>
|
||||
* </ul>
|
||||
* @return the value of this card
|
||||
*/
|
||||
public @Range(from = 1, to = 15) Integer getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suit of this {@linkplain #isNormal() normal card} or {@code null} for
|
||||
* {@linkplain #isSpecial() special cards}.
|
||||
* @return the suit of this card
|
||||
*/
|
||||
public Suit getSuit() {
|
||||
return suit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable set of all {@linkplain #isNormal() normal cards}.
|
||||
* @return a set of cards
|
||||
*/
|
||||
public static @Unmodifiable @NotNull Set<@NotNull Card> getNormalCards() {
|
||||
return NORMAL_CARDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable set of all {@linkplain #isSpecial() special cards}.
|
||||
* @return a set of cards
|
||||
*/
|
||||
public static @Unmodifiable @NotNull Set<@NotNull Card> getSpecialCards() {
|
||||
return SPECIAL_CARDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable set of all {@linkplain #isNormal() normal cards} with the specified
|
||||
* {@linkplain #getValue() value}.
|
||||
* @param value the card value between {@code 2} and {@code 14} (inclusive)
|
||||
* @return a set of cards
|
||||
*/
|
||||
public static @Unmodifiable @NotNull Set<@NotNull Card> getCardsByValue(@Range(from = 2, to = 14) int value) {
|
||||
return CARDS_BY_VALUE.getOrDefault(value, Collections.emptySet());
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
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>
|
||||
}
|
@ -0,0 +1,407 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Range;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A type of combination.
|
||||
*/
|
||||
public enum CombinationType {
|
||||
/**
|
||||
* A single card, {@linkplain Card#isNormal() normal} or {@linkplain Card#isSpecial() special}. This is the
|
||||
* correct {@code CombinationType} for playing the {@link Card#HOUND} or {@link Card#DRAGON}.
|
||||
*/
|
||||
SINGLE {
|
||||
private static final Set<Card> SPECIAL_CARDS = EnumSet.of(Card.PHOENIX, Card.MAHJONG, Card.DRAGON, Card.HOUND);
|
||||
|
||||
@Override
|
||||
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
|
||||
* {@link Card#PHOENIX} and a normal card.
|
||||
*/
|
||||
PAIR {
|
||||
private static final Set<Card> SPECIAL_CARDS = EnumSet.of(Card.PHOENIX);
|
||||
private static final int[] INDICES = new int[] {0, 1};
|
||||
|
||||
@Override
|
||||
public boolean check(@NotNull List<@NotNull Card> cards) {
|
||||
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}
|
||||
* or the {@linkplain Card#PHOENIX} and two normal cards of the same value.
|
||||
*/
|
||||
TRIPLE {
|
||||
private static final Set<Card> SPECIAL_CARDS = EnumSet.of(Card.PHOENIX);
|
||||
private static final int[] INDICES = new int[] {0, 1, 2};
|
||||
|
||||
@Override
|
||||
public boolean check(@NotNull List<@NotNull Card> cards) {
|
||||
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}.
|
||||
* @apiNote A list of cards representing a full house must have the triple at the first three indices and the pair
|
||||
* at the last two. This prevents any ambiguity, e.g. when the full house consists of two pairs and the phoenix.
|
||||
*/
|
||||
FULL_HOUSE {
|
||||
private static final Set<Card> SPECIAL_CARDS = EnumSet.of(Card.PHOENIX);
|
||||
|
||||
private static final int[] INDICES_TRIPLE = new int[] {0, 1, 2};
|
||||
private static final int[] INDICES_PAIR = new int[] {3, 4};
|
||||
|
||||
@Override
|
||||
public boolean check(@NotNull List<@NotNull Card> cards) {
|
||||
return checkBasics(cards, 5, SPECIAL_CARDS)
|
||||
&& 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
|
||||
* {@link Card#MAHJONG} and one of the cards may be the {@link Card#PHOENIX}.
|
||||
* @apiNote A list of cards representing a sequence must be sorted by ascending value. This allows to distinguish
|
||||
* between a sequence starting with the phoenix and a sequence ending with the phoenix.
|
||||
*/
|
||||
SEQUENCE {
|
||||
private static final Set<Card> SPECIAL_CARDS = EnumSet.of(Card.PHOENIX, Card.MAHJONG);
|
||||
|
||||
private int getStartValue(@NotNull List<@NotNull Card> cards) {
|
||||
return cards.get(0) == Card.PHOENIX
|
||||
? cards.get(1).getValue() - 1
|
||||
: cards.get(0).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(@NotNull List<@NotNull Card> cards) {
|
||||
if (!checkBasics(cards, null, SPECIAL_CARDS)) return false;
|
||||
if (cards.size() < 5) return false;
|
||||
|
||||
int start = getStartValue(cards);
|
||||
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
var card = cards.get(i);
|
||||
if (card == Card.PHOENIX) {
|
||||
if (start + i > 14) {
|
||||
return false;
|
||||
}
|
||||
} else if (card.getValue() != 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.
|
||||
* @apiNote Although not strictly necessary, for consistency with {@link #SEQUENCE} a list of cards representing
|
||||
* a sequence of pairs must be sorted by ascending value.
|
||||
*/
|
||||
PAIR_SEQUENCE {
|
||||
private static final Set<Card> SPECIAL_CARDS = EnumSet.of(Card.PHOENIX);
|
||||
|
||||
private int getStartValue(@NotNull List<@NotNull Card> cards) {
|
||||
return cards.get(0) == Card.PHOENIX
|
||||
? cards.get(1).getValue()
|
||||
: cards.get(0).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(@NotNull List<@NotNull Card> cards) {
|
||||
if (!checkBasics(cards, null, SPECIAL_CARDS)) return false;
|
||||
if (cards.size() < 4 || cards.size() % 2 == 1) return false;
|
||||
|
||||
int start = getStartValue(cards);
|
||||
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
var card = cards.get(i);
|
||||
|
||||
if (card != Card.PHOENIX && card.getValue() != 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}.
|
||||
*/
|
||||
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
|
||||
* {@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.
|
||||
* @apiNote
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
int start = getStartValue(cards);
|
||||
for (int i = 1; i < cards.size(); i++) {
|
||||
var card = cards.get(i);
|
||||
if (card.getValue() != 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());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the given cards form a valid combination of this type. See the documentation of a specific
|
||||
* {@code CombinationType} for more information on what cards form a combination of that particular type.
|
||||
* @param cards a list of cards
|
||||
* @return {@code true} if the cards form a valid combination of this type.
|
||||
*/
|
||||
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
|
||||
* <ul>
|
||||
* <li>that the list does not contain any duplicate cards</li>
|
||||
* <li>when {@code count != null}, that the list is of size {@code count}</li>
|
||||
* <li>
|
||||
* that the list does not contain any {@linkplain Card#isSpecial() special card} other than those in
|
||||
* {@code special}
|
||||
* </li>
|
||||
* </ul>
|
||||
* @param cards the list of cards
|
||||
* @param count the expected number of cards, or {@code null} when the number of cards should not be checked
|
||||
* @param special the set of allowed special cards or {@code null}, when all special cards are allowed.
|
||||
*/
|
||||
private static boolean checkBasics(@NotNull List<@NotNull Card> cards, Integer count, @NotNull Set<@NotNull Card> special) {
|
||||
if (count != null && cards.size() != count) return false;
|
||||
if (containsDuplicates(cards)) return false;
|
||||
|
||||
for (Card card : cards) {
|
||||
if (card.isSpecial() && !special.contains(card)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the list contains any duplicate cards, i.e. there exists {@code a} and {@code b} such that
|
||||
* <pre><code>cards.contains(a) && cards.contains(b) && a.equals(b)</code></pre>
|
||||
* @param cards a list of cards
|
||||
* @return {@code true} iff the list contains duplicates.
|
||||
*/
|
||||
private static boolean containsDuplicates(@NotNull List<@NotNull Card> cards) {
|
||||
if (cards.size() <= 1) return false;
|
||||
var set = EnumSet.noneOf(Card.class);
|
||||
for (Card card : cards) {
|
||||
if (!set.add(card)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the cards at the specified indices all have the same value.
|
||||
* {@linkplain Card#isSpecial() Special cards} are ignored.
|
||||
* @param cards a list of cards
|
||||
* @param indices an array of indices of cards in the list
|
||||
* @return {@code true} iff all of the specified cards have the same value
|
||||
*/
|
||||
private static boolean isSameValue(@NotNull List<@NotNull Card> cards, int @NotNull... indices) {
|
||||
Integer value = null;
|
||||
|
||||
for (int index : indices) {
|
||||
Card card = cards.get(index);
|
||||
if (card.isSpecial()) continue;
|
||||
|
||||
if (value == null) {
|
||||
value = card.getValue();
|
||||
} else if (!value.equals(card.getValue())) {
|
||||
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>
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.exceptions.IncompatibleCombinationException;
|
||||
import eu.jonahbauer.tichu.common.model.exceptions.TooLowCombinationException;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public final class Stack {
|
||||
private static final Stack EMPTY = new Stack(Collections.emptyList());
|
||||
|
||||
private final @Unmodifiable @NotNull List<@NotNull Combination> combinations;
|
||||
|
||||
public static @NotNull Stack of(@NotNull Combination @NotNull... combinations) {
|
||||
return of(Arrays.asList(combinations));
|
||||
}
|
||||
|
||||
public static @NotNull Stack of(@NotNull List<@NotNull Combination> combinations) {
|
||||
Stack out = new Stack(List.copyOf(combinations)); // List#copyOf performs null-checks
|
||||
|
||||
for (int i = 0; i < out.size(); i++) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static @NotNull Stack empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
private Stack(@Unmodifiable @NotNull List<@NotNull Combination> combinations) {
|
||||
this.combinations = combinations;
|
||||
}
|
||||
|
||||
@Contract(value = "_, _ -> new", pure = true)
|
||||
public @NotNull Stack substack(int fromIndex, int toIndex) {
|
||||
return new Stack(combinations.subList(fromIndex, toIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements on this stack.
|
||||
* @return the number of elements on this stack
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public int size() {
|
||||
return combinations.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this stack contains no elements.
|
||||
* @return {@code true} if this stack contains no elements
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public boolean isEmpty() {
|
||||
return combinations.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code index}-th Combination from the bottom of this stack.
|
||||
* @param index the index
|
||||
* @return a combination on this stack
|
||||
* @throws IndexOutOfBoundsException if the index is out of range ({@code index < 0 || index >= size()})
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public @NotNull Combination get(@Range(from = 0, to = Integer.MAX_VALUE) int index) {
|
||||
return combinations.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the topmost {@link Combination} on this stack. This is equivalent to {@link #top(int) top(0)}.
|
||||
* @return the topmost combination
|
||||
* @throws IndexOutOfBoundsException if the stack is {@linkplain #isEmpty() empty}
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public @NotNull Combination top() {
|
||||
return top(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ({@code -index})-th {@link Combination} from the top of this stack, i.e. {@code top(0)} returns the
|
||||
* topmost combination, {@code top(-1)} returns the second-topmost combination, and so on.
|
||||
* @param index the index
|
||||
* @return a combination on this stack
|
||||
* @throws IndexOutOfBoundsException if the index is out of range ({@code index > 0 || index <= -size()})
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public @NotNull Combination top(@Range(from = Integer.MIN_VALUE, to = 0) int index) {
|
||||
if (size() <= -index) throw new IndexOutOfBoundsException(index);
|
||||
return combinations.get(combinations.size() - 1 + index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the {@code combination} is {@linkplain CombinationType#isCompatible(Stack, List) compatible} with
|
||||
* this stack.
|
||||
* @param combination a combination
|
||||
* @return {@code true} iff the {@code combination} is compatible
|
||||
* @see CombinationType#isCompatible(Stack, List)
|
||||
* @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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a {@linkplain #isCompatible(Combination) compatible} {@code combination} is
|
||||
* {@linkplain CombinationType#isHigher(Stack, List) 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 #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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a {@code combination} can be played onto this stack, i.e. the {@code combination} is
|
||||
* {@linkplain #isCompatible(Combination) compatible} with this stack and {@linkplain #isHigher(Combination) higher}
|
||||
* 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 #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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the {@code combination} on top of this stack and returns a new, modified stack.
|
||||
* @param combination a combination
|
||||
* @return a new, modified stack
|
||||
* @throws IncompatibleCombinationException if the {@code combination} is
|
||||
* {@linkplain #isCompatible(Combination) incompatible}
|
||||
* @throws IllegalArgumentException if the {@code combination} is not {@linkplain #isHigher(Combination) higher}
|
||||
* than the combination currently on top of this tack
|
||||
*/
|
||||
@Contract(value = "_ -> new", pure = true)
|
||||
public @NotNull Stack put(@NotNull Combination combination) {
|
||||
Objects.requireNonNull(combination, "combination must not be null");
|
||||
if (!isCompatible(combination)) {
|
||||
throw new IncompatibleCombinationException(this, combination);
|
||||
} else if (!isHigher(combination)) {
|
||||
throw new TooLowCombinationException(this, combination);
|
||||
}
|
||||
|
||||
// copy
|
||||
var arr = combinations.toArray(new Combination[combinations.size() + 1]);
|
||||
arr[arr.length - 1] = combination;
|
||||
return new Stack(Arrays.asList(arr));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
StringBuilder out = new StringBuilder("Stack[");
|
||||
for (int i = combinations.size(); i --> 0;) {
|
||||
out.append("\n ").append(combinations.get(i));
|
||||
}
|
||||
out.append("\n]");
|
||||
return out.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
public enum Suit {
|
||||
GREEN, RED, BLACK, BLUE
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.jonahbauer.tichu.common.model.exceptions;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Combination;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
public class IncompatibleCombinationException extends IllegalArgumentException {
|
||||
private final @NotNull Stack stack;
|
||||
private final @NotNull Combination combination;
|
||||
|
||||
public IncompatibleCombinationException(@NotNull Stack stack, @NotNull Combination combination) {
|
||||
this.stack = Objects.requireNonNull(stack, "stack must not be null");
|
||||
this.combination = Objects.requireNonNull(combination, "combination must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Cannot play %s on top of %s.".formatted(combination, stack);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package eu.jonahbauer.tichu.common.model.exceptions;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
public class InvalidCombinationException extends IllegalArgumentException {
|
||||
private final @NotNull CombinationType type;
|
||||
private final @NotNull List<@NotNull Card> cards;
|
||||
|
||||
public InvalidCombinationException(@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
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "%s does not compose a valid %s".formatted(cards, type);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.jonahbauer.tichu.common.model.exceptions;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Combination;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
public class TooLowCombinationException extends IllegalArgumentException {
|
||||
private final @NotNull Stack stack;
|
||||
private final @NotNull Combination combination;
|
||||
|
||||
public TooLowCombinationException(@NotNull Stack stack, @NotNull Combination combination) {
|
||||
this.stack = Objects.requireNonNull(stack, "stack must not be null");
|
||||
this.combination = Objects.requireNonNull(combination, "combination must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Cannot play %s on top of %s.".formatted(combination, stack);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
module eu.jonahbauer.tichu.common {
|
||||
exports eu.jonahbauer.tichu.common.model;
|
||||
exports eu.jonahbauer.tichu.common.model.exceptions;
|
||||
|
||||
requires static lombok;
|
||||
requires static org.jetbrains.annotations;
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class CardTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(Card.class)
|
||||
public void testGetNormalCardsContainsAllNormalCards(Card card) {
|
||||
Set<Card> normalCards = Card.getNormalCards();
|
||||
|
||||
if (card.isNormal()) {
|
||||
assertTrue(
|
||||
normalCards.contains(card),
|
||||
"getNormalCards() should contain normal card %s".formatted(card)
|
||||
);
|
||||
} else {
|
||||
assertFalse(
|
||||
normalCards.contains(card),
|
||||
"getNormalCards() should not contain non-normal card %s".formatted(card)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(Card.class)
|
||||
public void testGetSpecialCardsContainsAllSpecialCards(Card card) {
|
||||
Set<Card> specialCards = Card.getSpecialCards();
|
||||
if (card.isSpecial()) {
|
||||
assertTrue(
|
||||
specialCards.contains(card),
|
||||
"getSpecialCards() should contain special card %s".formatted(card)
|
||||
);
|
||||
} else {
|
||||
assertFalse(
|
||||
specialCards.contains(card),
|
||||
"getSpecialCards() should not contain non-special card %s".formatted(card)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(Card.class)
|
||||
public void testIsNormalXorIsSpecial(Card card) {
|
||||
assertTrue(
|
||||
card.isNormal() ^ card.isSpecial(),
|
||||
"Card %s should be normal xor special".formatted(card)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,333 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.exceptions.IncompatibleCombinationException;
|
||||
import eu.jonahbauer.tichu.common.model.exceptions.InvalidCombinationException;
|
||||
import eu.jonahbauer.tichu.common.model.exceptions.TooLowCombinationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static eu.jonahbauer.tichu.common.model.Card.*;
|
||||
import static eu.jonahbauer.tichu.common.model.CombinationType.*;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class CombinationTest {
|
||||
private static final Map<CombinationType, List<List<List<Card>>>> VALID_COMBINATIONS = Map.of(
|
||||
SINGLE, Stream.concat(
|
||||
Arrays.stream(Card.values()) // [[RED_2]], [[GREEN_2]], [[BLUE_2]], ...
|
||||
.map(List::of)
|
||||
.map(List::of),
|
||||
Arrays.stream(Suit.values()) // [[RED_2], [RED_3], [RED_4], ...], [[BLUE_2], ...], ...
|
||||
.map(suit -> IntStream.rangeClosed(2, 14)
|
||||
.mapToObj(Card::getCardsByValue)
|
||||
.flatMap(cards -> cards.stream()
|
||||
.filter(card -> card.getSuit() == suit)
|
||||
)
|
||||
.map(List::of)
|
||||
.toList()
|
||||
)
|
||||
).toList(),
|
||||
PAIR, List.of(List.of(
|
||||
List.of(BLUE_5, RED_5),
|
||||
List.of(BLUE_JACK, BLACK_JACK),
|
||||
List.of(PHOENIX, GREEN_KING),
|
||||
List.of(RED_ACE, PHOENIX)
|
||||
)),
|
||||
TRIPLE, List.of(List.of(
|
||||
List.of(BLUE_5, RED_5, GREEN_5),
|
||||
List.of(BLUE_7, RED_7, PHOENIX),
|
||||
List.of(BLUE_10, PHOENIX, BLACK_10),
|
||||
List.of(PHOENIX, BLUE_QUEEN, RED_QUEEN)
|
||||
)),
|
||||
FULL_HOUSE, List.of(List.of(
|
||||
List.of(BLUE_5, RED_5, GREEN_5, BLACK_10, GREEN_10),
|
||||
List.of(BLUE_6, RED_6, GREEN_6, BLACK_4, PHOENIX),
|
||||
List.of(BLUE_8, RED_8, GREEN_8, PHOENIX, GREEN_4),
|
||||
List.of(BLUE_9, RED_9, PHOENIX, BLACK_ACE, GREEN_ACE),
|
||||
List.of(BLUE_JACK, PHOENIX, GREEN_JACK, BLACK_4, GREEN_4),
|
||||
List.of(PHOENIX, RED_KING, GREEN_KING, BLACK_4, GREEN_4)
|
||||
)),
|
||||
SEQUENCE, List.of(
|
||||
List.of(
|
||||
List.of(MAHJONG, RED_2, BLACK_3, RED_4, BLUE_5)
|
||||
),
|
||||
List.of(
|
||||
List.of(MAHJONG, PHOENIX, BLACK_3, RED_4, BLUE_5)
|
||||
),
|
||||
List.of(
|
||||
List.of(MAHJONG, RED_2, BLACK_3, PHOENIX, BLUE_5),
|
||||
List.of(BLUE_4, RED_5, BLACK_6, RED_7, BLUE_8),
|
||||
List.of(PHOENIX, RED_6, BLACK_7, RED_8, BLUE_9),
|
||||
List.of(BLUE_10, RED_JACK, BLACK_QUEEN, RED_KING, PHOENIX)
|
||||
),
|
||||
List.of(
|
||||
List.of(RED_6, BLUE_7, BLUE_8, RED_9, BLACK_10, RED_JACK, BLUE_QUEEN),
|
||||
List.of(PHOENIX, BLUE_8, BLUE_9, RED_10, BLACK_JACK, RED_QUEEN, BLUE_KING),
|
||||
List.of(RED_8, BLUE_9, BLUE_10, RED_JACK, BLACK_QUEEN, RED_KING, PHOENIX)
|
||||
)
|
||||
),
|
||||
PAIR_SEQUENCE, List.of(
|
||||
List.of(
|
||||
List.of(RED_2, BLUE_2, GREEN_3, BLACK_3),
|
||||
List.of(RED_3, BLUE_3, PHOENIX, BLACK_4)
|
||||
),
|
||||
List.of(
|
||||
List.of(RED_2, BLUE_2, GREEN_3, BLACK_3, BLACK_4, RED_4),
|
||||
List.of(RED_5, BLUE_5, PHOENIX, BLACK_6, BLACK_7, RED_7)
|
||||
)
|
||||
),
|
||||
BOMB, List.of(
|
||||
IntStream.rangeClosed(2, 14).mapToObj(Card::getCardsByValue).map(List::copyOf).toList()
|
||||
),
|
||||
BOMB_SEQUENCE, List.of(List.of(
|
||||
List.of(RED_2, RED_3, RED_4, RED_5, RED_6),
|
||||
List.of(RED_2, RED_3, RED_4, RED_5, RED_6, RED_7, RED_8, RED_9),
|
||||
List.of(BLUE_7, BLUE_8, BLUE_9, BLUE_10, BLUE_JACK, BLUE_QUEEN, BLUE_KING, BLUE_ACE)
|
||||
))
|
||||
);
|
||||
|
||||
private static final Map<CombinationType, List<List<Card>>> INVALID_COMBINATIONS = Map.of(
|
||||
SINGLE, List.of(
|
||||
List.of(BLUE_2, RED_2), // too many cards
|
||||
List.of(BLUE_2, RED_3) // too many cards
|
||||
),
|
||||
PAIR, List.of(
|
||||
List.of(RED_ACE, BLUE_ACE, GREEN_ACE), // too many cards
|
||||
List.of(RED_ACE, RED_ACE), // duplicate card
|
||||
List.of(RED_KING, RED_ACE), // different cards
|
||||
List.of(MAHJONG, PHOENIX), // phoenix cannot be mahjong / mahjong cannot be part of a tuple
|
||||
List.of(DRAGON, PHOENIX), // phoenix cannot be dragon / dragon cannot be part of a tuple
|
||||
List.of(HOUND, PHOENIX), // phoenix cannot be hound / hound cannot be part of a tuple
|
||||
List.of(RED_5) // too few cards
|
||||
),
|
||||
TRIPLE, List.of(
|
||||
List.of(RED_ACE, BLUE_ACE, GREEN_ACE, BLACK_ACE), // too many cards
|
||||
List.of(RED_ACE, BLUE_ACE, BLUE_ACE), // duplicate card
|
||||
List.of(RED_ACE, BLUE_ACE, GREEN_KING), // different cards
|
||||
List.of(MAHJONG, DRAGON, PHOENIX),
|
||||
List.of(RED_ACE, BLUE_ACE), // too few cards
|
||||
List.of(RED_ACE) // too few cards
|
||||
),
|
||||
FULL_HOUSE, List.of(
|
||||
List.of(BLUE_5, RED_5, BLUE_5, BLACK_4, GREEN_4, BLUE_4), // too many cards
|
||||
List.of(BLUE_5, RED_5, BLUE_5, BLACK_4), // too few cards
|
||||
List.of(BLUE_5, RED_5, BLUE_4, BLACK_4, GREEN_4), // wrong order
|
||||
List.of(BLUE_5, RED_5, BLUE_6, BLACK_4, GREEN_4), // different cards in triple
|
||||
List.of(BLUE_5, RED_5, GREEN_5, BLACK_3, GREEN_4), // different cards in pair
|
||||
List.of(BLUE_5, RED_5, GREEN_5, BLUE_5, GREEN_5), // duplicate cards
|
||||
List.of(BLUE_5, PHOENIX, GREEN_5, PHOENIX, RED_5), // duplicate cards
|
||||
List.of(BLUE_5, RED_5, GREEN_5, DRAGON, GREEN_4), // dragon cannot be part of a tuple
|
||||
List.of(BLUE_5, RED_5, GREEN_5, HOUND, GREEN_4), // hound cannot be part of a tuple
|
||||
List.of(BLUE_5, RED_5, GREEN_5, MAHJONG, PHOENIX) // mahjong cannot be part of a tuple
|
||||
),
|
||||
SEQUENCE, List.of(
|
||||
List.of(RED_ACE, RED_2, BLACK_3, RED_4, BLUE_5), // ace is no 1
|
||||
List.of(RED_2, BLACK_3, RED_4, BLUE_5), // too few cards
|
||||
List.of(RED_2, BLACK_3, RED_4), // too few cards
|
||||
List.of(RED_2, BLACK_3), // too few cards
|
||||
List.of(RED_2), // too few cards
|
||||
List.of(PHOENIX, MAHJONG, RED_2, BLACK_3, PHOENIX, BLUE_5), // phoenix cannot be 0
|
||||
List.of(RED_2, MAHJONG, BLACK_4, RED_5, GREEN_6), // wrong order
|
||||
List.of(BLACK_8, GREEN_7, BLUE_6, RED_5, GREEN_4), // wrong order
|
||||
List.of(BLACK_10, GREEN_JACK, BLUE_QUEEN, RED_KING, GREEN_ACE, DRAGON), // dragon cannot be part of sequence
|
||||
List.of(BLACK_10, GREEN_JACK, BLUE_QUEEN, RED_KING, GREEN_ACE, PHOENIX) // phoenix cannot be higher than ace
|
||||
),
|
||||
PAIR_SEQUENCE, List.of(
|
||||
List.of(RED_2, BLUE_2), // too few cards
|
||||
List.of(RED_2, BLUE_2, GREEN_3), // too few cards
|
||||
List.of(RED_2, BLUE_3, GREEN_4, BLUE_5), // missing pairs
|
||||
List.of(RED_2, BLUE_2, GREEN_3, BLUE_3, BLACK_4), // partially missing pairs
|
||||
List.of(RED_3, BLUE_3, GREEN_2, BLUE_2), // wrong order
|
||||
List.of(PHOENIX, MAHJONG, RED_2, BLUE_2), // phoenix cannot be mahjong
|
||||
List.of(RED_ACE, BLUE_ACE, DRAGON, PHOENIX) // phoenix cannot be dragon
|
||||
),
|
||||
BOMB, List.of(
|
||||
List.of(RED_2, RED_2, BLUE_2), // too few cards
|
||||
List.of(RED_2, RED_2, BLUE_2, GREEN_2, BLACK_2), // too many cards / duplicate cards
|
||||
List.of(RED_2, BLUE_2, GREEN_2, RED_2), // duplicate cards
|
||||
List.of(RED_2, BLUE_2, GREEN_2, RED_3), // different cards
|
||||
List.of(RED_2, BLUE_2, GREEN_2, PHOENIX) // phoenix cannot be part of a bomb
|
||||
),
|
||||
BOMB_SEQUENCE, List.of(
|
||||
List.of(RED_2, RED_3, RED_4, BLACK_5, RED_6), // wrong suit
|
||||
List.of(BLUE_10, PHOENIX, BLUE_QUEEN, BLUE_KING, BLUE_ACE), // phoenix cannot be part of a bomb
|
||||
List.of(MAHJONG, RED_2, RED_3, RED_4, RED_5), // mahjong cannot be part of a bomb
|
||||
List.of(RED_2, RED_3, RED_4, RED_5), // too few cards
|
||||
List.of(RED_2, RED_3, RED_4, RED_5, PHOENIX), // phoenix cannot be part of a bomb
|
||||
List.of(RED_2, RED_3, RED_5, RED_4, RED_6), // wrong order
|
||||
List.of(RED_6, RED_5, RED_4, RED_3, RED_2) // wrong order
|
||||
)
|
||||
);
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(CombinationType.class)
|
||||
public void testCheck(CombinationType type) {
|
||||
VALID_COMBINATIONS.get(type).stream().flatMap(List::stream).forEach(cards -> assertValidCombination(type, cards));
|
||||
INVALID_COMBINATIONS.get(type).forEach(cards -> assertInvalidCombination(type, cards));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(CombinationType.class)
|
||||
public void testIsCompatible(CombinationType type) {
|
||||
VALID_COMBINATIONS.get(type).forEach(combinations -> {
|
||||
for (List<Card> combinationA : combinations) {
|
||||
for (List<Card> combinationB : combinations) {
|
||||
assertIsCompatible(type, combinationA, combinationB);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(CombinationType.class)
|
||||
public void testIsHigher(CombinationType type) {
|
||||
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)));
|
||||
for (int j = 0; j < i; j++) {
|
||||
assertIsNotHigher(stack, type, combinations.get(j));
|
||||
}
|
||||
|
||||
for (int j = i + 1; j < combinations.size(); j++) {
|
||||
assertIsHigher(stack, type, combinations.get(j));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBombIsCompatible() {
|
||||
List<Stack> stacks = VALID_COMBINATIONS.entrySet().stream()
|
||||
.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(Stack::of)
|
||||
.toList();
|
||||
Stack hound = Stack.of(Combination.single(HOUND));
|
||||
|
||||
VALID_COMBINATIONS.get(BOMB).stream()
|
||||
.flatMap(List::stream)
|
||||
.forEach(bomb -> {
|
||||
stacks.forEach(stack -> assertIsCompatible(stack, BOMB, bomb));
|
||||
assertIsIncompatible(hound, BOMB, bomb);
|
||||
});
|
||||
|
||||
VALID_COMBINATIONS.get(BOMB_SEQUENCE).stream()
|
||||
.flatMap(List::stream)
|
||||
.forEach(bomb -> {
|
||||
stacks.forEach(stack -> assertIsCompatible(stack, BOMB_SEQUENCE, bomb));
|
||||
assertIsIncompatible(hound, BOMB_SEQUENCE, bomb);
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertValidCombination(CombinationType type, List<Card> cards) {
|
||||
try {
|
||||
new Combination(type, cards);
|
||||
} catch (InvalidCombinationException e) {
|
||||
fail("%s should be a valid %s.".formatted(cards, type), e);
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
type.check(cards),
|
||||
"%s should be a valid %s.".formatted(cards, type)
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertInvalidCombination(CombinationType type, List<Card> cards) {
|
||||
assertThrows(InvalidCombinationException.class, () -> {
|
||||
new Combination(type, cards);
|
||||
}, "%s should not be a valid %s.".formatted(cards, type));
|
||||
|
||||
assertFalse(
|
||||
type.check(cards),
|
||||
"%s should not be a valid %s.".formatted(cards, type)
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertIsCompatible(CombinationType type, List<Card> stack, List<Card> cards) {
|
||||
assertIsCompatible(Stack.of(new Combination(type, stack)), type, cards);
|
||||
}
|
||||
|
||||
private static void assertIsCompatible(Stack stack, CombinationType type, List<Card> cards) {
|
||||
assertTrue(
|
||||
type.isCompatible(stack, cards),
|
||||
"%s should be compatible with %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
stack.isCompatible(new Combination(type, cards)),
|
||||
"%s should be compatible with %s".formatted(cards, stack)
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertIsIncompatible(Stack stack, CombinationType type, List<Card> cards) {
|
||||
assertFalse(
|
||||
type.isCompatible(stack, cards),
|
||||
"%s should be incompatible with %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertFalse(
|
||||
stack.isCompatible(new Combination(type, cards)),
|
||||
"%s should be incompatible with %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertFalse(
|
||||
stack.isCompatibleAndHigher(new Combination(type, cards)),
|
||||
"%s should be incompatible with %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertThrows(IncompatibleCombinationException.class, () -> {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
stack.put(new Combination(type, cards));
|
||||
}, "%s should be incompatible with %s".formatted(cards, stack));
|
||||
}
|
||||
|
||||
private static void assertIsHigher(Stack stack, CombinationType type, List<Card> cards) {
|
||||
assertTrue(
|
||||
type.isHigher(stack, cards),
|
||||
"%s should be higher than %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
stack.isHigher(new Combination(type, cards)),
|
||||
"%s should be higher than %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
stack.isCompatibleAndHigher(new Combination(type, cards)),
|
||||
"%s should be higher than %s".formatted(cards, stack)
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertIsNotHigher(Stack stack, CombinationType type, List<Card> cards) {
|
||||
assertFalse(
|
||||
type.isHigher(stack, cards),
|
||||
"%s should not be higher than %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertFalse(
|
||||
stack.isHigher(new Combination(type, cards)),
|
||||
"%s should not be higher than %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertFalse(
|
||||
stack.isCompatibleAndHigher(new Combination(type, cards)),
|
||||
"%s should not be higher than %s".formatted(cards, stack)
|
||||
);
|
||||
|
||||
assertThrows(TooLowCombinationException.class, () -> {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
stack.put(new Combination(type, cards));
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue