Compare commits

...

No commits in common. "a5211b03498265aad32c08e6f78ca299590528ae" and "681942e02d1f23fc1626385de23780a9e0ef32f2" have entirely different histories.

31 changed files with 2155 additions and 42 deletions

77
.gitignore vendored
View File

@ -1,49 +1,42 @@
# ---> Gradle
.gradle
**/build/
!src/**/build/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
# Ignore Gradle GUI config
gradle-app.setting
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
# ---> Java
# Compiled class file
*.class
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

71
build.gradle.kts Normal file
View File

@ -0,0 +1,71 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.owasp:dependency-check-gradle:6.5.0.1")
}
}
subprojects {
apply(plugin = "java-library")
apply(plugin = "org.owasp.dependencycheck")
group = "eu.jonahbauer"
version = "1.0-SNAPSHOT"
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
}
configure<org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension> {
format = org.owasp.dependencycheck.reporting.ReportGenerator.Format.JUNIT
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

3
settings.gradle.kts Normal file
View File

@ -0,0 +1,3 @@
rootProject.name = "tichu"
include(":tichu-common")

View File

View File

@ -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::value, 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 value() {
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 suit() {
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 #value() 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());
}
}

View File

@ -0,0 +1,296 @@
package eu.jonahbauer.tichu.common.model;
import eu.jonahbauer.tichu.common.model.combinations.*;
import eu.jonahbauer.tichu.common.model.exceptions.InvalidCombinationException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.Function;
/**
* A type of combination.
*/
@RequiredArgsConstructor
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(Single.class, Single::of) {
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);
}
},
/**
* 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(Pair.class, Pair::of) {
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);
}
},
/**
* 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(Triple.class, Triple::of) {
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);
}
},
/**
* 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(FullHouse.class, FullHouse::of) {
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);
}
},
/**
* 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(SingleSequence.class, SingleSequence::of) {
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).value() - 1
: 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;
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.value() != start + i) {
return false;
}
}
return true;
}
},
/**
* 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(PairSequence.class, PairSequence::of) {
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).value()
: cards.get(0).value();
}
@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.value() != start + i / 2) {
return false;
}
}
return true;
}
},
/**
* A bomb, i.e. four {@linkplain Card#isNormal() normal cards} of the same {@linkplain Card#value() value}.
*/
BOMB(BombTuple.class, BombTuple::of) {
private static final int[] INDICES = new int[] {0, 1, 2, 3};
private static final Set<Card> SPECIAL_CARDS = Collections.emptySet();
@Override
public boolean check(@NotNull List<@NotNull Card> cards) {
return checkBasics(cards, 4, SPECIAL_CARDS)
&& isSameValue(cards, INDICES);
}
},
/**
* 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.
* @apiNote
*/
BOMB_SEQUENCE(BombSequence.class, BombSequence::of) {
private static final Set<Card> SPECIAL_CARDS = Collections.emptySet();
private int getStartValue(@NotNull List<@NotNull Card> cards) {
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::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.value() != start + i) {
return false;
}
}
return true;
}
};
private final @NotNull Class<? extends Combination> combinationClass;
private final @NotNull Function<@NotNull List<@NotNull Card>, @NotNull Combination> factory;
/**
* 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.
* @throws NullPointerException if the list or any of its elements are {@code null}.
*/
public abstract boolean check(@NotNull List<@NotNull Card> cards);
/**
* Creates a new combination of this type consisting of the given cards.
* @param cards a list of cards forming a {@linkplain #check(List) valid} combination
* @return a new combination.
* @throws InvalidCombinationException if the given cards do not form a valid combination.
* @throws NullPointerException if the list or any of its elements are {@code null}.
*/
public @NotNull Combination of(@NotNull List<@NotNull Card> cards) {
return factory.apply(cards);
}
/**
* Creates a new combination of this type consisting of the given cards.
* @param cards a list of cards forming a {@linkplain #check(List) valid} combination
* @return a new combination.
* @throws InvalidCombinationException if the given cards do not form a valid combination.
* @throws NullPointerException if the list or any of its elements are {@code null}.
*/
public @NotNull Combination of(@NotNull Card @NotNull... cards) {
return of(List.of(cards));
}
/**
* Returns the class representing a combination of this type.
* @return the class representing a combination of this type.
*/
public @NotNull Class<? extends Combination> getCombinationClass() {
return combinationClass;
}
//<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.value();
} else if (!value.equals(card.value())) {
return false;
}
}
return true;
}
//</editor-fold>
}

