Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
732f6a7
Docs(README) : README.md 작성
GiyunKim00 Oct 27, 2025
e629789
feat(Input): Input.kt 작성
GiyunKim00 Oct 27, 2025
345c20e
feat(Car): Car data class 작성
GiyunKim00 Oct 27, 2025
4039a19
feat(RandomNumberGenerator): 랜덤 숫자 생성기 작성
GiyunKim00 Oct 27, 2025
9d4ebb7
feat(Car): 거리 반환 코드 추가
GiyunKim00 Oct 27, 2025
7d8afb7
feat(CarRacingGame): Model CarRacingGame 작성
GiyunKim00 Oct 27, 2025
69e1357
feat(CarRacingGame): Model CarRacingGame 작성
GiyunKim00 Oct 27, 2025
d17655c
feat(CarRacingGame): Model CarRacingGame 현재 차량 위치 반환 코드 작성
GiyunKim00 Oct 27, 2025
f0a2ab5
feat(CarRacingGame): Model CarRacingGame 승자 반환 코드 작성
GiyunKim00 Oct 27, 2025
f0c9f89
fix(Car): 함수 명명 수정
GiyunKim00 Oct 27, 2025
60fe0b4
fix(CarRacingGameViewModel): 입력값 검증 코드 작성
GiyunKim00 Oct 27, 2025
4ec97b4
fix(CarRacingGameViewModel): 승자 출력 코드 작성
GiyunKim00 Oct 27, 2025
444986c
feat(CarRacingGameViewModel): 게임 수행 코드 작성
GiyunKim00 Oct 27, 2025
0402b77
fix(CarRacingGame): 변수명 수정
GiyunKim00 Oct 27, 2025
cd541ad
fix(CarRacingGameViewModel): 모든 게임 결과 저장 후 리턴하도록 수정
GiyunKim00 Oct 27, 2025
e9a0503
feat(Output): 출력 시작 문구 안내 및 게임 진행 별 결과 출력
GiyunKim00 Oct 27, 2025
2d8e2d9
feat(Output): 최종 우승자 출력 코드 작성
GiyunKim00 Oct 27, 2025
72854c7
fix(CarRacingGameViewModel): gameSetting 코드 내 검증파트 일부 수정
GiyunKim00 Oct 27, 2025
5c6a11e
feat(Input): 시도횟수 질문 문구 추가
GiyunKim00 Oct 27, 2025
73cdb15
feat(Application) : 전체 실행 로직 작성
GiyunKim00 Oct 27, 2025
1ca6b84
feat(CarRacingGameViewModel) : 자동차 이름 중복 방지 코드 작성
GiyunKim00 Oct 27, 2025
70b78d7
docs(README) : 기능 구현 여부 체크
GiyunKim00 Oct 27, 2025
1a3bc82
docs(README) : 리팩토링 내용 작성
GiyunKim00 Oct 27, 2025
9557837
test(CarTest) : 자동차 이름, 난수에 따른 전진 여부 테스트
GiyunKim00 Oct 27, 2025
0c10593
test(CarRacingViewModelText) : 시도 횟수 및 자동차 이름 중복 테스트 코드 작성
GiyunKim00 Oct 27, 2025
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
# kotlin-racingcar-precourse

## 목표
- 쉼표(`,`)를 구분자로 하는 자동차명 문자열을 입력받는다.
- 시도 횟수를 입력받는다.
- 게임을 진행한다.
- 우승자를 판별 및 형식에 맞춰 출력한다.
- 예외처리를 수행한다.

## 입출력
### 입력
- 구분자와 문자로 구성된 자동차명 문자열
- 구분자는 쉼표(`,`)이다.
- 자동차명의 길이는 1자 이상, 5자 이하이다.
- 자연수로 구성된 시도 횟수
- 시도 횟수는 `MAX_INT`까지 입력받는다.
- 자연수가 아니거나, 음수일 시 예외 처리한다.
- `0`일 시, 실질적인 경주 행위가 수행되었다고 보기 어렵기에 예외 처리한다.
### 출력
- 진행
- `pobi : --` 형식으로 출력한다.
- 결과
- `최종 우승자 : pobi, jun` 형식으로 출력한다.
- 오류
- `IllegalArgumentException`를 발생시킨 뒤 적절한 에러 문구를 반환한다.

