Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 118 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,118 @@
# kotlin-racingcar-precourse
# 자동차 경주 (Kotlin) – README

## 1) 목적

간단한 **자동차 경주 게임**을 구현한다. 사용자가 입력한 자동차 이름들과 이동 횟수를 바탕으로, 랜덤 값에 따라 전진하거나 멈추며, 마지막에는 우승자를 출력한다.

---

## 2) 요구사항 요약

* 입력: 자동차 이름(쉼표로 구분, 각 이름 1~5자), 시도 횟수(정수, 1 이상)
* 각 라운드마다 0~9 중 랜덤 값 생성 → 4 이상이면 전진
* 모든 라운드 종료 후 최종 우승자 출력 (동점 가능, 쉼표로 구분)
* 잘못된 입력이면 `IllegalArgumentException` 발생 후 프로그램 종료 (명시적 종료 금지)
* Kotlin 2.2.0 환경, `missionutils.Randoms`, `missionutils.Console` 사용

---

## 3) 패키지 구조 (예시)

```
src
└─ main/kotlin/racingcar
├─ Application.kt
├─ domain
│ ├─ Car.kt
│ ├─ MoveDecider.kt
│ └─ RandomMoveDecider.kt
├─ service
│ ├─ RaceRound.kt
│ └─ WinnerFinder.kt
└─ view
├─ InputView.kt
└─ OutputView.kt

src
└─ test/kotlin/racingcar
├─ ApplicationTest.kt
├─ CarTest.kt
├─ RaceRoundTest.kt
├─ WinnerFinderTest.kt
└─ InputValidationTest.kt
```

---

## 4) 기능 구현 목록 (진행 체크용)

* [x] **입력 처리** : 자동차 이름 입력받기 (`Console.readLine()` 이용)
* [x] **파싱 로직** : 쉼표 기준 split 및 trim 처리
* [x] **입력 검증** : 이름 길이(1~5자), 빈 문자열, 공백-only 금지
* [x] **시도 횟수 입력/검증** : 자연수만 허용, 0 이하/문자 입력 시 예외
* [x] **난수 생성 로직** : `Randoms.pickNumberInRange(0, 9)` 적용
* [x] **전진 조건** : 랜덤 값이 4 이상이면 전진
* [x] **Car 클래스** : 이름, 위치, 이동 로직(`moveIf`) 구현
* [x] **MoveDecider 인터페이스** : `canMove()` 정의
* [x] **RandomMoveDecider 구현체** : 랜덤 판단 로직 구현
* [x] **RaceRound 클래스** : 한 라운드 전체 실행 및 상태 갱신
* [x] **WinnerFinder 클래스** : 최대 position 기준 우승자 계산
* [x] **OutputView** : 라운드별 결과 및 최종 우승자 출력
* [x] **예외 처리** : 잘못된 입력 시 `IllegalArgumentException` 발생
* [x] **Application.main** : 전체 흐름 연결 및 실행
* [x] **테스트 코드 작성** : JUnit5 + AssertJ 기반 단위 테스트

> 기능을 완성하면 `[ ]` → `[x]` 로 바꿔 커밋하기.

---

## 5) 입출력 포맷 + 검증 규칙

### 입력 예시

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

### 출력 예시

