Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f3803a0
docs(README): 구현할 기능 목록 추가
Soldbone Oct 24, 2025
4d4a6d1
docs(README): ul 들여쓰기 수정
Soldbone Oct 24, 2025
065286c
feat: 경주 생성 기능 추가
Soldbone Oct 25, 2025
27584fd
refactor: 입력 및 입력 처리 기능 함수화
Soldbone Oct 25, 2025
72786bb
fix: 초기 차수 0에서 1로 정정
Soldbone Oct 25, 2025
96ecb8c
docs(README): 경주 생성 항목에서의 줄바꿈 누락 수정
Soldbone Oct 25, 2025
432dfef
feat: 경주 진행 기능 구현
Soldbone Oct 26, 2025
9fa34f0
feat: 경주 결과 출력기능 추가
Soldbone Oct 26, 2025
22ae208
refactor: 함수 기능 분리 및 최소화
Soldbone Oct 26, 2025
9493486
test: 기능 목록의 기능에 대한 각각의 테스트 작성
Soldbone Oct 26, 2025
2a09f6a
docs(README): 누락 내용 및 형식 일관성 추가
Soldbone Oct 26, 2025
0fa4713
refactor: 기능 분리 및 수정과 변수명 변경
Soldbone Oct 26, 2025
ceb100b
test: 기능 수정에 따라 테스트 코드도 변경
Soldbone Oct 26, 2025
3a809d9
refactoring: 입력 기능을 InputView로 클래스화 및 기능 축소
Soldbone Oct 27, 2025
bb7e5e1
refactor: 불필요한 import 제거
Soldbone Oct 27, 2025
231df92
refactor: 자동차 이름 입력을 검증하는 부분을 함수화
Soldbone Oct 27, 2025
1dcbe9b
test: 자동차 입력 방식 변경에 따른 테스트 코드 수정
Soldbone Oct 27, 2025
4e5ec1e
test: 직전 커밋에서 반영되지 않은 부분 재커밋
Soldbone Oct 27, 2025
16ca743
test: 자동차 이름 입력 검증 테스트 추가 및 미사용 import 제거
Soldbone 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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,66 @@
# kotlin-racingcar-precourse
## 2주차 - 자동차 경주
초간단 자동차 경주 게임을 구현한다.

## 📝 기능 목록

### 경주 생성
- 자동차 이름 입력 받기<br>
예외 처리
- 쉼표가 아닌 문자 중 알파벳이 아닌 문자가 존재하는 경우 예외 발생
- 자동차 이름이 5자를 초과하는 경우 예외 발생
- 자동차 이름이 비어 있는 경우 예외 발생
- 시도할 횟수 입력 받기<br>
예외 처리
- 시도 횟수가 비어 있는 경우 0으로 처리. 예외 발생시키지 않음
- 숫자가 아닌 경우 예외 발생
- 경주에 참여하는 자동차와 경주 생성

### 경주 진행
- 현재 차수가 입력 받은 시도 횟수보다 작은지 확인
- 시도 횟수를 초과하는 경우 우승자 선정
- 자동차마다 0~9 사이의 무작위 값 추출
- 추출된 값이 4 이상인 경우 움직임 데이터 1 증가
- 자동차마다 해당 차수의 실행 결과 출력
- 우승자를 List<Car>로 반환

### 경주 결과 출력
- 최종 우승자 출력

## 🔃플로우 차트
```mermaid
---
title: 자동차 경주
---
flowchart TD
subgraph INIT[경주 생성]
inputCarName[자동차 이름 입력] -->|입력 조건 예외 처리| inputRound[시도할 횟수 입력]
inputRound --> createCar[경주와 경주의 자동차 생성]
end INIT --> Game

subgraph Game[경주 진행]
initCurrentRound[현재 차수 = 1]
initCurrentRound --> isRoundNotFinished
isRoundNotFinished{현재 차수 <= 입력된 시도 횟수}
isRoundNotFinished -- YES --> extractRandomInt
isRoundNotFinished -- NO -----> winnerSelection["움직임 데이터가 가장 큰 우승자(들) 선정"]
winnerSelection --> END

extractRandomInt["자동차마다 0~9 사이의 값 추출"]
extractRandomInt --> isCanMove

isCanMove{추출된 값 >= 4}
isCanMove -- YES --> move
isCanMove -- NO --> printRound

move[움직임 데이터 1 증가]
move --> printRound

printRound[자동차마다 차수별 실행 결과 출력]
printRound --> isRoundNotFinished
end Game --> printResult

subgraph printResult[결과 출력하기]
printWinner[최종 우승자 출력]
end
```
102 changes: 102 additions & 0 deletions src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,107 @@
package racingcar

import camp.nextstep.edu.missionutils.Randoms
import racingcar.view.InputView