## 필수 요구사항
- `Kotlin` 2.2.0에서 실행가능해야하며, 제공된 라이브러리 이외 라이브러리는 사용할 수 없다.
- `camp.nextstep.edu.missionutils` 라이브러리에서 제공하는 API를 활용한다.
- `Console` API의 `readLine()`을 활용하여 문자열을 입력받는다.
- `Randoms` API의 `pickNumberInRange()`를 활용하여 Random 값을 추출한다.
- 파일, 패키지 명과 위치를 유지하며, 코틀린 코드 컨벤션을 준수한다.

## 코드 구성
- `MVVM` 패턴을 활용하여 코드를 작성한다.
- 함수명을 명확히 한다.
- `SRP`를 따르고자 노력한다.

## 기능 구현
- [X] "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"를 출력한다.
- [X] 제공된 `Console` API의 `readLine()`을 활용하여 사용자 입력을 받는다.
- [X] 문자열의 유효성을 확인한다.
- [X] 구분자로 문자열을 파싱한다.
- [X] 입력된 문자열이 요구사항에 부합하지 않는 경우 `IllegalArgumentException` 예외를 발생시킨다.
- [X] 숫자의 유효성을 확인한다.
- [X] 입력된 숫자가 요구사항에 부합하지 않는 경우 `IllegalArgumentException` 예외를 발생시킨다.
- [X] 게임을 진행하고, 결과를 판별한다.
- [X] 요구사항에 따라 결과를 출력한다.
- [X] 단독 우승의 경우, "최종 우승자 : OOO"를 출력한다.
- [X] 공동 우승의 경우, "최종 우승자 : OOO, OOO"를 출력한다.

## 리팩토링
- [X] 차량 이름 중복 방어 코드 작성
- [X] 에러를 그대로 throw하도록 작성
18 changes: 17 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
package racingcar

import racingcar.view.Input
import racingcar.view.Output
import racingcar.viewModel.CarRacingGameViewModel

fun main() {
// TODO: 프로그램 구현
val input = Input()
val output = Output()
val gameViewModel = CarRacingGameViewModel()
val carNames = gameViewModel.validateCarName(input.getCarsName())
val tryCount = gameViewModel.validateTryCount(Input().getTryCount())

gameViewModel.gameSetting(carNames)

gameViewModel.runAllGames(tryCount).forEach { gameResult ->
output.printGamesResult(gameResult)
}

output.finalWinners(gameViewModel.getWinnerNames())
}
22 changes: 22 additions & 0 deletions src/main/kotlin/racingcar/model/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package racingcar.model

data class Car(val name: String, var distance: Int = 0) {

companion object {
const val MAX_NAME_LENGTH = 5
const val MIN_MOVE_NUMBER = 4
}

init {
require(name.length <= MAX_NAME_LENGTH) { "자동차 이름은 ${MAX_NAME_LENGTH}자 이하만 가능합니다." }
require(name.isNotEmpty()) { "자동차 이름은 1자 이상 작성해야 합니다." }
}

fun move(generatedRandomNumber: Int) {
if (generatedRandomNumber >= MIN_MOVE_NUMBER) {
distance++
}
}
fun getCurrentDistance(): Int = distance
fun getCurrentDistanceWithHyphen(): String = "-".repeat(distance)
}
25 changes: 25 additions & 0 deletions src/main/kotlin/racingcar/model/CarRacingGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package racingcar.model

class CarRacingGame(
val carList : List<Car>,
) {
fun runRace(){
carList.forEach { car ->
car.move(RandomNumberGenerator().generate())
}
}

fun getCurrentCarDistances(): Map<String, String>{
return carList.associate { car ->
car.name to car.getCurrentDistanceWithHyphen()
}
}

fun getWinners(): List<String> {
val maxPosition = carList.maxOfOrNull { it.getCurrentDistance() } ?: 0
return carList
.filter { it.getCurrentDistance() == maxPosition }
.map { it.name }
}

}
9 changes: 9 additions & 0 deletions src/main/kotlin/racingcar/model/RandomNumberGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package racingcar.model

