diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..77e5b194e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# 핵심 기능 + +세 자리 수를 정답과 비교해서 결과를 알려준다. + +# 기능 요구사항 + +* 세 자리 수(입력수)를 정답과 비교한다. + +* 정답을 생성한다. + +* 입력수를 입력받는다. + +* 비교 결과를 출력한다. + +* 재시작 여부를 입력받는다. + +# 최종 UML + +![img.png](img.png) + diff --git a/docs/img.png b/docs/img.png new file mode 100644 index 000000000..bd8e6621a Binary files /dev/null and b/docs/img.png differ diff --git a/src/main/kotlin/baseball/Application.kt b/src/main/kotlin/baseball/Application.kt index 148d75cc3..cbfdbab84 100644 --- a/src/main/kotlin/baseball/Application.kt +++ b/src/main/kotlin/baseball/Application.kt @@ -1,5 +1,13 @@ package baseball +import baseball.controller.GameController +import baseball.model.RandomAnswerGenerator +import baseball.model.Referee + fun main() { - TODO("프로그램 구현") + val gameController = GameController( + referee = Referee(), + answerGenerator = RandomAnswerGenerator() + ) + gameController.start() } diff --git a/src/main/kotlin/baseball/controller/GameController.kt b/src/main/kotlin/baseball/controller/GameController.kt new file mode 100644 index 000000000..f7a770d6e --- /dev/null +++ b/src/main/kotlin/baseball/controller/GameController.kt @@ -0,0 +1,33 @@ +package baseball.controller + +import baseball.model.AnswerGenerator +import baseball.model.BaseballNumbers +import baseball.model.Referee +import baseball.view.Command +import baseball.view.InputView +import baseball.view.OutputView + +class GameController( + val referee: Referee, + val answerGenerator: AnswerGenerator +) { + private val inputView = InputView() + private val outputView = OutputView() + + fun start() { + outputView.showStartPrompt() + do { + val answer = answerGenerator.generate() + oneCycleGame(answer) + outputView.showSuccessPrompt() + } while (inputView.readCommand().toCommand() == Command.RESTART) + } + + private fun oneCycleGame(answer: BaseballNumbers) { + do { + val userBaseballNumbers = (inputView.readNumbers()).toBaseballNumbers() + val ballAndStrike = referee.compare(userBaseballNumbers, answer) + outputView.showTurnResult(ballAndStrike) + } while (!ballAndStrike.isSuccess()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/controller/NumbersConverter.kt b/src/main/kotlin/baseball/controller/NumbersConverter.kt new file mode 100644 index 000000000..47ed30aaa --- /dev/null +++ b/src/main/kotlin/baseball/controller/NumbersConverter.kt @@ -0,0 +1,35 @@ +package baseball.controller + +import baseball.model.BaseballNumber +import baseball.model.BaseballNumber.Companion.START_NUMBER +import baseball.model.BaseballNumbers +import baseball.model.BaseballNumbers.Companion.NUMBERS_DIGIT +import baseball.view.Command +import baseball.view.InputView.Companion.COMMAND_PROMPT + + +fun Int.toBaseballNumbers(): BaseballNumbers { + require(this >= START_NUMBER) { + INVALID_NUMBERS + } + + var tempNumber = this + val numbers = mutableListOf() + while (tempNumber > 0) { + val digit = tempNumber % 10 + numbers.add(0, BaseballNumber(digit)) + tempNumber /= 10 + } + return BaseballNumbers(numbers) +} + +fun Int.toCommand(): Command { + val command = Command.entries.firstOrNull { it.command == this } + require(command != null) { + INVALID_COMMAND + } + return command +} + +const val INVALID_NUMBERS = "0 이상의 $NUMBERS_DIGIT 자리 수를 입력해주세요." +val INVALID_COMMAND = "잘못된 입력입니다.\n $COMMAND_PROMPT" diff --git a/src/main/kotlin/baseball/model/AnswerGenerator.kt b/src/main/kotlin/baseball/model/AnswerGenerator.kt new file mode 100644 index 000000000..7173e689f --- /dev/null +++ b/src/main/kotlin/baseball/model/AnswerGenerator.kt @@ -0,0 +1,5 @@ +package baseball.model + +interface AnswerGenerator { + fun generate(): BaseballNumbers +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/model/BallAndStrike.kt b/src/main/kotlin/baseball/model/BallAndStrike.kt new file mode 100644 index 000000000..884f9e89a --- /dev/null +++ b/src/main/kotlin/baseball/model/BallAndStrike.kt @@ -0,0 +1,28 @@ +package baseball.model + +import baseball.model.BaseballNumbers.Companion.NUMBERS_DIGIT + +data class BallAndStrike( + val strikeCount: Int, + val ballCount: Int +) { + + fun isSuccess() = strikeCount == NUMBERS_DIGIT + + override fun toString(): String { + return when { + (strikeCount > 0 && ballCount > 0) -> + "$ballCount$BALL_SUFFIX $strikeCount$STRIKE_SUFFIX" + + (strikeCount > 0) -> "$strikeCount$STRIKE_SUFFIX" + (ballCount > 0) -> "$ballCount$BALL_SUFFIX" + else -> NOTHING + } + } + + companion object { + const val BALL_SUFFIX = "볼" + const val STRIKE_SUFFIX = "스트라이크" + const val NOTHING = "낫싱" + } +} diff --git a/src/main/kotlin/baseball/model/BaseballNumber.kt b/src/main/kotlin/baseball/model/BaseballNumber.kt new file mode 100644 index 000000000..11b5bee01 --- /dev/null +++ b/src/main/kotlin/baseball/model/BaseballNumber.kt @@ -0,0 +1,15 @@ +package baseball.model + +data class BaseballNumber(val number: Int) { + init { + require(number in START_NUMBER..END_NUMBER) { + INVALID_NUMBER_RANGE + } + } + + companion object { + const val START_NUMBER = 1 + const val END_NUMBER = 9 + const val INVALID_NUMBER_RANGE = "숫자는 $START_NUMBER 와 $END_NUMBER 사이의 수여야 합니다." + } +} diff --git a/src/main/kotlin/baseball/model/BaseballNumbers.kt b/src/main/kotlin/baseball/model/BaseballNumbers.kt new file mode 100644 index 000000000..5c7cc218f --- /dev/null +++ b/src/main/kotlin/baseball/model/BaseballNumbers.kt @@ -0,0 +1,19 @@ +package baseball.model + +data class BaseballNumbers(val numbers: List) { + + init { + require(numbers.size == 3) { + INVALID_NUMBERS_SIZE + } + require(numbers.distinct().size == 3) { + NUMBERS_DUPLICATED + } + } + + companion object { + const val NUMBERS_DIGIT = 3 + const val INVALID_NUMBERS_SIZE = "숫자는 $NUMBERS_DIGIT 자리 수가 되어야 합니다." + const val NUMBERS_DUPLICATED = "숫자에 중복된 수가 있습니다." + } +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/model/RandomAnswerGenerator.kt b/src/main/kotlin/baseball/model/RandomAnswerGenerator.kt new file mode 100644 index 000000000..c3e54d927 --- /dev/null +++ b/src/main/kotlin/baseball/model/RandomAnswerGenerator.kt @@ -0,0 +1,16 @@ +package baseball.model + +import baseball.model.BaseballNumber.Companion.END_NUMBER +import baseball.model.BaseballNumber.Companion.START_NUMBER +import baseball.model.BaseballNumbers.Companion.NUMBERS_DIGIT +import camp.nextstep.edu.missionutils.Randoms.pickNumberInRange + +class RandomAnswerGenerator : AnswerGenerator { + override fun generate(): BaseballNumbers { + val numbers = mutableSetOf() + while (numbers.size < NUMBERS_DIGIT) { + numbers.add(BaseballNumber(pickNumberInRange(START_NUMBER, END_NUMBER))) + } + return BaseballNumbers(numbers.toList()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/model/Referee.kt b/src/main/kotlin/baseball/model/Referee.kt new file mode 100644 index 000000000..059ed2e2c --- /dev/null +++ b/src/main/kotlin/baseball/model/Referee.kt @@ -0,0 +1,21 @@ +package baseball.model + +class Referee { + fun compare(inputNumbers: BaseballNumbers, answer: BaseballNumbers): BallAndStrike { + var strikeCount = 0 + var ballCount = 0 + for (i in 0 until inputNumbers.numbers.size) { + val inputNumber = inputNumbers.numbers[i] + val answerNumber = answer.numbers[i] + + if (inputNumber == answerNumber) { + strikeCount++ + } else if (answer.numbers.any { it == inputNumber }) { + ballCount++ + } + } + + return BallAndStrike(strikeCount, ballCount) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/view/Command.kt b/src/main/kotlin/baseball/view/Command.kt new file mode 100644 index 000000000..7a4ab90c6 --- /dev/null +++ b/src/main/kotlin/baseball/view/Command.kt @@ -0,0 +1,6 @@ +package baseball.view + +enum class Command(val commandName: String, val command: Int) { + RESTART("새로 시작", 1), + EXIT("종료", 2), +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/view/InputView.kt b/src/main/kotlin/baseball/view/InputView.kt new file mode 100644 index 000000000..1d5c0fb0e --- /dev/null +++ b/src/main/kotlin/baseball/view/InputView.kt @@ -0,0 +1,26 @@ +package baseball.view + + +import camp.nextstep.edu.missionutils.Console.readLine + +class InputView { + fun readNumbers(): Int { + print(INPUT_NUMBERS_PROMPT) + return readLine().toIntOrNull() ?: -1 + } + + fun readCommand(): Int { + println(COMMAND_PROMPT) + return readLine().toIntOrNull() ?: -1 + } + + companion object { + private const val DELIMITER = " : " + const val INPUT_NUMBERS_PROMPT = "숫자를 입력해주세요$DELIMITER" + + val COMMAND_PROMPT = + "게임을 ${Command.RESTART.commandName}하려면 ${Command.RESTART.command}, " + + "${Command.EXIT.commandName}하려면 ${Command.EXIT.command}를 입력하세요." + + } +} \ No newline at end of file diff --git a/src/main/kotlin/baseball/view/OutputView.kt b/src/main/kotlin/baseball/view/OutputView.kt new file mode 100644 index 000000000..7c8a121fd --- /dev/null +++ b/src/main/kotlin/baseball/view/OutputView.kt @@ -0,0 +1,15 @@ +package baseball.view + +import baseball.model.BallAndStrike + +class OutputView { + fun showStartPrompt() = println(START_PROMPT) + fun showSuccessPrompt() = println(SUCCESS_PROMPT) + + fun showTurnResult(ballAndStrike: BallAndStrike) = println(ballAndStrike) + + companion object { + const val START_PROMPT = "숫자 야구 게임을 시작합니다." + const val SUCCESS_PROMPT = "3개의 숫자를 모두 맞히셨습니다! 게임 종료" + } +} \ No newline at end of file diff --git a/src/test/kotlin/baseball/controller/NumbersConverterTest.kt b/src/test/kotlin/baseball/controller/NumbersConverterTest.kt new file mode 100644 index 000000000..4094c4b44 --- /dev/null +++ b/src/test/kotlin/baseball/controller/NumbersConverterTest.kt @@ -0,0 +1,64 @@ +package baseball.controller + +import baseball.model.BaseballNumber +import baseball.model.BaseballNumbers +import baseball.view.Command +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource +import java.util.stream.Stream + +class NumbersConverterTest { + + @ParameterizedTest + @CsvSource("123, 1, 2, 3", "324, 3, 2, 4", "281, 2, 8, 1") + fun `입력받은 수를 게임의 숫자 셋으로 변환한다`(inputNumber: Int, firstDigit: Int, secondDigit: Int, thirdDigit: Int) { + val expected = BaseballNumbers( + listOf(BaseballNumber(firstDigit), BaseballNumber(secondDigit), BaseballNumber(thirdDigit)) + ) + + val result = inputNumber.toBaseballNumbers() + assertThat(result).isEqualTo(expected) + } + + @ParameterizedTest + @ValueSource(ints = [-10, -9, -1, 0]) + fun `입력받은 수가 0 이하이면, 예외를 던진다`(inputNumber: Int) { + val exception = assertThrows { + inputNumber.toBaseballNumbers() + } + assertThat(exception.message).isEqualTo(INVALID_NUMBERS) + } + + @ParameterizedTest + @MethodSource("provideInputToCommand") + fun `입력받은 수를 커맨드로 변환한다`(inputNumber: Int, expectedCommand: Command) { + val result = inputNumber.toCommand() + assertThat(result).isEqualTo(expectedCommand) + } + + @ParameterizedTest + @ValueSource(ints = [0, 3, 4, -1, -2, 100]) + fun `입력받은 수가 커맨드가 아닌 수라면 예외를 던진다`(inputNumber: Int) { + val exception = assertThrows { + inputNumber.toCommand() + } + assertThat(exception.message).isEqualTo(INVALID_COMMAND) + } + + companion object { + @JvmStatic + fun provideInputToCommand(): Stream = Stream.of( + Arguments.of( + 1, Command.RESTART + ), + Arguments.of( + 2, Command.EXIT + ), + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/baseball/model/BaseballNumberTest.kt b/src/test/kotlin/baseball/model/BaseballNumberTest.kt new file mode 100644 index 000000000..f1f83cf0d --- /dev/null +++ b/src/test/kotlin/baseball/model/BaseballNumberTest.kt @@ -0,0 +1,27 @@ +package baseball.model + +import baseball.model.BaseballNumber.Companion.INVALID_NUMBER_RANGE +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class BaseballNumberTest { + @ParameterizedTest + @ValueSource(ints = [0, -1, 10, 100]) + fun `사용자가 입력한 숫자가 1~9 가 아니면 예외를 던진다`(inputNumber: Int) { + val exception = assertThrows { + BaseballNumber(inputNumber) + } + assertThat(exception.message).isEqualTo(INVALID_NUMBER_RANGE) + } + + @ParameterizedTest + @ValueSource(ints = [1, 2, 3, 4, 5, 6, 7, 8, 9]) + fun `사용자가 입력한 숫자가 1~9 가 이면 정상이다`(inputNumber: Int) { + assertDoesNotThrow { + BaseballNumber(inputNumber) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/baseball/model/BaseballNumbersTest.kt b/src/test/kotlin/baseball/model/BaseballNumbersTest.kt new file mode 100644 index 000000000..8080792c5 --- /dev/null +++ b/src/test/kotlin/baseball/model/BaseballNumbersTest.kt @@ -0,0 +1,63 @@ +package baseball.model + +import baseball.model.BaseballNumbers.Companion.INVALID_NUMBERS_SIZE +import baseball.model.BaseballNumbers.Companion.NUMBERS_DUPLICATED +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class BaseballNumbersTest { + @ParameterizedTest + @MethodSource("provideInvalidCountBaseballNumbers") + fun `게임에 사용되는 숫자 셋은 세자리가 아니면 예외를 던진다`(baseballNumbers: List) { + val exception = assertThrows { + BaseballNumbers(baseballNumbers) + } + assertThat(exception.message).isEqualTo(INVALID_NUMBERS_SIZE) + } + + @ParameterizedTest + @MethodSource("provideDuplicatedBaseballNumbers") + fun `게임에 사용되는 숫자 셋에는 중복이 있으면 예외를 던진다`(baseballNumbers: List) { + val exception = assertThrows { + BaseballNumbers(baseballNumbers) + } + assertThat(exception.message).isEqualTo(NUMBERS_DUPLICATED) + } + + @ParameterizedTest + @MethodSource("provideNormalBaseballNumbers") + fun `세자리 중복이 없는 숫자 셋의 경우 정상이다`(baseballNumbers: List) { + assertDoesNotThrow { + BaseballNumbers(baseballNumbers) + } + } + + companion object { + @JvmStatic + fun provideInvalidCountBaseballNumbers(): Stream> = Stream.of( + listOf(), + listOf(BaseballNumber(1)), + listOf(BaseballNumber(1), BaseballNumber(2)), + listOf(BaseballNumber(1), BaseballNumber(2), BaseballNumber(3), BaseballNumber(4)), + ) + + @JvmStatic + fun provideDuplicatedBaseballNumbers(): Stream> = Stream.of( + listOf(BaseballNumber(1), BaseballNumber(1), BaseballNumber(1)), + listOf(BaseballNumber(1), BaseballNumber(3), BaseballNumber(3)), + listOf(BaseballNumber(9), BaseballNumber(3), BaseballNumber(9)), + ) + + @JvmStatic + fun provideNormalBaseballNumbers(): Stream> = Stream.of( + listOf(BaseballNumber(1), BaseballNumber(2), BaseballNumber(3)), + listOf(BaseballNumber(1), BaseballNumber(3), BaseballNumber(7)), + listOf(BaseballNumber(9), BaseballNumber(3), BaseballNumber(1)), + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/baseball/model/RefereeTest.kt b/src/test/kotlin/baseball/model/RefereeTest.kt new file mode 100644 index 000000000..e352963c1 --- /dev/null +++ b/src/test/kotlin/baseball/model/RefereeTest.kt @@ -0,0 +1,47 @@ +package baseball.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + + +class RefereeTest { + + @ParameterizedTest + @CsvSource( + "1,2,3 , 4,5,6 , 0,0 ", + "1,2,3 , 1,2,3 , 3,0", + "1,2,3 , 3,1,2 , 0,3", + "1,2,3 , 1,3,2 , 1,2", + "4,6,2 , 2,7,9 , 0,1", + ) + fun `입력과 정답을 비교해서 결과를 알려준다`( + inputFirst: Int, inputSecond: Int, inputThird: Int, + answerFirst: Int, answerSecond: Int, answerThird: Int, + strikeCount: Int, ballCount: Int + ) { + // given + val inputNumbers = BaseballNumbers( + listOf( + BaseballNumber(inputFirst), + BaseballNumber(inputSecond), + BaseballNumber(inputThird) + ) + ) + val answer = BaseballNumbers( + listOf( + BaseballNumber(answerFirst), + BaseballNumber(answerSecond), + BaseballNumber(answerThird) + ) + ) + + // when + val result = Referee().compare(inputNumbers, answer) + val expected = BallAndStrike(strikeCount, ballCount) + + // then + assertThat(result).isEqualTo(expected) + + } +} \ No newline at end of file