diff --git a/README.md b/README.md index 62fa463c..232f7bd8 100644 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ -# kotlin-racingcar-precourse +# 프리코스 2주차 _ 자동차 경주 + +## 자동차 경주 + +📍구현할 기능 목록 +1. 경주할 자동차 이름 입력받기 +2. 입력받은 자동차 이름 유효성 검증 + + - 잘못된 값을 입력할 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션 종료 + 1. 이름을 5자 초과하여 입력한 경우 + 2. 빈 값을 입력한 경우 + 3. 쉼표가 아닌 구분자를 사용한 경우 (구분자를 쉼표(,)만 인식하도록 하여 하나의 문자열로 처리) + 4. 자동차 이름이 중복된 경우 + +3. 시도할 횟수 입력받기 +4. 입력받은 시도 횟수 유효성 검증 + + - 잘못된 값을 입력할 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션 종료 + 1. 음수를 입력한 경우 + 2. 0을 입력한 경우 + 3. 문자열을 입력한 경우 + 4. 입력한 수가 Int 범위를 벗어나는 경우 + +5. 0부터 9 사이의 랜덤값을 통해 전진 유무를 결정 +6. 입력받은 시도 횟수만큼 반복 +7. 실행 결과 출력 +8. 전진 횟수가 가장 많은 자동차 판별 +9. 최종 우승자 출력 + + +## 코드 구조 +MVC (모델 - 뷰 - 컨트롤러) 패턴을 사용하여 코드 구조 설계 + +### Model +1. Car + - 역할 : 자동차의 상태와 동작을 정의 + - 주요 메서드 + - `move()` : 자동차가 전진할 때 호출되는 메서드로 거리를 증가시킴 +2. Game + - 역할 : 자동차 경주 게임의 상태를 관리 + - 주요 메서드 + - `playRound()` : 각 자동차가 이동하는 게임 라운드를 실행 + - `getCars()` : 현재 게임에 참여하고 있는 자동차 리스트를 반환 + - `getWinners()` : 현재까지의 라운드에서 가장 멀리 이동한 자동차를 찾아 그 이름을 리스트로 반환 + - `isMoveForward()` : Ramdon값을 사용하여 자동차의 이동 여부를 결정 + +### View +1. InputView + - 역할 : 사용자로부터 입력을 받고, 해당 입력의 유효성을 검증 + - 주요 메서드 + - `inputCarNames()` : 사용자에게 자동차 이름을 입력받음 + - `inputRounds()` : 사용자에게 시도할 라운드 수를 입력받음 + - `validateEmptyInput()` : 입력이 비어있거나 공백만 포함된 경우 유효성 검사 + - `validateSeparator()` : 입력 문자열이 유효한 쉼표 구분 형식인지 확인 + - `validateCarNames()` : 자동차 이름의 유효성을 검사하는 메서드로, 이름의 길이와 중복 여부를 확인 + - `validateNameLength()` : 자동차 이름의 길이가 1자 이상 5자 이하인지 확인 + - `validateDuplicateNames()` : 자동차 이름 리스트에서 중복된 이름이 없는지 확인 + - `splitAndTrimNames()` : 입력 문자열을 쉼표로 분리하고, 각 이름의 앞뒤 공백을 제거하여 리스트로 반환 + - `parseRounds()` : 입력 문자열을 정수로 변환하고, 변환된 값이 유효한 라운드 수인지 확인 + - `validatePositiveRounds()` : 라운드 수가 1 이상, 정수 범위 내에 있는지 확인 +2. ResultView + - 역할 : 자동차 경주 게임의 결과를 출력 + - 주요 메서드 + - `printRoundResult()` : 각 자동차의 이름과 이동 거리를 출력 + - `printWinners()` : 최종 우승자 목록을 출력 + +### Controller +1. RacingGameController + - 역할 : 자동차 경주 게임의 주요 로직을 관리하며 View와 Model 간의 상호작용을 조정 + - 주요 메서드 + - `startGame()` : 게임을 시작하는 메서드 diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index 0d8f3a79..279c1cab 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -1,5 +1,8 @@ package racingcar +import racingcar.controller.RacingGameController + fun main() { - // TODO: 프로그램 구현 -} + val controller = RacingGameController() + controller.startGame() +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/controller/RacingGameController.kt b/src/main/kotlin/racingcar/controller/RacingGameController.kt new file mode 100644 index 00000000..91ab31a2 --- /dev/null +++ b/src/main/kotlin/racingcar/controller/RacingGameController.kt @@ -0,0 +1,23 @@ +package racingcar.controller + +import racingcar.model.Game +import racingcar.view.InputView +import racingcar.view.ResultView + +class RacingGameController( + private val inputView: InputView = InputView(), + private val resultView: ResultView = ResultView(), + private val racingGame: Game = Game(inputView.inputCarNames()) +) { + fun startGame() { + val rounds = inputView.inputRounds() + + repeat(rounds) { + racingGame.playRound() + resultView.printRoundResult(racingGame.getCars()) + } + + val winners = racingGame.getWinners() + resultView.printWinners(winners) + } +} \ 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..7455b978 --- /dev/null +++ b/src/main/kotlin/racingcar/model/Car.kt @@ -0,0 +1,10 @@ +package racingcar.model + +class Car(val name: String) { + var distance: Int = 0 + private set + + fun move() { + distance++ + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/model/Game.kt b/src/main/kotlin/racingcar/model/Game.kt new file mode 100644 index 00000000..f588de33 --- /dev/null +++ b/src/main/kotlin/racingcar/model/Game.kt @@ -0,0 +1,35 @@ +package racingcar.model + +import camp.nextstep.edu.missionutils.Randoms + +class Game(carNames: List) { + private val cars = carNames.map { Car(it) } + + fun playRound() { + cars.forEach { car -> + if (isMoveForward()) { + car.move() + } + } + } + + private fun isMoveForward(): Boolean { + return Randoms.pickNumberInRange(MIN_MOVE_THRESHOLD, MAX_MOVE_THRESHOLD) >= MOVE_THRESHOLD + } + + fun getCars(): List { + return cars + } + + fun getWinners(): List { + val maxDistance = cars.maxOf { it.distance } + + return cars.filter { it.distance == maxDistance }.map { it.name } + } + + companion object { + const val MOVE_THRESHOLD = 4 + const val MIN_MOVE_THRESHOLD = 0 + const val MAX_MOVE_THRESHOLD = 9 + } +} \ 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..2e1cf6b3 --- /dev/null +++ b/src/main/kotlin/racingcar/view/InputView.kt @@ -0,0 +1,91 @@ +package racingcar.view + +import camp.nextstep.edu.missionutils.Console + +class InputView { + + fun inputCarNames(): List { + println(MESSAGE_ENTER_CAR_NAMES) + val input = Console.readLine().orEmpty() + validateEmptyInput(input) + validateSeparator(input) + + val names = splitAndTrimNames(input) + validateCarNames(names) + + return names + } + + fun inputRounds(): Int { + println(MESSAGE_ENTER_ROUNDS) + val input = Console.readLine().orEmpty() + println() + + return parseRounds(input) + } + + private fun validateEmptyInput(input: String) { + if (input.isBlank()) throw IllegalArgumentException(ERROR_EMPTY_INPUT) + } + + private fun validateSeparator(input: String) { + val names = input.split(COMMA).map { it.trim() } + require(names.isNotEmpty() && names.all { it.isNotBlank() }) { + ERROR_INVALID_SEPARATOR + } + } + + private fun splitAndTrimNames(input: String): List { + return input.split(COMMA).map { it.trim() } + } + + private fun validateCarNames(names: List) { + validateNameLength(names) + validateDuplicateNames(names) + validateNumericNames(names) + } + + private fun validateNameLength(names: List) { + require(names.all { it.isNotBlank() && it.length <= MAX_NAME_LENGTH }) { + ERROR_NAME_LENGTH + } + } + + private fun validateDuplicateNames(names: List) { + require(names.distinct().size == names.size) { + ERROR_DUPLICATE_NAMES + } + } + + private fun validateNumericNames(names: List) { + require(names.none { it.all { char -> char.isDigit() } }) { + ERROR_NUMERIC_NAMES + } + } + + + private fun parseRounds(input: String): Int { + val rounds = input.toIntOrNull() ?: throw IllegalArgumentException(ERROR_INVALID_NUMBER) + validatePositiveRounds(rounds) + + return rounds + } + + private fun validatePositiveRounds(rounds: Int) { + require(rounds in 1..Int.MAX_VALUE) { ERROR_NON_POSITIVE_ROUNDS } + } + + companion object { + const val MESSAGE_ENTER_CAR_NAMES = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" + const val MESSAGE_ENTER_ROUNDS = "시도할 횟수는 몇 회인가요?" + const val ERROR_EMPTY_INPUT = "자동차 이름을 입력해야 합니다." + const val ERROR_INVALID_SEPARATOR = "자동차 이름은 쉼표(,)로 구분해야 합니다." + const val ERROR_NAME_LENGTH = "자동차 이름은 1자 이상 5자 이하여야 합니다." + const val ERROR_DUPLICATE_NAMES = "자동차 이름은 중복될 수 없습니다." + const val ERROR_INVALID_NUMBER = "이동 횟수는 숫자로 입력해야 합니다." + const val ERROR_NON_POSITIVE_ROUNDS = "입력한 이동 횟수가 범위를 벗어났습니다." + const val ERROR_NUMERIC_NAMES = "자동차 이름은 숫자일 수 없습니다." + const val COMMA = "," + const val MAX_NAME_LENGTH = 5 + } +} \ No newline at end of file diff --git a/src/main/kotlin/racingcar/view/ResultView.kt b/src/main/kotlin/racingcar/view/ResultView.kt new file mode 100644 index 00000000..b4210f42 --- /dev/null +++ b/src/main/kotlin/racingcar/view/ResultView.kt @@ -0,0 +1,21 @@ +package racingcar.view + +import racingcar.model.Car + +class ResultView { + + fun printRoundResult(cars: List) { + cars.forEach { car -> + println("${car.name} : ${"-".repeat(car.distance)}") + } + println() + } + + fun printWinners(winners: List) { + println("$MESSAGE_WINNERS: ${winners.joinToString(", ")}") + } + + companion object { + const val MESSAGE_WINNERS = "최종 우승자" + } +} \ No newline at end of file