diff --git a/docs/README.md b/docs/README.md index e69de29bb2..b8476ae90b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,25 @@ +# 숫자 야구 + +## 기능 목록 + +- [x] 컴퓨터는 1 ~ 9 사이에서 서로 다른 임의의 수 3개를 선택한다. +- [x] 플레이어는 서로 다른 수 3개를 입력한다. +- [x] 컴퓨터가 선택한 수와 플레이어가 입력한 수를 비교하여 결과를 출력한다. ex) 1볼 1스트라이크 + - [x] 같은 자리에 같은 수: `스트라이크` + - [x] 다른 자리 같은 수: `볼` + - [x] 같은 수가 전혀 없는 경우: `낫싱` +- [x] 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다. +- [x] 게임이 종료된 후 재시작/종료를 선택한다. 재시작: 1, 종료: 2 + +## 예외 + +사용자가 잘못된 값을 입력하는 경우 `IllegalArgumentException`을 발생시킨다. + +- [x] 동일한 수를 입력한 경우 +- [x] 숫자가 아닌 문자를 입력한 경우 +- [x] 숫자를 3개 입력하지 않은 경우 +- [x] 0을 입력한 경우 + +## 제약 조건 + +`camp.nextstep.edu.missionutils`의 `Randoms`와 `Console`를 사용한다. \ No newline at end of file diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java index dd95a34214..8d6fd4c6b2 100644 --- a/src/main/java/baseball/Application.java +++ b/src/main/java/baseball/Application.java @@ -1,7 +1,17 @@ package baseball; +import baseball.controller.BaseballGame; +import baseball.domain.Computer; +import baseball.domain.Player; +import baseball.view.InputView; +import baseball.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + BaseballGame baseballGame = new BaseballGame( + new OutputView(), new InputView(), + new Computer(), new Player() + ); + baseballGame.start(); } } diff --git a/src/main/java/baseball/controller/BaseballGame.java b/src/main/java/baseball/controller/BaseballGame.java new file mode 100644 index 0000000000..ffed32f0da --- /dev/null +++ b/src/main/java/baseball/controller/BaseballGame.java @@ -0,0 +1,52 @@ +package baseball.controller; + +import baseball.domain.BaseballJudge; +import baseball.domain.Computer; +import baseball.domain.Player; +import baseball.dto.CompareResult; +import baseball.view.InputView; +import baseball.view.OutputView; + +import java.util.List; + +public class BaseballGame { + + private final OutputView outputView; + private final InputView inputView; + private final Computer computer; + private final Player player; + + public BaseballGame(OutputView outputView, InputView inputView, Computer computer, Player player) { + this.outputView = outputView; + this.inputView = inputView; + this.computer = computer; + this.player = player; + } + + public void start() { + outputView.printStartMessage(); + + do { + play(); + } while (isRestart()); + } + + private void play() { + computer.generateNumbers(); + + CompareResult compareResult; + do { + List inputNumbers = inputView.enterUserNumbers(); + player.chooseNumbers(inputNumbers); + compareResult = BaseballJudge.judgeCompareResult(player, computer); + outputView.printCompareResult(compareResult); + } while (!compareResult.isThreeStrike()); + + outputView.printFinishMessage(); + } + + private boolean isRestart() { + int input = inputView.enterRestartOrEnd(); + return input == 1; + } +} diff --git a/src/main/java/baseball/domain/BaseballJudge.java b/src/main/java/baseball/domain/BaseballJudge.java new file mode 100644 index 0000000000..3247c7be64 --- /dev/null +++ b/src/main/java/baseball/domain/BaseballJudge.java @@ -0,0 +1,15 @@ +package baseball.domain; + +import baseball.dto.CompareResult; + +public class BaseballJudge { + + public static CompareResult judgeCompareResult(Player player, Computer computer) { + Numbers playerNumbers = player.getNumbers(); + Numbers computerNumbers = computer.getNumbers(); + + int strike = computerNumbers.calculateStrikes(playerNumbers); + int ball = computerNumbers.calculateBalls(playerNumbers); + return new CompareResult(strike, ball); + } +} diff --git a/src/main/java/baseball/domain/Computer.java b/src/main/java/baseball/domain/Computer.java new file mode 100644 index 0000000000..5e25682e11 --- /dev/null +++ b/src/main/java/baseball/domain/Computer.java @@ -0,0 +1,27 @@ +package baseball.domain; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.ArrayList; +import java.util.List; + +public class Computer { + + private Numbers numbers; + + public void generateNumbers() { + List numbers = new ArrayList<>(); + while (numbers.size() < 3) { + int number = Randoms.pickNumberInRange(Numbers.START_NUMBER, Numbers.END_NUMBER); + if (!numbers.contains(number)) { + numbers.add(number); + } + } + + this.numbers = new Numbers(numbers); + } + + public Numbers getNumbers() { + return numbers; + } +} diff --git a/src/main/java/baseball/domain/Numbers.java b/src/main/java/baseball/domain/Numbers.java new file mode 100644 index 0000000000..6ec6d4a757 --- /dev/null +++ b/src/main/java/baseball/domain/Numbers.java @@ -0,0 +1,49 @@ +package baseball.domain; + +import baseball.validator.NumbersValidator; + +import java.util.List; + +public class Numbers { + + public static final int START_NUMBER = 1; + public static final int END_NUMBER = 9; + public static final int SIZE = 3; + + private final List numbers; + + public Numbers(List numbers) { + validateNumbers(numbers); + this.numbers = numbers; + } + + private void validateNumbers(List numbers) { + NumbersValidator.validateNumbersSize(SIZE, numbers); + NumbersValidator.validateDuplicateNumbers(numbers); + NumbersValidator.validateContainsZero(numbers); + } + + public int calculateStrikes(Numbers compareNumbers) { + List numbers = compareNumbers.numbers; + int strikesCount = 0; + for (int i = 0; i < SIZE; i++) { + Integer number = numbers.get(i); + if (this.numbers.indexOf(number) == i) { + strikesCount++; + } + } + return strikesCount; + } + + public int calculateBalls(Numbers compareNumbers) { + List numbers = compareNumbers.numbers; + int ballsCount = 0; + for (int i = 0; i < SIZE; i++) { + Integer number = numbers.get(i); + if (this.numbers.contains(number) && this.numbers.indexOf(number) != i) { + ballsCount++; + } + } + return ballsCount; + } +} diff --git a/src/main/java/baseball/domain/Player.java b/src/main/java/baseball/domain/Player.java new file mode 100644 index 0000000000..40604bb715 --- /dev/null +++ b/src/main/java/baseball/domain/Player.java @@ -0,0 +1,16 @@ +package baseball.domain; + +import java.util.List; + +public class Player { + + private Numbers numbers; + + public void chooseNumbers(List numbers) { + this.numbers = new Numbers(numbers); + } + + public Numbers getNumbers() { + return numbers; + } +} diff --git a/src/main/java/baseball/dto/CompareResult.java b/src/main/java/baseball/dto/CompareResult.java new file mode 100644 index 0000000000..23f192bfd4 --- /dev/null +++ b/src/main/java/baseball/dto/CompareResult.java @@ -0,0 +1,8 @@ +package baseball.dto; + +public record CompareResult(int strike, int ball) { + + public boolean isThreeStrike() { + return strike == 3; + } +} diff --git a/src/main/java/baseball/exception/ErrorMessages.java b/src/main/java/baseball/exception/ErrorMessages.java new file mode 100644 index 0000000000..4e284fdd25 --- /dev/null +++ b/src/main/java/baseball/exception/ErrorMessages.java @@ -0,0 +1,9 @@ +package baseball.exception; + +public class ErrorMessages { + + public static final String NUMBER_FORMAT_ERROR = "[ERROR] 숫자만 입력해야 합니다."; + public static final String INVALID_SIZE_ERROR = "[ERROR] 숫자는 %d개이어야 합니다."; + public static final String DUPLICATE_NUMBERS_ERROR = "[ERROR] 숫자는 중복될 수 없습니다."; + public static final String INVALID_NUMBER_ERROR = "[ERROR] 유효하지 않은 수입니다."; +} diff --git a/src/main/java/baseball/validator/NumbersValidator.java b/src/main/java/baseball/validator/NumbersValidator.java new file mode 100644 index 0000000000..524f692a94 --- /dev/null +++ b/src/main/java/baseball/validator/NumbersValidator.java @@ -0,0 +1,29 @@ +package baseball.validator; + +import baseball.exception.ErrorMessages; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class NumbersValidator { + + public static void validateNumbersSize(int validSize, List numbers) { + if (numbers.size() != validSize) { + throw new IllegalArgumentException(String.format(ErrorMessages.INVALID_SIZE_ERROR, validSize)); + } + } + + public static void validateDuplicateNumbers(List numbers) { + Set set = new HashSet<>(numbers); + if (set.size() != numbers.size()) { + throw new IllegalArgumentException(ErrorMessages.DUPLICATE_NUMBERS_ERROR); + } + } + + public static void validateContainsZero(List numbers) { + if (numbers.contains(0)) { + throw new IllegalArgumentException(ErrorMessages.INVALID_NUMBER_ERROR); + } + } +} diff --git a/src/main/java/baseball/view/BaseballMessages.java b/src/main/java/baseball/view/BaseballMessages.java new file mode 100644 index 0000000000..1b73fdeeab --- /dev/null +++ b/src/main/java/baseball/view/BaseballMessages.java @@ -0,0 +1,13 @@ +package baseball.view; + +public class BaseballMessages { + + public static final String START_MESSAGE = "숫자 야구 게임을 시작합니다."; + public static final String INPUT_NUMBERS_MESSAGE = "숫자를 입력해주세요 : "; + public static final String CONTINUE_OR_END_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."; + public static final String FINISH_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임 종료"; + + public static final String NOTHING = "낫싱"; + public static final String BALL = "볼"; + public static final String STRIKE = "스트라이크"; +} diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 0000000000..ff4b0d32e9 --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,35 @@ +package baseball.view; + +import baseball.exception.ErrorMessages; +import camp.nextstep.edu.missionutils.Console; + +import java.util.Arrays; +import java.util.List; + +public class InputView { + + public List enterUserNumbers() { + System.out.print(BaseballMessages.INPUT_NUMBERS_MESSAGE); + String input = Console.readLine() + .trim(); + try { + return Arrays.stream(input.split("")) + .map(Integer::parseInt) + .toList(); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException(ErrorMessages.NUMBER_FORMAT_ERROR); + } + } + + public int enterRestartOrEnd() { + System.out.println(BaseballMessages.CONTINUE_OR_END_MESSAGE); + String input = Console.readLine() + .trim(); + + try { + return Integer.parseInt(input); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException(ErrorMessages.NUMBER_FORMAT_ERROR); + } + } +} diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java new file mode 100644 index 0000000000..e2d63bb801 --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,34 @@ +package baseball.view; + +import baseball.dto.CompareResult; + +public class OutputView { + + public void printStartMessage() { + System.out.println(BaseballMessages.START_MESSAGE); + } + + public void printCompareResult(CompareResult compareResult) { + int ball = compareResult.ball(); + int strike = compareResult.strike(); + if (strike == 0 && ball == 0) { + System.out.println(BaseballMessages.NOTHING); + return; + } + + StringBuilder message = new StringBuilder(); + if (ball != 0) { + message.append(ball).append(BaseballMessages.BALL) + .append(" "); + } + if (strike != 0) { + message.append(strike).append(BaseballMessages.STRIKE); + } + + System.out.println(message); + } + + public void printFinishMessage() { + System.out.println(BaseballMessages.FINISH_MESSAGE); + } +} diff --git a/src/test/java/baseball/domain/ComputerTest.java b/src/test/java/baseball/domain/ComputerTest.java new file mode 100644 index 0000000000..de94b886c1 --- /dev/null +++ b/src/test/java/baseball/domain/ComputerTest.java @@ -0,0 +1,26 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("컴퓨터 도메인 테스트") +class ComputerTest { + + // 서로 다른 수만 존재하는지 어떻게 테스트할 수 있을까..? + @DisplayName("컴퓨터는 서로 다른 임의의 수 3개를 선택한다.") + @Test + void generateComputerNumbers() { + // given + Computer computer = new Computer(); + + // when + computer.generateNumbers(); + + // then + assertThat(computer).extracting("numbers") + .extracting("numbers").asList() + .hasSize(3); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/domain/NumbersTest.java b/src/test/java/baseball/domain/NumbersTest.java new file mode 100644 index 0000000000..553348af7b --- /dev/null +++ b/src/test/java/baseball/domain/NumbersTest.java @@ -0,0 +1,112 @@ +package baseball.domain; + +import baseball.exception.ErrorMessages; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("숫자 테스트") +class NumbersTest { + + @DisplayName("숫자가 중복된 경우 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"111", "122", "332"}) + void duplicateNumbers(String input) { + // given + List numbers = Arrays.stream(input.split("")) + .map(Integer::parseInt) + .toList(); + + // when & then + assertThatThrownBy(() -> new Numbers(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessages.DUPLICATE_NUMBERS_ERROR); + } + + @DisplayName("숫자를 3개 입력하지 않으면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"12", "1234", "1"}) + void invalidSize(String input) { + // given + List numbers = Arrays.stream(input.split("")) + .map(Integer::parseInt) + .toList(); + + // when & then + assertThatThrownBy(() -> new Numbers(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(ErrorMessages.INVALID_SIZE_ERROR, Numbers.SIZE)); + } + + @DisplayName("유효하지 않은 수를 입력하면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"012"}) + void invalidNumber(String input) { + // given + List numbers = Arrays.stream(input.split("")) + .map(Integer::parseInt) + .toList(); + + // when & then + assertThatThrownBy(() -> new Numbers(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(ErrorMessages.INVALID_NUMBER_ERROR)); + } + + @DisplayName("스트라이크 개수를 계산한다.") + @ParameterizedTest + @CsvSource(textBlock = """ + 123, 923, 2 + 123, 453, 1 + 123, 123, 3 + """) + void calculateStrikes(String computer, String player, int expectedStrikes) { + // given + List numbers1 = Arrays.stream(computer.split("")) + .map(Integer::parseInt) + .toList(); + List numbers2 = Arrays.stream(player.split("")) + .map(Integer::parseInt) + .toList(); + + // when + Numbers computerNumbers = new Numbers(numbers1); + Numbers playerNumbers = new Numbers(numbers2); + + // then + int strikes = computerNumbers.calculateStrikes(playerNumbers); + assertThat(strikes).isEqualTo(expectedStrikes); + } + + @DisplayName("볼 개수를 계산한다.") + @ParameterizedTest + @CsvSource(textBlock = """ + 123, 412, 2 + 123, 451, 1 + 123, 231, 3 + """) + void calculateBalls(String computer, String player, int expectedBalls) { + // given + List numbers1 = Arrays.stream(computer.split("")) + .map(Integer::parseInt) + .toList(); + List numbers2 = Arrays.stream(player.split("")) + .map(Integer::parseInt) + .toList(); + + // when + Numbers computerNumbers = new Numbers(numbers1); + Numbers playerNumbers = new Numbers(numbers2); + + // then + int balls = computerNumbers.calculateBalls(playerNumbers); + assertThat(balls).isEqualTo(expectedBalls); + } +} \ No newline at end of file