diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..7e42a8b9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/java +{ + "name": "Java", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/java:0-17", + + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "none", + "installMaven": "false", + "installGradle": "true" + } + } + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/README.md b/README.md index 8fe71120..9f26c919 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,133 @@ +## 이전에 지키지 못한 설계 원칙 +* 하나하나 지시하지 말고 요청해라. + * 예를들어, 판사가 증인에게 1) 목격했던 장면을 떠올리고, 2) 떠오르는 시간을 순서대로 구성하고, 3) 말로 간결하게 표현해라 라고 요청하지 않는다. 그냥 "증언하라" 라고 요청한다. + * 마찬가지로 객체의 설계단계에서도 책임이 있는 객체에 요청만 하도록 설계한다. +* 역할과 책임을 확실히 분리하기 + * 이전 코드에서 InputView가 단순 input을 받고 보여주는 역할을 넘어 게임의 로직까지 관리함 + + +## 구현 +* Strategy pattern 사용 +* String, int 등의 자료형 사용 x. 다 포장하기 +* 딜러의 수익금 규칙은 명확하지 않으니 spec out +* 객체의 다형성을 이용해 조건문 줄이기 (아래 UML 사용) + + + [이미지 출처](https://newwisdom.tistory.com/27) +* 주요 객체들은 첫 구현 시 equals method & hashCode method를 override 해줄 것 + +## 주요 객체 + * User + * Player + * Dealer + * card 관련 + * enums + * Denomination + * Suit + * Card + * Cards + * Deck + * State + * Running + * Hit + * Finished + * Bust + * Stay + * Blackjack + * Controller + +## 게임 규칙(from [toney parky](https://github.com/woowacourse/java-blackjack/tree/toneyparky)) +- [x] 플레이어의 이름을 입력 받는다. + - [x] 문자열 처리 + - [x] 참여인원 제한 + - [x] 플레이어 생성 +- [x] 플레이어마다 베팅 금액을 입력 받는다. + - [x] 문자열 처리 + - [x] 입력 범위 처리 +- [x] 게임 시작시 카드 2장을 딜러와 플레이어에게 나누어 주어야 한다. + - [x] Deck에서 2장을 pop해서 준다. (2장 pop하는 메서드도 가능) + - [x] Deck을 만들어야 한다. +- [x] 딜러는 1장, 플레이어는 2장의 카드를 공개한다. +- [x] 처음에 블랙잭일시 플레이어와 딜러의 패에 따라 경기 처리 +- [x] 플레이어가 카드를 더 받을수 있는지 검사 해야한다. + - [x] ACE는 1 또는 11로 계산할 수 있다. + - [x] K,Q,J는 10으로 계산한다. +- [x] 플레이어가 카드를 더 받고 싶어하는지 검사 해야한다. +- [x] 받을수 있고, 받고자 한다면 카드를 나누어준다. +- [x] 플레이어가 카드를 받으면 카드의 상태를 출력한다. +- [x] 플레이어 페이즈가 끝나면 딜러가 카드를 받는다. + - [x] 딜러가 16이하의 점수를 갖고 있으면 넘을때 까지 카드를 받는다. +- [x] 카드 결과를 출력한다. +- [x] 블랙잭 게임 결과에 따른 베팅 금액을 출력한다. + - [x] 딜러가 블랙잭이면 블랙잭을 가진 사람 이외의 플레이어는 전부 패배하여 베팅 금액을 돌려 받지 못한다. (둘다 블랙잭이면 돌려 받음.) + - [x] 둘다 버스트면 딜러가 이긴다. + - [x] 처음 2장으로 21이 만들어지면 블랙잭이다. + - [x] 버스트면 0점으로 간주한다. + - [x] 플레이어가 블랙잭으로 승리시 배팅 금액의 1.5배를 받는다. + - [x] 플레이어가 승리시 베팅 금액만큼을 받는다. + - [x] 플레이어가 패배시 베팅 금액만큼을 잃는다. + +## 기능 요구 사항 +블랙잭 게임을 변형한 프로그램을 구현한다. 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. + +- 플레이어는 게임을 시작할 때 배팅 금액을 정해야 한다. +- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. +- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. + - 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. 단, 카드를 추가로 뽑아 21을 초과할 경우 배팅 금액을 모두 잃게 된다. +- 처음 두 장의 카드 합이 21일 경우 블랙잭이 되면 베팅 금액의 1.5 배를 딜러에게 받는다. + - 딜러와 플레이어가 모두 동시에 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다. +- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. + - 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해 베팅 금액을 받는다. + +- BlackJack이 아닐 경우, 플레이어는 Hit(카드를 받는다), Stay(그만 받는다) 중 하나를 선택할 수 있다. + +- Stay를 선택할 경우 그 판에서는 더이상 카드를 받을 수 없으므로, 게임이 종료된다. + +- Hit를 선택할 시, 카드를 한 장 더 받게되고 +이 때, 카드의 합이 21이 초과했을 경우 Bust로 딜러의 카드 합 및 결과와 관계없이 패배한다. + +- 21이하인 경우는, 본인 판단하에 Stay를 선택해 게임을 종료하여 +딜러의 카드 합과 플레이어의 카드 합 중 높은 쪽이 승리하게 되는 게임이다. + +## 실행결과 +``` +게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리) +pobi,jason + +pobi의 배팅 금액은? +10000 + +jason의 배팅 금액은? +20000 + +딜러와 pobi, jason에게 2장의 나누었습니다. +딜러: 3다이아몬드 +pobi카드: 2하트, 8스페이드 +jason카드: 7클로버, K스페이드 + +pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +y +pobi카드: 2하트, 8스페이드, A클로버 +pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +n +jason은 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +n +jason카드: 7클로버, K스페이드 + +딜러는 16이하라 한장의 카드를 더 받았습니다. + +딜러 카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20 +pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21 +jason카드: 7클로버, K스페이드 - 결과: 17 + +## 최종 수익 +딜러: 10000 +pobi: 10000 +jason: -20000 + +``` + + ## [NEXTSTEP 플레이그라운드의 미션 진행 과정](https://github.com/next-step/nextstep-docs/blob/master/playground/README.md) --- diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c0..41d9927a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f80bbf5..aa991fce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..1b6c7873 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# 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. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# 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 -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 +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 -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +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" +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 - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +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 @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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 @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "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 +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 -# 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 +# 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" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # 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 +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 - 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\"" + 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 - i=`expr $i + 1` + # 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 - 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 -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# 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. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/src/main/java/blackjack/Main.java b/src/main/java/blackjack/Main.java new file mode 100644 index 00000000..279668ac --- /dev/null +++ b/src/main/java/blackjack/Main.java @@ -0,0 +1,9 @@ +package blackjack; + +import blackjack.controller.Controller; + +public class Main { + public static void main(String[] args) { + Controller.run(); + } +} diff --git a/src/main/java/blackjack/controller/Controller.java b/src/main/java/blackjack/controller/Controller.java new file mode 100644 index 00000000..2e76c3e4 --- /dev/null +++ b/src/main/java/blackjack/controller/Controller.java @@ -0,0 +1,78 @@ +package blackjack.controller; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.state.State; +import blackjack.domain.state.StateFactory; +import blackjack.domain.user.Betting; +import blackjack.domain.user.Dealer; +import blackjack.domain.user.Names; +import blackjack.domain.user.Player; +import blackjack.domain.user.Players; +import blackjack.view.InputView; +import blackjack.view.OutputView; + +public class Controller { + private final static Deck deck = Deck.getInstance(); + + private Controller() { + } + + public static void run() { + Players players = enrollPlayers(); + Dealer dealer = enrollDealer(); + + OutputView.printInitStatus(players, dealer); + + playerDrawPhase(players); + dealerDrawPhase(dealer); + + OutputView.printResult(players, dealer); + } + + private static Players enrollPlayers() { + Names names = Names.of(InputView.inputPlayerNames()); + Players players = new Players(); + + names.forEach((name) -> { + Betting betting = Betting.of(InputView.inputPlayerBettingMoney(name)); + + Card card1 = deck.draw(); + Card card2 = deck.draw(); + State firstState = StateFactory.firstDraw(card1, card2); + + players.add(new Player(name, betting, firstState)); + }); + return players; + } + + private static Dealer enrollDealer() { + Card card1 = deck.draw(); + Card card2 = deck.draw(); + State firstState = StateFactory.firstDraw(card1, card2); + + return new Dealer(firstState); + } + + private static void playerDrawPhase(Players players) { + players.forEach((player) -> { + while (!player.isFinishied() && wantDraw(player)) { + player.draw(deck.draw()); + OutputView.printPlayerStatus(player); + } + }); + } + + private static void dealerDrawPhase(Dealer dealer) { + OutputView.printDealerDrew(dealer); + dealer.draw(deck.draw()); + } + + private static boolean wantDraw(Player player) { + String intention = InputView.inputPlayerIntention(player); + boolean drawStops = intention.equals("n"); + if (drawStops) + player.stay(); + return intention.equals("y"); + } +} diff --git a/src/main/java/blackjack/domain/card/Card.java b/src/main/java/blackjack/domain/card/Card.java new file mode 100644 index 00000000..189b1da3 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,42 @@ +package blackjack.domain.card; + +import java.util.Objects; + +public class Card { + private final Denomination denomination; + private final Suit suit; + private final static String NULL_EXCEPTION_MESSAGE = "Denomination 또는 Suit로 null이 존재합니다."; + + public Card(Denomination denomination, Suit suit) { + Objects.requireNonNull(denomination, NULL_EXCEPTION_MESSAGE); + Objects.requireNonNull(suit, NULL_EXCEPTION_MESSAGE); + + this.denomination = denomination; + this.suit = suit; + } + + public Denomination getDenomitation() { + return denomination; + } + + public Suit getSuit() { + return suit; + } + + @Override + public String toString() { + return denomination.getName() + suit.getName(); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Card other = (Card) o; + + return this.getDenomitation() == other.getDenomitation() + && this.getSuit() == other.getSuit(); + } +} diff --git a/src/main/java/blackjack/domain/card/Cards.java b/src/main/java/blackjack/domain/card/Cards.java new file mode 100644 index 00000000..0962dffe --- /dev/null +++ b/src/main/java/blackjack/domain/card/Cards.java @@ -0,0 +1,95 @@ +package blackjack.domain.card; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.stream.Collectors; + +public class Cards { + + private final List cards; + private final static String NULL_CARD_EXCEPTION_MESSAGE = "card의 값은 null이 될 수 없습니다"; + private final static String DUPLICATED_CARD_EXCEPTION_MESSAGE = "이미 같은 card가 존재합니다"; + private final static String DELEMITER = ", "; + private final static Integer BLACKJACK_SCORE = 21; + private final static Random RANDOM = new Random(); + + public Cards() { + this.cards = new LinkedList<>(); + } + + public Cards(Card... cards) { + this(); + for (Card card : cards) { + this.cards.add(card); + } + } + + public void add(Card card) { + Objects.requireNonNull(card, NULL_CARD_EXCEPTION_MESSAGE); + if (cards.contains(card)) + throw new IllegalArgumentException(DUPLICATED_CARD_EXCEPTION_MESSAGE); + cards.add(card); + } + + public Integer size() { + return cards.size(); + } + + public Card get(int index) { + return cards.get(index); + } + + public boolean isBlackjack() { + return score() == BLACKJACK_SCORE; + } + + public boolean isBust() { + return score() > BLACKJACK_SCORE; + } + + public Integer score() { + int initialScore = cards.stream() + .mapToInt(card -> card.getDenomitation().getScore()) + .sum(); + + if (initialScore <= BLACKJACK_SCORE) + return initialScore; + + return cards.stream() + .mapToInt(card -> card.getDenomitation().getScore(initialScore)) + .sum(); + } + + public Card pop() { + Integer randomIndex = RANDOM.nextInt(cards.size()); + Card card = cards.get(randomIndex); + cards.remove(card); + return card; + } + + @Override + public String toString() { + return cards.stream() + .map(Card::toString) + .collect(Collectors.joining(DELEMITER)); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Cards other = (Cards) o; + + return Objects.equals(cards, other.cards); + } + + @Override + public int hashCode() { + return Objects.hashCode(cards); + } +} diff --git a/src/main/java/blackjack/domain/card/Deck.java b/src/main/java/blackjack/domain/card/Deck.java new file mode 100644 index 00000000..ff08d469 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,28 @@ +package blackjack.domain.card; + +public class Deck { + private final static Cards DECK = new Cards(); + private static Deck instance = null; + + private Deck() { + for (Denomination denomination : Denomination.values()) { + for (Suit suit : Suit.values()) { + DECK.add(new Card(denomination, suit)); + } + } + } + + public static Deck getInstance() { + if (instance == null) + instance = new Deck(); + return instance; + } + + public Card draw() { + return DECK.pop(); + } + + public Integer size() { + return DECK.size(); + } +} diff --git a/src/main/java/blackjack/domain/card/Denomination.java b/src/main/java/blackjack/domain/card/Denomination.java new file mode 100644 index 00000000..4c11d7da --- /dev/null +++ b/src/main/java/blackjack/domain/card/Denomination.java @@ -0,0 +1,59 @@ +package blackjack.domain.card; + +import java.util.function.Predicate; +import java.util.function.Supplier; + +public enum Denomination { + ACE(11, "A"), + TWO(2, "2"), + THREE(3, "3"), + FOUR(4, "4"), + FIVE(5, "5"), + SIX(6, "6"), + SEVEN(7, "7"), + EIGHT(8, "8"), + NINE(9, "9"), + TEN(10, "10"), + JACK(10, "J"), + QUEEN(10, "Q"), + KING(10, "K"); + + private final int score; + private final String name; + private final static Integer BIG_ACE_SCORE = 11; + private final static Integer BALCKJACK_SCORE = 21; + private final static Predicate scoreBurstPredicate = score -> score + BIG_ACE_SCORE > BALCKJACK_SCORE; + private final static Supplier smallAceScoreSupplier = () -> 1; + private final static String SCORE_EXCEPTION_MESSAGE = "score는 0보다 작을 수 없습니다."; + + Denomination(final int score, final String name) { + this.score = score; + this.name = name; + } + + public boolean isAce() { + return this == ACE; + } + + public Integer getScore() { + return score; + } + + public Integer getScore(int userScore) { + if (userScore < 0) + throw new IllegalArgumentException(SCORE_EXCEPTION_MESSAGE); + if (isAce()) + return getAceScore(userScore); + return score; + } + + public String getName() { + return name; + } + + private Integer getAceScore(int userScore) { + if (scoreBurstPredicate.test(userScore)) + return smallAceScoreSupplier.get(); + return BIG_ACE_SCORE; + } +} diff --git a/src/main/java/blackjack/domain/card/Suit.java b/src/main/java/blackjack/domain/card/Suit.java new file mode 100644 index 00000000..94eecf47 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Suit.java @@ -0,0 +1,15 @@ +package blackjack.domain.card; + +public enum Suit { + CLUBS("클로버"), DIAMONDS("다이아몬드"), HEARTS("하트"), SPADES("스페이드"); + + private final String name; + + Suit (String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/state/Started.java b/src/main/java/blackjack/domain/state/Started.java new file mode 100644 index 00000000..7e7ececc --- /dev/null +++ b/src/main/java/blackjack/domain/state/Started.java @@ -0,0 +1,21 @@ +package blackjack.domain.state; + +import blackjack.domain.card.Cards; + +public abstract class Started implements State { + + protected final Cards cards; + + public Started(final Cards cards) { + this.cards = cards; + } + + public Started() { + this.cards = new Cards(); + } + + @Override + public Cards cards() { + return cards; + } +} diff --git a/src/main/java/blackjack/domain/state/State.java b/src/main/java/blackjack/domain/state/State.java new file mode 100644 index 00000000..057541a0 --- /dev/null +++ b/src/main/java/blackjack/domain/state/State.java @@ -0,0 +1,18 @@ +package blackjack.domain.state; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.user.Betting; +import blackjack.domain.user.Dealer; + +public interface State { + State draw(Card card); + + State stay(); + + boolean isFinishied(); + + Cards cards(); + + double profit(Dealer dealer, Betting betting); +} diff --git a/src/main/java/blackjack/domain/state/StateFactory.java b/src/main/java/blackjack/domain/state/StateFactory.java new file mode 100644 index 00000000..60121613 --- /dev/null +++ b/src/main/java/blackjack/domain/state/StateFactory.java @@ -0,0 +1,16 @@ +package blackjack.domain.state; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.state.finished.Blackjack; +import blackjack.domain.state.running.Hit; + +public class StateFactory { + public static State firstDraw(Card card1, Card card2) { + Cards cards = new Cards(card1, card2); + + if (cards.isBlackjack()) + return new Blackjack(cards); + return new Hit(cards); + } +} diff --git a/src/main/java/blackjack/domain/state/finished/Blackjack.java b/src/main/java/blackjack/domain/state/finished/Blackjack.java new file mode 100644 index 00000000..b2db365f --- /dev/null +++ b/src/main/java/blackjack/domain/state/finished/Blackjack.java @@ -0,0 +1,18 @@ +package blackjack.domain.state.finished; + +import blackjack.domain.card.Cards; +import blackjack.domain.user.Dealer; + +// 블랙잭(Blackjack): 처음 두 장의 카드 합이 21인 경우, 베팅 금액의 1.5배 +public class Blackjack extends Finished { + public Blackjack(final Cards cards) { + super(cards); + } + + @Override + public double earningRate(Dealer dealer) { + if (cards().isBlackjack() && dealer.isBlackjack()) + return 0; + return 1.5; + } +} diff --git a/src/main/java/blackjack/domain/state/finished/Bust.java b/src/main/java/blackjack/domain/state/finished/Bust.java new file mode 100644 index 00000000..0346fd1c --- /dev/null +++ b/src/main/java/blackjack/domain/state/finished/Bust.java @@ -0,0 +1,18 @@ +package blackjack.domain.state.finished; + +import blackjack.domain.card.Cards; +import blackjack.domain.user.Dealer; + +// 버스트(Bust): 카드 총합이 21을 넘는 경우. 배당금을 잃는다. +public class Bust extends Finished { + + public Bust(Cards cards) { + super(cards); + } + + @Override + public double earningRate(Dealer dealer) { + return 0; + } + +} diff --git a/src/main/java/blackjack/domain/state/finished/Finished.java b/src/main/java/blackjack/domain/state/finished/Finished.java new file mode 100644 index 00000000..a9c65fd6 --- /dev/null +++ b/src/main/java/blackjack/domain/state/finished/Finished.java @@ -0,0 +1,39 @@ +package blackjack.domain.state.finished; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.state.Started; +import blackjack.domain.state.State; +import blackjack.domain.user.Betting; +import blackjack.domain.user.Dealer; + +// 끝난 상태를 표현하기 위한 클래스 +// profit()을 얻을 수 있지만 draw(), stay()는 사용이 불가능 하도록 예외처리 +public abstract class Finished extends Started { + public Finished(final Cards cards) { + super(cards); + } + + @Override + public boolean isFinishied() { + return true; + } + + @Override + public double profit(Dealer dealer, Betting betting) { + return betting.value() * earningRate(dealer); + } + + public abstract double earningRate(Dealer dealer); + + @Override + public State draw(final Card card) { + throw new UnsupportedOperationException(); + } + + @Override + public State stay() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/blackjack/domain/state/finished/Stay.java b/src/main/java/blackjack/domain/state/finished/Stay.java new file mode 100644 index 00000000..68153460 --- /dev/null +++ b/src/main/java/blackjack/domain/state/finished/Stay.java @@ -0,0 +1,20 @@ +package blackjack.domain.state.finished; + +import blackjack.domain.card.Cards; +import blackjack.domain.user.Dealer; + +// 스테이(Stay): 카드를 더 뽑지 않고 차례를 마치는 것 +public class Stay extends Finished { + public Stay(Cards cards) { + super(cards); + } + + @Override + public double earningRate(Dealer dealer) { + if (dealer.isBust() || cards().score() > dealer.score()) + return 1; + if (!dealer.isBust() && cards().score() == dealer.score()) + return 0; + return -1; + } +} diff --git a/src/main/java/blackjack/domain/state/running/Hit.java b/src/main/java/blackjack/domain/state/running/Hit.java new file mode 100644 index 00000000..6cb3f4bd --- /dev/null +++ b/src/main/java/blackjack/domain/state/running/Hit.java @@ -0,0 +1,28 @@ +package blackjack.domain.state.running; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.state.State; +import blackjack.domain.state.finished.Bust; +import blackjack.domain.state.finished.Stay; + +// 힛(Hit): 처음 2장의 상태에서 카드를 더 뽑는 것 +public class Hit extends Running { + + public Hit(final Cards cards) { + super(cards); + } + + @Override + public State draw(final Card card) { + cards.add(card); + if (cards.isBust()) + return new Bust(cards); + return new Hit(cards); + } + + @Override + public State stay() { + return new Stay(cards); + } +} diff --git a/src/main/java/blackjack/domain/state/running/Running.java b/src/main/java/blackjack/domain/state/running/Running.java new file mode 100644 index 00000000..b43d1425 --- /dev/null +++ b/src/main/java/blackjack/domain/state/running/Running.java @@ -0,0 +1,23 @@ +package blackjack.domain.state.running; + +import blackjack.domain.card.Cards; +import blackjack.domain.state.Started; +import blackjack.domain.user.Betting; +import blackjack.domain.user.Dealer; + +public abstract class Running extends Started { + + public Running(final Cards cards) { + super(cards); + } + + @Override + public boolean isFinishied() { + return false; + } + + @Override + public double profit(Dealer dealer, Betting betting) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/blackjack/domain/user/AbstractUser.java b/src/main/java/blackjack/domain/user/AbstractUser.java new file mode 100644 index 00000000..52bf5cb6 --- /dev/null +++ b/src/main/java/blackjack/domain/user/AbstractUser.java @@ -0,0 +1,55 @@ +package blackjack.domain.user; + +import blackjack.domain.card.Cards; +import blackjack.domain.state.State; + +public abstract class AbstractUser implements User { + private final Name name; + private final Betting betting; + protected State state; + + public AbstractUser(Name name, Betting betting, State state) { + this.name = name; + this.betting = betting; + this.state = state; + } + + public Name getName() { + return name; + } + + public Betting getBetting() { + return betting; + } + + protected void changeState(State state) { + this.state = state; + } + + public void stay() { + changeState(state.stay()); + } + + public boolean isFinishied() { + return state.isFinishied(); + } + + public boolean isBlackjack() { + return getCards().isBlackjack(); + } + + protected Cards getCards() { + return state.cards(); + } + + public Integer score() { + return getCards().score(); + } + + @Override + public String toString() { + String cardsInString = getCards().toString(); + + return getName().value() + " 카드: " + cardsInString; + } +} diff --git a/src/main/java/blackjack/domain/user/Betting.java b/src/main/java/blackjack/domain/user/Betting.java new file mode 100644 index 00000000..523ef963 --- /dev/null +++ b/src/main/java/blackjack/domain/user/Betting.java @@ -0,0 +1,26 @@ +package blackjack.domain.user; + +public class Betting { + + private final Double betting; + private final static String ARGUMENT_EXCEPTION_STRING = "베팅 금액은 0보다 큰 정수여야 합니다."; + private final static int MIN_BETTING = 0; + + public Betting(double betting) { + validate(betting); + this.betting = betting; + } + + public static Betting of(String betting) { + return new Betting(Double.parseDouble(betting)); + } + + private void validate(double betting) { + if (betting < MIN_BETTING) + throw new IllegalArgumentException(ARGUMENT_EXCEPTION_STRING); + } + + public Double value() { + return betting; + } +} diff --git a/src/main/java/blackjack/domain/user/Dealer.java b/src/main/java/blackjack/domain/user/Dealer.java new file mode 100644 index 00000000..2e7144f7 --- /dev/null +++ b/src/main/java/blackjack/domain/user/Dealer.java @@ -0,0 +1,34 @@ +package blackjack.domain.user; + +import blackjack.domain.card.Card; +import blackjack.domain.state.State; + +public class Dealer extends AbstractUser { + + public static final Integer STANDARD = 16; + + public Dealer(State state) { + super(new Name("딜러"), null, state); + } + + @Override + public void draw(Card card) { + if (canDraw()) { + changeState(state.draw(card)); + return; + } + stay(); + } + + public String initStatus() { + return getName().value() + ": " + getCards().get(0).toString(); + } + + public boolean isBust() { + return getCards().isBust(); + } + + public boolean canDraw() { + return score() <= STANDARD; + } +} diff --git a/src/main/java/blackjack/domain/user/Name.java b/src/main/java/blackjack/domain/user/Name.java new file mode 100644 index 00000000..b8e84064 --- /dev/null +++ b/src/main/java/blackjack/domain/user/Name.java @@ -0,0 +1,27 @@ +package blackjack.domain.user; + +import java.util.Objects; + +public class Name { + + private final String name; + private final static String NULL_NAME_EXCEPTION_MESSAGE = "이름은 null이될 수 없습니다"; + + public Name(String name) { + validate(name); + this.name = name; + } + + public void validate(String name) { + Objects.requireNonNull(name, NULL_NAME_EXCEPTION_MESSAGE); + } + + public String value() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/user/Names.java b/src/main/java/blackjack/domain/user/Names.java new file mode 100644 index 00000000..3a8680cd --- /dev/null +++ b/src/main/java/blackjack/domain/user/Names.java @@ -0,0 +1,39 @@ +package blackjack.domain.user; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +public class Names implements Iterable { + private final List names; + + private static final String SPLIT_DELIMITER = ","; + + public Names(List names) { + this.names = names; + } + + public static Names of(List names) { + return new Names(names); + } + + public static Names of(String names) { + String trimmedNames = names.trim(); + return of(Arrays.stream(trimmedNames.split(SPLIT_DELIMITER)) + .map(String::trim) + .map(Name::new) + .collect(Collectors.toList())); + } + + public List getNames() { + return Collections.unmodifiableList(names); + } + + @Override + public Iterator iterator() { + return names.iterator(); + } + +} diff --git a/src/main/java/blackjack/domain/user/Player.java b/src/main/java/blackjack/domain/user/Player.java new file mode 100644 index 00000000..361fa367 --- /dev/null +++ b/src/main/java/blackjack/domain/user/Player.java @@ -0,0 +1,21 @@ +package blackjack.domain.user; + +import blackjack.domain.card.Card; +import blackjack.domain.state.State; + +public class Player extends AbstractUser { + + public Player(Name name, Betting betting, State state) { + super(name, betting, state); + } + + public double profit(Dealer dealer) { + return state.profit(dealer, getBetting()); + } + + @Override + public void draw(Card card) { + changeState(state.draw(card)); + } + +} diff --git a/src/main/java/blackjack/domain/user/Players.java b/src/main/java/blackjack/domain/user/Players.java new file mode 100644 index 00000000..466745a2 --- /dev/null +++ b/src/main/java/blackjack/domain/user/Players.java @@ -0,0 +1,50 @@ +package blackjack.domain.user; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Players implements Iterable { + private final List players; + + public Players() { + players = new ArrayList<>(); + } + + public void add(Player player) { + players.add(player); + } + + public Player get(int index) { + return players.get(index); + } + + @Override + public Iterator iterator() { + return players.iterator(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Players other = (Players) o; + return Objects.equals(players, other.players); + } + + @Override + public int hashCode() { + return Objects.hash(players); + } + + @Override + public String toString() { + return players.stream() + .map(Player::toString) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/blackjack/domain/user/User.java b/src/main/java/blackjack/domain/user/User.java new file mode 100644 index 00000000..5df4f0a2 --- /dev/null +++ b/src/main/java/blackjack/domain/user/User.java @@ -0,0 +1,13 @@ +package blackjack.domain.user; + +import blackjack.domain.card.Card; + +public interface User { + Name getName(); + Betting getBetting(); + void draw(Card card); + void stay(); + boolean isFinishied(); + boolean isBlackjack(); + Integer score(); +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000..b0a0b3f2 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,27 @@ +package blackjack.view; + +import java.util.Scanner; + +import blackjack.domain.user.Name; +import blackjack.domain.user.Player; + +public class InputView { + private static final Scanner SCANNER = new Scanner(System.in); + + private InputView() {} + + public static String inputPlayerNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + return SCANNER.nextLine(); + } + + public static String inputPlayerBettingMoney(Name name) { + System.out.println("\n" + name + "의 베팅 금액은?"); + return SCANNER.nextLine(); + } + + public static String inputPlayerIntention(Player player) { + System.out.println(player.getName() + "는 한장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)"); + return SCANNER.nextLine(); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 00000000..e2111f83 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,44 @@ +package blackjack.view; + +import blackjack.domain.user.Dealer; +import blackjack.domain.user.Player; +import blackjack.domain.user.Players; + +public class OutputView { + public static void printInitStatus(Players players, Dealer dealer) { + printNewLine(); + System.out.println(dealer.initStatus()); + System.out.println(players); + printNewLine(); + } + + public static void printPlayerStatus(Player player) { + System.out.println(player); + } + + public static void printDealerDrew(Dealer dealer) { + if (dealer.canDraw()) { + printNewLine(); + System.out.println("딜러는 " + Dealer.STANDARD + "이하라 한장의 카드를 더 받았습니다."); + printNewLine(); + } + } + + public static void printResult(Players players, Dealer dealer) { + printNewLine(); + System.out.println(dealer + " - 결과: " + dealer.score()); + players.forEach(player -> { + System.out.println(player + " - 결과: " + player.score()); + }); + printNewLine(); + + System.out.println("## 최종 수익"); + players.forEach(player -> { + System.out.println(player.getName() + ": " + player.profit(dealer)); + }); + } + + private static void printNewLine() { + System.out.println(); + } +} diff --git a/src/main/java/nextstep/fp/Conditional.java b/src/main/java/nextstep/fp/Conditional.java new file mode 100644 index 00000000..49e095c1 --- /dev/null +++ b/src/main/java/nextstep/fp/Conditional.java @@ -0,0 +1,5 @@ +package nextstep.fp; + +public interface Conditional { + boolean test (Integer number); +} diff --git a/src/main/java/nextstep/fp/Lambda.java b/src/main/java/nextstep/fp/Lambda.java index bd68fe1c..70b3260b 100644 --- a/src/main/java/nextstep/fp/Lambda.java +++ b/src/main/java/nextstep/fp/Lambda.java @@ -26,31 +26,20 @@ public void run() { }).start(); } - public static int sumAll(List numbers) { + public static int sumAll(List numbers, Conditional conditon) { int total = 0; for (int number : numbers) { - total += number; + if (conditon.test(number)) + total += number; } return total; } public static int sumAllEven(List numbers) { - int total = 0; - for (int number : numbers) { - if (number % 2 == 0) { - total += number; - } - } - return total; + return sumAll(numbers, number -> number % 2 == 0); } public static int sumAllOverThree(List numbers) { - int total = 0; - for (int number : numbers) { - if (number > 3) { - total += number; - } - } - return total; + return sumAll(numbers, number -> number > 3); } } diff --git a/src/main/java/nextstep/fp/StreamStudy.java b/src/main/java/nextstep/fp/StreamStudy.java index b446983a..697520a8 100644 --- a/src/main/java/nextstep/fp/StreamStudy.java +++ b/src/main/java/nextstep/fp/StreamStudy.java @@ -15,10 +15,7 @@ public static long countWords() throws IOException { .get("src/main/resources/fp/war-and-peace.txt")), StandardCharsets.UTF_8); List words = Arrays.asList(contents.split("[\\P{L}]+")); - long count = 0; - for (String w : words) { - if (w.length() > 12) count++; - } + long count = words.stream().filter(w -> w.length() > 12).count(); return count; } @@ -28,6 +25,12 @@ public static void printLongestWordTop100() throws IOException { List words = Arrays.asList(contents.split("[\\P{L}]+")); // TODO 이 부분에 구현한다. + words.stream() + .filter(w -> w.length() > 12) + .distinct() + .sorted() + .limit(100) + .forEach(w -> System.out.println(w.toLowerCase())); } public static List doubleNumbers(List numbers) { @@ -39,6 +42,6 @@ public static long sumAll(List numbers) { } public static long sumOverThreeAndDouble(List numbers) { - return 0; + return numbers.stream().filter(n -> n > 3).map(n -> n * 2).reduce(0, Integer::sum); } } \ No newline at end of file diff --git a/src/main/java/nextstep/optional/Expression.java b/src/main/java/nextstep/optional/Expression.java index 1c98cd6a..d455d981 100644 --- a/src/main/java/nextstep/optional/Expression.java +++ b/src/main/java/nextstep/optional/Expression.java @@ -1,5 +1,7 @@ package nextstep.optional; +import java.util.stream.Stream; + enum Expression { PLUS("+"), MINUS("-"), TIMES("*"), DIVIDE("/"); @@ -14,12 +16,12 @@ private static boolean matchExpression(Expression e, String expression) { } static Expression of(String expression) { - for (Expression v : values()) { - if (matchExpression(v, expression)) { - return v; - } - } + return Stream.of(values()) + .filter(v -> matchExpression(v, expression)) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("%s는 사칙연산에 해당하지 않는 표현식입니다.", expression))); - throw new IllegalArgumentException(String.format("%s는 사칙연산에 해당하지 않는 표현식입니다.", expression)); } } diff --git a/src/main/java/nextstep/optional/User.java b/src/main/java/nextstep/optional/User.java index 9614c2f4..f877348b 100644 --- a/src/main/java/nextstep/optional/User.java +++ b/src/main/java/nextstep/optional/User.java @@ -1,5 +1,7 @@ package nextstep.optional; +import java.util.Optional; + public class User { private String name; private Integer age; @@ -26,14 +28,20 @@ public static boolean ageIsInRange1(User user) { if (user != null && user.getAge() != null && (user.getAge() >= 30 - && user.getAge() <= 45)) { + && user.getAge() <= 45)) { isInRange = true; } return isInRange; } public static boolean ageIsInRange2(User user) { - return false; + Optional optionalUser = Optional.ofNullable(user); + if (!optionalUser.isPresent()) + return false; + + return optionalUser.map(u -> u.getAge()) + .filter(age -> age >= 30 && age <= 45) + .isPresent(); } @Override diff --git a/src/main/java/nextstep/optional/Users.java b/src/main/java/nextstep/optional/Users.java index 6293040d..8a285b68 100644 --- a/src/main/java/nextstep/optional/Users.java +++ b/src/main/java/nextstep/optional/Users.java @@ -13,11 +13,9 @@ public class Users { new User("honux", 45)); User getUser(String name) { - for (User user : users) { - if (user.matchName(name)) { - return user; - } - } - return DEFAULT_USER; + return users.stream() + .filter(u -> u.matchName(name)) + .findFirst() + .orElseGet(() -> DEFAULT_USER); } } diff --git a/src/test/java/blackjack/domain/Fixture.java b/src/test/java/blackjack/domain/Fixture.java new file mode 100644 index 00000000..851647e0 --- /dev/null +++ b/src/test/java/blackjack/domain/Fixture.java @@ -0,0 +1,14 @@ +package blackjack.domain; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Denomination; +import blackjack.domain.card.Suit; + +public class Fixture { + public static final Card ACE_SPADE = new Card(Denomination.ACE, Suit.SPADES); + public static final Card TWO_SPADE = new Card(Denomination.TWO, Suit.SPADES); + public static final Card KING_SPADE = new Card(Denomination.KING, Suit.SPADES); + public static final Card EIGHT_SPADE = new Card(Denomination.EIGHT, Suit.SPADES); + public static final Card NINE_SPADE = new Card(Denomination.NINE, Suit.SPADES); + public static final Card KING_HEART = new Card(Denomination.KING, Suit.HEARTS); +} diff --git a/src/test/java/blackjack/domain/card/CardTest.java b/src/test/java/blackjack/domain/card/CardTest.java new file mode 100644 index 00000000..8879627c --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,44 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import blackjack.domain.Fixture; + +public class CardTest { + Card card; + + @BeforeEach + void setup() { + card = new Card(Denomination.ACE, Suit.CLUBS); + } + + @Test + void null_argument_test() { + assertThatNullPointerException().isThrownBy(() -> { + new Card(null, null); + }); + + assertThatNullPointerException().isThrownBy(() -> { + new Card(null, Suit.CLUBS); + }); + + assertThatNullPointerException().isThrownBy(() -> { + new Card(Denomination.ACE, null); + }); + } + + @Test + void toString_test() { + assertThat(card.toString().equals("A클로버")); + } + + @Test + void equals_test() { + assertThat(card.equals(new Card(Denomination.ACE, Suit.CLUBS))).isTrue(); + assertThat(card.equals(Fixture.ACE_SPADE)).isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/card/CardsTest.java b/src/test/java/blackjack/domain/card/CardsTest.java new file mode 100644 index 00000000..c51e8aef --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardsTest.java @@ -0,0 +1,105 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import blackjack.domain.Fixture; + +public class CardsTest { + Cards cards; + + @BeforeEach + void setup() { + cards = new Cards(); + } + + @AfterEach + void teardown() { + cards = null; + } + + @Test + void add_null_test() { + assertThatNullPointerException().isThrownBy(() -> { + cards.add(null); + }); + } + + @Test + void add_duplicated_card_test() { + cards.add(Fixture.ACE_SPADE); + assertThatIllegalArgumentException().isThrownBy(() -> { + cards.add(Fixture.ACE_SPADE); + }); + } + + @Test + void sum_test_under_bust_with_ace() { + cards.add(Fixture.ACE_SPADE); + cards.add(Fixture.KING_SPADE); + + assertThat(cards.score()).isEqualTo(21); + } + + @Test + void sum_test_over_bust_with_ace() { + cards.add(Fixture.ACE_SPADE); + cards.add(Fixture.KING_SPADE); + cards.add(Fixture.KING_HEART); + + assertThat(cards.score()).isEqualTo(21); + } + + @Test + void isBust_test() { + cards.add(Fixture.KING_SPADE); + cards.add(Fixture.KING_HEART); + + assertThat(cards.isBust()).isFalse(); + + cards.add(new Card(Denomination.TWO, Suit.HEARTS)); + assertThat(cards.isBust()).isTrue(); + } + + @Test + void pop_test() { + Card card = Fixture.KING_SPADE; + cards.add(card); + + assertThat(cards.pop().equals(card)).isTrue(); + assertThat(cards.size()).isEqualTo(0); + } + + @Test + void equals_test() { + Card card1 = Fixture.KING_SPADE; + Card card2 = Fixture.KING_HEART; + cards.add(card1); + cards.add(card2); + + Cards other = new Cards(); + other.add(card1); + other.add(card2); + + assertThat(cards.equals(other)).isTrue(); + + cards.add(Fixture.TWO_SPADE); + + assertThat(cards.equals(other)).isFalse(); + } + + @Test + void toString_test() { + Card card1 = Fixture.KING_SPADE; + Card card2 = Fixture.KING_HEART; + cards.add(card1); + cards.add(card2); + + assertThat(cards.toString().equals("K스페이드, K하트")).isTrue(); + } +} diff --git a/src/test/java/blackjack/domain/card/DeckTest.java b/src/test/java/blackjack/domain/card/DeckTest.java new file mode 100644 index 00000000..641d64b3 --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,24 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class DeckTest { + + @Test + void getInstance_test() { + Deck deck = Deck.getInstance(); + assertThat(deck).isInstanceOf(Deck.class); + } + + @Test + void draw_test() { + Deck deck = Deck.getInstance(); + Card card = deck.draw(); + int totalDeckSize = 52; + + assertThat(card).isInstanceOf(Card.class); + assertThat(deck.size()).isEqualTo(totalDeckSize - 1); + } +} diff --git a/src/test/java/blackjack/domain/card/DenominationTest.java b/src/test/java/blackjack/domain/card/DenominationTest.java new file mode 100644 index 00000000..c128270b --- /dev/null +++ b/src/test/java/blackjack/domain/card/DenominationTest.java @@ -0,0 +1,25 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +public class DenominationTest { + @Test + void getScore_test() { + assertThat(Denomination.ACE.getScore(10)) + .isEqualTo(11); + assertThat(Denomination.ACE.getScore(11)) + .isEqualTo(1); + assertThat(Denomination.KING.getScore(11)) + .isEqualTo(10); + } + + @Test + void getScore_illegalArgumenttExceptionTest() { + assertThatIllegalArgumentException().isThrownBy(() -> { + Denomination.SEVEN.getScore(-1); + }); + } +} diff --git a/src/test/java/blackjack/domain/state/HitTest.java b/src/test/java/blackjack/domain/state/HitTest.java new file mode 100644 index 00000000..95ba25bd --- /dev/null +++ b/src/test/java/blackjack/domain/state/HitTest.java @@ -0,0 +1,27 @@ +package blackjack.domain.state; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import blackjack.domain.Fixture; +import blackjack.domain.card.Cards; +import blackjack.domain.state.finished.Bust; +import blackjack.domain.state.running.Hit; + +public class HitTest { + + @Test + void hit_test() { + Hit hit = new Hit(new Cards(Fixture.ACE_SPADE, Fixture.TWO_SPADE)); + State state = hit.draw(Fixture.KING_HEART); + assertThat(state).isInstanceOf(Hit.class); + } + + @Test + void bust_test() { + Hit hit = new Hit(new Cards(Fixture.KING_HEART, Fixture.KING_SPADE, Fixture.ACE_SPADE)); + State state = hit.draw(Fixture.TWO_SPADE); + assertThat(state).isInstanceOf(Bust.class); + } +} diff --git a/src/test/java/blackjack/domain/user/DealerTest.java b/src/test/java/blackjack/domain/user/DealerTest.java new file mode 100644 index 00000000..529e3c31 --- /dev/null +++ b/src/test/java/blackjack/domain/user/DealerTest.java @@ -0,0 +1,20 @@ +package blackjack.domain.user; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.Fixture; +import blackjack.domain.state.State; +import blackjack.domain.state.StateFactory; + +import org.junit.jupiter.api.Test; + +public class DealerTest { + @Test + void firstPhase() { + State firstState = StateFactory.firstDraw(Fixture.KING_HEART, Fixture.KING_SPADE); + + Dealer dealer = new Dealer(firstState); + + assertThat(dealer.initStatus().equals("딜러: K하트")).isTrue(); + } +} diff --git a/src/test/java/blackjack/domain/user/PlayerTest.java b/src/test/java/blackjack/domain/user/PlayerTest.java new file mode 100644 index 00000000..05ab6a9d --- /dev/null +++ b/src/test/java/blackjack/domain/user/PlayerTest.java @@ -0,0 +1,81 @@ +package blackjack.domain.user; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import blackjack.domain.Fixture; +import blackjack.domain.state.State; +import blackjack.domain.state.StateFactory; + +public class PlayerTest { + Player player; + Player blackjackPlayer; + Dealer dealer; + Dealer blackjackDealer; + + @BeforeEach + void setup() { + Name name = new Name("hyeonwoo"); + Betting betting = new Betting(100); + + State firstState = StateFactory.firstDraw(Fixture.EIGHT_SPADE, Fixture.KING_SPADE); + player = new Player(name, betting, firstState); + player.draw(Fixture.TWO_SPADE); + + + State blackjack = StateFactory.firstDraw(Fixture.ACE_SPADE, Fixture.KING_SPADE); + blackjackPlayer = new Player(name, betting, blackjack); + + State dealerState = StateFactory.firstDraw(Fixture.TWO_SPADE, Fixture.KING_SPADE); + dealer = new Dealer(dealerState); + dealer.draw(Fixture.ACE_SPADE); + + State blackjackDealerState = StateFactory.firstDraw(Fixture.ACE_SPADE, Fixture.KING_SPADE); + blackjackDealer = new Dealer(blackjackDealerState); + } + + @Test + void draw_isFinished_true() { + assertThat(player.isFinishied()).isFalse(); + } + + @Test + void bust() { + player.draw(Fixture.KING_HEART); + + assertThat(player.isFinishied()).isTrue(); + assertThat(player.profit(dealer)).isEqualTo(0.0); + } + + @Test + void user_toString() { + assertThat(player.toString().equals("hyeonwoo 카드: 8스페이드, K스페이드, 2스페이드")).isTrue(); + } + + @Test + void profit_player_score_bigger_than_dealer_score() { + player.stay(); + + assertThat(player.profit(dealer)).isEqualTo(100.0); + } + + @Test + void profit_should_be_0_when_both_blackjack_case() { + player.stay(); + assertThat(blackjackPlayer.profit(blackjackDealer)).isEqualTo(0.0); + } + + @Test + void profit_should_be_negative100_when_only_dealer_is_blackjack() { + player.stay(); + assertThat(player.profit(blackjackDealer)).isEqualTo(-100.0); + } + + @Test + void profit_should_be_0_when_bust_player_low_dealer_score() { + player.draw(Fixture.KING_HEART); + assertThat(player.profit(dealer)).isEqualTo(0.0); + } +} diff --git a/src/test/java/nextstep/fp/CarTest.java b/src/test/java/nextstep/fp/CarTest.java index 1ab1106f..ecab481a 100644 --- a/src/test/java/nextstep/fp/CarTest.java +++ b/src/test/java/nextstep/fp/CarTest.java @@ -8,24 +8,14 @@ public class CarTest { @Test public void 이동() { Car car = new Car("pobi", 0); - Car actual = car.move(new MoveStrategy() { - @Override - public boolean isMovable() { - return true; - } - }); + Car actual = car.move(() -> true); assertThat(actual).isEqualTo(new Car("pobi", 1)); } @Test public void 정지() { Car car = new Car("pobi", 0); - Car actual = car.move(new MoveStrategy() { - @Override - public boolean isMovable() { - return false; - } - }); + Car actual = car.move(() -> false); assertThat(actual).isEqualTo(new Car("pobi", 0)); } } diff --git a/src/test/java/nextstep/fp/LambdaTest.java b/src/test/java/nextstep/fp/LambdaTest.java index f240ac65..b2c99380 100644 --- a/src/test/java/nextstep/fp/LambdaTest.java +++ b/src/test/java/nextstep/fp/LambdaTest.java @@ -33,7 +33,7 @@ public void runThread() throws Exception { @Test public void sumAll() throws Exception { - int sum = Lambda.sumAll(numbers); + int sum = Lambda.sumAll(numbers, (number) -> true); assertThat(sum).isEqualTo(21); }