diff --git a/docs/README.md b/docs/README.md index e69de29bb..1e3336c32 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,65 @@ +## 기능 목록(한 눈에 보기) + +``` +• 게임 시작 전 + ◦ 경주할 자동차 이름을 입력받는다. (1) - InputView#getUserInput() + ▫︎ 자동차 이름은 쉼표로 구분한다. - Util#seperatorNameByComma() + ▫︎ 자동차 이름은 5자 이하여야 하며, 넘어갈 시 애러를 발생시킨다. (3) - Validator#validateLength() + ▫︎ 자동자 이름에 중복이 있다면 에러를 발생시킨다. (3) - Validator#validateUnique() + ▫︎ 자동차 이름에 null값이 들어오면 에러를 발생시킨다. (3) - Validator#validateNotNull() + ◦ 게임 횟수를 입력받는다. (2) - InputView#getUserInputCount() + ▫︎ 입력이 숫자가 아닐 시 이러를 발생시킨다. (3) - Validator#validateInteger() + ▫︎ 입력의 범위를 벗어날 시 에러를 발생시킨다. (3) - Validator#validateRange() + +• 게임 중 + ◦ 0부터 9까지의 숫자를 랜덤하게 구한다. - MainController#getRandomNumber() + ▫︎ 구한 숫자가 4이상의 숫자인지 확인한다. - MainController#checkMovable() + ◦ 4이상의 숫자일 시 자동차를 전진한다. - Car#moveForward() + ▫︎ 자동차 전진은 '-'으로 표기한다. - OutputView#printDistance() + ▫︎ 단 첫번째 출력시에는 자동차 이름도 같이 표기한다. - OutputView#printCarName() + (ex. pobi : -) + ◦ 게임 횟수만큼 반복한다. - MainController#playOneCycle() + +• 게임 종료 + ◦ 최종 우승자 리스트를 가져온다. - Cars#getWinners() + ◦ 최종 우승자를 출력한다. (4) - OutputView#printWinners() + ▫︎ 게임 우승자는 여러명이 나올 수도 있으며, 여러명일 경우에는 쉼표로 구분하여 출력한다. + +(1) 랜덤값 추출은 camp.nextstep.edu.missionutils.Randoms.pickNumberInRange()을 이용한다. +(2) 사용자 입력은 camp.nextstep.edu.missionutils.Console.readLine()을 이용한다. +(3) 예외처리는 IllegalArgumentException을 발생시켜 애플리케이션을 종료시킨다. +(4) 게임 종료 시 System.exit()를 호출하지 않는다. +``` + +

+ +## 기능 목록 체크리스트 + +- 게임 시작 전 +- [x] 게임 시작 문구를 출력한다. +- [x] 자동차 경주 이름을 입력받는다. +- [x] 자동차 이름들을 구분한다. +- [x] 자동차 이름의 유효성을 검사한다. +- [x] 게임 횟수를 입력받는다. +- [x] 게임 횟수의 유효성을 검사한다. + +
+ +- 게임 시작 +- [x] 0부터 9까지의 숫자를 정한다. +- [x] 만약 정한 숫자가 4보다 크면 한칸 움직인다. +- [x] 처음에 입력 받는 횟수만큼 반복한다. + +
+ +- 게임 종료 +- [x] 최종 우승자를 출력한다. +- [x] 여러 명일 경우에는 쉼표로 구분하여 출력한다. + +

