commit 63575a0e31ac1bf007d0b6b8257dfede1d8d7f4b Author: Nathan Cannon Date: Thu Jan 12 22:43:15 2017 +0000 Repo init. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d816087 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Created by .ignore support plugin (hsz.mobi) +### Gradle template +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties +### Java template +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.idea + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b0b7f67 --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +group 'uk.co.neviyn' +version 'DEVELOPMENT' + +apply plugin: 'java' +apply plugin: 'application' + +sourceCompatibility = 1.8 +applicationDefaultJvmArgs = ["-Dfile.encoding=UTF-8", "-Dorg.slf4j.simpleLogger.defaultLogLevel=warn"] +mainClassName = "uk.co.neviyn.pokergame.App" + +repositories { + mavenCentral() +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +dependencies { + compile 'org.slf4j:slf4j-api:1.7.22' + compile 'org.slf4j:slf4j-simple:1.7.22' + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +test { + systemProperty "file.encoding", "utf-8" +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d48ef43 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 12 00:40:47 GMT 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +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" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 + +@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= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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 init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +: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 %CMD_LINE_ARGS% + +: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 diff --git a/pokergame.iml b/pokergame.iml new file mode 100644 index 0000000..d56dd68 --- /dev/null +++ b/pokergame.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d65ffd8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'pokergame' + diff --git a/src/main/java/uk/co/neviyn/pokergame/App.java b/src/main/java/uk/co/neviyn/pokergame/App.java new file mode 100644 index 0000000..6dbf2b7 --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/App.java @@ -0,0 +1,58 @@ +package uk.co.neviyn.pokergame; + +import uk.co.neviyn.pokergame.model.Card; +import uk.co.neviyn.pokergame.game.Deck; +import uk.co.neviyn.pokergame.game.Hand; +import uk.co.neviyn.pokergame.model.Result; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +public class App +{ + + public static void main( String[] args ) + { + Deck deck = new Deck(); + Hand hand = new Hand(deck); + boolean running = true; + System.out.println("♠♥♦♣ POKER ♠♥♦♣"); + System.out.println("Type q after a round to quit"); + try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { + while (running) { + // Start a new round. + System.out.println("----------"); + List newCards = hand.newRound(); + newCards.forEach(x -> System.out.print(x.getShortName() + " ")); + System.out.println(); + System.out.print("Toggle keeping a card (1-5)"); + while(true){ + String input = reader.readLine(); + try{ + int selection = Integer.parseInt(input); + hand.toggleKeepCard(selection - 1); + newCards.forEach(x -> System.out.print(x.getShortName() + (hand.cardKept(x) ? "*" : "") + " ")); + } catch (NumberFormatException ex){ + break; + } + } + System.out.println("----------"); + newCards = hand.drawHand(); + newCards.forEach(x -> System.out.print(x.getShortName() + " ")); + System.out.println(); + Result result = hand.checkForWin(); + if (result.isWin()) + System.out.println("Win! " + result.toString()); + else + System.out.println("Lose!"); + if(reader.readLine().equals("q")){ + running = false; + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/uk/co/neviyn/pokergame/game/Deck.java b/src/main/java/uk/co/neviyn/pokergame/game/Deck.java new file mode 100644 index 0000000..a3fa849 --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/game/Deck.java @@ -0,0 +1,58 @@ +package uk.co.neviyn.pokergame.game; + +import uk.co.neviyn.pokergame.model.Card; +import uk.co.neviyn.pokergame.model.Suit; +import uk.co.neviyn.pokergame.model.Value; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Stack; + +public class Deck implements IDeck{ + + private static final List baseDeck = generateDeck(); + private Stack currentDeck = new Stack<>(); + + public Deck() { + resetDeck(); + } + + /** + * @return Create a brand new deck. + */ + private static List generateDeck(){ + List output = new ArrayList<>(53); + for (Suit suit : Suit.standardSuits()){ + for(Value value : Value.standardValues()){ + output.add(new Card(suit, value)); + } + } + output.add(new Card(Suit.JOKER, Value.JOKER)); + return output; + } + + /** + * @return Number of cards remaining in current deck. + */ + public int getCardCount(){ + return currentDeck.size(); + } + + /** + * Draw a card from the deck. + * @return Drawn card. + */ + public Card drawCard(){ + return currentDeck.pop(); + } + + /** + * Reset the current deck to a new copy of the base deck. + */ + public void resetDeck(){ + currentDeck.clear(); + Collections.shuffle(baseDeck); + baseDeck.forEach(currentDeck::push); + } +} diff --git a/src/main/java/uk/co/neviyn/pokergame/game/Hand.java b/src/main/java/uk/co/neviyn/pokergame/game/Hand.java new file mode 100644 index 0000000..551ccdc --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/game/Hand.java @@ -0,0 +1,279 @@ +package uk.co.neviyn.pokergame.game; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.co.neviyn.pokergame.model.Card; +import uk.co.neviyn.pokergame.model.Result; +import uk.co.neviyn.pokergame.model.Suit; +import uk.co.neviyn.pokergame.model.Value; + +import java.util.*; + +public class Hand { + + private static final int HAND_SIZE = 5; + private static Logger LOGGER = LoggerFactory.getLogger(Hand.class); + + private List cards = new ArrayList<>(); + private boolean[] keepCard = new boolean[HAND_SIZE]; + private IDeck deck; + + /** + * Represents the players hand which can hold up to HAND_SIZE cards. + * @param deck Deck the player can draw cards from. + */ + public Hand(IDeck deck) { + this.deck = deck; + newRound(); + } + + /** + * Draw a new hand, discard any and all existing cards. + */ + private void drawNewHand() { + LOGGER.debug("Drawing a brand new hand."); + List newCards = new ArrayList<>(HAND_SIZE); + resetKeepCards(); + for (int i = 0; i < HAND_SIZE; i++) { + Card draw = deck.drawCard(); + newCards.add(draw); + LOGGER.info("Drew " + draw.getShortName()); + } + LOGGER.debug("Overwriting old hand with new cards."); + cards = newCards; + LOGGER.debug("New hand generated."); + } + + /** + * Draw a new hand but keep cards selected by player. + */ + public List drawHand() { + List newCards = new ArrayList<>(HAND_SIZE); + for (int i = 0; i < HAND_SIZE; i++) { + Card draw = keepCard[i] ? cards.get(i) : deck.drawCard(); + newCards.add(draw); + LOGGER.info("Drew " + draw.getShortName()); + } + resetKeepCards(); + LOGGER.debug("Overwriting old hand with new cards."); + cards = newCards; + LOGGER.debug("New hand generated."); + return cards; + } + + /** + * Start a new round with a new deck. + */ + public List newRound() { + LOGGER.debug("Starting a new round."); + deck.resetDeck(); + drawNewHand(); + LOGGER.debug("Round begun."); + return cards; + } + + /** + * Toggle whether to keep a card in the next non-new round. + * @param cardPos Position ID of card to keep. + */ + public void toggleKeepCard(int cardPos) { + if (cardPos >= 0 && cardPos < HAND_SIZE) { + LOGGER.debug("Setting card keep for position " + cardPos + " to " + !keepCard[cardPos]); + keepCard[cardPos] = !keepCard[cardPos]; + } + } + + /** + * Check whether a card is marked to be kept on a second draw. + * @param card Card to check. + * @return True if this card will be kept. + */ + public boolean cardKept(Card card){ + return keepCard[cards.indexOf(card)]; + } + + /** + * Reset all cards to be discarded on next draw. + */ + private void resetKeepCards(){ + LOGGER.debug("Resetting card keep statuses."); + keepCard = new boolean[HAND_SIZE]; + } + + /** + * Check whether the current hand matches any of the win conditions. + * @return Did you win. + */ + public Result checkForWin(){ + LOGGER.debug("Checking if current hand has won."); + return checkForWin(cards); + } + + /** + * Check whether a list of cards fulfills any win conditions. + * @param cardList List of cards. + * @return Did you win. + */ + private Result checkForWin(List cardList) { + Map valueFrequency = new HashMap<>(); + Map suitFrequency = new HashMap<>(); + for (Card card : cardList) { + valueFrequency.put(card.getValue(), valueFrequency.getOrDefault(card.getValue(), 0) + 1); + suitFrequency.put(card.getSuit(), suitFrequency.getOrDefault(card.getSuit(), 0) + 1); + } + boolean hasJoker = suitFrequency.containsKey(Suit.JOKER); + LOGGER.debug("There is " + (hasJoker ? "a" : "no") + " joker this hand."); + // Ordering of these conditions is important, particularly because of the Joker card! + if (royalStraightFlush(valueFrequency.keySet(), suitFrequency.values(), hasJoker)) { + return Result.ROYAL_STRAIGHT_FLUSH; + } + if (fiveOfAKind(valueFrequency, hasJoker)) { + return Result.FIVE_OF_A_KIND; + } + if (straightFlush(valueFrequency.keySet(), suitFrequency.values(), hasJoker)) { + return Result.STRAIGHT_FLUSH; + } + if (fourOfAKind(valueFrequency.values(), hasJoker)) { + return Result.FOUR_OF_A_KIND; + } + if (fullHouse(valueFrequency.values())) { + return Result.FULL_HOUSE; + } + if (flush(suitFrequency.values(), hasJoker)) { + return Result.FLUSH; + } + if (straight(valueFrequency.keySet(), hasJoker)) { + return Result.STRAIGHT; + } + if (threeOfAKind(valueFrequency.values(), hasJoker)) { + return Result.THREE_OF_A_KIND; + } + if (twoPair(valueFrequency.values())) { + return Result.TWO_OF_A_KIND; + } + return Result.LOSS; + } + + // Win Conditions + + /** + * Check for a Royal Straight Flush. 10,J,Q,K,A of the same suit. + * @param values Tally of values contained in hand. + * @param suitsCount Tally of suits. + * @param hasJoker Whether this hand contains a joker. + * @return True if this hand is a Royal Straight Flush. + */ + private boolean royalStraightFlush(final Collection values, Collection suitsCount, final boolean hasJoker) { + return values.contains(Value.TEN) && // Has a 10 we can start on + straight(values, hasJoker) && // Is a straight + flush(suitsCount, hasJoker); // All the same suit + } + + /** + * Check for Five of a Kind. Four cards of equal value and a joker. + * @param values Tally of values. + * @param hasJoker Whether this hand has a joker. + * @return True if this hand is a Five of a Kind. + */ + private boolean fiveOfAKind(final Map values, final boolean hasJoker){ + return hasJoker && values.values().contains(4); + } + + /** + * Check for a Straight Flush. Five cards, consecutive values, same suit. + * @param values Tally of card values. + * @param suitsCount Tally of card suits. + * @param hasJoker Whether this hand has a joker. + * @return True if this hand is a Straight Flush. + */ + private boolean straightFlush(final Collection values, final Collection suitsCount, final boolean hasJoker) { + return straight(values, hasJoker) && flush(suitsCount, hasJoker); + } + + /** + * Check for Four of a Kind. Four cards of the same value. + * @param valuesCount Tally of unique values. + * @param hasJoker Whether this hand has a joker. + * @return True if this hand is a Four of a Kind. + */ + private boolean fourOfAKind(final Collection valuesCount, final boolean hasJoker) { + return valuesCount.contains(4) || valuesCount.contains(3) && hasJoker; + } + + /** + * Checks for a full house i.e. (4,4,4,J,J) + * Ignores Jokers as a Joker would mean a superior four of a kind. + * @param valuesCount Counts of value frequencies. + * @return Whether valuesCount contains both a frequency of 3 and 2. + */ + private boolean fullHouse(final Collection valuesCount) { + return valuesCount.contains(3) && valuesCount.contains(2); + } + + /** + * Check for a Flush. Five cards of the same suit. + * @param suitsCount Tally of card suits. + * @param hasJoker Whether this hand has a joker. + * @return True if this hand is a Flush. + */ + private boolean flush(final Collection suitsCount, final boolean hasJoker) { + return suitsCount.contains(HAND_SIZE) || suitsCount.contains(HAND_SIZE - 1) && hasJoker; + } + + /** + * Check for a Straight, consecutive values but any suit i.e.(5,6,7,8,9)||(8,9,10,J,Q). + * @param values Tally of values contained in hand. + * @param hasJoker Whether this hand contains a joker. + * @return True if this hand is a Straight. + */ + private boolean straight(final Collection values, final boolean hasJoker) { + if(values.size() < HAND_SIZE & !hasJoker) + return false; + Iterator sortedValues = new TreeSet<>(values).iterator(); + return checkConsecutiveValues(sortedValues, hasJoker); + } + + /** + * Check for Three of a Kind i.e. (8,8,8). + * @param valuesCount Tally of unique values. + * @param hasJoker Whether this hand has a joker. + * @return True if this hand is a Three of a Kind. + */ + private boolean threeOfAKind(final Collection valuesCount, final boolean hasJoker) { + return valuesCount.contains(3) || valuesCount.contains(2) && hasJoker; + } + + /** + * Checks for two pairs of cards i.e. (4,4,J,J) + * Don't bother with a joker check as a joker+pair would make a superior three of a kind at least. + * @param valuesCount Counts of value frequencies. + * @return Whether two of the frequency counts equal two. + */ + private boolean twoPair(final Collection valuesCount) { + return Collections.frequency(valuesCount, 2) == 2; + } + + /** + * Checks whether the values in the given iterator are sequential, it assumes they are sorted, + * + * @param iterator Iterator over sorted values. + * @return Whether values are a series incrementing by one. + */ + private boolean checkConsecutiveValues(final Iterator iterator, final boolean hasJoker) { + boolean hasJokerLocalCopy = hasJoker; + Value current = iterator.next(); + if (hasJokerLocalCopy && current == Value.JOKER) // Skip Joker card, if we have a Joker only 4 need to match. + current = iterator.next(); + while (iterator.hasNext()) { + Value tmp = iterator.next(); + if (tmp.getValue() - current.getValue() != 1) + // If we have a Joker then allow one gap of two (filled by the Joker card) + if (hasJokerLocalCopy && tmp.getValue() - current.getValue() == 2) + hasJokerLocalCopy = false; + else + return false; // If non-consecutive return false. + current = tmp; + } + return true; + } +} diff --git a/src/main/java/uk/co/neviyn/pokergame/game/IDeck.java b/src/main/java/uk/co/neviyn/pokergame/game/IDeck.java new file mode 100644 index 0000000..43f07d5 --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/game/IDeck.java @@ -0,0 +1,9 @@ +package uk.co.neviyn.pokergame.game; + +import uk.co.neviyn.pokergame.model.Card; + +public interface IDeck { + + Card drawCard(); + void resetDeck(); +} diff --git a/src/main/java/uk/co/neviyn/pokergame/model/Card.java b/src/main/java/uk/co/neviyn/pokergame/model/Card.java new file mode 100644 index 0000000..4ba7f75 --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/model/Card.java @@ -0,0 +1,54 @@ +package uk.co.neviyn.pokergame.model; + +public class Card implements Comparable{ + + private final Suit suit; + private final Value value; + + /** + * Representation of a playing card. + * @param suit Card suit. + * @param value Card face or pip value. + */ + public Card(Suit suit, Value value) { + this.suit = suit; + this.value = value; + } + + /** + * @return Full length string representation of this card. + */ + public String getName(){ + if(suit == Suit.JOKER) + return "Joker"; + return value.toString() + " of " + suit.toString(); + } + + /** + * @return Short form of the name of this card. + */ + public String getShortName(){ + if(suit == Suit.JOKER) + return suit.getRepresentation().toString(); + return "" + value.getValue() + suit.getRepresentation().toString(); + } + + /** + * @return Face or pip value of this card. + */ + public Value getValue(){ + return value; + } + + /** + * @return Suit of this card. + */ + public Suit getSuit(){ + return suit; + } + + @Override + public int compareTo(Object o) { + return value.getValue() - ((Card)o).value.getValue(); + } +} diff --git a/src/main/java/uk/co/neviyn/pokergame/model/Result.java b/src/main/java/uk/co/neviyn/pokergame/model/Result.java new file mode 100644 index 0000000..f265609 --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/model/Result.java @@ -0,0 +1,25 @@ +package uk.co.neviyn.pokergame.model; + +public enum Result { + ROYAL_STRAIGHT_FLUSH(true), + FIVE_OF_A_KIND(true), + STRAIGHT_FLUSH(true), + FOUR_OF_A_KIND(true), + FULL_HOUSE(true), + FLUSH(true), + STRAIGHT(true), + THREE_OF_A_KIND(true), + TWO_OF_A_KIND(true), + LOSS(false) + ; + + private final boolean win; + + Result(boolean win) { + this.win = win; + } + + public boolean isWin() { + return win; + } +} diff --git a/src/main/java/uk/co/neviyn/pokergame/model/Suit.java b/src/main/java/uk/co/neviyn/pokergame/model/Suit.java new file mode 100644 index 0000000..5da3cac --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/model/Suit.java @@ -0,0 +1,42 @@ +package uk.co.neviyn.pokergame.model; + +import java.awt.Color; + +public enum Suit { + CLUBS(Color.BLACK, '♣'), + SPADES(Color.BLACK, '♠'), + HEARTS(Color.RED, '♥'), + DIAMONDS(Color.RED, '♦'), + JOKER(Color.BLACK, 'J') + ; + + private final Color color; + private final Character representation; + + Suit(Color color, Character representation){ + this.color = color; + this.representation = representation; + } + + /** + * @return Color used for this suit. + */ + public Color getColor(){ + return color; + } + + /** + * @return Unicode character representing this suit. + */ + public Character getRepresentation(){ + return representation; + } + + /** + * @return All suits excluding the joker. + */ + public static Suit[] standardSuits(){ + return new Suit[]{CLUBS, SPADES, HEARTS, DIAMONDS}; + } + +} diff --git a/src/main/java/uk/co/neviyn/pokergame/model/Value.java b/src/main/java/uk/co/neviyn/pokergame/model/Value.java new file mode 100644 index 0000000..bf211c6 --- /dev/null +++ b/src/main/java/uk/co/neviyn/pokergame/model/Value.java @@ -0,0 +1,39 @@ +package uk.co.neviyn.pokergame.model; + +public enum Value{ + JOKER(-1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(11), + QUEEN(12), + KING(13), + ACE(14) + ; + + private final int numericalValue; + + Value(int numericalValue) { + this.numericalValue = numericalValue; + } + + /** + * @return Numerical value. + */ + public int getValue(){ + return numericalValue; + } + + /** + * @return All values excluding the joker. + */ + public static Value[] standardValues(){ + return new Value[]{TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING, ACE}; + } +} diff --git a/src/test/java/uk/co/neviyn/pokergame/model/HandTest.java b/src/test/java/uk/co/neviyn/pokergame/model/HandTest.java new file mode 100644 index 0000000..db0fe43 --- /dev/null +++ b/src/test/java/uk/co/neviyn/pokergame/model/HandTest.java @@ -0,0 +1,189 @@ +package uk.co.neviyn.pokergame.model; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import uk.co.neviyn.pokergame.game.Hand; +import uk.co.neviyn.pokergame.game.IDeck; + +import java.util.Stack; + +import static org.junit.Assert.*; + +public class HandTest { + + private FakeDeck deck = new FakeDeck(); + + @Before + public void setUp() throws Exception { + deck.clearDeck(); + } + + @Test + public void royalStraightFlush() { + deck.addCard(new Card(Suit.CLUBS, Value.TEN)); + deck.addCard(new Card(Suit.CLUBS, Value.JACK)); + deck.addCard(new Card(Suit.CLUBS, Value.QUEEN)); + deck.addCard(new Card(Suit.CLUBS, Value.KING)); + deck.addCard(new Card(Suit.CLUBS, Value.ACE)); + Hand hand = new Hand(deck); + assertEquals(Result.ROYAL_STRAIGHT_FLUSH, hand.checkForWin()); + } + + @Test + public void royalStraightFlushWithJoker() { + deck.addCard(new Card(Suit.CLUBS, Value.TEN)); + deck.addCard(new Card(Suit.CLUBS, Value.JACK)); + deck.addCard(new Card(Suit.JOKER, Value.JOKER)); + deck.addCard(new Card(Suit.CLUBS, Value.KING)); + deck.addCard(new Card(Suit.CLUBS, Value.ACE)); + Hand hand = new Hand(deck); + assertEquals(Result.ROYAL_STRAIGHT_FLUSH, hand.checkForWin()); + } + + @Test + public void fiveOfAKind(){ + deck.addCard(new Card(Suit.CLUBS, Value.TEN)); + deck.addCard(new Card(Suit.SPADES, Value.TEN)); + deck.addCard(new Card(Suit.HEARTS, Value.TEN)); + deck.addCard(new Card(Suit.DIAMONDS, Value.TEN)); + deck.addCard(new Card(Suit.JOKER, Value.JOKER)); + Hand hand = new Hand(deck); + assertEquals(Result.FIVE_OF_A_KIND, hand.checkForWin()); + } + + @Test + @Ignore + public void fiveOfAKindWithJoker(){} + + @Test + public void straightFlush() { + deck.addCard(new Card(Suit.SPADES, Value.TWO)); + deck.addCard(new Card(Suit.SPADES, Value.THREE)); + deck.addCard(new Card(Suit.SPADES, Value.FOUR)); + deck.addCard(new Card(Suit.SPADES, Value.FIVE)); + deck.addCard(new Card(Suit.SPADES, Value.SIX)); + Hand hand = new Hand(deck); + assertEquals(Result.STRAIGHT_FLUSH, hand.checkForWin()); + } + + @Test + @Ignore + public void straightFlushWithJoker() {} + + @Test + public void fourOfAKind() { + deck.addCard(new Card(Suit.SPADES, Value.TWO)); + deck.addCard(new Card(Suit.HEARTS, Value.TWO)); + deck.addCard(new Card(Suit.DIAMONDS, Value.TWO)); + deck.addCard(new Card(Suit.CLUBS, Value.TWO)); + deck.addCard(new Card(Suit.SPADES, Value.SIX)); + Hand hand = new Hand(deck); + assertEquals(Result.FOUR_OF_A_KIND, hand.checkForWin()); + } + + @Test + @Ignore + public void fourOfAKindWithJoker() {} + + @Test + public void fullHouse() { + deck.addCard(new Card(Suit.SPADES, Value.FOUR)); + deck.addCard(new Card(Suit.HEARTS, Value.FOUR)); + deck.addCard(new Card(Suit.DIAMONDS, Value.FOUR)); + deck.addCard(new Card(Suit.CLUBS, Value.JACK)); + deck.addCard(new Card(Suit.SPADES, Value.JACK)); + Hand hand = new Hand(deck); + assertEquals(Result.FULL_HOUSE, hand.checkForWin()); + } + + @Test + public void flush() { + deck.addCard(new Card(Suit.SPADES, Value.TWO)); + deck.addCard(new Card(Suit.SPADES, Value.FOUR)); + deck.addCard(new Card(Suit.SPADES, Value.ACE)); + deck.addCard(new Card(Suit.SPADES, Value.NINE)); + deck.addCard(new Card(Suit.SPADES, Value.KING)); + Hand hand = new Hand(deck); + assertEquals(Result.FLUSH, hand.checkForWin()); + } + + @Test + @Ignore + public void flushWithJoker() {} + + @Test + public void straight() { + deck.addCard(new Card(Suit.SPADES, Value.TWO)); + deck.addCard(new Card(Suit.DIAMONDS, Value.THREE)); + deck.addCard(new Card(Suit.HEARTS, Value.FOUR)); + deck.addCard(new Card(Suit.SPADES, Value.FIVE)); + deck.addCard(new Card(Suit.CLUBS, Value.SIX)); + Hand hand = new Hand(deck); + assertEquals(Result.STRAIGHT, hand.checkForWin()); + } + + @Test + public void nearStraight() { + deck.addCard(new Card(Suit.SPADES, Value.THREE)); + deck.addCard(new Card(Suit.DIAMONDS, Value.THREE)); + deck.addCard(new Card(Suit.HEARTS, Value.FOUR)); + deck.addCard(new Card(Suit.SPADES, Value.FIVE)); + deck.addCard(new Card(Suit.CLUBS, Value.SIX)); + Hand hand = new Hand(deck); + assertEquals(Result.LOSS, hand.checkForWin()); + } + + @Test + @Ignore + public void straightWithJoker() {} + + @Test + public void threeOfAKind() { + deck.addCard(new Card(Suit.SPADES, Value.FOUR)); + deck.addCard(new Card(Suit.HEARTS, Value.FOUR)); + deck.addCard(new Card(Suit.DIAMONDS, Value.FOUR)); + deck.addCard(new Card(Suit.CLUBS, Value.THREE)); + deck.addCard(new Card(Suit.SPADES, Value.ACE)); + Hand hand = new Hand(deck); + assertEquals(Result.THREE_OF_A_KIND, hand.checkForWin()); + } + + @Test + @Ignore + public void threeOfAKindWithJoker() {} + + @Test + public void twoPair() { + deck.addCard(new Card(Suit.SPADES, Value.FOUR)); + deck.addCard(new Card(Suit.HEARTS, Value.FOUR)); + deck.addCard(new Card(Suit.DIAMONDS, Value.TEN)); + deck.addCard(new Card(Suit.CLUBS, Value.ACE)); + deck.addCard(new Card(Suit.SPADES, Value.ACE)); + Hand hand = new Hand(deck); + assertEquals(Result.TWO_OF_A_KIND, hand.checkForWin()); + } + + private class FakeDeck implements IDeck { + + private Stack cards = new Stack<>(); + + public void addCard(Card card){ + cards.push(card); + } + + @Override + public Card drawCard() { + return cards.pop(); + } + + @Override + public void resetDeck() { + // FakeDeck has no base deck to reset to. + } + + protected void clearDeck(){ + cards = new Stack<>(); + } + } +} \ No newline at end of file