diff --git a/build.gradle b/build.gradle index 8172fb7..6b428de 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation "org.junit.jupiter:junit-jupiter:5.7.2" testImplementation "org.assertj:assertj-core:3.19.0" + testImplementation "org.mockito:mockito-junit-jupiter:5.2.0" } test { diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 0000000..d30d00c --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,15 @@ +import app.BaseballApplication; +import base.Application; +import io.OutputHandler; + +public class Main { + + public static void main(String[] args) { + Application baseballApplication = new BaseballApplication(); + try { + baseballApplication.startGame(); + } catch (Exception e) { + OutputHandler.printDefaultExceptionMessage(e); + } + } +} diff --git a/src/main/java/app/BaseballApplication.java b/src/main/java/app/BaseballApplication.java new file mode 100644 index 0000000..ec4f08a --- /dev/null +++ b/src/main/java/app/BaseballApplication.java @@ -0,0 +1,113 @@ +package app; + +import base.Application; +import domain.Computer; +import domain.Judgment; +import domain.Score; +import domain.User; +import io.InputHandler; +import io.OutputHandler; +import java.util.ArrayList; +import core.AppException; +import util.RandomGenerator; + +import static core.SystemConstant.*; + + +public class BaseballApplication implements Application { + + private final Computer computer = new Computer(new RandomGenerator()); + private final InputHandler inputHandler = new InputHandler(); + private final OutputHandler outputHandler = new OutputHandler(); + + + public void startGame() { + boolean isGameRunning = true; + + while (isGameRunning) { + Judgment judgment = readyToGame(); + outputHandler.gameStartCommentPrint(); + run(judgment); + + isGameRunning = pauseForUserGameRunOptionSelect(); + } + } + + private String userSelectGameRestartOrStop() { + outputHandler.gameEndCommentPrint(); + return inputHandler.getUserInput(); + } + + private boolean isGameRestart(String gameFlag) { + return GAME_RESTART_FLAG.equals(gameFlag); + } + + private boolean isGameStop(String gameFlag) { + return GAME_STOP_FLAG.equals(gameFlag); + } + + private void run(Judgment judgment) { + try { + actionToGameStartByJudgement(judgment); + + } catch (AppException e) { + outputHandler.printAppExceptionMessage(e); + + run(judgment); + } + } + + private boolean pauseForUserGameRunOptionSelect() { + try { + String gameFlag = userSelectGameRestartOrStop(); + + return isGameContinue(gameFlag); + + } catch (AppException e) { + outputHandler.printAppExceptionMessage(e); + return pauseForUserGameRunOptionSelect(); + } + } + + private boolean isGameContinue(String gameFlag) { + if (isGameRestart(gameFlag)) { + return true; + } + + if (isGameStop(gameFlag)) { + return false; + } + + throw new AppException("게임 시작 명령어를 잘못 입력 하였습니다."); + } + + private void actionToGameStartByJudgement(Judgment judgment) { + String userInputValue = userInput(); + User user = new User(userInputValue); + ArrayList getUserInputArrayStringNumbers = user.getUserInputNumbers(); + + Score score = judgment.judge(getUserInputArrayStringNumbers); + + String scoreResultMessage = score.getScoreRecordResult(); + outputHandler.printMessage(scoreResultMessage); + + boolean isGameSet = score.isStrikeCountEqualToWinningStrikeCount(); + + if (stillGameRunning(isGameSet)) { + actionToGameStartByJudgement(judgment); + } + } + + private boolean stillGameRunning(boolean isGameSet) { + return !isGameSet; + } + + private String userInput() { + outputHandler.questionToUserAboutInputNumber(); + return inputHandler.getUserInput(); + } + + private Judgment readyToGame() { + return new Judgment(computer.readyToGameStart()); + } +} diff --git a/src/main/java/base/Application.java b/src/main/java/base/Application.java new file mode 100644 index 0000000..55eadd0 --- /dev/null +++ b/src/main/java/base/Application.java @@ -0,0 +1,6 @@ +package base; + +public interface Application { + + void startGame(); +} diff --git a/src/main/java/core/AppException.java b/src/main/java/core/AppException.java new file mode 100644 index 0000000..b61440c --- /dev/null +++ b/src/main/java/core/AppException.java @@ -0,0 +1,8 @@ +package core; + +public class AppException extends RuntimeException { + + public AppException(String message) { + super(message); + } +} diff --git a/src/main/java/core/SystemConstant.java b/src/main/java/core/SystemConstant.java new file mode 100644 index 0000000..3998e7b --- /dev/null +++ b/src/main/java/core/SystemConstant.java @@ -0,0 +1,9 @@ +package core; + +public class SystemConstant { + public static final int INPUT_LIMIT_LENGTH = 3; + public static final int WINNING_STRIKE_COUNT = 3; + public static final String GAME_STOP_FLAG = "2"; + public static final String GAME_RESTART_FLAG = "1"; + +} diff --git a/src/main/java/domain/Computer.java b/src/main/java/domain/Computer.java new file mode 100644 index 0000000..cf6c231 --- /dev/null +++ b/src/main/java/domain/Computer.java @@ -0,0 +1,41 @@ +package domain; + +import static core.SystemConstant.*; +import java.util.ArrayList; +import util.RandomGenerator; + +public class Computer { + + public static final int BOUND = 9; + private final RandomGenerator randomGenerator; + private final ArrayList randomNumbers = new ArrayList<>(); + + public Computer(RandomGenerator randomGenerator) { + this.randomGenerator = randomGenerator; + } + + public ArrayList readyToGameStart() { + + if (isGenerateRandomNumberSizeEqualToLimit()) { + return randomNumbers; + } + String number = randomGenerator.getRandomNumberToString(BOUND); + + if (doesNotDuplicate(number)) { + append(number); + } + return readyToGameStart(); + } + + private void append(String number) { + randomNumbers.add(number); + } + + private boolean isGenerateRandomNumberSizeEqualToLimit() { + return randomNumbers.size() == INPUT_LIMIT_LENGTH; + } + + private boolean doesNotDuplicate(String compareInteger) { + return !randomNumbers.contains(compareInteger); + } +} diff --git a/src/main/java/domain/Judgment.java b/src/main/java/domain/Judgment.java new file mode 100644 index 0000000..070c552 --- /dev/null +++ b/src/main/java/domain/Judgment.java @@ -0,0 +1,46 @@ +package domain; + +import java.util.ArrayList; + +public class Judgment { + + private final Score score = new Score(); + private final ArrayList computerRandomNumbers; + + + public Judgment(ArrayList computerRandomNumbers) { + this.computerRandomNumbers = computerRandomNumbers; + } + + public Score judge(ArrayList userInputNumbers) { + newScore(); + + for (int seq = 0; seq < userInputNumbers.size(); seq++) { + scoreRecord(userInputNumbers, seq); + } + return score; + } + + private void newScore() { + score.clean(); + } + + private void scoreRecord(ArrayList userInputNumbers, int arrayInPosition) { + if (isStrike(userInputNumbers, arrayInPosition)) { + score.strikeIncrement(); + return; + } + + if (isBall(userInputNumbers, arrayInPosition)) { + score.ballIncrement(); + } + } + + private boolean isBall(ArrayList userInputNumbers, int arrayInPosition) { + return computerRandomNumbers.contains(userInputNumbers.get(arrayInPosition)); + } + + private boolean isStrike(ArrayList userInputNumbers, int arrayInPosition) { + return computerRandomNumbers.get(arrayInPosition).equals(userInputNumbers.get(arrayInPosition)); + } +} diff --git a/src/main/java/domain/Score.java b/src/main/java/domain/Score.java new file mode 100644 index 0000000..f92ce9f --- /dev/null +++ b/src/main/java/domain/Score.java @@ -0,0 +1,68 @@ +package domain; + +import static core.SystemConstant.*; + +import core.AppException; + +public class Score { + + private int strikeCount; + private int ballCount; + + private static final String BALL_MESSAGE = "볼"; + private static final String STRIKE_MESSAGE = "스트라이크"; + private static final String UN_HANDLE_MESSAGE = "낫싱"; + + void clean() { + this.strikeCount = 0; + this.ballCount = 0; + } + + public void strikeIncrement() { + if (strikeCount + 1 > WINNING_STRIKE_COUNT) { + throw new AppException("스트라이크 카운트는 최대 " + WINNING_STRIKE_COUNT + "까지 증가할 수 있습니다."); + } + this.strikeCount += 1; + } + + public void ballIncrement() { + if (ballCount + 1 > WINNING_STRIKE_COUNT) { + throw new AppException("볼 카운트는 최대 " + WINNING_STRIKE_COUNT + "까지 증가할 수 있습니다."); + } + + this.ballCount += 1; + } + + public String getScoreRecordResult() { + if (isBallAndStrike()) { + return ballCount + BALL_MESSAGE + " " + strikeCount + STRIKE_MESSAGE; + } + + if (isOnlyBall()) { + return ballCount + BALL_MESSAGE; + } + + if (isOnlyStrike()) { + return strikeCount + STRIKE_MESSAGE; + } + + return UN_HANDLE_MESSAGE; + } + + private boolean isOnlyStrike() { + return strikeCount > 0; + } + + private boolean isOnlyBall() { + return ballCount > 0; + } + + private boolean isBallAndStrike() { + return ballCount > 0 && strikeCount > 0; + } + + public boolean isStrikeCountEqualToWinningStrikeCount() { + return strikeCount == WINNING_STRIKE_COUNT; + } + +} diff --git a/src/main/java/domain/User.java b/src/main/java/domain/User.java new file mode 100644 index 0000000..c137fb2 --- /dev/null +++ b/src/main/java/domain/User.java @@ -0,0 +1,56 @@ +package domain; + + +import static core.SystemConstant.*; + +import java.util.ArrayList; +import java.util.Arrays; +import core.AppException; + +public class User { + + private final static int NUMBER_LIMIT_LENGTH = INPUT_LIMIT_LENGTH; + private final ArrayList userInputNumbers; + + public User(String userInputNumber) { + if (invalidLengthUserInputNumber(userInputNumber)) { + throw new AppException("입력한 숫자의 길이가 올바르지 않습니다."); + } + + if (doesNotNumeric(userInputNumber)) { + throw new AppException("지정 되지 않은 타입의 입력입니다."); + } + + this.userInputNumbers = convertStringToArrayList(userInputNumber); + + if (hasZeroNumber()) { + throw new AppException("0은 입력 할 수 없습니다."); + } + + } + + public ArrayList getUserInputNumbers() { + return userInputNumbers; + } + + private static ArrayList convertStringToArrayList(String userInput) { + return new ArrayList<>(Arrays.asList(userInput.split(""))); + } + + private boolean hasZeroNumber() { + return this.userInputNumbers.contains("0"); + } + + private boolean doesNotNumeric(String userInput) { + try { + Integer.valueOf(userInput); + return false; + } catch (Exception e) { + return true; + } + } + + private boolean invalidLengthUserInputNumber(String userInput) { + return !(userInput.length() == NUMBER_LIMIT_LENGTH); + } +} diff --git a/src/main/java/io/InputHandler.java b/src/main/java/io/InputHandler.java new file mode 100644 index 0000000..75ba1fd --- /dev/null +++ b/src/main/java/io/InputHandler.java @@ -0,0 +1,12 @@ +package io; + +import java.util.Scanner; + +public class InputHandler { + + private static final Scanner SCANNER = new Scanner(System.in); + + public String getUserInput() { + return SCANNER.nextLine(); + } +} diff --git a/src/main/java/io/OutputHandler.java b/src/main/java/io/OutputHandler.java new file mode 100644 index 0000000..f78df45 --- /dev/null +++ b/src/main/java/io/OutputHandler.java @@ -0,0 +1,36 @@ +package io; + +import core.AppException; + +public class OutputHandler { + + public static void printDefaultExceptionMessage(Exception e) { + System.out.println("알 수 없는 오류로 인해 시스템이 종료 됩니다."); + System.out.println("############# [DEBUG] Traceback Logging ############"); + System.out.println(e.getMessage()); + } + + public void printMessage(String message) { + System.out.println(message); + } + + public void printAppExceptionMessage(AppException e) { + System.out.println(e.getMessage()); + } + + public void gameStartCommentPrint() { + System.out.println("##################################"); + System.out.println("#\t\t\t\t숫자 야구 게임 시작!\t\t\t\t#"); + System.out.println("##################################"); + System.out.println(); + } + + public void questionToUserAboutInputNumber() { + System.out.print("숫자를 입력 해주세요: "); + } + + public void gameEndCommentPrint() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + System.out.print("> "); + } +} diff --git a/src/main/java/util/RandomGenerator.java b/src/main/java/util/RandomGenerator.java new file mode 100644 index 0000000..ca4e866 --- /dev/null +++ b/src/main/java/util/RandomGenerator.java @@ -0,0 +1,12 @@ +package util; + +import java.util.Random; + +public class RandomGenerator { + + private final Random randomGenerator = new Random(); + + public String getRandomNumberToString(int bound) { + return String.valueOf(randomGenerator.nextInt(bound) + 1); + } +} diff --git a/src/test/java/baseball/domain/ComputerTest.java b/src/test/java/baseball/domain/ComputerTest.java new file mode 100644 index 0000000..aef4381 --- /dev/null +++ b/src/test/java/baseball/domain/ComputerTest.java @@ -0,0 +1,49 @@ +package baseball.domain; + +import static org.assertj.core.api.Assertions.*; + +import domain.Computer; +import java.util.ArrayList; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import util.RandomGenerator; + +class ComputerTest { + + @DisplayName("컴퓨터 플레이어의 난수 생성 메서드는 3개여야한다") + @Test + void 길이_검증() { + // Given + Computer computer = new Computer(new RandomGenerator()); + + // When + int randomNumberGeneratorSize = computer.readyToGameStart().size(); + + // Then + assertThat(randomNumberGeneratorSize).isEqualTo(3); + } + + @DisplayName("컴퓨터 플레이어의 난수 생성 메서드는 중복을 포함할 수 없다") + @Test + void 요소_중복_검사() { + // Given + RandomGenerator mockRandom = Mockito.mock(RandomGenerator.class); + Mockito.when(mockRandom.getRandomNumberToString(9)) + .thenReturn("1", "1", "2", "3"); + + Computer computer = new Computer(mockRandom); + + // When + ArrayList fixture = computer.readyToGameStart(); + System.out.println("fixture = " + fixture); + int randomNumberGeneratorSize = (int) fixture.stream() + .distinct() + .count(); + + // Then + assertThat(randomNumberGeneratorSize).isEqualTo(3); + assertThat(fixture).containsExactly("1", "2", "3"); + } + +} \ No newline at end of file diff --git a/src/test/java/baseball/domain/JudgmentTest.java b/src/test/java/baseball/domain/JudgmentTest.java new file mode 100644 index 0000000..c7fa1ae --- /dev/null +++ b/src/test/java/baseball/domain/JudgmentTest.java @@ -0,0 +1,75 @@ +package baseball.domain; + +import static org.assertj.core.api.Assertions.*; + +import domain.Judgment; +import domain.Score; +import domain.User; +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class JudgmentTest { + + private static ArrayList randomNumbers; + + @BeforeAll + static void setUp() { + randomNumbers = new ArrayList<>(Arrays.asList("1", "2", "3")); + } + + @DisplayName("같은 위치에 숫자는 다르지만 다른 위치에 속해 있는 경우 볼로 판정한다.") + @ParameterizedTest + @CsvSource({"345,1볼", "314,2볼", "312,3볼"}) + void 심판_볼_판정(String numberToString, String result) { + Judgment judgmentFixture = new Judgment(randomNumbers); + User userFixture = new User(numberToString); + + // Given + ArrayList userInputNumbers = userFixture.getUserInputNumbers(); + + // When + Score score = judgmentFixture.judge(userInputNumbers); + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo(result); + } + + @DisplayName("같은 위치에 같은 숫자가 있는 경우 스트라이크로 판정한다.") + @ParameterizedTest + @CsvSource({"145,1스트라이크", "125,2스트라이크", "123,3스트라이크"}) + void 심판_스트라이크_판정(String numberToString, String result) { + Judgment judgmentFixture = new Judgment(randomNumbers); + User userFixture = new User(numberToString); + + // Given + ArrayList userInputNumbers = userFixture.getUserInputNumbers(); + + // When + Score score = judgmentFixture.judge(userInputNumbers); + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo(result); + } + + @DisplayName("같은 위치에 같은 숫자가 있는 경우 스트라이크로 판정한다.") + @Test + void 심판_낫싱_판정() { + Judgment judgmentFixture = new Judgment(randomNumbers); + User userFixture = new User("789"); + + // Given + ArrayList userInputNumbers = userFixture.getUserInputNumbers(); + + // When + Score score = judgmentFixture.judge(userInputNumbers); + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo("낫싱"); + } + +} \ No newline at end of file diff --git a/src/test/java/baseball/domain/ScoreTest.java b/src/test/java/baseball/domain/ScoreTest.java new file mode 100644 index 0000000..46f75a5 --- /dev/null +++ b/src/test/java/baseball/domain/ScoreTest.java @@ -0,0 +1,136 @@ +package baseball.domain; + +import static org.assertj.core.api.Assertions.*; + +import core.AppException; +import domain.Score; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + + +public class ScoreTest { + + @DisplayName("스코어의 볼을 증가 시키면 변경 된 볼 카운트를 반환한다") + @Test + void 볼_증가() { + // Given + Score score = new Score(); + + // When + score.ballIncrement(); + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo("1볼"); + } + + @DisplayName("스코어의 스트라이크를 증가 시키면 변경 된 스트라이크 카운트를 반환한다") + @Test + void 스트라이크_증가() { + // Given + Score score = new Score(); + + // When + score.strikeIncrement(); + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo("1스트라이크"); + } + + @DisplayName("볼 카운트는 최대 3까지 증가 할 수 있다") + @Test + void 볼_카운트_최대값() { + // Given + Score score = new Score(); + + // When + score.ballIncrement(); + score.ballIncrement(); + score.ballIncrement(); + assertThatThrownBy(() -> { + score.ballIncrement(); + }) + // Then + .isInstanceOf(AppException.class) + .hasMessageContaining("볼 카운트는 최대 3까지 증가할 수 있습니다"); + } + + @DisplayName("스트라이크 카운트는 최대 3까지 증가 할 수 있다") + @Test + void 스트라이크_카운트_최대값() { + // Given + Score score = new Score(); + + // When + score.strikeIncrement(); + score.strikeIncrement(); + score.strikeIncrement(); + assertThatThrownBy(() -> { + score.strikeIncrement(); + }) + // Then + .isInstanceOf(AppException.class) + .hasMessageContaining("스트라이크 카운트는 최대 3까지 증가할 수 있습니다"); + } + + + @DisplayName("스코어의 스트라이크와 볼을 동시에 증가 시키면 볼과 스트라이크의 결과를 반환한다") + @Test + void 볼_스트라이크_반환() { + // Given + Score score = new Score(); + + // When + score.strikeIncrement(); + score.ballIncrement(); + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo("1볼 1스트라이크"); + } + + @DisplayName("스코어에서 스트라이크 카운트가 3이라면 아웃 상태 값을 반환한다") + @Test + void 스트라이크3_아웃_반환() { + // Given + Score score = new Score(); + + // When + score.strikeIncrement(); + score.strikeIncrement(); + score.strikeIncrement(); + + System.out.println("score.getScoreRecordResult() = " + score.getScoreRecordResult()); + + // Then + assertThat(score.isStrikeCountEqualToWinningStrikeCount()).isTrue(); + } + + @DisplayName("스코어에서 스트라이크 카운트가 3 미만이라면 아웃 상태가 아닌 값을 반환한다") + @Test + void 스트라이크3_아웃_아님_반환() { + // Given + Score score = new Score(); + + // When + score.strikeIncrement(); + score.strikeIncrement(); + + // Then + assertThat(score.isStrikeCountEqualToWinningStrikeCount()).isFalse(); + } + + @DisplayName("스코어는 볼과 스트라이크 카운트가 증가하지 않으면 낫싱을 반환한다") + @Test + void 스코어_낫싱_반환() { + // Given + Score score = new Score(); + + // When + // Pass + + // Then + assertThat(score.getScoreRecordResult()).isEqualTo("낫싱"); + + } +} diff --git a/src/test/java/baseball/domain/UserPlayerTest.java b/src/test/java/baseball/domain/UserPlayerTest.java new file mode 100644 index 0000000..4632f1f --- /dev/null +++ b/src/test/java/baseball/domain/UserPlayerTest.java @@ -0,0 +1,73 @@ +package baseball.domain; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.*; + +import domain.User; +import core.AppException; +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class UserPlayerTest { + + @DisplayName("플레이어는 3자리 숫자만 입력 할 수 있다") + @Test + void 유저_입력_자리수_검증() { + // Given + String inputFixture = "1234"; + + // When + assertThatThrownBy(() -> { + User user = new User(inputFixture); + }) + // Then + .isInstanceOf(AppException.class) + .hasMessageContaining("입력한 숫자의 길이가 올바르지 않습니다."); + } + + @DisplayName("유저는 문자를 입력 할 수 없다") + @Test + void 유저_입력_자료형_검증() { + // Given + String userInputFixture = "asd"; + + // When + assertThatThrownBy(() -> { + User user = new User(userInputFixture); + }) + // Then + .isInstanceOf(AppException.class) + .hasMessageContaining("지정 되지 않은 타입의 입력입니다."); + } + + @DisplayName("유저는 0을 입력 할 수 없다") + @Test + void 유저_입력_0_검증() { + // Given + String userInputFixture = "120"; + + // When + assertThatThrownBy(() -> { + User user = new User(userInputFixture); + }) + // Then + .isInstanceOf(AppException.class) + .hasMessageContaining("0은 입력 할 수 없습니다."); + } + + @DisplayName("유저가 정상적으로 입력한 숫자를 문자 타입의 배열로 반환한다") + @Test + void 유저_정상입력_반환() { + // Given + String userInputFixture = "123"; + + // When + User user = new User(userInputFixture); + + // Then + assertThat(user.getUserInputNumbers()).isInstanceOf(ArrayList.class); + assertThat(user.getUserInputNumbers()).isEqualTo(new ArrayList<>(Arrays.asList("1", "2", "3"))); + } +} \ No newline at end of file