diff --git a/docs/README.md b/docs/README.md index e69de29bb2..c3d802bef1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,28 @@ +# 숫자 야구 + +## 기능 요구사항 정리 + +- [x] 게임 시작 문구 출력한다. +- [x] 랜덤 숫자를 생성한다. + - 숫자는 3자리다. + - 숫자는 중복되지 않아야 한다. +- [x] 숫자를 입력받는다. + - 숫자는 1~9 사이의 3자리 정수다. + - [x] 입력 값에서 공백을 제거한다. + - [x] 3자리가 아니면 에러가 발생한다. => `IllegalArgumentException` + - [x] 정수가 아닌 값을 포함하면 에러가 발생한다. => `IllegalArgumentException` + - [x] 1~9 범위를 벗어나면 에러가 발생한다. => `IllegalArgumentException` + - [x] 숫자는 중복되지 않아야 한다. => `IllegalArgumentException` +- [x] 랜덤 숫자와 입력받은 숫자를 비교한다. + - 같은 수가 같은 자리에 있으면 스트라이크, 같은 수가 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱 +- [x] 비교 결과를 출력한다. + - 결과는 볼, 스트라이크 개수로 표시한다. + - [x] 3스트라이크이면 게임을 종료한다. + - [x] 3스트라이크가 아니면 게임으로 돌아간다. +- [x] 게임이 종료되면 재진행 여부를 묻는다. + - [x] 재진행 여부를 입력받는다. + - 재진행 여부는 1 또는 2인 정수다. + - [x] 입력 값에서 공백을 제거한다. + - [x] 정수가 아닌 값을 포함하면 에러가 발생한다. => `IllegalArgumentException` + - [x] 1~2 범위를 벗어나면 에러가 발생한다. => `IllegalArgumentException` + - [x] 게임을 재진행 또는 종료한다. \ No newline at end of file diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java index dd95a34214..d8d55952e3 100644 --- a/src/main/java/baseball/Application.java +++ b/src/main/java/baseball/Application.java @@ -1,7 +1,12 @@ package baseball; +import baseball.service.Game; +import baseball.service.NumberBaseballGame; + public class Application { + public static void main(String[] args) { - // TODO: 프로그램 구현 + Game game = new NumberBaseballGame(); + game.run(); } } diff --git a/src/main/java/baseball/domain/GameNumber.java b/src/main/java/baseball/domain/GameNumber.java new file mode 100644 index 0000000000..0d8e7b95d2 --- /dev/null +++ b/src/main/java/baseball/domain/GameNumber.java @@ -0,0 +1,45 @@ +package baseball.domain; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.Objects; + +public class GameNumber { + + private static final int MIN_VALUE = 1; + private static final int MAX_VALUE = 9; + + private final int value; + + private GameNumber(int value) { + validateRange(value); + this.value = value; + } + + public static GameNumber from(int input) { + return new GameNumber(input); + } + + public static GameNumber create() { + return new GameNumber(Randoms.pickNumberInRange(MIN_VALUE, MAX_VALUE)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GameNumber that = (GameNumber) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + private static void validateRange(int value) { + if (value < MIN_VALUE || value > MAX_VALUE) { + throw new IllegalArgumentException(String.format("%d~%d 범위를 벗어나는 숫자가 포함되어 있습니다.", MIN_VALUE, MAX_VALUE)); + } + } +} diff --git a/src/main/java/baseball/domain/GameNumberGenerator.java b/src/main/java/baseball/domain/GameNumberGenerator.java new file mode 100644 index 0000000000..6d0b5e3824 --- /dev/null +++ b/src/main/java/baseball/domain/GameNumberGenerator.java @@ -0,0 +1,8 @@ +package baseball.domain; + +import java.util.List; + +public interface GameNumberGenerator { + + List generate(int limit); +} diff --git a/src/main/java/baseball/domain/GameNumbers.java b/src/main/java/baseball/domain/GameNumbers.java new file mode 100644 index 0000000000..c5753e803f --- /dev/null +++ b/src/main/java/baseball/domain/GameNumbers.java @@ -0,0 +1,55 @@ +package baseball.domain; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class GameNumbers { + + private static final int THE_NUMBER_OF_GAME_NUMBER = 3; + + private final List gameNumbers; + + private GameNumbers(List gameNumbers) { + validateTheNumberOf(gameNumbers); + validateDuplication(gameNumbers); + this.gameNumbers = new ArrayList<>(gameNumbers); + } + + public static GameNumbers from(GameNumberGenerator gameNumberGenerator) { + return new GameNumbers(gameNumberGenerator.generate(THE_NUMBER_OF_GAME_NUMBER)); + } + + public GameResult compare(GameNumbers other) { + int strike = 0; + int ball = 0; + for (int order = 0; order < THE_NUMBER_OF_GAME_NUMBER; order++) { + if (isStrike(other, order)) { + strike++; + } else if (isBall(other, gameNumbers.get(order))) { + ball++; + } + } + return GameResult.from(strike, ball, THE_NUMBER_OF_GAME_NUMBER); + } + + private static void validateTheNumberOf(List gameNumbers) { + if (gameNumbers.size() != THE_NUMBER_OF_GAME_NUMBER) { + throw new IllegalArgumentException(String.format("%d자리 값이 아닙니다.", THE_NUMBER_OF_GAME_NUMBER)); + } + } + + private void validateDuplication(List gameNumbers) { + if (new HashSet<>(gameNumbers).size() != THE_NUMBER_OF_GAME_NUMBER) { + throw new IllegalArgumentException("중복되는 값이 포함되어 있습니다."); + } + } + + private boolean isStrike(GameNumbers other, int order) { + return this.gameNumbers.get(order).equals(other.gameNumbers.get(order)); + } + + private boolean isBall(GameNumbers computer, GameNumber gameNumber) { + return computer.gameNumbers.contains(gameNumber); + } +} diff --git a/src/main/java/baseball/domain/GameResult.java b/src/main/java/baseball/domain/GameResult.java new file mode 100644 index 0000000000..fb306d046a --- /dev/null +++ b/src/main/java/baseball/domain/GameResult.java @@ -0,0 +1,39 @@ +package baseball.domain; + +public class GameResult { + + private static final int THE_NUMBER_OF_GAME_NUMBER = 3; + + private final String value; + private final boolean isEnd; + + private GameResult(String value, boolean isEnd) { + this.value = value; + this.isEnd = isEnd; + } + + public static GameResult from(int strike, int ball, int theNumberOfGameNumber) { + if (strike == 0 && ball == 0) { + return new GameResult("낫싱", false); + } + StringBuilder result = new StringBuilder(); + if (ball != 0) { + result.append(ball).append("볼 "); + } + if (strike != 0) { + result.append(strike).append("스트라이크"); + } + if (strike == theNumberOfGameNumber) { + result.append("\n").append(theNumberOfGameNumber).append("개의 숫자를 모두 맞히셨습니다! 게임 종료"); + } + return new GameResult(result.toString(), strike == THE_NUMBER_OF_GAME_NUMBER); + } + + public String getValue() { + return value; + } + + public boolean isEnd() { + return isEnd; + } +} diff --git a/src/main/java/baseball/domain/GameStatus.java b/src/main/java/baseball/domain/GameStatus.java new file mode 100644 index 0000000000..d3f4a80e66 --- /dev/null +++ b/src/main/java/baseball/domain/GameStatus.java @@ -0,0 +1,30 @@ +package baseball.domain; + +import baseball.util.Convertor; + +public class GameStatus { + + private static final int RESTART = 1; + private static final int END = 2; + + private final int status; + + private GameStatus(int status) { + validateRange(status); + this.status = status; + } + + public static GameStatus from(String input) { + return new GameStatus(Convertor.toInteger(input)); + } + + public boolean isRestart() { + return status == RESTART; + } + + private static void validateRange(int status) { + if (status != RESTART && status != END) { + throw new IllegalArgumentException("1 또는 2 이외의 숫자가 포함되어 있습니다."); + } + } +} diff --git a/src/main/java/baseball/domain/RandomNumberGenerator.java b/src/main/java/baseball/domain/RandomNumberGenerator.java new file mode 100644 index 0000000000..83545f6264 --- /dev/null +++ b/src/main/java/baseball/domain/RandomNumberGenerator.java @@ -0,0 +1,15 @@ +package baseball.domain; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RandomNumberGenerator implements GameNumberGenerator { + + public List generate(int limit) { + return Stream.generate(GameNumber::create) + .distinct() + .limit(limit) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/baseball/domain/UserNumberGenerator.java b/src/main/java/baseball/domain/UserNumberGenerator.java new file mode 100644 index 0000000000..b013109e81 --- /dev/null +++ b/src/main/java/baseball/domain/UserNumberGenerator.java @@ -0,0 +1,33 @@ +package baseball.domain; + +import baseball.util.Convertor; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class UserNumberGenerator implements GameNumberGenerator { + + private static final String DELIMITER = ""; + + private final String input; + + public UserNumberGenerator(String input) { + this.input = input; + } + + public List generate(int limit) { + String[] inputNumbers = input.split(DELIMITER); + validateTheNumberOf(inputNumbers, limit); + return Arrays.stream(inputNumbers) + .map(Convertor::toInteger) + .map(GameNumber::from) + .collect(Collectors.toList()); + } + + private void validateTheNumberOf(String[] inputNumbers, int limit) { + if (inputNumbers.length != limit) { + throw new IllegalArgumentException(String.format("%d자리 값이 아닙니다.", limit)); + } + } +} diff --git a/src/main/java/baseball/service/Game.java b/src/main/java/baseball/service/Game.java new file mode 100644 index 0000000000..b46a228719 --- /dev/null +++ b/src/main/java/baseball/service/Game.java @@ -0,0 +1,6 @@ +package baseball.service; + +public interface Game { + + void run(); +} diff --git a/src/main/java/baseball/service/NumberBaseballGame.java b/src/main/java/baseball/service/NumberBaseballGame.java new file mode 100644 index 0000000000..dfb7b9353d --- /dev/null +++ b/src/main/java/baseball/service/NumberBaseballGame.java @@ -0,0 +1,38 @@ +package baseball.service; + +import baseball.domain.*; +import baseball.util.Input; +import baseball.util.Output; + +public class NumberBaseballGame implements Game { + + private static final GameNumberGenerator randomNumberGenerator = new RandomNumberGenerator(); + + public void run() { + Output.printStartMessage(); + start(); + } + + private void start() { + play(GameNumbers.from(randomNumberGenerator)); + askPlayAgain(); + } + + private void play(GameNumbers computer) { + while (true) { + GameNumberGenerator userNumberGenerator = new UserNumberGenerator(Input.readGameNumber()); + GameNumbers user = GameNumbers.from(userNumberGenerator); + GameResult gameResult = user.compare(computer); + Output.printResult(gameResult.getValue()); + if (gameResult.isEnd()) { + break; + } + } + } + + private void askPlayAgain() { + if (GameStatus.from(Input.readGameStatus()).isRestart()) { + start(); + } + } +} diff --git a/src/main/java/baseball/util/Convertor.java b/src/main/java/baseball/util/Convertor.java new file mode 100644 index 0000000000..089047e8c6 --- /dev/null +++ b/src/main/java/baseball/util/Convertor.java @@ -0,0 +1,15 @@ +package baseball.util; + +public class Convertor { + + private Convertor() { + } + + public static int toInteger(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("정수가 아닌 값이 포함되어 있습니다."); + } + } +} diff --git a/src/main/java/baseball/util/Input.java b/src/main/java/baseball/util/Input.java new file mode 100644 index 0000000000..41b0a4fad8 --- /dev/null +++ b/src/main/java/baseball/util/Input.java @@ -0,0 +1,26 @@ +package baseball.util; + +import camp.nextstep.edu.missionutils.Console; + +public class Input { + + private static final String BLANK = " "; + private static final String DELETE = ""; + + private Input() { + } + + public static String readGameNumber() { + System.out.print("숫자를 입력해주세요 : "); + return removeBlank(Console.readLine()); + } + + public static String readGameStatus() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + return removeBlank(Console.readLine()); + } + + private static String removeBlank(String input) { + return input.replaceAll(BLANK, DELETE); + } +} diff --git a/src/main/java/baseball/util/Output.java b/src/main/java/baseball/util/Output.java new file mode 100644 index 0000000000..8befee9ca0 --- /dev/null +++ b/src/main/java/baseball/util/Output.java @@ -0,0 +1,15 @@ +package baseball.util; + +public class Output { + + private Output() { + } + + public static void printStartMessage() { + System.out.println("숫자 야구 게임을 시작합니다."); + } + + public static void printResult(String result) { + System.out.println(result); + } +} diff --git a/src/test/java/baseball/domain/GameNumbersTest.java b/src/test/java/baseball/domain/GameNumbersTest.java new file mode 100644 index 0000000000..f441b36e2f --- /dev/null +++ b/src/test/java/baseball/domain/GameNumbersTest.java @@ -0,0 +1,42 @@ +package baseball.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GameNumbersTest { + + private GameNumbers computer; + + @BeforeEach + void setUp() { + GameNumberGenerator gameNumberGenerator = new UserNumberGenerator("123"); + computer = GameNumbers.from(gameNumberGenerator); + } + + @DisplayName("게임 종료") + @Test + void 스트라이크_세번이면_게임_종료() { + // given + GameNumberGenerator userNumberGenerator = new UserNumberGenerator("123"); + GameNumbers user = GameNumbers.from(userNumberGenerator); + // when + GameResult gameResult = user.compare(computer); + // then + assertThat(gameResult.isEnd()).isTrue(); + } + + @DisplayName("게임 재시작") + @Test + void 스트라이크_세번_아니면_게임_재시작() { + // given + GameNumberGenerator userNumberGenerator = new UserNumberGenerator("124"); + GameNumbers user = GameNumbers.from(userNumberGenerator); + // when + GameResult gameResult = user.compare(computer); + // then + assertThat(gameResult.isEnd()).isFalse(); + } +} diff --git a/src/test/java/baseball/domain/GameResultTest.java b/src/test/java/baseball/domain/GameResultTest.java new file mode 100644 index 0000000000..326cc9beb0 --- /dev/null +++ b/src/test/java/baseball/domain/GameResultTest.java @@ -0,0 +1,32 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GameResultTest { + + private static final int LIMIT = 3; + + @DisplayName("같은 수가 없으면 낫싱 출력") + @Test + void 같은_수_없으면_낫싱() { + assertThat(GameResult.from(0, 0, LIMIT).getValue()) + .isEqualTo("낫싱"); + } + + @DisplayName("같은 수가 같은 자리에 있으면 스트라이크") + @Test + void 같은_수_같은_자리면_스트라이크() { + assertThat(GameResult.from(2, 0, LIMIT).getValue()) + .isEqualTo("2스트라이크"); + } + + @DisplayName("같은 수가 다른 자리에 있으면 볼") + @Test + void 같은_수_다른_자리면_볼() { + assertThat(GameResult.from(0, 1, LIMIT).getValue()) + .isEqualTo("1볼 "); + } +} diff --git a/src/test/java/baseball/domain/GameStatusTest.java b/src/test/java/baseball/domain/GameStatusTest.java new file mode 100644 index 0000000000..c4fbc0f110 --- /dev/null +++ b/src/test/java/baseball/domain/GameStatusTest.java @@ -0,0 +1,32 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GameStatusTest { + + @DisplayName("형식 외 입력 시 예외 발생") + @ParameterizedTest + @ValueSource(strings = {"1234", "-", "3"}) + void 잘못된_입력(String input) { + assertThatThrownBy(() -> GameStatus.from(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("1 입력 시 재시작") + @Test + void 입력값_1이면_재시작() { + assertThat(GameStatus.from("1").isRestart()).isTrue(); + } + + @DisplayName("2 입력 시 종료") + @Test + void 입력값_2이면_종료() { + assertThat(GameStatus.from("2").isRestart()).isFalse(); + } +} diff --git a/src/test/java/baseball/domain/UserNumberGeneratorTest.java b/src/test/java/baseball/domain/UserNumberGeneratorTest.java new file mode 100644 index 0000000000..17c6cf5b92 --- /dev/null +++ b/src/test/java/baseball/domain/UserNumberGeneratorTest.java @@ -0,0 +1,37 @@ +package baseball.domain; + +import baseball.domain.GameNumberGenerator; +import baseball.domain.UserNumberGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class UserNumberGeneratorTest { + + private static final int LIMIT = 3; + + @DisplayName("형식 외 입력 시 예외 발생") + @ParameterizedTest + @ValueSource(strings = {"1234", "12-", "120"}) + void 잘못된_입력(String input) { + // given + GameNumberGenerator userNumberGenerator = new UserNumberGenerator(input); + // when & then + assertThatThrownBy(() -> userNumberGenerator.generate(LIMIT)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("3자리 아닌 수 입력 시 통과") + @ParameterizedTest + @ValueSource(strings = {"123", "483", "679"}) + void 올바른_입력(String input) { + // given + GameNumberGenerator userNumberGenerator = new UserNumberGenerator(input); + // when & then + assertThatCode(() -> userNumberGenerator.generate(LIMIT)) + .doesNotThrowAnyException(); + } +}