import camp.nextstep.edu.missionutils.Randoms

class RandomNumberGenerator {
fun generate(): Int {
return Randoms.pickNumberInRange(0, 9)
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/racingcar/view/Input.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package racingcar.view

import camp.nextstep.edu.missionutils.Console

class Input {
fun getCarsName(): String {
println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")
return Console.readLine()
}

fun getTryCount(): String {
println("시도할 횟수는 몇 회인가요?")
return Console.readLine()
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/racingcar/view/Output.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package racingcar.view

class Output {
fun startPrintResult(){
println("실행 결과")
}

fun printGamesResult(results: Map<String, String>) {
results.forEach { (name, currentDistance) ->
println("$name : $currentDistance")
}
println()
}

fun finalWinners(winners: String) {
println("최종 우승자 : $winners")
}
}
44 changes: 44 additions & 0 deletions src/main/kotlin/racingcar/viewModel/CarRacingGameViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package racingcar.viewModel

import racingcar.model.Car
import racingcar.model.CarRacingGame

class CarRacingGameViewModel {

private lateinit var game: CarRacingGame

fun gameSetting(cars: List<Car>) {
game = CarRacingGame(cars)
}

fun runAllGames(tryCount: Int): List<Map<String, String>> {
val allGamesResult = mutableListOf<Map<String, String>>()

repeat(tryCount) {
game.runRace()
allGamesResult.add(game.getCurrentCarDistances())
}
return allGamesResult
}

fun validateCarName(carNames: String): List<Car> {
val carName = carNames.split(",").map { it.trim() }

if (carName.toSet().size != carName.size) {
throw IllegalArgumentException("자동차 이름은 중복될 수 없습니다.")
}

return carName.map { Car(it) }
}

fun validateTryCount(tryCountString: String): Int {
val tryCount = tryCountString.toIntOrNull() ?: throw IllegalArgumentException("시도횟수가 올바르지 않습니다.")
require(tryCount > 0) { "시도 횟수는 자연수로 제합됩니다." }

return tryCount
}

fun getWinnerNames(): String {
return game.getWinners().joinToString(", ")
}
}
34 changes: 34 additions & 0 deletions src/test/kotlin/racingcar/CarRacingGameViewModelTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package racingcar

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import racingcar.viewModel.CarRacingGameViewModel

class CarRacingGameViewModelTest {

private val viewModel = CarRacingGameViewModel()

@Test
fun `시도 횟수가 1 미만이거나 숫자가 아니면 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
val testList: List<String> = listOf("0", "-1", "abc")
testList.forEach { str ->
viewModel.validateTryCount(str)
}
}
}

@Test
fun `시도 횟수가 1 이상이면 정상적으로 반환된다`() {
val tryCount = viewModel.validateTryCount("5")
assertEquals(5, tryCount)
}

@Test
fun `자동차 이름이 중복되면 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
viewModel.validateCarName("aaaa,aaaa,bbbb")
}
}
}
52 changes: 52 additions & 0 deletions src/test/kotlin/racingcar/CarTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package racingcar

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import racingcar.model.Car

class CarTest {
@Test
fun `자동차 이름이 5자 초과이면 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
Car("123456")
}
}

@Test
fun `자동차 이름이 5자 이하이면 정상적으로 생성된다`() {
assertDoesNotThrow {
Car("1234")
}
}

@Test
fun `자동차 이름이 공백일 시 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
Car("")
}
}

@Test
fun `난수가 4 이상일 시 전진한다`() {
val car = Car("test")

car.move(4)
car.move(9)

assertEquals(2, car.getCurrentDistance())
assertEquals("--", car.getCurrentDistanceWithHyphen())
}

@Test
fun `난수가 3 이하일 시 전진하지 않는다`() {
val car = Car("test")

car.move(0)
car.move(3)

assertEquals(0, car.getCurrentDistance())
assertEquals("", car.getCurrentDistanceWithHyphen())
}
}