diff --git a/.gitignore b/.gitignore index 5dca701a..e297eab2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ out/ ### Mac OS ### .DS_Store + +### Local configuration file ### +local.properties \ No newline at end of file diff --git a/README.md b/README.md index 62fa463c..c81ad9e0 100644 --- a/README.md +++ b/README.md @@ -1 +1,95 @@ -# kotlin-racingcar-precourse +# 자동차 경주 +## 🚀 기능 요구 사항 +초간단 자동차 경주 게임을 구현한다. + +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. +- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. +- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. + +### 입출력 요구 사항 +#### 입력 +- 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) +``` +pobi,woni,jun +``` +- 시도할 횟수 +``` +5 +``` + +#### 출력 +- 차수별 실행 결과 +``` +pobi : -- +woni : ---- +jun : --- +``` +- 단독 우승자 안내 문구 +``` +최종 우승자 : pobi +``` +- 공동 우승자 안내 문구 +``` +최종 우승자 : pobi, jun +``` + +#### 실행 결과 예시 +``` +경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) +pobi,woni,jun +시도할 횟수는 몇 회인가요? +5 + +실행 결과 +pobi : - +woni : +jun : - + +pobi : -- +woni : - +jun : -- + +pobi : --- +woni : -- +jun : --- + +pobi : ---- +woni : --- +jun : ---- + +pobi : ----- +woni : ---- +jun : ----- + +최종 우승자 : pobi, jun +``` + +--- +## ✏️ 기능 목록 +- **사용자 입력** + - [x] 경주할 자동차 이름을 입력받는다. (이름은 쉼표(,) 기준으로 구분) + - [x] 시도할 횟수를 입력받는다. +- **자동차 경주** + - [x] 자동차 별로 0에서 9 사이의 무작위 값을 구한다. + - [x] 무작위 값이 4 이상인 경우 전진한다. + - [x] 차수별 실행 결과를 출력한다. +- **우승자 출력** + - [x] 자동차 경주 게임을 완료한 후 최종 우승자를 출력한다. + - [x] 우승자가 여러 명인 경우 쉼표(,)를 이용하여 구분한다. +- **예외 발생** + - [x] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 애플리케이션을 종료한다. + +## 🚨 예외 상황 +- **자동차 이름** + - 빈 문자열인 경우 + - 5자 이하가 아닌 경우 + - 이름이 중복된 경우 +- **시도할 횟수** + - 숫자 아닌 값을 입력한 경우 + - 0 또는 음수인 경우 \ No newline at end of file diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index 0d8f3a79..79f6531c 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -1,5 +1,8 @@ package racingcar +import racingcar.controller.RacingCarController + fun main() { - // TODO: 프로그램 구현 + val racingCarController = RacingCarController() + racingCarController.start() } diff --git a/src/main/kotlin/racingcar/controller/RacingCarController.kt b/src/main/kotlin/racingcar/controller/RacingCarController.kt new file mode 100644 index 00000000..d85209d4 --- /dev/null +++ b/src/main/kotlin/racingcar/controller/RacingCarController.kt @@ -0,0 +1,13 @@ +package racingcar.controller + +import racingcar.domain.RacingGame +import racingcar.view.InputView + +class RacingCarController { + fun start() { + val carNames: List = InputView.inputCarNames() + val gameRound: Int = InputView.inputGameRound() + val racingGame = RacingGame(carNames, gameRound) + racingGame.play() + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/domain/RacingCar.kt b/src/main/kotlin/racingcar/domain/RacingCar.kt new file mode 100644 index 00000000..ec9f27d2 --- /dev/null +++ b/src/main/kotlin/racingcar/domain/RacingCar.kt @@ -0,0 +1,28 @@ +package racingcar.domain + +import camp.nextstep.edu.missionutils.Randoms +import racingcar.view.OutputView + +class RacingCar( + val name: String, + var position: Int = 0 +) { + fun play() { + val randomNumber = getRandomNumber() + moveForward(randomNumber) + } + + private fun getRandomNumber(): Int { + return Randoms.pickNumberInRange(0, 9) + } + + fun moveForward(number: Int) { + if (number >= 4) { + position++ + } + } + + fun printCarPosition() { + OutputView.printCarPosition(name, position) + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/domain/RacingGame.kt b/src/main/kotlin/racingcar/domain/RacingGame.kt new file mode 100644 index 00000000..b740c8dc --- /dev/null +++ b/src/main/kotlin/racingcar/domain/RacingGame.kt @@ -0,0 +1,41 @@ +package racingcar.domain + +import racingcar.view.OutputView + +class RacingGame( + private val carNames: List, + private val gameRound: Int +) { + private var racingCars: List? = null + + init { + setRacingCars() + } + + private fun setRacingCars() { + racingCars = carNames.map { RacingCar(it) } + } + + fun play() { + OutputView.printExecutionResult() + for (i in 1..gameRound) { + playRound() + } + OutputView.printWinners(getWinners()) + } + + private fun playRound() { + racingCars?.forEach { it.play() } + printRoundResult() + } + + private fun printRoundResult() { + racingCars?.forEach { it.printCarPosition() } + println() + } + + private fun getWinners(): List { + val maxPosition = racingCars?.maxOf { it.position } + return racingCars?.filter { it.position == maxPosition }?.map { it.name } ?: emptyList() + } +} \ 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 00000000..8fe80617 --- /dev/null +++ b/src/main/kotlin/racingcar/view/InputView.kt @@ -0,0 +1,45 @@ +package racingcar.view + +import camp.nextstep.edu.missionutils.Console + +object InputView { + fun inputCarNames(): List { + println(INPUT_CAR_NAMES_MESSAGE) + val carNames = Console.readLine().split(CAR_NAME_DELIMITER) + validateCarNames(carNames) + return carNames + } + + fun inputGameRound(): Int { + println(INPUT_GAME_ROUND_MESSAGE) + val gameRound = Console.readLine().toIntOrNull() ?: throw IllegalArgumentException(ERROR_INVALID_GAME_ROUND) + validateGameRound(gameRound) + return gameRound + } + + fun validateCarNames(carNames: List) { + val carNamesSet = mutableSetOf() + for (name in carNames) { + if (name.isBlank() || name.length > 5) { + throw IllegalArgumentException(ERROR_INVALID_CAR_NAME) + } + if (!carNamesSet.add(name)) { + throw IllegalArgumentException(ERROR_DUPLICATE_CAR_NAME) + } + } + } + + fun validateGameRound(gameRound: Int) { + if (gameRound <= 0) { + throw IllegalArgumentException(ERROR_INVALID_GAME_ROUND) + } + } + + private const val INPUT_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" + private const val INPUT_GAME_ROUND_MESSAGE = "시도할 횟수는 몇 회인가요?" + private const val ERROR_INVALID_CAR_NAME = "[ERROR] 자동차 이름은 5자 이하여야 합니다." + private const val ERROR_DUPLICATE_CAR_NAME = "[ERROR] 자동차 이름은 중복될 수 없습니다." + private const val ERROR_INVALID_GAME_ROUND = "[ERROR] 0 이상의 정수값을 입력해야 합니다." + + private const val CAR_NAME_DELIMITER = ',' +} \ 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 00000000..90a89a63 --- /dev/null +++ b/src/main/kotlin/racingcar/view/OutputView.kt @@ -0,0 +1,22 @@ +package racingcar.view + +object OutputView { + fun printExecutionResult() { + println(OUTPUT_EXECUTION_RESULT_MESSAGE) + } + + fun printCarPosition(name: String, position: Int) { + println(name + RESULT_DELIMITER + CAR_POSITION_SYMBOL.repeat(position)) + } + + fun printWinners(winners: List) { + println(OUTPUT_WINNER_MESSAGE + RESULT_DELIMITER + winners.joinToString(WINNER_DELIMITER)) + } + + private const val OUTPUT_EXECUTION_RESULT_MESSAGE = "실행 결과" + private const val OUTPUT_WINNER_MESSAGE = "최종 우승자" + + private const val CAR_POSITION_SYMBOL = "-" + private const val RESULT_DELIMITER = " : " + private const val WINNER_DELIMITER = ", " +} \ No newline at end of file diff --git a/src/test/kotlin/racingcar/InputTest.kt b/src/test/kotlin/racingcar/InputTest.kt new file mode 100644 index 00000000..fe798fe6 --- /dev/null +++ b/src/test/kotlin/racingcar/InputTest.kt @@ -0,0 +1,54 @@ +package racingcar + +import org.junit.jupiter.api.Test +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 +import racingcar.view.InputView + +class InputTest { + @Test + fun `자동자 이름 입력 확인`() { + assertDoesNotThrow { + InputView.validateCarNames(listOf("pobi", "woni", "jun")) + } + } + + @Test + fun `자동차 이름이 빈 문자열인 경우 예외 발생`() { + assertThrows { + InputView.validateCarNames(listOf("", "car")) + } + } + + @Test + fun `자동차 이름이 5자 이하가 아닌 경우 예외 발생`() { + assertThrows { + InputView.validateCarNames(listOf("car", "carName")) + } + } + + @Test + fun `자동차 이름이 중복된 경우 예외 발생`() { + assertThrows { + InputView.validateCarNames(listOf("car1", "car2", "car1")) + } + } + + @ParameterizedTest + @ValueSource(ints = [1, 2, 3, 4, 10]) + fun `시도할 횟수 입력 확인`(input: Int) { + assertDoesNotThrow { + InputView.validateGameRound(input) + } + } + + @ParameterizedTest + @ValueSource(ints = [0, -1]) + fun `시도할 횟수가 0 또는 음수인 경우 예외 발생`(input: Int) { + assertThrows { + InputView.validateGameRound(input) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/racingcar/RacingCarTest.kt b/src/test/kotlin/racingcar/RacingCarTest.kt new file mode 100644 index 00000000..19b86e9e --- /dev/null +++ b/src/test/kotlin/racingcar/RacingCarTest.kt @@ -0,0 +1,29 @@ +package racingcar + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import racingcar.domain.RacingCar + +class RacingCarTest { + private lateinit var car: RacingCar + + @BeforeEach + fun init() { + car = RacingCar("name") + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3]) + fun `숫자가 4 미만인 경우 전진하지 않는지 확인`(input: Int) { + car.moveForward(input) + assert(car.position == 0) + } + + @ParameterizedTest + @ValueSource(ints = [4, 5, 6, 7, 8, 9]) + fun `숫자가 4 이상인 경우 전진하는지 확인`(input: Int) { + car.moveForward(input) + assert(car.position == 1) + } +} \ No newline at end of file