diff --git a/README.md b/README.md index 62fa463c..1a914dee 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ # kotlin-racingcar-precourse +## 기능 요구 사항 +### 사용자 입력 +- [ ] `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 이용한다. +- [ ] 자동차 이름을 입력 받는다. +- [ ] 자동차 이름은 쉼표를 기준으로 구분한다. +- [ ] 이름은 1 ~ 5자이다. +- [ ] 횟수를 입력받는다. + +### 출력 +- [ ] 이름 입력 요청 시, '경주할 자동차 이름을 입력하세요.' 문구를 출력한다. +- [ ] 횟수 입력 요청 시, '시도할 횟수는 몇 회인가요?' 문구를 출력한다. +- [ ] 실행 결과 출력 시, '실행 결과' 문구를 출력한다. +- [ ] 매 라운드 실행 결과를 출력한다. +- [ ] 최종 우승자를 출력 시, '최종 우승자 : ${user}'를 출력한다. +- [ ] 우승자가 여러명이라면, 쉼표로 구분하여 출력한다. + + +### 자동차: Car +- [ ] 자동차 객체는 `name(이름)`, `moving(이동 칸)`을 변수로 가지고 있다. + +### 자동차 이동: Car - move() +- [ ] 4 이상이면 한 칸 이동한다. + +### 자동차 경주: Race +- [ ] 참여하는 자동차의 정보 `cars: List`를 변수로 갖는다. + +### 우승자 결정: Race - getWinner() +- [ ] 횟수동안 가장 많이 이동한 자동차가 우승한다. +- [ ] 우승자는 여러명일 수 있다. + + +### 자동차 생성: CarController - makeCar() +- [ ] 사용자에게 이름을 입력받으면 자동차를 하나씩 생성하고, `cars`로 만든다. + +### 자동차 이동: CarController - moveCar() +- [ ] `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`로 나온 값을 Car-move()로 호출한다. + + +### 테스트 +- [ ] 자동차 이름이 입력되지 않으면 오류가 발생한다. +- [ ] 자동차 이름이 6자 이상이면 오류가 발생한다. +- [ ] 횟수는 숫자만 가능하다. +- [ ] 횟수가 입력되지 않으면 오류가 발생한다. +- [ ] 우승자가 한 명일 경우 +- [ ] 우승자가 여러명일 경우 +- [ ] 4 이상이면 한 칸 이동한다. \ No newline at end of file diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index 0d8f3a79..aae5dec0 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -1,5 +1,13 @@ package racingcar +import racingcar.controller.RaceController +import racingcar.view.Input +import racingcar.view.Output + fun main() { - // TODO: 프로그램 구현 -} + val input = Input() + val output = Output() + + val raceController = RaceController(input.getCarNames(), input.getRaceRound(), output) + raceController.start() +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/constant/Error.kt b/src/main/kotlin/racingcar/constant/Error.kt new file mode 100644 index 00000000..11dc5704 --- /dev/null +++ b/src/main/kotlin/racingcar/constant/Error.kt @@ -0,0 +1,8 @@ +package racingcar.constant + +object Error { + val CAR_NAME_RANGE = 1..5 + val NOT_VALID_CAR_NAME_LENGTH = + "자동차 이름은 ${CAR_NAME_RANGE.first} 이상 ${CAR_NAME_RANGE.last} 이하로 입력해주세요. %s의 이름은 %d자 입니다." + const val NOT_VALID_ROUND_TYPE = "경주 횟수는 숫자만 입력 가능합니다. %s은 숫자가 아닙니다." +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/constant/Message.kt b/src/main/kotlin/racingcar/constant/Message.kt new file mode 100644 index 00000000..7526065a --- /dev/null +++ b/src/main/kotlin/racingcar/constant/Message.kt @@ -0,0 +1,18 @@ +package racingcar.constant + +object Message { + const val INFO_GET_CAR_NAMES = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" + const val INFO_GET_ROUND = "시도할 횟수는 몇 회인가요?" + const val INFO_ROUND_RESULT = "실행 결과" + + const val COMMA = "," + + val RANDOM_VALUE_RANGE = 0..9 + const val CAR_MOVING_MIN_NUMBER = 4 + + const val ROUND_RESULT_FORMAT = "%s : " + const val MOVING_SYMBOL = "-" + + const val RACE_RESULT_FORMAT = "최종 우승자 : %s" + const val RACE_WINNER_DELIMITER = ", " +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/controller/RaceController.kt b/src/main/kotlin/racingcar/controller/RaceController.kt new file mode 100644 index 00000000..00fb7a02 --- /dev/null +++ b/src/main/kotlin/racingcar/controller/RaceController.kt @@ -0,0 +1,32 @@ +package racingcar.controller + +import camp.nextstep.edu.missionutils.Randoms.pickNumberInRange +import racingcar.constant.Message.RANDOM_VALUE_RANGE +import racingcar.model.Car +import racingcar.model.Race +import racingcar.view.Output + +class RaceController(carNames: List, private val round: Int, private val output: Output) { + private val race = Race() + + init { + race.addCar(carNames) + } + + private fun moveCar() = + race.cars.forEach { it.move(pickNumberInRange(RANDOM_VALUE_RANGE.first, RANDOM_VALUE_RANGE.last)) } + + private fun getWinner(): List { + val maxMoving = race.maxMoving() + return race.cars.filter { car -> car.moving == maxMoving } + } + + fun start() { + repeat(round) { + moveCar() + output.showRoundResult(race.cars) + } + + output.showRaceResult(getWinner()) + } +} \ 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 00000000..9a28104e --- /dev/null +++ b/src/main/kotlin/racingcar/model/Car.kt @@ -0,0 +1,12 @@ +package racingcar.model + +import racingcar.constant.Message.CAR_MOVING_MIN_NUMBER + +data class Car( + val name: String, + var moving: Int = 0 +) { + fun move(value: Int) { + if (value >= CAR_MOVING_MIN_NUMBER) moving++ + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/model/Race.kt b/src/main/kotlin/racingcar/model/Race.kt new file mode 100644 index 00000000..80af8e4b --- /dev/null +++ b/src/main/kotlin/racingcar/model/Race.kt @@ -0,0 +1,8 @@ +package racingcar.model + +data class Race( + val cars: MutableList = mutableListOf() +) { + fun addCar(carNames: List) = carNames.forEach { name -> cars.add(Car(name)) } + fun maxMoving() = cars.maxOf { it.moving } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/validation/InputValidation.kt b/src/main/kotlin/racingcar/validation/InputValidation.kt new file mode 100644 index 00000000..60966a2c --- /dev/null +++ b/src/main/kotlin/racingcar/validation/InputValidation.kt @@ -0,0 +1,13 @@ +package racingcar.validation + +import racingcar.constant.Error.CAR_NAME_RANGE +import racingcar.constant.Error.NOT_VALID_CAR_NAME_LENGTH +import racingcar.constant.Error.NOT_VALID_ROUND_TYPE + +class InputValidation { + fun carName(name: String) = + require(name.length in CAR_NAME_RANGE) { NOT_VALID_CAR_NAME_LENGTH.format(name, name.length) } + + fun raceRound(round: String) = + round.toIntOrNull() ?: throw IllegalArgumentException(NOT_VALID_ROUND_TYPE.format(round)) +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/view/Input.kt b/src/main/kotlin/racingcar/view/Input.kt new file mode 100644 index 00000000..7fb316e1 --- /dev/null +++ b/src/main/kotlin/racingcar/view/Input.kt @@ -0,0 +1,28 @@ +package racingcar.view + +import camp.nextstep.edu.missionutils.Console +import racingcar.constant.Message.COMMA +import racingcar.constant.Message.INFO_GET_CAR_NAMES +import racingcar.constant.Message.INFO_GET_ROUND +import racingcar.validation.InputValidation + +class Input { + private val inputValidation = InputValidation() + + fun getCarNames(): List { + println(INFO_GET_CAR_NAMES) + return readLine() + .split(COMMA) + .map { + inputValidation.carName(it) + it.trim() + } + } + + fun getRaceRound(): Int { + println(INFO_GET_ROUND) + return readLine().also { inputValidation.raceRound(it) }.toInt() + } + + private fun readLine(): String = Console.readLine() +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/view/Output.kt b/src/main/kotlin/racingcar/view/Output.kt new file mode 100644 index 00000000..8a1afe8f --- /dev/null +++ b/src/main/kotlin/racingcar/view/Output.kt @@ -0,0 +1,26 @@ +package racingcar.view + +import racingcar.constant.Message.INFO_ROUND_RESULT +import racingcar.constant.Message.MOVING_SYMBOL +import racingcar.constant.Message.RACE_RESULT_FORMAT +import racingcar.constant.Message.RACE_WINNER_DELIMITER +import racingcar.constant.Message.ROUND_RESULT_FORMAT +import racingcar.model.Car + +class Output { + fun showRoundResult(cars: List) { + println(INFO_ROUND_RESULT) + + cars.forEach { car -> + print(ROUND_RESULT_FORMAT.format(car.name)) + repeat(car.moving) { + print(MOVING_SYMBOL) + } + println() + } + println() + } + + fun showRaceResult(cars: List) = + println(RACE_RESULT_FORMAT.format(cars.joinToString(RACE_WINNER_DELIMITER) { it.name })) +} \ No newline at end of file diff --git a/src/test/kotlin/racingcar/ApplicationTest.kt b/src/test/kotlin/racingcar/ApplicationTest.kt index 3c601c8e..f5463284 100644 --- a/src/test/kotlin/racingcar/ApplicationTest.kt +++ b/src/test/kotlin/racingcar/ApplicationTest.kt @@ -4,11 +4,13 @@ import camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeT import camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest import camp.nextstep.edu.missionutils.test.NsTest import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class ApplicationTest : NsTest() { @Test + @DisplayName("우승자가 한 명일 경우") fun `기능 테스트`() { assertRandomNumberInRangeTest( { @@ -20,12 +22,41 @@ class ApplicationTest : NsTest() { } @Test + @DisplayName("우승자가 여러 명일 경우") + fun returnMultipleWinnersWhenSameScore() { + assertRandomNumberInRangeTest( + { + run("pobi,woni", "1") + assertThat(output()).contains("pobi : -", "woni : -", "최종 우승자 : pobi, woni") + }, + MOVING_FORWARD, MOVING_FORWARD + ) + } + + @Test + @DisplayName("자동차 이름이 입력되어야 한다.") + fun throwExceptionWhenCarNameIsEmpty() { + assertSimpleTest { + assertThrows { runException("", "1") } + } + } + + @Test + @DisplayName("자동차 이름이 6자 이상이면 예외가 발생해야 한다.") fun `예외 테스트`() { assertSimpleTest { assertThrows { runException("pobi,javaji", "1") } } } + @Test + @DisplayName("레이싱 횟수는 숫자만 가능하다.") + fun throwExceptionWhenRoundIsNotNumber() { + assertSimpleTest { + assertThrows { runException("pobi,woni", "a") } + } + } + override fun runMain() { main() }