```
실행 결과
pobi : -
woni :
jun : -

pobi : --
woni : -
jun : --

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

### 검증 규칙 상세

* **자동차 이름**

* 쉼표로 구분된 각 이름을 `trim()` 후 검사
* 빈 문자열(`""`)이나 공백(`" "`)은 예외 발생
* 이름 길이는 1~5자까지만 허용
* **시도 횟수**

* 숫자가 아닌 입력, 0 이하 값은 예외 발생
* **전진 조건**

* `Randoms.pickNumberInRange(0, 9)` 결과가 4 이상이면 전진
* **출력 형식**

* 각 라운드마다 입력 순서대로 `이름 : ---` 출력
* 전진 안 하면 대시 없음 (`이름 :` 뒤 공백)
* **종료 방식**

* 잘못된 입력이면 `IllegalArgumentException` 던지고 프로그램 종료 (명시적 `System.exit` 금지)

---

> 💡 **팁:** 테스트에서는 랜덤 대신 `FakeDecider`를 만들어 이동 여부를 고정하면 결과 검증이 쉬움.
22 changes: 21 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
package racingcar

import racingcar.domain.Car
import racingcar.domain.RandomMoveDecider
import racingcar.service.RaceRound
import racingcar.service.WinnerFinder
import racingcar.view.InputView
import racingcar.view.OutputView

fun main() {
// TODO: 프로그램 구현
val names = InputView.readNames()
val trialCount = InputView.readTrialCount()

val cars = names.map{ Car(it)}
val decider= RandomMoveDecider()
val raceRound= RaceRound(decider)

OutputView.printStart()
repeat(trialCount){
raceRound.runOnce(cars)
OutputView.printRound(cars)
}
OutputView.printWinners(
WinnerFinder.findWinners(cars));
}
18 changes: 18 additions & 0 deletions src/main/kotlin/racingcar/domain/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package racingcar.domain

class Car(val name: String){
var position:Int =0
private set
init{
validateName(name)
}
fun moveIf(decider: MoveDecider): Boolean{
val willMove=decider.canMove()
if(willMove) position +=1
return willMove
}
private fun validateName(name: String){
if(name.isBlank()) throw IllegalArgumentException("Name can't be blank")
if(name.length !in 1..5) throw IllegalArgumentException("Name length must be 1<=..<=5")
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/racingcar/domain/MoveDecider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.domain

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

import camp.nextstep.edu.missionutils.Randoms

class RandomMoveDecider : MoveDecider {
override fun canMove(): Boolean {
return Randoms.pickNumberInRange(0, 9) >= MOVE_THRESHOLD
}

private companion object {
const val MOVE_THRESHOLD = 4
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/racingcar/service/RaceRound.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package racingcar.service

import racingcar.domain.Car
import racingcar.domain.MoveDecider

class RaceRound(private val decider: MoveDecider){
/**
* 모든 자동차에 대해 한 번의 전진 여부를 판단하고 이동을 적용한다.
* @return 각 자동차가 이동했는지 여부(Boolean)의 리스트 (입력 cars 순서 유지)
*/
fun runOnce(cars: List<Car>): List<Boolean> {
return cars.map { it.moveIf(decider) }
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/racingcar/service/WinnerFinder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racingcar.service

import racingcar.domain.Car

object WinnerFinder {
fun findWinners(cars: List<Car>): List<String> {
if (cars.isEmpty()) return emptyList()
val maxPos = cars.maxOf { it.position }
return cars.filter { it.position == maxPos }.map { it.name }
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package racingcar.view

import camp.nextstep.edu.missionutils.Console

object InputView {
fun readNames(): List<String> {
println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")
val raw = Console.readLine()
val names = raw.split(",").map { it.trim() }
validateNames(names)
return names
}

fun readTrialCount(): Int {
println("시도할 횟수는 몇 회인가요?")
val raw = Console.readLine()
val count = raw.toIntOrNull() ?: throw IllegalArgumentException("시도 횟수는 정수여야 합니다.")
if (count < 1) throw IllegalArgumentException("시도 횟수는 1 이상이어야 합니다.")
return count
}

private fun validateNames(names: List<String>) {
if (names.isEmpty()) throw IllegalArgumentException("이름을 하나 이상 입력하세요.")
names.forEach { name ->
if (name.isBlank()) throw IllegalArgumentException("빈 이름은 허용되지 않습니다.")
if (name.length !in 1..5) throw IllegalArgumentException("각 이름은 1~5자여야 합니다.")
}
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/racingcar/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package racingcar.view

import racingcar.domain.Car

object OutputView {
fun printStart() {
println()
println("실행 결과")
}

fun printWinners(winners: List<String>) {
println("최종 우승자 : ${winners.joinToString(", ")}")
}
fun printRound(cars: List<Car>){
cars.forEach { car-> println("${car.name} : ${"-".repeat(car.position)}") }
println()
}
}
36 changes: 36 additions & 0 deletions src/test/kotlin/racingcar/CarTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class CarTest {

@Test
fun `자동차 이름 검증 및 초기 위치는 0이어야 한다`() {
val car = Car("pobi")
assertThat(car.name).isEqualTo("pobi")
assertThat(car.position).isZero()
}

@Test
fun `MoveDecider가 true일 때 전진한다`() {
val alwaysMove = MoveDecider { true }
val car = Car("pobi")

val moved = car.moveIf(alwaysMove)

assertThat(moved).isTrue()
assertThat(car.position).isEqualTo(1)
}

@Test
fun `MoveDecider가 false일 때 멈춘다`() {
val neverMove = MoveDecider { false }
val car = Car("pobi")

val moved = car.moveIf(neverMove)

assertThat(moved).isFalse()
assertThat(car.position).isEqualTo(0)
}
}
Loading