Compare commits
No commits in common. "a5211b03498265aad32c08e6f78ca299590528ae" and "681942e02d1f23fc1626385de23780a9e0ef32f2" have entirely different histories.
a5211b0349
...
681942e02d
77
.gitignore
vendored
77
.gitignore
vendored
@ -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
71
build.gradle.kts
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
234
gradlew
vendored
Normal 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
89
gradlew.bat
vendored
Normal 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
3
settings.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
||||
rootProject.name = "tichu"
|
||||
|
||||
include(":tichu-common")
|
0
tichu-common/build.gradle.kts
Normal file
0
tichu-common/build.gradle.kts
Normal 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());
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package eu.jonahbauer.tichu.common.model;
|
||||
|
||||
public enum Suit {
|
||||
GREEN, RED, BLACK, BLUE
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import eu.jonahbauer.tichu.common.model.exceptions.InvalidCombinationException;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@EqualsAndHashCode
|
||||
abstract sealed class AbstractCombination implements Combination permits Tuple, Sequence, FullHouse, Single {
|
||||
private final @NotNull CombinationType type;
|
||||
private final @Unmodifiable @NotNull List<@NotNull Card> cards;
|
||||
|
||||
AbstractCombination(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards) {
|
||||
this.type = Objects.requireNonNull(type, "type must not be null");
|
||||
this.cards = List.copyOf(Objects.requireNonNull(cards, "cards must not be null"));
|
||||
|
||||
if (!this.type.check(this.cards)) {
|
||||
throw new InvalidCombinationException(type, cards);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CombinationType type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Unmodifiable @NotNull List<@NotNull Card> cards() {
|
||||
return cards;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final @NotNull Card card(int index) {
|
||||
return cards.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int size() {
|
||||
return cards.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompatibleWith(@NotNull Stack stack) {
|
||||
return stack.isEmpty() || stack.top().type() == type() && stack.top().size() == size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompatibleAndHigher(@NotNull Stack stack) {
|
||||
return isCompatibleWith(stack) && isHigherThan(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type + cards.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public sealed interface Bomb extends Combination permits BombTuple, BombSequence{
|
||||
@Override
|
||||
default boolean isCompatibleWith(@NotNull Stack stack) {
|
||||
return stack.isEmpty()
|
||||
|| stack.top().type() != CombinationType.SINGLE
|
||||
|| stack.top().card(0) != Card.HOUND;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static eu.jonahbauer.tichu.common.model.CombinationType.BOMB_SEQUENCE;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#BOMB_SEQUENCE}
|
||||
*/
|
||||
public final class BombSequence extends Sequence implements Bomb {
|
||||
public static BombSequence of(@NotNull List<@NotNull Card> cards) {
|
||||
return new BombSequence(cards);
|
||||
}
|
||||
|
||||
BombSequence(@NotNull List<@NotNull Card> cards) {
|
||||
super(BOMB_SEQUENCE, cards, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompatibleWith(@NotNull Stack stack) {
|
||||
return Bomb.super.isCompatibleWith(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHigherThan(@NotNull Stack stack) {
|
||||
assert isCompatibleWith(stack);
|
||||
return stack.isEmpty()
|
||||
|| stack.top().type() != BOMB_SEQUENCE
|
||||
|| size() > stack.top().size()
|
||||
|| start() > ((BombSequence) stack.top()).start();
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static eu.jonahbauer.tichu.common.model.CombinationType.BOMB;
|
||||
import static eu.jonahbauer.tichu.common.model.CombinationType.BOMB_SEQUENCE;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#BOMB}
|
||||
*/
|
||||
public final class BombTuple extends Tuple implements Bomb {
|
||||
public static BombTuple of(@NotNull List<@NotNull Card> cards) {
|
||||
return new BombTuple(cards);
|
||||
}
|
||||
|
||||
public static BombTuple of(@NotNull Card card1, @NotNull Card card2, @NotNull Card card3, @NotNull Card card4) {
|
||||
return new BombTuple(List.of(
|
||||
Objects.requireNonNull(card1, "card1 must not be null"),
|
||||
Objects.requireNonNull(card2, "card2 must not be null"),
|
||||
Objects.requireNonNull(card3, "card3 must not be null"),
|
||||
Objects.requireNonNull(card4, "card4 must not be null")
|
||||
));
|
||||
}
|
||||
|
||||
BombTuple(@NotNull List<@NotNull Card> cards) {
|
||||
super(BOMB, cards);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompatibleWith(@NotNull Stack stack) {
|
||||
return Bomb.super.isCompatibleWith(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHigherThan(@NotNull Stack stack) {
|
||||
assert isCompatibleWith(stack);
|
||||
return stack.isEmpty()
|
||||
|| stack.top().type() != BOMB && stack.top().type() != BOMB_SEQUENCE
|
||||
|| stack.top().type() == BOMB && value() > ((BombTuple) stack.top()).value();
|
||||
}
|
||||
}
|
@ -0,0 +1,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);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#FULL_HOUSE}
|
||||
*/
|
||||
public final class FullHouse extends AbstractCombination {
|
||||
@Contract("_ -> new")
|
||||
public static @NotNull FullHouse of(@NotNull List<@NotNull Card> cards) {
|
||||
return new FullHouse(cards);
|
||||
}
|
||||
|
||||
@Contract("_, _, _, _, _ -> new")
|
||||
public static @NotNull FullHouse of(
|
||||
@NotNull Card triple1, @NotNull Card triple2, @NotNull Card triple3,
|
||||
@NotNull Card pair1, @NotNull Card pair2
|
||||
) {
|
||||
return new FullHouse(List.of(
|
||||
Objects.requireNonNull(triple1, "triple1 must not be null"),
|
||||
Objects.requireNonNull(triple2, "triple2 must not be null"),
|
||||
Objects.requireNonNull(triple3, "triple3 must not be null"),
|
||||
Objects.requireNonNull(pair1, "pair1 must not be null"),
|
||||
Objects.requireNonNull(pair2, "pair2 must not be null")
|
||||
));
|
||||
}
|
||||
|
||||
private final int triple;
|
||||
private final int pair;
|
||||
|
||||
FullHouse(@NotNull List<@NotNull Card> cards) {
|
||||
super(CombinationType.FULL_HOUSE, cards);
|
||||
this.triple = Tuple.getSameValue(cards.subList(0, 3));
|
||||
this.pair = Tuple.getSameValue(cards.subList(3, 5));
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@linkplain Card#value() value} of the triple.
|
||||
*/
|
||||
public int triple() {
|
||||
return triple;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@linkplain Card#value() value} of the pair.
|
||||
*/
|
||||
public int pair() {
|
||||
return pair;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHigherThan(@NotNull Stack stack) {
|
||||
assert isCompatibleWith(stack);
|
||||
return stack.isEmpty() || triple() > ((FullHouse) stack.top()).triple();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#PAIR}
|
||||
*/
|
||||
public final class Pair extends Tuple {
|
||||
@Contract("_ -> new")
|
||||
public static @NotNull Pair of(@NotNull List<@NotNull Card> cards) {
|
||||
return new Pair(cards);
|
||||
}
|
||||
|
||||
@Contract("_, _ -> new")
|
||||
public static @NotNull Pair of(@NotNull Card card1, @NotNull Card card2) {
|
||||
return new Pair(List.of(
|
||||
Objects.requireNonNull(card1, "card1 must not be null"),
|
||||
Objects.requireNonNull(card2, "card2 must not be null")
|
||||
));
|
||||
}
|
||||
|
||||
Pair(@NotNull List<@NotNull Card> cards) {
|
||||
super(CombinationType.PAIR, cards);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#PAIR_SEQUENCE}
|
||||
*/
|
||||
public final class PairSequence extends Sequence {
|
||||
public static PairSequence of(@NotNull List<@NotNull Card> cards) {
|
||||
return new PairSequence(cards);
|
||||
}
|
||||
|
||||
public static PairSequence of(@NotNull Card @NotNull... cards) {
|
||||
return new PairSequence(List.of(cards));
|
||||
}
|
||||
|
||||
PairSequence(@NotNull List<@NotNull Card> cards) {
|
||||
super(CombinationType.PAIR_SEQUENCE, cards, 2);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public abstract sealed class Sequence extends AbstractCombination permits SingleSequence, PairSequence, BombSequence {
|
||||
private final int start;
|
||||
private final int length;
|
||||
|
||||
Sequence(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards, int n) {
|
||||
super(type, cards);
|
||||
this.length = cards.size() / n;
|
||||
this.start = getStartValue(cards, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@linkplain Card#value() value} of the lowest card in this sequence.
|
||||
*/
|
||||
public int start() {
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of this sequence.
|
||||
*/
|
||||
public int length() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHigherThan(@NotNull Stack stack) {
|
||||
assert isCompatibleWith(stack);
|
||||
return stack.isEmpty() || start() > ((Sequence) stack.top()).start();
|
||||
}
|
||||
|
||||
static int getStartValue(@NotNull List<@NotNull Card> cards, int n) {
|
||||
for (int i = 0, size = cards.size(); i < size; i++) {
|
||||
Integer value = cards.get(i).value();
|
||||
if (value != null) return value - (i / n);
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#SINGLE}
|
||||
*/
|
||||
public final class Single extends AbstractCombination {
|
||||
@Contract("_ -> new")
|
||||
public static @NotNull Single of(@NotNull List<@NotNull Card> cards) {
|
||||
return new Single(cards);
|
||||
}
|
||||
|
||||
@Contract("_ -> new")
|
||||
public static @NotNull Single of(@NotNull Card card) {
|
||||
return new Single(List.of(Objects.requireNonNull(card, "card must not be null")));
|
||||
}
|
||||
|
||||
Single(@NotNull List<@NotNull Card> cards) {
|
||||
super(CombinationType.SINGLE, cards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single card this combination consists of. This is shorthand for {@link #card(int) card(0)}.
|
||||
* @return the single card this combination consists of.
|
||||
*/
|
||||
public @NotNull Card card() {
|
||||
return card(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@linkplain Card#value() value} of the {@linkplain #card() card}.
|
||||
* @return the value of the card.
|
||||
*/
|
||||
public Integer value() {
|
||||
return card().value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHigherThan(@NotNull Stack stack) {
|
||||
assert isCompatibleWith(stack);
|
||||
if (stack.isEmpty()) return true; // you can play any card onto an empty stack
|
||||
|
||||
var top = (Single) stack.top();
|
||||
if (top.card() == Card.HOUND) return false; // you cannot play any card onto a hound
|
||||
|
||||
return switch (card()) {
|
||||
case HOUND, MAHJONG -> false;
|
||||
case DRAGON -> true;
|
||||
case PHOENIX -> top.card() != Card.DRAGON;
|
||||
default -> {
|
||||
if (top.card() != Card.PHOENIX) {
|
||||
yield value() > top.value();
|
||||
} else if (stack.size() == 1) {
|
||||
// every normal card has a value of at least 2, which is higher that the phoenix 1½
|
||||
yield true;
|
||||
} else {
|
||||
yield value() > ((Single) stack.top(-1)).value();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#SEQUENCE}
|
||||
*/
|
||||
public final class SingleSequence extends Sequence {
|
||||
public static SingleSequence of(@NotNull List<@NotNull Card> cards) {
|
||||
return new SingleSequence(cards);
|
||||
}
|
||||
|
||||
public static SingleSequence of(@NotNull Card @NotNull... cards) {
|
||||
return new SingleSequence(List.of(cards));
|
||||
}
|
||||
|
||||
SingleSequence(@NotNull List<@NotNull Card> cards) {
|
||||
super(CombinationType.SEQUENCE, cards, 1);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A combination of type {@link CombinationType#TRIPLE}
|
||||
*/
|
||||
public final class Triple extends Tuple {
|
||||
@Contract("_ -> new")
|
||||
public static @NotNull Triple of(@NotNull List<@NotNull Card> cards) {
|
||||
return new Triple(cards);
|
||||
}
|
||||
|
||||
@Contract("_, _, _ -> new")
|
||||
public static @NotNull Triple of(@NotNull Card card1, @NotNull Card card2, @NotNull Card card3) {
|
||||
return new Triple(List.of(
|
||||
Objects.requireNonNull(card1, "card1 must not be null"),
|
||||
Objects.requireNonNull(card2, "card2 must not be null"),
|
||||
Objects.requireNonNull(card3, "card3 must not be null")
|
||||
));
|
||||
}
|
||||
|
||||
Triple(@NotNull List<@NotNull Card> cards) {
|
||||
super(CombinationType.TRIPLE, cards);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package eu.jonahbauer.tichu.common.model.combinations;
|
||||
|
||||
import eu.jonahbauer.tichu.common.model.Card;
|
||||
import eu.jonahbauer.tichu.common.model.CombinationType;
|
||||
import eu.jonahbauer.tichu.common.model.Stack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public abstract sealed class Tuple extends AbstractCombination permits Pair, Triple, BombTuple {
|
||||
private final int value;
|
||||
|
||||
Tuple(@NotNull CombinationType type, @NotNull List<@NotNull Card> cards) {
|
||||
super(type, cards);
|
||||
this.value = getSameValue(cards);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@linkplain Card#value() value} of the cards in this tuple.
|
||||
*/
|
||||
public int value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHigherThan(@NotNull Stack stack) {
|
||||
assert isCompatibleWith(stack);
|
||||
return stack.isEmpty() || value() > ((Tuple) stack.top()).value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assuming that all the {@linkplain Card#isNormal() normal cards} at the specified indices have the same value
|
||||
* returns that value. The return value is unspecified if the assumption is violated.
|
||||
* @param cards a list of cards
|
||||
* @throws NoSuchElementException if there is no normal card among the specified indices
|
||||
*/
|
||||
static int getSameValue(@NotNull List<@NotNull Card> cards) {
|
||||
for (var card : cards) {
|
||||
if (card.isSpecial()) continue;
|
||||
|
||||
return card.value();
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.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);
|
||||
}
|
||||
}
|
8
tichu-common/src/main/java/module-info.java
Normal file
8
tichu-common/src/main/java/module-info.java
Normal 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;
|
||||
}
|
@ -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,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);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user