fun main() {
// TODO: 프로그램 구현
val inputView = InputView()
val carNamesInput = inputView.getInputCarNames()
validateCarNamesInput(carNamesInput)

val splitCarNames = splitCarName(carNamesInput)
val roundInput = inputView.getInputRound()
val round = stringToInt(roundInput)

val carInstances = createCars(splitCarNames)
val racingGame = RacingGame(round, carInstances)
racingGame.play()
val winners = racingGame.getWinners()

printOutput(winners)
}

fun createCars(splitCarNames: List<String>): List<Car> = splitCarNames.map { Car(it) }

fun validateCarNamesInput(carNamesInput: String) {
require(carNamesInput.isNotBlank()) { "이름이 입력되지 않았습니다." }
val hasInvalidChar = carNamesInput.any { it != ',' && !it.isLetter() }
require(!hasInvalidChar) { "이름을 올바르게 입력해주세요." }
}

fun splitCarName(carNamesInput: String): List<String> {
val splitCarNames = carNamesInput.split(",")
val hasLengthFiveOrLess = splitCarNames.all { it.length <= 5 && it.isNotBlank() }
require(hasLengthFiveOrLess) { "이름은 5글자를 초과하거나 비어있을 수 없습니다." }

return splitCarNames
}

fun stringToInt(roundInput: String): Int = if (roundInput.isNotBlank()) {
roundInput.toIntOrNull() ?: throw IllegalArgumentException("숫자를 입력해주세요.")
} else {
0
}
Comment on lines +40 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 값을 입력하면 0으로 반환하도록 처리하셨는데,
이렇게 되면 게임 결과 계산 시 모든 유저가 우승으로 처리될 것 같습니다.
해당 케이스를 에러로 처리하지 않고 0을 반환하도록 하신 이유가 궁금합니다!

또한 가독성을 위해서 명시적으로 return을 사용하는 형태로 바꾸면 더 읽기 쉬울 것 같습니다!

Suggested change
fun stringToInt(roundInput: String): Int = if (roundInput.isNotBlank()) {
roundInput.toIntOrNull() ?: throw IllegalArgumentException("숫자를 입력해주세요.")
} else {
0
}
fun stringToInt(roundInput: String): Int {
if (roundInput.isNotBlank()) {
return roundInput.toIntOrNull() ?: throw IllegalArgumentException("숫자를 입력해주세요.")
}
return 0
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 값을 입력하면 0으로 반환하도록 처리하셨는데, 이렇게 되면 게임 결과 계산 시 모든 유저가 우승으로 처리될 것 같습니다. 해당 케이스를 에러로 처리하지 않고 0을 반환하도록 하신 이유가 궁금합니다!

이건 제 요구사항 분석 과정에서 잘못된 판단이 들어간 것 같습니다. 말씀하신 대로 round 입력이 0이거나 들어오지 않을 경우에는 모종의 이유로 게임 개최는 했지만 진행할 수 없는 상황이라고 판단하고 모든 유저가 우승자가 되도록 구현했습니다.

지금 생각해보니 IllegalArgumentException을 발생시키거나 우승자가 없도록 하는 것이 더 올바른 구현일 것 같네요.


fun printOutput(winners: List<Car>?) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InputView는 만드셨는데 OutputView는 따로 만드시지 않았네요. printOutput에서 우승자 출력을 결정하시는데 OutputView.kt를 따로 만드셔서 분리하시는 게 좋을 거 같습니다~

val winnerNames = winners?.joinToString(separator = ", ") { it.name }
println("최종 우승자 : $winnerNames")
}

data class Car(val name: String) {
var position: Int = 0
}

