Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,76 @@
# kotlin-racingcar-precourse

초간단 자동차 경주 게임을 구현합니다. 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있으며, 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려줍니다.


## 구현할 기능 목록


1. 입력 검증 로직 구현
- 입력 검증 로직 테스트 구현
- 자동차 이름 및 시도 횟수 검증 로직 구현

2. 사용자 입력 처리 기능
- 입력 처리 및 파싱 기능 테스트 구현
- 사용자 입력 처리 및 파싱 기능 구현

3. 자동차 도메인 모델 구현
- Car 클래스 기능 테스트 구현
- Car 클래스 구현 및 전진 기능 구현

4. 자동차 컬렉션 관리
- Cars 클래스 및 우승자 계산 테스트 구현
- Cars 클래스 구현 및 우승자 계산 기능

5. 결과 출력 처리 기능
- 출력 형식 및 우승자 표시 테스트 구현
- 결과 출력 기능 구현

6. 게임 진행 로직 구현
- 게임 진행 로직 및 무작위 이동 판단 테스트 구현
- 게임 진행 로직 및 무작위 이동 판단 구현

7. 애플리케이션 통합
- 통합 테스트 추가
- 전체 애플리케이션 흐름 통합 및 예외 처리

## 입출력 예시
입력 예시:
```
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 횟수는 몇 회인가요?
5
```

출력 예시(요약):
```
pobi : -
woni :
jun : -

pobi : --
woni : -
jun : --
...

최종 우승자 : pobi, jun
```

## 프로그래밍 제약 및 규칙
- Kotlin으로만 구현한다 (모든 소스는 .kt).
- Kotlin 2.2.0에서 실행 가능해야 한다.
- 프로그램 진입점은 `Application.main()`이다.
- 외부 라이브러리는 제공된 것(camp.nextstep.edu.missionutils)만 사용한다.
- 애플리케이션 종료 시 `System.exit()` 또는 `exitProcess()`를 호출하면 안 된다.
- 들여쓰기(indent) 깊이는 2를 초과하면 안 된다(최대 2).

## 테스트
- JUnit5와 AssertJ로 기능 단위 테스트를 작성한다.
- 핵심 로직은 단위 테스트로 검증한다: 이름 파싱, 시도 횟수 파싱, 전진 판정, 우승자 결정 등.

## 미션유틸 사용 안내
- 사용자 입력: `camp.nextstep.edu.missionutils.Console.readLine()` 사용
- 랜덤 값: `camp.nextstep.edu.missionutils.Randoms.pickNumberInRange(0, 9)` 사용

---
49 changes: 48 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
package racingcar

import racingcar.domain.Cars
import racingcar.game.RacingGame
import racingcar.view.InputView
import racingcar.view.OutputView

fun main() {
// TODO: 프로그램 구현

val racingCarApplication = RacingCarApplication()
racingCarApplication.run()
}

