diff --git a/docs/README.md b/docs/README.md index e69de29bb..23bce33ea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,28 @@ +# 기능 목록 + +### 입력 + +- 경주할 자동차 이름을 입력하는 기능 +- 몇 번의 이동을 할 지 입력하는 기능 + +### 출력 + +- `“경주할 자동차 이름을 입력하세요."`를 출력하는 기능 +- `“시도할 횟수는 몇 회인가요?”`를 출력하는 기능 +- `“최종 우승자 : "`를 출력하는 기능 +- 각 차수별 실행 결과를 출력하는 기능 + +### 경주 + +- 무작위 수를 생성하는 기능 +- 자동차가 전진하는지 정지하는지 판단하는 기능 (무작위 수가 4 이상, 미만) +- 우승자를 판별하는 기능 +- 각 자동차마다 움직인 거리 저장 기능 + +### 검사 및 예외처리 + +- 자동차 이름이 올바른 값인 지 검사하는 기능 + - 올바른값: Null X, 5자리 이하, 중복 X +- 사용자가 몇 번 이동할 지 입력 할 때 숫자를 올바른 값을 입력하는 지 검사하는 기능 + - 올바른값: Null X, 0 보다 큰 정수 +- 사용자 입력 값에 대해 올바르지 않을 경우 `IllegalArgumentException` 를 발생시키는 기능 diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index 0d8f3a79d..563dafc79 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -1,5 +1,10 @@ package racingcar +import racingcar.controller.RacingController + + fun main() { - // TODO: 프로그램 구현 + val racingController = RacingController() + racingController.run() } + diff --git a/src/main/kotlin/racingcar/controller/RacingController.kt b/src/main/kotlin/racingcar/controller/RacingController.kt new file mode 100644 index 000000000..d8ea2ad2c --- /dev/null +++ b/src/main/kotlin/racingcar/controller/RacingController.kt @@ -0,0 +1,41 @@ +package racingcar.controller + +import racingcar.model.AttemptsNumber +import racingcar.model.Car +import racingcar.model.Cars +import racingcar.model.Racing +import racingcar.util.Constants +import racingcar.view.InputView +import racingcar.view.OutputView + +class RacingController() { + private var attemptsNum = 0 + private lateinit var carList: List + private val inputView = InputView() + private val outputView = OutputView() + + fun run() { + printStartAndGetCarList() + requestAttemptsNumber() + doRacingAndPrintResult() + } + + private fun doRacingAndPrintResult() { + val racing = Racing(carList) + repeat(attemptsNum) { racing.runRaceOnce().apply { outputView.printMatchProgress(carList) } } + racing.getWinner() + outputView.printWinner(racing.winner) + } + + private fun requestAttemptsNumber() { + outputView.requestAttemptsNumber(Constants.NUMBER_ATTEMPTS_MSG) + val attemptsNumber = AttemptsNumber(inputView.getAttemptsNumber()) + attemptsNum = attemptsNumber.validAttemptsNum + } + + private fun printStartAndGetCarList() { + outputView.printRaceStart(Constants.RACE_START_MSG) + val cars = Cars(inputView.getCarList()) + carList = cars.carList + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/model/AttemptsNumber.kt b/src/main/kotlin/racingcar/model/AttemptsNumber.kt new file mode 100644 index 000000000..064c01f3a --- /dev/null +++ b/src/main/kotlin/racingcar/model/AttemptsNumber.kt @@ -0,0 +1,15 @@ +package racingcar.model + +import racingcar.util.Validator.isNumberAttemptsValid + +class AttemptsNumber(private val attempts: String) { + + val validAttemptsNum: Int + get() = getValidAttemptsNum().toInt() + + private fun getValidAttemptsNum(): String { + isNumberAttemptsValid(attempts) + return attempts + } + +} \ 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..1a5454811 --- /dev/null +++ b/src/main/kotlin/racingcar/model/Car.kt @@ -0,0 +1,6 @@ +package racingcar.model + +data class Car( + var name:String = "", + var distance:Int = 0 +) \ 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..7649df60e --- /dev/null +++ b/src/main/kotlin/racingcar/model/Cars.kt @@ -0,0 +1,31 @@ +package racingcar.model + +import racingcar.util.Validator.isCarNameLengthValid +import racingcar.util.Validator.isCarNameNotEmpty +import racingcar.util.Validator.isCarNameUnique + +class Cars(private val carNameList: List) { + + private var _carNames = getValidCarName() + + val carList: List + get() = _carList + private var _carList = initializeCars() + + private fun getValidCarName(): List { + isCarNameUnique(carNameList) + carNameList.forEach { name -> + isCarNameLengthValid(name) + isCarNameNotEmpty(name) + } + _carNames = carNameList + return _carNames + } + + private fun initializeCars(): List { + val initializedCarList: List = _carNames.indices.map { Car(name = "", distance = 0) } + _carNames.forEachIndexed { idx, name -> initializedCarList[idx].name = name } + return initializedCarList + } + +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/model/Racing.kt b/src/main/kotlin/racingcar/model/Racing.kt new file mode 100644 index 000000000..4582f0209 --- /dev/null +++ b/src/main/kotlin/racingcar/model/Racing.kt @@ -0,0 +1,30 @@ +package racingcar.model + +import camp.nextstep.edu.missionutils.Randoms +import racingcar.util.Constants + +class Racing(private val cars: List) { + val winner: String + get() = _winner + + private var _winner = "" + + + private fun makeRandomNumber() = Randoms.pickNumberInRange(0, 9) + + private fun determineMoveOrStop(randomNumber: Int) = randomNumber >= Constants.BASE_NUMBER + + fun runRaceOnce() { + cars.forEach { car -> + val randomNumber = makeRandomNumber() + if (determineMoveOrStop(randomNumber)) car.distance++ + println(car.distance) + } + } + + fun getWinner() { + val maxDistance = cars.maxOfOrNull { it.distance } + val winnerList = cars.filter { it.distance == maxDistance } + _winner = winnerList.joinToString { 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..f9037b374 --- /dev/null +++ b/src/main/kotlin/racingcar/util/Constants.kt @@ -0,0 +1,10 @@ +package racingcar.util + +object Constants { + const val CAR_NAME_MAX_LENGTH = 5 + const val RACE_START_MSG = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" + const val NUMBER_ATTEMPTS_MSG = "시도할 횟수는 몇 회인가요?" + const val BASE_NUMBER = 4 + const val WINNER_MSG = "최종 우승자 : " + const val DISTANCE_MSG = "-" +} \ 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..33470c34e --- /dev/null +++ b/src/main/kotlin/racingcar/util/Validator.kt @@ -0,0 +1,28 @@ +package racingcar.util + +object Validator { + private const val DUPLICATE_NAME_ERROR_MSG = "Duplicate car names are not allowed." + private const val NAME_OVER_LENGTH_MSG = + "Car name exceeds the maximum allowed length of ${Constants.CAR_NAME_MAX_LENGTH}." + private const val NAME_EMPTY_MSG = "Car name cannot be empty." + private const val INVALID_NUMBER_FORMAT_MSG = + "Invalid format for number of attempts. Please use the specified format." + + fun isCarNameUnique(carList: List) { + if (carList.size != carList.toSet().size) throw IllegalArgumentException(DUPLICATE_NAME_ERROR_MSG) + } + + fun isCarNameLengthValid(carName: String) { + if (carName.length > Constants.CAR_NAME_MAX_LENGTH) throw IllegalArgumentException(NAME_OVER_LENGTH_MSG) + } + + fun isCarNameNotEmpty(carName: String) { + if (carName.trim().isEmpty()) throw IllegalArgumentException(NAME_EMPTY_MSG) + } + + fun isNumberAttemptsValid(numberAttempts: String) { + if (!(numberAttempts.all { it.isDigit() }) || numberAttempts.toInt() < 1) throw IllegalArgumentException( + INVALID_NUMBER_FORMAT_MSG + ) + } +} \ 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..8899667f6 --- /dev/null +++ b/src/main/kotlin/racingcar/view/InputView.kt @@ -0,0 +1,8 @@ +package racingcar.view + +import camp.nextstep.edu.missionutils.Console + +class InputView { + fun getCarList() = Console.readLine().split(",") + fun getAttemptsNumber(): String = Console.readLine().trim() +} \ 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..70800b1e2 --- /dev/null +++ b/src/main/kotlin/racingcar/view/OutputView.kt @@ -0,0 +1,23 @@ +package racingcar.view + +import racingcar.model.Car +import racingcar.util.Constants + +class OutputView { + fun printRaceStart(raceStartMsg: String) { + println(raceStartMsg) + } + + fun requestAttemptsNumber(numberAttemptsMsg: String) { + println(numberAttemptsMsg) + } + + fun printMatchProgress(cars: List) { + cars.forEach { car -> println("${car.name} : ${Constants.DISTANCE_MSG.repeat(car.distance)}") } + println() + } + + fun printWinner(winnerNames: String) { + println("${Constants.WINNER_MSG}$winnerNames") + } +} \ No newline at end of file diff --git a/src/test/kotlin/racingcar/ApplicationTest.kt b/src/test/kotlin/racingcar/ApplicationTest.kt index 2cb36835c..e286d778d 100644 --- a/src/test/kotlin/racingcar/ApplicationTest.kt +++ b/src/test/kotlin/racingcar/ApplicationTest.kt @@ -6,8 +6,18 @@ import camp.nextstep.edu.missionutils.test.NsTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import racingcar.controller.RacingController +import racingcar.model.Car +import racingcar.util.Constants.NUMBER_ATTEMPTS_MSG +import racingcar.util.Constants.RACE_START_MSG +import racingcar.util.Validator.isCarNameLengthValid +import racingcar.util.Validator.isCarNameNotEmpty +import racingcar.util.Validator.isCarNameUnique +import racingcar.util.Validator.isNumberAttemptsValid class ApplicationTest : NsTest() { + private val racingController: RacingController = RacingController() + @Test fun `전진 정지`() { assertRandomNumberInRangeTest( @@ -26,10 +36,128 @@ class ApplicationTest : NsTest() { } } + @Test + fun `이름 중복에 대한 예외 처리`() { + assertSimpleTest { + val carNames = listOf("abc", "abc", "aa", "bb") + assertThrows { isCarNameUnique(carNames) } + } + } + + @Test + fun `이름 길이에 대한 예외 처리`() { + assertSimpleTest { + val carNames = listOf("hello", "sponge", "bob", "haha") + assertThrows { + carNames.forEach { isCarNameLengthValid(it) } + } + } + } + + @Test + fun `이름 공백에 대한 예외 처리`() { + assertSimpleTest { + val carNames = listOf("hello", " ", "", "hey") + assertThrows { + carNames.forEach { isCarNameNotEmpty(it) } + } + } + } + + @Test + fun `시도 횟수 입력에 대한 예외 처리`() { + assertSimpleTest { + val attempts = listOf("0", "", "2", "10", "a", "ㄱ") + assertThrows { + attempts.forEach { isNumberAttemptsValid(it) } + } + } + } + + @Test + fun `전진 정지 판단`() { + assertSimpleTest { + val randomNumbers = listOf(1, 5, 4, 2, 6) + val results: MutableList = MutableList(5) { false } + randomNumbers.forEachIndexed { idx, i -> results[idx] = racingController.determineMoveOrStop(i) } + assertThat(results).isEqualTo(listOf(false, true, true, false, true)) + } + } + + @Test + fun `단일 우승자 판별`() { + assertSimpleTest { + val cars: List = listOf(Car("T1", 10), Car("KT", 2), Car("Gen.G", 7)) + val results = racingController.getWinner(cars) + assertThat(results).isEqualTo("T1") + } + } + + @Test + fun `다중 우승자 판별`() { + assertSimpleTest { + val cars: List = listOf(Car("Bear", 3), Car("Dog", 6), Car("Cat", 7), Car("Tiger", 7)) + val results = racingController.getWinner(cars) + assertThat(results).isEqualTo("Cat, Tiger") + } + } + + @Test + fun `움직인 거리 출력`() { + assertSimpleTest { + val attempts = 5 + val cars: List = listOf(Car("Bear", 0), Car("Dog", 0), Car("Cat", 0), Car("Tiger", 0)) + racingController.doRacing(attempts, cars) + } + } + + @Test + fun `최종 우승자 출력`() { + assertSimpleTest { + run { + val cars: List = listOf(Car("Bear", 3), Car("Dog", 6), Car("Cat", 7), Car("Tiger", 7)) + racingController.printWinner(racingController.getWinner(cars)) + } + assertThat(output()).isEqualTo("최종 우승자 : Cat, Tiger") + } + } + + @Test + fun `경기 과정 출력`() { + assertSimpleTest { + run { + val cars: List = listOf(Car("Cat", 1), Car("Dog", 3)) + racingController.printMatchProgress(cars) + } + assertThat(output()).isEqualTo("Cat : -\n" + "Dog : ---" ) + } + } + + @Test + fun `경기 시작 출력`() { + assertSimpleTest { + run { + printRaceStart(RACE_START_MSG) + } + assertThat(output()).isEqualTo("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)") + } + } + + @Test + fun `시도 횟수 메세지 출력`() { + assertSimpleTest { + run { + printNumberAttempts(NUMBER_ATTEMPTS_MSG) + } + assertThat(output()).isEqualTo("시도할 횟수는 몇 회인가요?") + } + } + public override fun runMain() { main() } + companion object { private const val MOVING_FORWARD = 4 private const val STOP = 3