diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5d1261c --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation "org.assertj:assertj-core:3.25.3" +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e411586 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# 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\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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" = "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 +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 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 + # 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=`expr $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 + +# 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, 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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3a4a665 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-baseball' + diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java new file mode 100644 index 0000000..99cf5b9 --- /dev/null +++ b/src/main/java/baseball/Application.java @@ -0,0 +1,8 @@ +package baseball; + + +public class Application { + public static void main(String[] args) throws IllegalAccessException { + BaseballGame.launch(); + } +} diff --git a/src/main/java/baseball/BaseballGame.java b/src/main/java/baseball/BaseballGame.java new file mode 100644 index 0000000..b48c9da --- /dev/null +++ b/src/main/java/baseball/BaseballGame.java @@ -0,0 +1,56 @@ +package baseball; + +import static java.util.UUID.randomUUID; + +import baseball.domain.Computer; +import baseball.domain.ComputerDigit; +import baseball.domain.UserDigit; +import baseball.presentation.GameManager; +import baseball.domain.Referee; +import baseball.presentation.GameStatus; +import baseball.presentation.MessagePrinter; +import baseball.presentation.RandomNumberGenerator; +import baseball.presentation.input.Input; +import baseball.presentation.input.InputProvider; + +import java.util.List; +import java.util.Scanner; + +public class BaseballGame { + + public static void launch() throws IllegalAccessException { + final Computer computer = new Computer(new RandomNumberGenerator()); + final Referee referee = new Referee(); + final InputProvider inputProvider = new Input(new Scanner(System.in)); + final GameManager gameManager = new GameManager(new MessagePrinter(), inputProvider); + + boolean isApplicationOver = false; + while (!isApplicationOver) { + GameStatus gameStatus = gameManager.init(); + String gameId = randomUUID().toString(); + ComputerDigit computerDigit = null; + + if (gameStatus.equals(GameStatus.START)) { + List computerDigits = gameManager.getComputerDigits(computer); + computerDigit = new ComputerDigit(gameId, computerDigits); + + } else if (gameStatus.equals(GameStatus.QUIT)) { + gameManager.applicationOver(); + } + + boolean isOut = false; + int tryCount = 0; + while (!isOut) { + tryCount++; + List userDigits = gameManager.getUserDigits(); + UserDigit userDigit = new UserDigit(gameId, tryCount, userDigits); + + isOut = referee.judge(computerDigit, userDigit); + String callMessage = referee.getCallMessage(); + gameManager.call(callMessage); + } + + gameManager.gameOver(); + } + } +} diff --git a/src/main/java/baseball/common/exception/InvalidGameStatusException.java b/src/main/java/baseball/common/exception/InvalidGameStatusException.java new file mode 100644 index 0000000..8928635 --- /dev/null +++ b/src/main/java/baseball/common/exception/InvalidGameStatusException.java @@ -0,0 +1,10 @@ +package baseball.common.exception; + +public class InvalidGameStatusException extends Exception{ + + private static final String MESSAGE = "1(시작) 또는 9(종료)를 눌러주세요."; + + public InvalidGameStatusException() { + super(MESSAGE); + } +} diff --git a/src/main/java/baseball/common/exception/InvalidLengthException.java b/src/main/java/baseball/common/exception/InvalidLengthException.java new file mode 100644 index 0000000..cb63b53 --- /dev/null +++ b/src/main/java/baseball/common/exception/InvalidLengthException.java @@ -0,0 +1,10 @@ +package baseball.common.exception; + +public class InvalidLengthException extends Exception{ + + private static final String MESSAGE = "자리 숫자만 가능합니다."; + + public InvalidLengthException(int length) { + super(String.valueOf(length) + MESSAGE); + } +} diff --git a/src/main/java/baseball/common/exception/InvalidNumberException.java b/src/main/java/baseball/common/exception/InvalidNumberException.java new file mode 100644 index 0000000..1a7d559 --- /dev/null +++ b/src/main/java/baseball/common/exception/InvalidNumberException.java @@ -0,0 +1,10 @@ +package baseball.common.exception; + +public class InvalidNumberException extends Exception{ + + private static final String MESSAGE = "서로 다른 숫자만 가능합니다."; + + public InvalidNumberException() { + super(MESSAGE); + } +} diff --git a/src/main/java/baseball/common/utils/converter.java b/src/main/java/baseball/common/utils/converter.java new file mode 100644 index 0000000..763ea49 --- /dev/null +++ b/src/main/java/baseball/common/utils/converter.java @@ -0,0 +1,19 @@ +package baseball.common.utils; + +import java.util.ArrayList; +import java.util.List; + +public class converter { + + /** + * int를 맨 앞 자리부터 하나씩 리스트로 변환 + */ + public static List intToDigits(int number) { + List digits = new ArrayList<>(); + while (number > 0) { + digits.add(0, number % 10); + number /= 10; + } + return digits; + } +} diff --git a/src/main/java/baseball/domain/CallResult.java b/src/main/java/baseball/domain/CallResult.java new file mode 100644 index 0000000..9810cc4 --- /dev/null +++ b/src/main/java/baseball/domain/CallResult.java @@ -0,0 +1,19 @@ +package baseball.domain; + +public enum CallResult { + + BALL_CALL("볼"), + STRIKE_CALL("스트라이크"), + NOTHING_CALL("낫싱") + ; + + private final String description; + + CallResult(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/baseball/domain/Computer.java b/src/main/java/baseball/domain/Computer.java new file mode 100644 index 0000000..ee38729 --- /dev/null +++ b/src/main/java/baseball/domain/Computer.java @@ -0,0 +1,32 @@ +package baseball.domain; + +import baseball.presentation.NumberGenerator; +import java.util.ArrayList; +import java.util.List; + +public class Computer { + + private static final int COMPUTER_DIGITS_SIZE = 3; + private final NumberGenerator randomNumberGenerator; + + + public Computer(NumberGenerator randomNumberGenerator) { + this.randomNumberGenerator = randomNumberGenerator; + } + + public List generateRandomDigits() { + List digits = new ArrayList<>(); + + while (digits.size() < COMPUTER_DIGITS_SIZE) { + addToDigits(digits, randomNumberGenerator.generate()); + } + + return digits; + } + + private void addToDigits(List digits, int randomNumber) { + if (!digits.contains(randomNumber)) { + digits.add(randomNumber); + } + } +} diff --git a/src/main/java/baseball/domain/ComputerDigit.java b/src/main/java/baseball/domain/ComputerDigit.java new file mode 100644 index 0000000..6a0040f --- /dev/null +++ b/src/main/java/baseball/domain/ComputerDigit.java @@ -0,0 +1,34 @@ +package baseball.domain; + +import java.util.List; + +public class ComputerDigit { + + private final String gameId; + private final int firstNumber; + private final int secondNumber; + private final int thirdNumber; + + public ComputerDigit(String gameId, List digits) { + this.gameId = gameId; + this.firstNumber = digits.get(0); + this.secondNumber = digits.get(1); + this.thirdNumber = digits.get(2); + } + + public String getGameId() { + return gameId; + } + + public int getFirstNumber() { + return firstNumber; + } + + public int getSecondNumber() { + return secondNumber; + } + + public int getThirdNumber() { + return thirdNumber; + } +} diff --git a/src/main/java/baseball/domain/Referee.java b/src/main/java/baseball/domain/Referee.java new file mode 100644 index 0000000..2b65643 --- /dev/null +++ b/src/main/java/baseball/domain/Referee.java @@ -0,0 +1,77 @@ +package baseball.domain; + +import java.util.List; + +import static baseball.domain.CallResult.*; + +public class Referee { + + private static final String SPACE_CHARACTER = " "; + private static final int INIT_COUNT = 0; + private static final int OUT_COUNT = 3; + + private int ballCount = INIT_COUNT; + private int strikeCount = INIT_COUNT; + + public boolean judge(ComputerDigit computerDigit, UserDigit userDigit) throws IllegalAccessException { + if (!computerDigit.getGameId().equals(userDigit.getGameId())) { + throw new IllegalAccessException("gameId가 일치하지 않습니다."); + } + + resetCounts(); + compareDigits( + List.of(computerDigit.getFirstNumber(), computerDigit.getSecondNumber(), computerDigit.getThirdNumber()), + List.of(userDigit.getFirstNumber(), userDigit.getSecondNumber(), userDigit.getThirdNumber()) + ); + return isOut(); + } + + public String getCallMessage() { + String callMessage = generateCallMessage(); + if (callMessage.isEmpty()) { + callMessage = NOTHING_CALL.getDescription(); + } + return callMessage; + } + + private void resetCounts() { + ballCount = INIT_COUNT; + strikeCount = INIT_COUNT; + } + + private void compareDigits(List computerDigits, List userDigits) { + for (int index = 0; index < computerDigits.size(); index++) { + countBallOrStrike(computerDigits, userDigits, index); + } + } + + private void countBallOrStrike(List computerDigits, List userDigits, int index) { + Integer userDigit = userDigits.get(index); + Integer computerDigit = computerDigits.get(index); + if (userDigit.equals(computerDigit)) { + strikeCount++; + } else if (computerDigits.contains(userDigit)) { + ballCount++; + } + } + + private boolean isOut() { + return strikeCount == OUT_COUNT; + } + + private String generateCallMessage() { + StringBuilder call = new StringBuilder(); + if (ballCount != INIT_COUNT) { + call.append(ballCount) + .append(BALL_CALL.getDescription()) + .append(SPACE_CHARACTER); + } + + if (strikeCount != INIT_COUNT) { + call.append(strikeCount) + .append(STRIKE_CALL.getDescription()); + } + + return call.toString().trim(); + } +} diff --git a/src/main/java/baseball/domain/UserDigit.java b/src/main/java/baseball/domain/UserDigit.java new file mode 100644 index 0000000..286e884 --- /dev/null +++ b/src/main/java/baseball/domain/UserDigit.java @@ -0,0 +1,42 @@ +package baseball.domain; + +import java.util.List; + +public class UserDigit { + + private final String gameId; + private final int tryCount; + private final int firstNumber; + private final int secondNumber; + private final int thirdNumber; + + public UserDigit(String gameId, int tryCount, List digits) { + this.gameId = gameId; + this.tryCount = tryCount; + this.firstNumber = digits.get(0); + this.secondNumber = digits.get(1); + this.thirdNumber = digits.get(2); + } + + public String getGameId() { + return gameId; + } + + public int getTryCount() { + return tryCount; + } + + public int getFirstNumber() { + return firstNumber; + } + + public int getSecondNumber() { + return secondNumber; + } + + public int getThirdNumber() { + return thirdNumber; + } +} + + diff --git a/src/main/java/baseball/presentation/FixedNumberGenerator.java b/src/main/java/baseball/presentation/FixedNumberGenerator.java new file mode 100644 index 0000000..acb0997 --- /dev/null +++ b/src/main/java/baseball/presentation/FixedNumberGenerator.java @@ -0,0 +1,9 @@ +package baseball.presentation; + +public class FixedNumberGenerator implements NumberGenerator { + + @Override + public int generate() { + return 1; + } +} diff --git a/src/main/java/baseball/presentation/GameManager.java b/src/main/java/baseball/presentation/GameManager.java new file mode 100644 index 0000000..92afbff --- /dev/null +++ b/src/main/java/baseball/presentation/GameManager.java @@ -0,0 +1,93 @@ +package baseball.presentation; + +import baseball.domain.Computer; +import baseball.common.exception.InvalidLengthException; +import baseball.common.exception.InvalidNumberException; +import baseball.presentation.input.InputProvider; + +import java.util.*; + +import static baseball.common.utils.converter.intToDigits; + + +public class GameManager { + + private static final int USER_DIGITS_SIZE = 3; + + private final MessagePrinter messagePrinter; + private final InputProvider inputProvider; + + public GameManager(MessagePrinter messagePrinter, InputProvider inputProvider) { + this.messagePrinter = messagePrinter; + this.inputProvider = inputProvider; + } + + public GameStatus init() { + GameStatus gameStatus; + do { + try { + messagePrinter.initMessage(); + String input = inputProvider.getInput(); + gameStatus = GameStatus.find(input); + break; + } catch (Exception e) { + messagePrinter.errorMessage(e.getMessage()); + } + } while (true); + + return gameStatus; + } + + public List getUserDigits() { + List userDigits; + do { + try { + messagePrinter.inputNumberMessage(); + String input = inputProvider.getInput(); + int userNumber = Integer.parseInt(input); + userDigits = intToDigits(userNumber); + + validateLength(userDigits); + validateDuplication(userDigits); + + break; + } catch (Exception e) { + messagePrinter.errorMessage(e.getMessage()); + } + } while (true); + return userDigits; + } + + public List getComputerDigits(Computer computer) { + List digits = computer.generateRandomDigits(); + messagePrinter.computerSelectionMessage(); + return digits; + } + + public void call(String callString) { + messagePrinter.judgeMessage(callString); + } + + public void gameOver() { + messagePrinter.userWinMessage(); + messagePrinter.gameOverMessage(); + } + + public void applicationOver() { + messagePrinter.applicationOverMessage(); + System.exit(0); + } + + private void validateLength(List digits) throws InvalidLengthException { + if (digits.size() != USER_DIGITS_SIZE) { + throw new InvalidLengthException(USER_DIGITS_SIZE); + } + } + + private void validateDuplication(List digits) throws InvalidNumberException { + Set digitSet = new HashSet<>(digits); + if (digitSet.size() != USER_DIGITS_SIZE) { + throw new InvalidNumberException(); + } + } +} diff --git a/src/main/java/baseball/presentation/GameStatus.java b/src/main/java/baseball/presentation/GameStatus.java new file mode 100644 index 0000000..76d0046 --- /dev/null +++ b/src/main/java/baseball/presentation/GameStatus.java @@ -0,0 +1,35 @@ +package baseball.presentation; + +import baseball.common.exception.InvalidGameStatusException; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum GameStatus { + + START("1"), + QUIT("9"); + + private final String description; + private static final Map descriptions = + Collections.unmodifiableMap(Stream.of(GameStatus.values()) + .collect(Collectors.toMap(GameStatus::getDescription, Function.identity()))); + + GameStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + public static GameStatus find(String description) throws InvalidGameStatusException { + return Optional.ofNullable(descriptions.get(description)).orElseThrow( + () -> new InvalidGameStatusException() + ); + } +} diff --git a/src/main/java/baseball/presentation/MessagePrinter.java b/src/main/java/baseball/presentation/MessagePrinter.java new file mode 100644 index 0000000..1353ca3 --- /dev/null +++ b/src/main/java/baseball/presentation/MessagePrinter.java @@ -0,0 +1,36 @@ +package baseball.presentation; + +public class MessagePrinter { + + public void initMessage() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 9를 입력하세요."); + } + + public void computerSelectionMessage() { + System.out.println("컴퓨터가 숫자를 뽑았습니다."); + } + + public void inputNumberMessage() { + System.out.println("숫자를 입력해주세요 : "); + } + + public void userWinMessage() { + System.out.println("3개의 숫자를 모두 맞히셨습니다.\n"); + } + + public void gameOverMessage() { + System.out.println("-------게임 종료-------\n"); + } + + public void applicationOverMessage() { + System.out.println("애플리케이션이 종료되었습니다."); + } + + public void errorMessage(String errorMessage) { + System.out.println(errorMessage); + } + + public void judgeMessage(String callString) { + System.out.println(callString); + } +} diff --git a/src/main/java/baseball/presentation/NumberGenerator.java b/src/main/java/baseball/presentation/NumberGenerator.java new file mode 100644 index 0000000..8aeae7b --- /dev/null +++ b/src/main/java/baseball/presentation/NumberGenerator.java @@ -0,0 +1,6 @@ +package baseball.presentation; + +public interface NumberGenerator { + + int generate(); +} diff --git a/src/main/java/baseball/presentation/RandomNumberGenerator.java b/src/main/java/baseball/presentation/RandomNumberGenerator.java new file mode 100644 index 0000000..218f8d2 --- /dev/null +++ b/src/main/java/baseball/presentation/RandomNumberGenerator.java @@ -0,0 +1,13 @@ +package baseball.presentation; + +import java.util.Random; + +public class RandomNumberGenerator implements NumberGenerator { + + private Random random = new Random(); + + @Override + public int generate() { + return random.nextInt(9) + 1; + } +} diff --git a/src/main/java/baseball/presentation/input/Input.java b/src/main/java/baseball/presentation/input/Input.java new file mode 100644 index 0000000..dd152d1 --- /dev/null +++ b/src/main/java/baseball/presentation/input/Input.java @@ -0,0 +1,17 @@ +package baseball.presentation.input; + +import java.util.Scanner; + +public class Input implements InputProvider { + + private final Scanner scanner; + + public Input(Scanner scanner) { + this.scanner = scanner; + } + + @Override + public String getInput() { + return scanner.nextLine(); + } +} diff --git a/src/main/java/baseball/presentation/input/InputProvider.java b/src/main/java/baseball/presentation/input/InputProvider.java new file mode 100644 index 0000000..3386fd5 --- /dev/null +++ b/src/main/java/baseball/presentation/input/InputProvider.java @@ -0,0 +1,5 @@ +package baseball.presentation.input; + +public interface InputProvider { + String getInput(); +} diff --git a/src/test/java/baseball/common/utils/converter/converterTest.java b/src/test/java/baseball/common/utils/converter/converterTest.java new file mode 100644 index 0000000..2206716 --- /dev/null +++ b/src/test/java/baseball/common/utils/converter/converterTest.java @@ -0,0 +1,25 @@ +package baseball.common.utilsr; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static baseball.common.utils.converter.intToDigits; +import static org.assertj.core.api.Assertions.assertThat; + +class converterTest { + + + @Test + void int타입의_숫자를_한자리씩_리스트로_변환한다() { + // given + int input = 123; + List expected = List.of(1, 2, 3); + + // when + List actual = intToDigits(input); + + // then + assertThat(actual).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/domain/ComputerTest.java b/src/test/java/baseball/domain/ComputerTest.java new file mode 100644 index 0000000..ef6c35a --- /dev/null +++ b/src/test/java/baseball/domain/ComputerTest.java @@ -0,0 +1,78 @@ +package baseball.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import baseball.presentation.FixedNumberGenerator; +import baseball.presentation.NumberGenerator; +import baseball.presentation.RandomNumberGenerator; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import java.util.List; + +class ComputerTest { + + private NumberGenerator randomNumberGenerator = new RandomNumberGenerator(); + private NumberGenerator fixedNumberGenerator = new FixedNumberGenerator(); + + private Computer computer = new Computer(randomNumberGenerator); + + @Test + void 길이를_검증한다() { + // when + List result = computer.generateRandomDigits(); + + // then + assertThat(result.size()).isEqualTo(3); + } + + @Test + void 중복을_검증한다() { + // when + List result = computer.generateRandomDigits(); + + // then + assertThat(result).doesNotHaveDuplicates(); + } + + @Test + void 랜덤_숫자생성기를_주입받으면_숫자생성이_정상완료된다() throws InterruptedException { + // given + CountDownLatch latch = new CountDownLatch(1); + Thread testThread = new Thread(() -> { + try { + computer.generateRandomDigits(); + } finally { + latch.countDown(); + } + }); + + // when + testThread.start(); + + // then + boolean result = latch.await(1, TimeUnit.SECONDS); + assertThat(result).isTrue(); + } + + @Test + void 고정된_숫자생성기를_주입받으면_무한루프에_빠진다() throws InterruptedException { + // given + Computer fakeComputer = new Computer(fixedNumberGenerator); + CountDownLatch latch = new CountDownLatch(1); + Thread testThread = new Thread(() -> { + try { + fakeComputer.generateRandomDigits(); + } finally { + latch.countDown(); + } + }); + + // when + testThread.start(); + + // then + boolean result = latch.await(1, TimeUnit.SECONDS); + assertThat(result).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/domain/RefereeTest.java b/src/test/java/baseball/domain/RefereeTest.java new file mode 100644 index 0000000..7085c19 --- /dev/null +++ b/src/test/java/baseball/domain/RefereeTest.java @@ -0,0 +1,119 @@ +package baseball.domain; + +import baseball.common.exception.InvalidGameStatusException; +import baseball.presentation.GameStatus; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static java.util.UUID.randomUUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RefereeTest { + + @Test + void 컴퓨터와_유저의_숫자가_모두_같을때_참이다() throws IllegalAccessException { + // given + List computerDigits = List.of(1, 2, 3); + List userDigits = List.of(1, 2, 3); + + String gameId = randomUUID().toString(); + int tryCount = 1; + ComputerDigit computerDigit = new ComputerDigit(gameId, computerDigits); + UserDigit userDigit = new UserDigit(gameId, tryCount, userDigits); + + Referee referee = new Referee(); + + // when + boolean result = referee.judge(computerDigit, userDigit); + + // then + assertThat(result).isTrue(); + } + + @Test + void 컴퓨터와_유저의_숫자가_하나라도_다르면_거짓이다() throws IllegalAccessException { + // given + List computerDigits = List.of(1, 2, 3); + List userDigits = List.of(1, 2, 4); + + String gameId = randomUUID().toString(); + int tryCount = 1; + ComputerDigit computerDigit = new ComputerDigit(gameId, computerDigits); + UserDigit userDigit = new UserDigit(gameId, tryCount, userDigits); + + Referee referee = new Referee(); + + // when + boolean result = referee.judge(computerDigit, userDigit); + + // then + assertThat(result).isFalse(); + } + + @Test + void 게임아이디가_다르면_에러를_반환한다() throws IllegalAccessException { + // given + List computerDigits = List.of(1, 2, 3); + List userDigits = List.of(1, 2, 3); + + String gameId1 = randomUUID().toString(); + String gameId2 = randomUUID().toString(); + + int tryCount = 1; + ComputerDigit computerDigit = new ComputerDigit(gameId1, computerDigits); + UserDigit userDigit = new UserDigit(gameId2, tryCount, userDigits); + + Referee referee = new Referee(); + + // when && then + assertThatThrownBy(() -> referee.judge(computerDigit, userDigit)) + .isInstanceOf(IllegalAccessException.class) + .hasMessage("gameId가 일치하지 않습니다."); } + + + @Test + void 심판콜을_생성한다() throws IllegalAccessException { + // given + List computerDigits = List.of(1, 2, 3); + List userDigits = List.of(3, 2, 1); + String message = "2볼 1스트라이크"; + + String gameId = randomUUID().toString(); + int tryCount = 1; + ComputerDigit computerDigit = new ComputerDigit(gameId, computerDigits); + UserDigit userDigit = new UserDigit(gameId, tryCount, userDigits); + + Referee referee = new Referee(); + + // when + referee.judge(computerDigit, userDigit); + String callMessage = referee.getCallMessage(); + + // then + assertThat(callMessage).isEqualTo(message); + } + + @Test + void 낫싱_심판콜을_생성한다() throws IllegalAccessException { + // given + List computerDigits = List.of(1, 2, 3); + List userDigits = List.of(4, 5, 6); + String message = "낫싱"; + + String gameId = randomUUID().toString(); + int tryCount = 1; + ComputerDigit computerDigit = new ComputerDigit(gameId, computerDigits); + UserDigit userDigit = new UserDigit(gameId, tryCount, userDigits); + + Referee referee = new Referee(); + + // when + referee.judge(computerDigit, userDigit); + String callMessage = referee.getCallMessage(); + + // then + assertThat(callMessage).isEqualTo(message); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/presentation/GameManagerTest.java b/src/test/java/baseball/presentation/GameManagerTest.java new file mode 100644 index 0000000..12dbe26 --- /dev/null +++ b/src/test/java/baseball/presentation/GameManagerTest.java @@ -0,0 +1,56 @@ +package baseball.presentation; + +import static org.assertj.core.api.Assertions.assertThat; + +import baseball.presentation.input.InputProvider; +import org.junit.jupiter.api.Test; +import java.util.List; + + +class GameManagerTest { + + @Test + void 게임시작_입력값이_1이면_START를_반환한다() { + // given + MessagePrinter messagePrinter = new MessagePrinter(); + InputProvider inputProvider = () -> "1"; + GameManager gameManager = new GameManager(messagePrinter, inputProvider); + + // when + GameStatus actual = gameManager.init(); + + // then + assertThat(actual).isEqualTo(GameStatus.START); + } + + @Test + void 게임시작_입력값이_9이면_QUIT를_반환한다() { + // given + MessagePrinter messagePrinter = new MessagePrinter(); + InputProvider inputProvider = () -> "9"; + GameManager gameManager = new GameManager(messagePrinter, inputProvider); + + // when + GameStatus actual = gameManager.init(); + + // then + assertThat(actual).isEqualTo(GameStatus.QUIT); + } + + @Test + void 숫자입력_입력값이_올바르면_길이가_3이고_중복없는_리스트를_반환한다() { + // given + MessagePrinter messagePrinter = new MessagePrinter(); + InputProvider inputProvider = () -> "123"; + GameManager gameManager = new GameManager(messagePrinter, inputProvider); + List expected = List.of(1, 2, 3); + + // when + List actual = gameManager.getUserDigits(); + + // then + assertThat(actual).isEqualTo(expected); + assertThat(actual.size()).isEqualTo(expected.size()); + assertThat(actual).doesNotHaveDuplicates(); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/presentation/GameStatusTest.java b/src/test/java/baseball/presentation/GameStatusTest.java new file mode 100644 index 0000000..61d5d36 --- /dev/null +++ b/src/test/java/baseball/presentation/GameStatusTest.java @@ -0,0 +1,29 @@ +package baseball.presentation; + +import baseball.common.exception.InvalidGameStatusException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; + +class GameStatusTest { + + @ParameterizedTest + @CsvSource({ + "START, 1", + "QUIT, 9" + }) + void 유효한_번호를_입력하면_해당하는_게임상태를_반환한다(GameStatus gameStatus, String description) throws InvalidGameStatusException { + assertThat(GameStatus.find(description)).isEqualByComparingTo(gameStatus); + } + + @ParameterizedTest + @ValueSource(strings = {"2", "3", "가", "a", ""}) + void 유효하지_않은_번호를_입력하면_에러를_반환한다(String description) { + assertThatThrownBy(() -> GameStatus.find(description)) + .isInstanceOf(InvalidGameStatusException.class) + .hasMessage("1(시작) 또는 9(종료)를 눌러주세요."); + } + +} \ No newline at end of file