View File

@ -0,0 +1,211 @@
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;
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;
/**
* 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(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
for (int i = 0; i < out.size(); i++) {
Stack lower = out.substack(0, i);
Combination top = out.get(i);
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;
}
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} {@linkplain Combination#isCompatibleWith(Stack) is compatible with}
* this stack.
* @param combination a combination
* @return {@code true} iff the {@code combination} is compatible
* @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.isCompatibleWith(this);
}
/**
* 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 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.isHigherThan(this);
}
/**
* 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 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.isCompatibleAndHigher(this);
}
/**
* 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();
}
}

View File

@ -0,0 +1,5 @@
package eu.jonahbauer.tichu.common.model;
public enum Suit {
GREEN, RED, BLACK, BLUE
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,102 @@
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 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 cards a list of cards forming a {@linkplain CombinationType#check(List) valid} combination
* @param type a combination type
* @return a new combination.
* @throws InvalidCombinationException if the given cards do not form a valid combination.
* @throws NullPointerException if the type, the list or any of its elements are {@code null}.
*/
@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);
};
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,24 @@
package eu.jonahbauer.tichu.common.model.exceptions;
import eu.jonahbauer.tichu.common.model.Stack;
import eu.jonahbauer.tichu.common.model.combinations.Combination;
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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,24 @@
package eu.jonahbauer.tichu.common.model.exceptions;
import eu.jonahbauer.tichu.common.model.Stack;
import eu.jonahbauer.tichu.common.model.combinations.Combination;
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);
}
}

View File

@ -0,0 +1,8 @@
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;
}

View File

@ -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)
);
}
}

View File

@ -0,0 +1,351 @@
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;
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.suit() == 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(Combination.of(type, combinations.get(i)));
for (int j = 0; j < i; j++) {
assertIsNotHigher(stack, Combination.of(type, combinations.get(j)));
}
for (int j = i + 1; j < combinations.size(); j++) {
assertIsHigher(stack, Combination.of(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 -> Combination.of(entry.getKey(), cards))
)
.map(Stack::of)
.toList();
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));
assertIsIncompatible(hound, bomb);
});
VALID_COMBINATIONS.get(BOMB_SEQUENCE).stream()
.flatMap(List::stream)
.map(BombSequence::of)
.forEach(bomb -> {
stacks.forEach(stack -> assertIsCompatible(stack, bomb));
assertIsIncompatible(hound, bomb);
});
}
private static void assertValidCombination(CombinationType type, List<Card> cards) {
try {
Combination.of(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, () -> {
Combination.of(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(Combination.of(type, stack)), Combination.of(type, cards));
}
private static void assertIsCompatible(Stack stack, Combination combination) {
assertTrue(
combination.isCompatibleWith(stack),
"%s should be compatible with %s".formatted(combination, stack)
);
assertTrue(
stack.isCompatible(combination),
"%s should be compatible with %s".formatted(combination, stack)
);
}
private static void assertIsIncompatible(Stack stack, Combination combination) {
assertFalse(
stack.isCompatible(combination),
"%s should be incompatible with %s".formatted(combination, stack)
);
assertFalse(
stack.isCompatibleAndHigher(combination),
"%s should be incompatible with %s".formatted(combination, stack)
);
assertFalse(
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(combination);
}, "%s should be incompatible with %s".formatted(combination, stack));
}
private static void assertIsHigher(Stack stack, Combination combination) {
assertTrue(
combination.isHigherThan(stack),
"%s should be higher than %s".formatted(combination, stack)
);
assertTrue(
stack.isHigher(combination),
"%s should be higher than %s".formatted(combination, stack)
);
assertTrue(
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, Combination combination) {
assertFalse(
combination.isHigherThan(stack),
"%s should not be higher than %s".formatted(combination, stack)
);
assertFalse(
stack.isHigher(combination),
"%s should not be higher than %s".formatted(combination, stack)
);
assertFalse(
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(combination);
});
}
}