diff --git a/docs/README.md b/docs/README.md index e69de29..87922e9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,39 @@ +# 요구사항 명세서 +### 카테고리 +1. 컴퓨터 +2. 사용자 +3. 시스템 + +### 요구사항 내용 +1. 컴퓨터 + - 1 ~ 9까지 서로 다른 임의의 수 3개를 선택한다. + - 사용자가 입력한 숫자에 대한 결과를 출력한다. + + + +2. 사용자 + - 서로 다른 3개의 숫자를 입력한다. + - 게임을 종료할 때 완전 종료와 다시 시작을 선택한다. + + +3. 시스템 + - 게임 시작 문구를 출력한다. + - 사용자와 컴퓨터의 동작을 반복한다. + - 3개의 숫자를 모두 맞히면 게임을 종료한다. + 게임이 종료할 때 사용자의 선택에 따라서 완전 종료하거나 다시 시작한다. + - 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 App을 종료한다. + +# 기능 목록 +1. 게임 시작 + - 게임 시작 문구 출력 + + +2. 게임 진행 + - 컴퓨터는 1 ~ 9까지 서로 다른 임의의 수 선택 + - 사용자가 서로 다른 3개의 숫자를 입력 + - 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 전혀 없으면 낫싱 출력 + + +3. 게임 종료 + - 사용자가 컴퓨터의 수를 맞추면 게임 종료 출력 + - 재시작(1) / 종료(2) \ No newline at end of file diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java index dd95a34..1227abf 100644 --- a/src/main/java/baseball/Application.java +++ b/src/main/java/baseball/Application.java @@ -1,7 +1,10 @@ package baseball; +import baseball.controller.BaseballController; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + new BaseballController().run(); } } diff --git a/src/main/java/baseball/controller/BaseballController.java b/src/main/java/baseball/controller/BaseballController.java new file mode 100644 index 0000000..ab56d7b --- /dev/null +++ b/src/main/java/baseball/controller/BaseballController.java @@ -0,0 +1,85 @@ +package baseball.controller; + +import baseball.model.Computer; +import baseball.model.User; +import baseball.view.InputView; +import camp.nextstep.edu.missionutils.Console; +import java.util.List; +import java.util.stream.Stream; + +import static baseball.view.InputView.inputNumber; +import static baseball.view.OutputView.*; + +public class BaseballController { + private static final String RESTART_STATUS = "1"; + private static final int THREE_STRIKE = 3; + private static final boolean NOT_THREE_STRIKE = true; + + private User user; + private Computer computer; + private int strikeCount; + private int ballCount; + + public BaseballController() { + this.strikeCount = 0; + this.ballCount = 0; + } + + public void run() { + printGameStartMessage(); + do { + computer = gameStart(); + playGame(); + } while (gameOver()); + } + + private Computer gameStart() { + return new Computer(); + } + + private void playGame() { + user = new User(); + + while (NOT_THREE_STRIKE) { + user.setNumber(inputNumber()); + strikeCount = getStrikeCount(user, computer); + ballCount = getBallCount(user, computer); + + printGameResultMessage(strikeCount, ballCount); + if (strikeCount == THREE_STRIKE) { + break; + } + } + } + + private boolean gameOver() { + return !isTerminate(InputView.terminateSignUserInput()); + } + + private int getStrikeCount(final User user, final Computer computer) { + List computerRandomNumber = computer.getRandomNumber(); + List userNumber = user.getNumber(); + + return Stream.iterate(0, n -> n + 1).limit(computerRandomNumber.size()) + .filter(i -> computerRandomNumber.get(i) == userNumber.get(i)) + .reduce(0, (cnt, b) -> cnt + 1); + } + + private int getBallCount(final User user, final Computer computer) { + List computerRandomNumber = computer.getRandomNumber(); + List userNumber = user.getNumber(); + + return Stream.iterate(1, n -> n + 1).limit(9) + .filter(i -> computerRandomNumber.contains(i) && userNumber.contains(i)) + .filter(i -> computerRandomNumber.indexOf(i) != userNumber.indexOf(i)) + .reduce(0, (cnt, b) -> cnt + 1); + } + + private boolean isTerminate(final String restartStatus) { + if(restartStatus.equals(RESTART_STATUS)){ + return false; + } else { + return true; + } + } +} diff --git a/src/main/java/baseball/model/Computer.java b/src/main/java/baseball/model/Computer.java new file mode 100644 index 0000000..5987645 --- /dev/null +++ b/src/main/java/baseball/model/Computer.java @@ -0,0 +1,55 @@ +package baseball.model; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.ArrayList; +import java.util.List; + +public class Computer { + public static final int NUMBER_LENGTH = 3; + public static final int MIN_RANGE = 1; + public static final int MAX_RANGE = 9; + private List randomNumber; + + public Computer() { + saveRandomNumberWithGameStart(); + } + + public List getRandomNumber() { + return randomNumber; + } + + private void saveRandomNumberWithGameStart() { + randomNumber = new ArrayList<>(); + Integer digit; + + while (checkLengthSmallThanThree()) { + digit = getRandomDigit(); + if (!hasDuplicateDigitInRandomNumber(digit)) { + randomNumber.add(digit); + } + } + } + + private boolean checkLengthSmallThanThree() { + if (randomNumber.size() < NUMBER_LENGTH) { + return true; + } + return false; + } + + private Integer getRandomDigit() { + return Randoms.pickNumberInRange(MIN_RANGE, MAX_RANGE); + } + + private boolean hasDuplicateDigitInRandomNumber(final Integer digit) { + if (randomNumber.contains(digit)) { + return true; + } + return false; + } + + public void setRandomNumber(final List randomNumber) { + this.randomNumber = randomNumber; + } +} diff --git a/src/main/java/baseball/model/User.java b/src/main/java/baseball/model/User.java new file mode 100644 index 0000000..ba156d4 --- /dev/null +++ b/src/main/java/baseball/model/User.java @@ -0,0 +1,48 @@ +package baseball.model; + + +import java.util.List; +import java.util.Set; + +public class User { + + private static final String INPUT_NUMBER_LENGTH_NOT_THREE_EXCEPTION = "입력 숫자는 3자리입니다."; + private static final String INPUT_DIGIT_LENGTH_NOT_ONE_EXCEPTION = "입력 숫자는 1 ~ 9입니다."; + public static final int NUMBER_SIZE = 3; + public static final int MIN_DIGIT = 1; + public static final int MAX_DIGIT = 9; + public static final String INPUT_NUMBER_DUPLICATE_EXCEPTION = "입력숫자의 각 자리는 중복될 수 없습니다."; + + public enum RestartStatus { RESTART, TERMINATE } + + private List number; + + public void setNumber(List number) { + validateNumberSize(number); + validateNumberRange(number); + validateDuplicateDigit(number); + this.number = number; + } + + private void validateNumberSize(final List number) { + if (number.size() > NUMBER_SIZE || number.size() < NUMBER_SIZE) { + throw new IllegalArgumentException(INPUT_NUMBER_LENGTH_NOT_THREE_EXCEPTION); + } + } + + private void validateNumberRange(final List number) { + if (!number.stream().allMatch(digit -> digit >= MIN_DIGIT && digit <= MAX_DIGIT)) { + throw new IllegalArgumentException(INPUT_DIGIT_LENGTH_NOT_ONE_EXCEPTION); + } + } + + private void validateDuplicateDigit(final List number) { + if (Set.copyOf(number).size() != number.size()) { + throw new IllegalArgumentException(INPUT_NUMBER_DUPLICATE_EXCEPTION); + } + } + + public List getNumber() { + return number; + } +} diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 0000000..d52ef93 --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,67 @@ +package baseball.view; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class InputView { + + private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해주세요 : "; + private static final String CHARACTER_REGEX = "[-+]?\\d*\\.?\\d+"; + private static final String RESTART_STATUS = "1"; + private static final String NOT_RESTART_STATUS = "2"; + private static final String RESTART_OR_END_ONE_OR_TWO_EXCEPTION = "재시작은 1, 완전 종료는 2 입니다."; + private static final String RESTART_OR_END_NOT_NUMBER_BECAUSE_STRING_EXCEPTION = "재시작은 1, 완전 종료는 2인 정수입니다."; + private static final String RESTART_OR_END_NOT_NUMBER_BECAUSE_DOUBLE_EXCEPTION = "재시작은 1, 완전 종료는 2인 정수로 소수를 입력할 수 없습니다."; + public static final String INPUT_NUMBER_SPECIAL_CHARACTER_EXCEPTION = "특수 문자는 입력할 수 없습니다."; + + public static List inputNumber() { + System.out.print(INPUT_NUMBER_MESSAGE); + String inputNumber = Console.readLine(); + validateNotSpecialCharacterInUserNumber(inputNumber); + + return Arrays.stream(inputNumber.split("")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + public static String terminateSignUserInput() { + String inputRestartStatus = Console.readLine(); + validateRestartStatus(inputRestartStatus); + return inputRestartStatus; + } + + private static void validateNotSpecialCharacterInUserNumber(String inputNumber) { + if (!Arrays.stream(inputNumber.split("")).allMatch(digit -> digit.matches(CHARACTER_REGEX))) { + throw new IllegalArgumentException(INPUT_NUMBER_SPECIAL_CHARACTER_EXCEPTION); + } + } + + private static void validateRestartStatus(final String restartStatus) { + validateNotStringRestartStatus(restartStatus); + validateNotDoubleRestartStatus(restartStatus); + validateRangeRestartStatus(restartStatus); + } + + private static void validateRangeRestartStatus(final String restartStatus) { + if (!restartStatus.equals(RESTART_STATUS) && !restartStatus.equals(NOT_RESTART_STATUS)) { + throw new IllegalArgumentException(RESTART_OR_END_ONE_OR_TWO_EXCEPTION); + } + } + + private static void validateNotStringRestartStatus(final String restartStatus) { + if (!(restartStatus != null && restartStatus.matches(CHARACTER_REGEX))) { + throw new IllegalArgumentException(RESTART_OR_END_NOT_NUMBER_BECAUSE_STRING_EXCEPTION); + } + } + + private static void validateNotDoubleRestartStatus(final String restartStatus) { + if (!restartStatus.chars().allMatch(Character::isDigit)) { + throw new IllegalArgumentException(RESTART_OR_END_NOT_NUMBER_BECAUSE_DOUBLE_EXCEPTION); + } + } +} diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java new file mode 100644 index 0000000..7a494e9 --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,33 @@ +package baseball.view; + +public class OutputView { + + private static final String GAME_START_MESSAGE = "숫자 야구 게임을 시작합니다."; + private static final String STRIKE_MESSAGE = "스트라이크"; + private static final String GAME_OVER_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임종료"; + private static final String GAME_RESTART_OR_END_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요"; + private static final String NOTHING_MESSAGE = "낫싱"; + private static final String BALL_MESSAGE = "볼 "; + public static final int THREE_STRIKE = 3; + public static final int ZERO = 0; + + public static void printGameStartMessage() { + System.out.println(GAME_START_MESSAGE); + } + + public static void printGameResultMessage(final int strikeCount, final int ballCount) { + if (strikeCount == THREE_STRIKE) { + System.out.println(strikeCount + STRIKE_MESSAGE); + System.out.println(GAME_OVER_MESSAGE); + System.out.println(GAME_RESTART_OR_END_MESSAGE); + } else if (strikeCount == ZERO && ballCount == ZERO) { + System.out.println(NOTHING_MESSAGE); + } else if (strikeCount != ZERO && ballCount == ZERO) { + System.out.println(strikeCount + STRIKE_MESSAGE); + } else if (strikeCount == ZERO && ballCount != ZERO) { + System.out.println(ballCount + BALL_MESSAGE); + } else { + System.out.println(ballCount + BALL_MESSAGE + strikeCount + STRIKE_MESSAGE); + } + } +} diff --git a/src/test/java/baseball/controller/BaseballControllerTest.java b/src/test/java/baseball/controller/BaseballControllerTest.java new file mode 100644 index 0000000..1f7a7a9 --- /dev/null +++ b/src/test/java/baseball/controller/BaseballControllerTest.java @@ -0,0 +1,146 @@ +package baseball.controller; + +import baseball.model.Computer; +import baseball.model.User; +import baseball.view.InputView; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.InstanceOfAssertFactories.comparable; +import static org.junit.jupiter.api.Assertions.*; + +class BaseballControllerTest { + @Test + @DisplayName("재시작 검증 테스트") + void ValidateRangeRestartStatusTest() { + // given + String restartStatus = "5"; + BaseballController controller = new BaseballController(); + + // when + + + // then + assertThatThrownBy(() -> controller.validateRangeRestartStatus(restartStatus)) + .isInstanceOf(IllegalArgumentException.class); + + } + + @Test + @DisplayName("재시작 검증 테스트") + void ValidateNotStringRestartStatusTest() { + // given + String restartStatus = "*"; + BaseballController controller = new BaseballController(); + + // when + + + // then + assertThatThrownBy(() -> controller.validateNotStringRestartStatus(restartStatus)) + .isInstanceOf(IllegalArgumentException.class); + + } + + @Test + @DisplayName("재시작 검증 테스트") + void validateNotDoubleRestartStatusTest() { + // given + String restartStatus = "1.2"; + BaseballController controller = new BaseballController(); + + // when + + + // then + assertThatThrownBy(() -> controller.validateNotDoubleRestartStatus(restartStatus)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("사용자 입력에 따른 종료 테스트") + void terminateTest() { + // given + String restartStatus = "1"; + BaseballController controller = new BaseballController(); + + // when + + // then + assertThat(controller.isTerminate(restartStatus)).isFalse(); + } + + @Test + @DisplayName("스트라이크 개수 세기 테스트") + void getStrikeCountTest() { + // given + BaseballController controller = new BaseballController(); + Computer computer = new Computer(); + User user = new User(); + + // when + user.setNumber(List.of(1, 2, 3)); + computer.setRandomNumber(List.of(1, 4, 4)); + int strikeCount = controller.getStrikeCount(user, computer); + + // then + assertThat(strikeCount).isEqualTo(1); + } + + @Test + @DisplayName("볼 갯수 세기 테스트") + void getBallCountTest() { + // given + BaseballController controller = new BaseballController(); + Computer computer = new Computer(); + User user = new User(); + + // when + user.setNumber(List.of(1, 2, 3)); + computer.setRandomNumber(List.of(4, 7, 5)); + int ballCount = controller.getBallCount(user, computer); + + // then + assertThat(ballCount).isEqualTo(0); + } + + @Test + @DisplayName("사용자 3자리 수 입력 테스트") + void inputUserNumberTest() { + // given + InputView inputView = new InputView(); + + String userInput = "123"; + InputStream inputStream = new ByteArrayInputStream(userInput.getBytes()); + System.setIn(inputStream); + + // when + List inputNumber = inputView.inputNumber(); + + // then + assertThat(inputNumber).isEqualTo(List.of(1, 2, 3)); + } + + @Test + @DisplayName("게임 종료 테스트") + void gameOverTest() { + // given + BaseballController controller = new BaseballController(); + + String userInput = "2"; + InputStream inputStream = new ByteArrayInputStream(userInput.getBytes()); + System.setIn(inputStream); + + // when + boolean isGameOver = controller.gameOver(); + + // then + assertThat(isGameOver).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/model/ComputerTest.java b/src/test/java/baseball/model/ComputerTest.java new file mode 100644 index 0000000..c89d509 --- /dev/null +++ b/src/test/java/baseball/model/ComputerTest.java @@ -0,0 +1,68 @@ +package baseball.model; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class ComputerTest { + @Test + @DisplayName("컴퓨터의 랜덤 수를 저장할 때 중복된 숫자를 가지지 않는지 테스트 ") + void hasDigitTest() { + // given + Computer computer = new Computer(); + + // when + computer.randomNumber = new ArrayList<>(Arrays.asList(1, 2, 3)); + + // then + assertThat(computer.hasDuplicateDigitInRandomNumber(1)).isTrue(); + } + + @Test + @DisplayName("한자리 랜덤 수 생성 테스트") + void getRandomDigitTest() { + // given + Computer computer = new Computer(); + + // when + Integer randomDigit = computer.getRandomDigit(); + + // then + assertThat(randomDigit >= 1 && randomDigit <= 9).isTrue(); + } + + @Test + @DisplayName("랜덤수가 3자리 이하인지 체크 테스트") + void checkLengthTest() { + // given + Computer computer = new Computer(); + + // when + computer.randomNumber = new ArrayList<>(Arrays.asList(1, 2)); + + // then + assertThat(computer.checkLengthSmallThanThree()).isTrue(); + } + + @Test + @DisplayName("랜덤 수 생성 테스트") + void saveRandomNumberTest() { + // given + Computer computer = new Computer(); + + // when + computer.saveRandomNumberWithGameStart(); + List randomNumber = computer.randomNumber; + + // then + assertThat(randomNumber.stream().allMatch(digit -> digit >= 111 && digit <= 999)); + System.out.println(randomNumber); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/model/UserTest.java b/src/test/java/baseball/model/UserTest.java new file mode 100644 index 0000000..7c3b6d1 --- /dev/null +++ b/src/test/java/baseball/model/UserTest.java @@ -0,0 +1,55 @@ +package baseball.model; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class UserTest { + @Test + @DisplayName("3자리가 아닌 입력 숫자 검증 기능 테스트") + void validateNumberLengthTest() { + // given + User user = new User(); + + // when + ArrayList number = new ArrayList<>(Arrays.asList(1, 2)); + + // then + assertThatThrownBy(() -> user.validateNumber(number)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("3자리가 아닌 입력 숫자 검증 기능 테스트") + void validateDigitLengthTest() { + // given + User user = new User(); + + // when + ArrayList number = new ArrayList<>(Arrays.asList(1, 2, 33)); + + // then + assertThatThrownBy(() -> user.validateNumber(number)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("RestartStatus 테스트") + void UserTest() { + // given + User user = new User(); + + // when + user.terminate(); + + // then + assertThat(user.restartStatus).isEqualTo(User.RestartStatus.TERMINATE); + } +} \ No newline at end of file