+ +## 프로그래밍 요구 사항 + +- [ ] indent(인덴트, 들여쓰기)는 depth를 3이 넘지 않도록 구현한다. +- [ ] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. +- [ ] JUnit 5와 AssertJ를 이용하여 기능 목록이 정상적으로 작동하는지 테스트 코드를 작성하여 확인한다. \ No newline at end of file diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index 0d8f3a79d..75f9c5607 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -1,5 +1,12 @@ package racingcar +import racingcar.controller.MainController +import racingcar.view.InputView +import racingcar.view.OutputView + fun main() { - // TODO: 프로그램 구현 + val outputView = OutputView() + val inputView = InputView() + val controller = MainController(inputView, outputView) + controller.run() } diff --git a/src/main/kotlin/racingcar/controller/MainController.kt b/src/main/kotlin/racingcar/controller/MainController.kt new file mode 100644 index 000000000..803ee9c6f --- /dev/null +++ b/src/main/kotlin/racingcar/controller/MainController.kt @@ -0,0 +1,54 @@ +package racingcar.controller + +import racingcar.view.InputView +import racingcar.view.OutputView +import racingcar.model.Cars +import racingcar.util.Util.getRandomNumber +import racingcar.util.Util.isMovable + +class MainController(private val inputView: InputView, private val outputView: OutputView) { + private val cars = Cars() + private var gameCount = 0 + + fun run() { + gameInit() + gameStart() + gameEnd() + } + + private fun gameStart() { + repeat(gameCount) { + playOneCycle() + } + } + + private fun gameInit() { + inputCarNames() + inputGameCount() + } + + private fun playOneCycle() { + cars.carNames.forEach { car -> + val isMovable = isMovable(getRandomNumber()) + car.isMove(isMovable) + outputView.printCarDistance(car) + } + println() + } + + private fun gameEnd() { + val winners = cars.getWinners() + outputView.printWinners(winners) + } + + private fun inputCarNames() { + outputView.printInputCarNameMessage() + val carNames = inputView.getInputCarNames() + cars.addAllList(carNames) + } + + private fun inputGameCount() { + outputView.printInputCountMessage() + gameCount = inputView.getInputCount() + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/model/Car.kt b/src/main/kotlin/racingcar/model/Car.kt new file mode 100644 index 000000000..fefc622cd --- /dev/null +++ b/src/main/kotlin/racingcar/model/Car.kt @@ -0,0 +1,17 @@ +package racingcar.model + +class Car(private val _name: String) { + + val name: String + get() = _name + + private var _distance = 0 + val distance: Int + get() = _distance + + fun isMove(movable: Boolean) { + if (movable) _distance++ + } + + fun isWinner(maxValue: Int): Boolean = _distance == maxValue +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/model/Cars.kt b/src/main/kotlin/racingcar/model/Cars.kt new file mode 100644 index 000000000..24df1eee7 --- /dev/null +++ b/src/main/kotlin/racingcar/model/Cars.kt @@ -0,0 +1,16 @@ +package racingcar.model + +class Cars { + private var _carNames = mutableListOf() + val carNames: List + get() = _carNames + + fun addAllList(carNames: List) = _carNames.addAll(carNames.map { Car(it) }) + + fun getMaxDistance(): Int = _carNames.maxOf { it.distance } + + fun getWinners(): List { + val maxDistance = getMaxDistance() + return _carNames.filter { it.isWinner(maxDistance) }.map { it.name } + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/util/Constants.kt b/src/main/kotlin/racingcar/util/Constants.kt new file mode 100644 index 000000000..d9914ce6f --- /dev/null +++ b/src/main/kotlin/racingcar/util/Constants.kt @@ -0,0 +1,9 @@ +package racingcar.util + +object Constants { + const val MAX_NAME_LENGTH = 5 + const val START_NUMBER = 0 + const val END_NUMBER = 9 + const val MIN_COUNT_NUMBER = 1 + const val MOVABLE_NUMBER = 4 +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/util/ExceptionMessage.kt b/src/main/kotlin/racingcar/util/ExceptionMessage.kt new file mode 100644 index 000000000..8f6114f6e --- /dev/null +++ b/src/main/kotlin/racingcar/util/ExceptionMessage.kt @@ -0,0 +1,11 @@ +package racingcar.util + +enum class ExceptionMessage(private val message: String) { + INVALID_LENGTH("자동차의 이름이 5보다 깁니다."), + INVALID_UNIQUE_NAME("중복된 자동차의 이름이 존재합니다."), + INVALID_CAR_NAME("자동차 이름에 널값이 존재합니다."), + INVALID_INTEGER("사용자의 입력이 정수가 아닙니다."), + INVALID_RANGE("사용자의 입력이 유효한 범위가 아닙니다."); + + fun getMessage(): String = message +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/util/Util.kt b/src/main/kotlin/racingcar/util/Util.kt new file mode 100644 index 000000000..d056893d3 --- /dev/null +++ b/src/main/kotlin/racingcar/util/Util.kt @@ -0,0 +1,11 @@ +package racingcar.util + +import camp.nextstep.edu.missionutils.Randoms + +object Util { + fun separateNameByComma(carNames: String): List = carNames.split(",") + + fun getRandomNumber(): Int = Randoms.pickNumberInRange(Constants.START_NUMBER, Constants.END_NUMBER) + + fun isMovable(number: Int): Boolean = number >= Constants.MOVABLE_NUMBER +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/util/Validator.kt b/src/main/kotlin/racingcar/util/Validator.kt new file mode 100644 index 000000000..be6cff345 --- /dev/null +++ b/src/main/kotlin/racingcar/util/Validator.kt @@ -0,0 +1,33 @@ +package racingcar.util + +import racingcar.util.Constants.MAX_NAME_LENGTH +import racingcar.util.Constants.MIN_COUNT_NUMBER +import java.lang.IllegalArgumentException + +object Validator { + fun validateLength(numberList: List) { + numberList.forEach { + if (it.length > MAX_NAME_LENGTH) throw IllegalArgumentException(ExceptionMessage.INVALID_LENGTH.getMessage()) + } + } + + fun validateUnique(numberList: List) { + val validation = numberList.toSet() + if (validation.size != numberList.size) throw IllegalArgumentException(ExceptionMessage.INVALID_UNIQUE_NAME.getMessage()) + } + + fun validateNotNull(numberList: List) { + numberList.forEach { + if (it.trim().isEmpty()) throw IllegalArgumentException(ExceptionMessage.INVALID_CAR_NAME.getMessage()) + } + } + + fun validateInteger(number: String) { + number.toIntOrNull() ?: throw IllegalArgumentException(ExceptionMessage.INVALID_INTEGER.getMessage()) + } + + fun validateRange(number: String) { + val validation = number.toInt() + if (validation !in MIN_COUNT_NUMBER..Int.MAX_VALUE) throw IllegalArgumentException(ExceptionMessage.INVALID_RANGE.getMessage()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/view/InputView.kt b/src/main/kotlin/racingcar/view/InputView.kt new file mode 100644 index 000000000..d9b72e3b8 --- /dev/null +++ b/src/main/kotlin/racingcar/view/InputView.kt @@ -0,0 +1,28 @@ +package racingcar.view + +import camp.nextstep.edu.missionutils.Console +import racingcar.util.Util.separateNameByComma +import racingcar.util.Validator.validateInteger +import racingcar.util.Validator.validateLength +import racingcar.util.Validator.validateNotNull +import racingcar.util.Validator.validateRange +import racingcar.util.Validator.validateUnique + +class InputView { + private fun getUserInput(): String = Console.readLine() + + fun getInputCarNames(): List { + val input = separateNameByComma(getUserInput()) + validateLength(input) + validateUnique(input) + validateNotNull(input) + return input + } + + fun getInputCount(): Int { + val input = getUserInput() + validateInteger(input) + validateRange(input) + return input.toInt() + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/view/OutputView.kt b/src/main/kotlin/racingcar/view/OutputView.kt new file mode 100644 index 000000000..2b7ba7aa7 --- /dev/null +++ b/src/main/kotlin/racingcar/view/OutputView.kt @@ -0,0 +1,22 @@ +package racingcar.view + +import racingcar.model.Car + +class OutputView { + fun printInputCarNameMessage() { + println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)") + } + + fun printInputCountMessage() { + println("시도할 횟수는 몇 회인가요?") + } + + fun printCarDistance(car: Car) { + println("${car.name} : ${"-".repeat(car.distance)}") + } + + fun printWinners(winners: List) { + val output = winners.joinToString(", ") + println("최종 우승자 : $output") + } +} \ No newline at end of file diff --git a/src/test/kotlin/racingcar/ApplicationTest.kt b/src/test/kotlin/racingcar/ApplicationTest.kt index 2cb36835c..bdc896bdf 100644 --- a/src/test/kotlin/racingcar/ApplicationTest.kt +++ b/src/test/kotlin/racingcar/ApplicationTest.kt @@ -3,11 +3,27 @@ package racingcar import camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest import camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest import camp.nextstep.edu.missionutils.test.NsTest +import racingcar.model.Car +import racingcar.model.Cars import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import racingcar.controller.MainController +import racingcar.util.Util.isMovable +import racingcar.util.Util.separateNameByComma +import racingcar.util.Validator.validateInteger +import racingcar.util.Validator.validateLength +import racingcar.util.Validator.validateNotNull +import racingcar.util.Validator.validateRange +import racingcar.util.Validator.validateUnique +import racingcar.view.InputView +import racingcar.view.OutputView class ApplicationTest : NsTest() { + private val inputView = InputView() + private val outputView = OutputView() + private val controller = MainController(inputView, outputView) + @Test fun `전진 정지`() { assertRandomNumberInRangeTest( @@ -26,6 +42,123 @@ class ApplicationTest : NsTest() { } } + @Test + fun `컴마로 구분하여 리스트 반환 검증`() { + val input = "lh99j,pobi" + val validation = separateNameByComma(input) + val result = listOf("lh99j", "pobi") + assertThat(validation).isEqualTo(result) + } + + @Test + fun `자동차 이름 길이 검증`() { + assertSimpleTest { + assertThrows { validateLength(listOf("pobi", "javaji")) } + assertThrows { validateLength(listOf("abcdef, abcdefg")) } + + } + } + + @Test + fun `자동차 이름 중복 검증`() { + assertSimpleTest { + assertThrows { validateUnique(listOf("pobi", "javaji", "pobi")) } + assertThrows { validateUnique(listOf("a", "b", "c", "a")) } + + } + } + + @Test + fun `자동차 이름 널값 검증`() { + assertSimpleTest { + assertThrows { validateNotNull(listOf("pobi", "javaji", " ")) } + assertThrows { validateNotNull(listOf("a", "b", "c", "")) } + assertThrows { validateNotNull(listOf("a", "b", " ")) } + } + } + + @Test + fun `사용자 입력 정수 검증`() { + assertSimpleTest { + assertThrows { validateInteger("a") } + assertThrows { validateInteger("1a") } + assertThrows { validateInteger("4_@") } + } + } + + @Test + fun `사용자 입력 유효 범위 검증`() { + assertSimpleTest { + assertThrows { validateRange((Int.MAX_VALUE.toLong() + 1).toString()) } + assertThrows { validateRange("-1") } + assertThrows { validateRange("0") } + } + } + + @Test + fun `자동차가 움직였는지 검증`() { + val car = Car("lh99j") + car.isMove(true) + val validation = car.distance + val result = 1 + assertThat(validation).isEqualTo(result) + } + + @Test + fun `움직일 수 있는지 반환 기능 검증`() { + val falseInput = 3 + val falseValidation = isMovable(falseInput) + val falseResult = false + assertThat(falseValidation).isEqualTo(falseResult) + + val trueInput = 4 + val trueValidation = isMovable(trueInput) + val trueResult = true + assertThat(trueValidation).isEqualTo(trueResult) + } + + @Test + fun `최대 거리 반환 기능 검증`() { + val input = listOf("pobi", "lh99j", "anjji", "amin") + val cars = Cars() + cars.addAllList(input) + cars.carNames[1].isMove(true) + cars.carNames[1].isMove(true) + cars.carNames[1].isMove(true) + cars.carNames[1].isMove(true) + val validation = cars.getMaxDistance() + val result = 4 + assertThat(validation).isEqualTo(result) + } + + @Test + fun `우승자 확인 기능 검증`() { + val input = listOf(Car("pobi"), Car("lh99j"), Car("abc")) + input[1].isMove(true) + input[1].isMove(true) + + val falseValidation = input[0].isWinner(2) + val falseResult = false + assertThat(falseValidation).isEqualTo(falseResult) + + val trueValidation = input[1].isWinner(2) + val trueResult = true + assertThat(trueValidation).isEqualTo(trueResult) + } + + @Test + fun `우승자 반환 기능 검증`() { + val input = listOf("pobi", "lh99j", "abcd") + val cars = Cars() + cars.addAllList(input) + cars.carNames[0].isMove(true) + cars.carNames[1].isMove(true) + + val validation = cars.getWinners() + val result = listOf("pobi", "lh99j") + assertThat(validation).isEqualTo(result) + } + public override fun runMain() { main() }