class RacingCarApplication {

fun run() {
val carNames = readUserInput()
val roundCount = InputView.readRoundCount()

val game = createGame(carNames)
playGameWithOutput(game, roundCount)
printFinalWinners(game)
}

private fun readUserInput(): List<String> {
return InputView.readCarNames()
}

private fun createGame(carNames: List<String>): RacingGame {
val cars = Cars(carNames)
return RacingGame.withRandomDecider(cars)
}

private fun playGameWithOutput(game: RacingGame, roundCount: Int) {
OutputView.printResultHeader()

repeat(roundCount) {
playSingleRound(game)
}
}

private fun playSingleRound(game: RacingGame) {
game.playRound()
val roundResult = game.getRoundResult()
OutputView.printRoundResult(roundResult)
}

private fun printFinalWinners(game: RacingGame) {
val winners = game.getWinners()
OutputView.printWinners(winners)
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/racingcar/domain/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package racingcar.domain

class Car(private val name: String) {
private var position = 0

fun moveForward() {
position++
}

fun getName(): String {
return name
}

fun getPosition(): Int {
return position
}
}
31 changes: 31 additions & 0 deletions src/main/kotlin/racingcar/domain/Cars.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racingcar.domain

class Cars(carNames: List<String>) {
private val cars: List<Car> = carNames.map { name -> Car(name) }

fun moveAll(moveDecider: () -> Boolean) {
cars.forEach { car ->
if (moveDecider()) {
car.moveForward()
}
}
}

fun getWinnerNames(): List<String> {
val maxPosition = findMaxPosition()
return findCarsAtPosition(maxPosition)
}

private fun findMaxPosition(): Int {
return cars.maxOf { car -> car.getPosition() }
}

private fun findCarsAtPosition(targetPosition: Int): List<String> {
return cars.filter { car -> car.getPosition() == targetPosition }
.map { car -> car.getName() }
}

fun getCars(): List<Car> {
return cars
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/racingcar/game/MoveDecider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.game

interface MoveDecider {
fun shouldMove(): Boolean
}
34 changes: 34 additions & 0 deletions src/main/kotlin/racingcar/game/RacingGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package racingcar.game

import racingcar.domain.Car
import racingcar.domain.Cars

class RacingGame(
private val cars: Cars,
private val moveDecider: MoveDecider
) {

fun playGame(roundCount: Int) {
repeat(roundCount) {
playRound()
}
}

fun playRound() {
cars.moveAll { moveDecider.shouldMove() }
}

fun getRoundResult(): List<Car> {
return cars.getCars()
}

fun getWinners(): List<String> {
return cars.getWinnerNames()
}

companion object {
fun withRandomDecider(cars: Cars): RacingGame {
return RacingGame(cars, RandomMoveDecider())
}
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/racingcar/game/RandomMoveDecider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.game

import camp.nextstep.edu.missionutils.Randoms

class RandomMoveDecider : MoveDecider {
companion object {
private const val MIN_RANDOM_VALUE = 0
private const val MAX_RANDOM_VALUE = 9
private const val MOVE_THRESHOLD = 4
}

override fun shouldMove(): Boolean {
val randomValue = generateRandomValue()
return isMovingCondition(randomValue)
}

private fun generateRandomValue(): Int {
return Randoms.pickNumberInRange(MIN_RANDOM_VALUE, MAX_RANDOM_VALUE)
}

private fun isMovingCondition(randomValue: Int): Boolean {
return randomValue >= MOVE_THRESHOLD
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/racingcar/validator/InputValidator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package racingcar.validator

object InputValidator {
private const val MAX_CAR_NAME_LENGTH = 5
private const val MIN_ROUND_COUNT = 1

fun validateCarNames(carNames: List<String>) {
validateCarNamesNotEmpty(carNames)
carNames.forEach { carName ->
validateSingleCarName(carName)
}
}

private fun validateCarNamesNotEmpty(carNames: List<String>) {
require(carNames.isNotEmpty()) {
"최소 1대의 자동차가 필요합니다."
}
}

private fun validateSingleCarName(carName: String) {
validateCarNameNotBlank(carName)
validateCarNameLength(carName)
}

private fun validateCarNameNotBlank(carName: String) {
require(carName.isNotBlank()) {
"자동차 이름은 빈 값일 수 없습니다."
}
}

private fun validateCarNameLength(carName: String) {
require(carName.length <= MAX_CAR_NAME_LENGTH) {
"자동차 이름은 ${MAX_CAR_NAME_LENGTH}자 이하여야 합니다."
}
}

fun validateRoundCount(roundCount: Int) {
require(roundCount >= MIN_ROUND_COUNT) {
"시도 횟수는 ${MIN_ROUND_COUNT} 이상이어야 합니다."
}
}
}
40 changes: 40 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package racingcar.view

import camp.nextstep.edu.missionutils.Console
import racingcar.validator.InputValidator

object InputView {
private const val CAR_NAMES_PROMPT = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
private const val ROUND_COUNT_PROMPT = "시도할 횟수는 몇 회인가요?"
private const val DELIMITER = ","

fun readCarNames(): List<String> {
println(CAR_NAMES_PROMPT)
val input = Console.readLine()
val carNames = parseCarNames(input)
InputValidator.validateCarNames(carNames)
return carNames
}

fun readRoundCount(): Int {
println(ROUND_COUNT_PROMPT)
val input = Console.readLine()
val roundCount = parseRoundCount(input)
InputValidator.validateRoundCount(roundCount)
return roundCount
}

fun parseCarNames(input: String): List<String> {
return input.split(DELIMITER).map { carName ->
carName.trim()
}
}

fun parseRoundCount(input: String): Int {
return try {
input.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("시도 횟수는 숫자여야 합니다.")
}
}
}
38 changes: 38 additions & 0 deletions src/main/kotlin/racingcar/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racingcar.view

import racingcar.domain.Car

object OutputView {
private const val RESULT_HEADER = "실행 결과"
private const val WINNER_PREFIX = "최종 우승자 : "
private const val CAR_STATUS_FORMAT = " : "
private const val DASH = "-"
private const val WINNER_DELIMITER = ", "

fun printResultHeader() {
println()
println(RESULT_HEADER)
}

fun printRoundResult(cars: List<Car>) {
cars.forEach { car ->
printSingleCarStatus(car)
}
println() // 자동차 출력 후 한 줄
println() // 라운드 간 빈 줄
}

private fun printSingleCarStatus(car: Car) {
val dashes = createDashes(car.getPosition())
println("${car.getName()}$CAR_STATUS_FORMAT$dashes")
}

fun createDashes(position: Int): String {
return DASH.repeat(position)
}

fun printWinners(winners: List<String>) {
val winnersText = winners.joinToString(WINNER_DELIMITER)
println("$WINNER_PREFIX$winnersText")
}
}
Loading