class RacingGame(val round: Int, val cars: List<Car>) {
private var currentRound: Int = 1

fun play() {
println()
println("실행 결과")
while (!this.isFinished()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반복문이 돌 때마다 isFinished()로 비교하는 것보다
repeat(round)나 for 문을 활용하면 불필요한 비교 연산을 줄일 수 있을 것 같습니다.
그렇게 하면 currentRound 변수도 없어져서 코드가 좀 더 단순해질 것 같아요!

또한 클래스 내부 메서드 호출 시에 this를 반복적으로 사용하셨는데,
스코프가 명확한 상황에서는 this를 생략해도 되기 때문에
제거하면 코드가 조금 더 깔끔해질 것 같습니다.

this.makeMove()
this.printRoundResult(cars)
this.increaseCurrentRound()
}
}

fun getWinners(): List<Car> {
check(this.isFinished()) { "게임이 아직 시작되지 않았습니다." }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check를 통해 모든 라운드가 끝났는지 검사하시는 것 같습니다. 그런데, 이미 play 함수에서 isFinished를 사용해서 반복문을 검사하고 있는데 이 부분이 또 필요한 지 모르겠습니다.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check를 통해 모든 라운드가 끝났는지 검사하시는 것 같습니다. 그런데, 이미 play 함수에서 isFinished를 사용해서 반복문을 검사하고 있는데 이 부분이 또 필요한 지 모르겠습니다.

RacingGame.getWinners() 메서드를 RacingGame.play()를 호출하기 이전에 호출하는 경우가 존재할 수 있다고 생각했습니다.
생각해보니 굳이 한번 더 체크하기보다는 널이나 빈 리스트를 반환해도 될 것 같네요.

val winner = cars.maxBy { it.position }
val winners = cars.filter { it.position == winner.position }

return winners
}

private fun isFinished(): Boolean = this.currentRound > this.round

private fun makeMove() {
cars.map { car ->
val randomNumber = this.getRandomNumber()
if (canMove(randomNumber)) {
increaseCarPosition(car)
}
}
}

private fun getRandomNumber(): Int {
return Randoms.pickNumberInRange(0, 9)
}

private fun canMove(randomNumber: Int): Boolean = randomNumber >= 4

private fun increaseCarPosition(car: Car) {
car.position++
}

private fun increaseCurrentRound() {
this.currentRound++
}

private fun printRoundResult(cars: List<Car>) {
cars.forEach { car ->
println("${car.name} : ${"-".repeat(car.position)} ")
}
println()
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package racingcar.view

import camp.nextstep.edu.missionutils.Console

class InputView {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일반적인 형식의 class로 만드시는 것보다 object class 형식으로 정의하시면 더 보기 좋은 코드가 될 거 같습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InputView만 따로 view 폴더에 빼신 이유가 궁금합니다.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InputView만 따로 view 폴더에 빼신 이유가 궁금합니다.

과제 마지막에 MVC를 조금이라도 적용해보려다 보니 입력 기능만 따로 빼게 되었습니다

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일반적인 형식의 class로 만드시는 것보다 object class 형식으로 정의하시면 더 보기 좋은 코드가 될 거 같습니다!

찾아보니 싱글톤 객체라는 개념이 있네요! 감사합니다.

fun getInputCarNames(): String {
println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")
val carNamesInput = Console.readLine()

return carNamesInput
}

fun getInputRound(): String {
println("시도할 횟수는 몇 회인가요?")
val roundInput = Console.readLine()

return roundInput
}
}
22 changes: 22 additions & 0 deletions src/test/kotlin/racingcar/CarAndRacingGameCreationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package racingcar

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

class CarAndRacingGameCreationTest {
@Test
fun `Car 및 Game 생성 기능 테스트`() {
// given
val testNames = listOf("pobi", "woni", "jun")
val testRound = 5

// when
val carInstances = createCars(testNames)
val racingGame = RacingGame(testRound, carInstances)

// then
assertThat(carInstances).extracting("name").containsOnly("pobi", "woni", "jun")
assertThat(racingGame.round).isEqualTo(testRound)
assertThat(racingGame.cars).isEqualTo(carInstances)
}
}
92 changes: 92 additions & 0 deletions src/test/kotlin/racingcar/InputProcessingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package racingcar

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

class InputProcessingTest {
@Test
fun `자동차 이름 입력 분할 기능 테스트`() {
// given
val testInput = "miso,seroi,hasta"

// when
val splitInput = splitCarName(testInput)

// then
assertThat(splitInput).containsOnly("miso", "seroi", "hasta")
}

@Test
fun `차수 입력 정수 변환 기능 테스트`() {
// given
val testInput = "5"

// when
val intInput = stringToInt(testInput)

// then
assertThat(intInput).isEqualTo(5)
}

@Test
fun `차수 입력 빈 문자열 테스트`() {
// given
val testInput = ""

// when
val intInput = stringToInt(testInput)

// then
assertThat(intInput).isEqualTo(0)
}

@Test
fun `차수 입력 whitespace 테스트`() {
// given
val testInput = " "

// when
val intInput = stringToInt(testInput)

// then
assertThat(intInput).isEqualTo(0)
}

@Test
fun `입력 분할 5글자 초과 예외 테스트`() {
// given
val testInput = "miso,saeroi,hasta"

// when
val exception = assertThrows<IllegalArgumentException> { splitCarName(testInput) }

// then
assertThat(exception.message).isEqualTo("이름은 5글자를 초과하거나 비어있을 수 없습니다.")
}

@Test
fun `입력 분할 빈 문자열 예외 테스트`() {
// given
val testInput = "miso,seroi,hasta,"

// when
val exception = assertThrows<IllegalArgumentException> { splitCarName(testInput) }

// then
assertThat(exception.message).isEqualTo("이름은 5글자를 초과하거나 비어있을 수 없습니다.")
}

@Test
fun `차수 입력 숫자가 아닌 문자 테스트`() {
// given
val testInput = "YourAreEverglowing"

// when
val exception = assertThrows<IllegalArgumentException> { stringToInt(testInput) }

// then
assertThat(exception.message).isEqualTo("숫자를 입력해주세요.")
}

}
Loading