Skip to content
94 changes: 93 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,93 @@
# kotlin-racingcar-precourse
# 자동차 경주

## 개요

초간단 자동차 경주 게임

## 기능 목록

- 경기
- 시도 횟수
- 양수만 입력 가능
- 자동차들
- 경기 결과
- 진행 상황
- 우승자들
- 진행 상황 추가
- 우승자 선발
- 경기 시작
- 라운드 시작
- 각 자동차 이동
- 자동차
- 이름
- 1자 이상, 5자 이하만 가능
- 위치
- 이동
- 전진
- 조건: 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우
- 멈춤
- 조건: 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
```
34 changes: 33 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
package racingcar

import camp.nextstep.edu.missionutils.Console

fun main() {
// TODO: 프로그램 구현
val race = input()
val raceResult = race.start()
output(raceResult)
}

fun output(raceResult: RaceResult) {
println(Message.RACE_RESULT_GUIDE_MESSAGE)
print(raceResult.progress())
println("${Message.RACE_WINNER_GUIDE_MESSAGE}${raceResult.winners.joinToString(", ")}")
}

fun input(): Race {
val cars = inputCars()
val round = inputRound()
return Race(round, cars)
}

private fun inputRound(): Int {
println(Message.ROUND_GUIDE_MESSAGE)
val roundInput = Console.readLine()
val round = roundInput.toIntOrNull() ?: throw IllegalArgumentException(Message.ROUND_ERROR_MESSAGE)
return round
}

private fun inputCars(): List<Car> {
println(Message.RACING_CAR_NAME_GUIDE_MESSAGE)
val carNames = Console.readLine().split(",")
val cars = carNames.map {
Car(it.trim())
}
return cars
}
24 changes: 24 additions & 0 deletions src/main/kotlin/racingcar/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar

class Car(val name: String) {

companion object {
private const val MAX_NAME_LENGTH = 5
private const val MIN_NAME_LENGTH = 1
private const val MOVE_STANDARD = 4
private const val NAME_LENGTH_ERROR_MESSAGE = "자동차 이름은 1자 이상, 5자 이하만 가능합니다."
}

var position: Int = 0
private set

init {
require(name.length in MIN_NAME_LENGTH..MAX_NAME_LENGTH) { NAME_LENGTH_ERROR_MESSAGE }
}

fun move(number: Int) {
if (number >= MOVE_STANDARD) {
position += 1
}
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/racingcar/Message.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package racingcar

object Message {
const val RACING_CAR_NAME_GUIDE_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
const val ROUND_GUIDE_MESSAGE = "시도할 횟수는 몇 회인가요?"
const val RACE_RESULT_GUIDE_MESSAGE = "\n실행 결과"
const val RACE_WINNER_GUIDE_MESSAGE = "최종 우승자 : "
const val ROUND_ERROR_MESSAGE = "시도 횟수는 1 이상의 숫자이어야 합니다."
}
29 changes: 29 additions & 0 deletions src/main/kotlin/racingcar/Race.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package racingcar

import camp.nextstep.edu.missionutils.Randoms


class Race(private val round: Int, private val cars: List<Car>, private val raceResult: RaceResult = RaceResult()) {
init {
require(round > 0) { Message.ROUND_ERROR_MESSAGE }
}

fun start(pickRandomNumber: () -> Int = { Randoms.pickNumberInRange(0, 9) }): RaceResult {
playRound(pickRandomNumber)
raceResult.setWinners(cars)
return raceResult
}

private fun playRound(pickRandomNumber: () -> Int) {
repeat(round) {
moveCar(pickRandomNumber)
raceResult.addRoundProgress(cars)
}
}

private fun moveCar(pickRandomNumber: () -> Int) {
cars.forEach { car ->
car.move(pickRandomNumber())
}
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/racingcar/RaceResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racingcar

class RaceResult {
private val progress: StringBuilder = StringBuilder()
var winners: List<String> = emptyList()
private set

fun addRoundProgress(cars: List<Car>) {
cars.forEach { car ->
addCarPosition(car)
}
progress.appendLine()
}

private fun addCarPosition(car: Car) {
progress.append("${car.name} : ")
repeat(car.position) { progress.append("-") }
progress.appendLine()
}

fun progress(): String = progress.toString()

fun setWinners(cars: List<Car>) {
val maxPositon = cars.maxOfOrNull { it.position } ?: return
winners = cars.filter { it.position == maxPositon }.map(Car::name)
}
}
20 changes: 17 additions & 3 deletions src/test/kotlin/racingcar/ApplicationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.junit.jupiter.api.assertThrows

class ApplicationTest : NsTest() {
@Test
fun `기능 테스트`() {
fun `올바른 결과를 반환한다`() {
assertRandomNumberInRangeTest(
{
run("pobi,woni", "1")
Expand All @@ -20,12 +20,26 @@ class ApplicationTest : NsTest() {
}

@Test
fun `예외 테스트`() {
fun `예외 상황에서 예외가 발생한다`() {
assertSimpleTest {
assertThrows<IllegalArgumentException> { runException("pobi,javaji", "1") }
}
}

@Test
fun `시도 횟수 입력이 숫자가 아니면 예외가 발생한다`() {
assertSimpleTest {
assertThrows<IllegalArgumentException> { runException("pobi,java", "abc") }
}
}

@Test
fun `시도 횟수가 0이하이면 예외가 발생한다`() {
assertSimpleTest {
assertThrows<IllegalArgumentException> { runException("pobi,java", "-2") }
}
}

override fun runMain() {
main()
}
Expand All @@ -34,4 +48,4 @@ class ApplicationTest : NsTest() {
private const val MOVING_FORWARD: Int = 4
private const val STOP: Int = 3
}
}
}
38 changes: 38 additions & 0 deletions src/test/kotlin/racingcar/CarTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racingcar

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

class CarTest {
@Test
fun `이름이 1~5자면 유효하다`() {
assertThat(Car("pobi").name).isEqualTo("pobi")
}

@Test
fun `이름이 6자 이상이면 예외가 발생한다`() {
val exception = assertThrows<IllegalArgumentException> { Car("pobiii") }
assertThat(exception.message).isEqualTo("자동차 이름은 1자 이상, 5자 이하만 가능합니다.")
}

@Test
fun `이름이 0자이면 예외가 발생한다`() {
val exception = assertThrows<IllegalArgumentException> { Car("") }
assertThat(exception.message).isEqualTo("자동차 이름은 1자 이상, 5자 이하만 가능합니다.")
}

@Test
fun `랜덤 값이 4 이상이면 전진한다`() {
val car = Car("pobi")
car.move(4)
assertThat(car.position).isEqualTo(1)
}

@Test
fun `랜덤 값이 3 이하면 멈춘다`() {
val car = Car("pobi")
car.move(3)
assertThat(car.position).isEqualTo(0)
}
}
31 changes: 31 additions & 0 deletions src/test/kotlin/racingcar/RaceResultTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racingcar

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

class RaceResultTest {
@Test
fun `라운드별 진행 상황을 추가한다`() {
val cars = listOf(Car("pobi"), Car("woni"))
cars[0].move(5)
val result = RaceResult()

result.addRoundProgress(cars)
val output = result.progress()

assertThat(output).contains("pobi : -")
assertThat(output).contains("woni : ")
}

@Test
fun `최대 위치값을 가진 자동차들이 우승자이다`() {
val cars = listOf(Car("pobi"), Car("woni"), Car("jun"))
cars[0].move(5)
cars[2].move(5)
val result = RaceResult()

result.setWinners(cars)

assertThat(result.winners).containsExactlyInAnyOrder("pobi", "jun")
}
}
33 changes: 33 additions & 0 deletions src/test/kotlin/racingcar/RaceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package racingcar

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

class RaceTest {
@Test
fun `시도 횟수가 0 이하일 경우 예외가 발생한다`() {
val cars = listOf(Car("1234"))
val exception = assertThrows<IllegalArgumentException> { Race(0, cars) }
assertThat(exception.message).contains("시도 횟수는 1 이상의 숫자이어야 합니다.")
}

@Test
fun `시도 횟수만큼 라운드를 반복하며 진행한다`() {
val cars = listOf(Car("pobi"), Car("woni"))
val race = Race(3, cars)

val result = race.start { 9 }
assertThat(cars.all { it.position == 3 }).isTrue()
assertThat(result.progress()).contains("pobi : ---", "woni : ---")
}

@Test
fun `우승자를 올바르게 결정한다`() {
val cars = listOf(Car("pobi"), Car("jun"))
val race = Race(1, cars)

val result = race.start { 9 }
assertThat(result.winners).containsExactlyInAnyOrder("pobi", "jun")
}
}