Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[자동차 경주] 이서현 미션 제출합니다. #111

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2a204a3
docs(README): 요구 사항 및 기능 명세 작성
nuyhhyun Oct 28, 2024
96c138b
docs(README): 입력 및 차고지 기능 명세 수정
nuyhhyun Oct 28, 2024
0a23b5e
feat(Input): 경주할 자동차 이름들 입력 기능 구현
nuyhhyun Oct 28, 2024
900f4ab
feat(Input): 시도 횟수 입력 기능 구현
nuyhhyun Oct 28, 2024
2a369f7
feat(Input): 입력 문자열 유효성 검증 기능 구현
nuyhhyun Oct 28, 2024
763a5a1
docs(README): 누락된 완료 기능 명세 체크 업데이트
nuyhhyun Oct 28, 2024
325eeff
docs($InputViewTest): 테스트 함수명 기능 명세대로 변경
nuyhhyun Oct 28, 2024
f31e983
feat(Input): 쉼표(,) 기준 이름 분리 기능 구현
nuyhhyun Oct 28, 2024
f90b595
docs(README): 불필요한 기능 명세 삭제
nuyhhyun Oct 28, 2024
8a3ca40
test($Car): 자동차 이름 부여 기능 구현 테스트
nuyhhyun Oct 28, 2024
4654d6c
feat($Car): 자동차 이름 유효성 검증 기능 구현
nuyhhyun Oct 28, 2024
a066cfe
refactor($InputViewTest): 테스트 코드 형식 수정
nuyhhyun Oct 28, 2024
5dd39f9
refactor($GarageTest): 테스트 코드 형식 수정
nuyhhyun Oct 28, 2024
1e479c5
refactor($CarTest): 테스트 코드 형식 수정
nuyhhyun Oct 28, 2024
fe67521
feat($Car): 자동차 위치 보유 기능 구현
nuyhhyun Oct 28, 2024
e086501
feat($Car): 전진 조건에 따른 전진 기능 구현
nuyhhyun Oct 28, 2024
79115c0
docs($README): 기능 명세 명확하게 수정
nuyhhyun Oct 28, 2024
9c94695
feat($RandomNumber): 랜덤 수 생성 기능 구현
nuyhhyun Oct 28, 2024
c451088
feat($Race): 시도 횟수만큼 전진 기능 구현
nuyhhyun Oct 28, 2024
196bdfd
docs($README): 출력 기능 명세 위치 수정
nuyhhyun Oct 28, 2024
04aeb7d
feat(Output): 현재 경주 상황 출력 기능 구현
nuyhhyun Oct 28, 2024
6122a1c
feat($Race): 우승자 선정 기능 구현
nuyhhyun Oct 28, 2024
603e152
docs($README): 다중 우승자 기능 명세 분리
nuyhhyun Oct 28, 2024
ca9c7c5
test($RaceTest): 다중 우승자 기능 테스트
nuyhhyun Oct 28, 2024
99ec1d2
feat(Output): 우승자 출력 기능 구현
nuyhhyun Oct 28, 2024
5c4598a
docs($README): 시도 횟수 유효성 검증 기능 명세 위치 수정
nuyhhyun Oct 28, 2024
abcca82
feat(InputView): 시도 횟수 유효성 검증 기능 구현
nuyhhyun Oct 28, 2024
7d88d81
feat($Exception): 예외 처리 기능 구현
nuyhhyun Oct 28, 2024
76a4be5
feat($RaceManager): 컨트롤러 구현
nuyhhyun Oct 28, 2024
994d58d
style(formating): 코드 포맷팅
nuyhhyun Oct 28, 2024
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
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,86 @@
# kotlin-racingcar-precourse
# 🏎 kotlin-racingcar-precourse 🏎

> 초간단 자동차 경주 게임을 구현한다.
***

## ❗️요구 사항

- **주어진 횟수** 동안 **n대**의 자동차는 **전진 또는 멈출** 수 있다.
- 각 자동차에 **이름을 부여**할 수 있다.
- **전진**하는 자동차를 출력할 때 자동차 **이름을 같이 출력**한다.
- 예:
```
실행 결과
pobi : --
woni : ----
jun : ---

pobi : --
woni : -
jun : --

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

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

pobi : -----
woni : ----
jun : -----
```
- **자동차 이름은 쉼표(,)를 기준으로 구분**하며 **이름은 5자 이하**만 가능하다.
- 예:
```
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
```
- 사용자는 **몇 번의 이동을 할 것인지를 입력**할 수 있어야 한다.
- 예:
```
시도할 횟수는 몇 회인가요?
5
```
- **전진하는 조건**은 **0에서 9 사이에서 무작위 값**을 구한 후 무작위 값이 **4 이상**일 경우이다.
- 자동차 경주 게임을 완료한 후 **누가 우승했는지를 알려준다**.
- 예: `최종 우승자 : pobi`
- 우승자는 **한 명 이상**일 수 있다.
- 예: `최종 우승자 : pobi, jun`
- **우승자**가 여러 명일 경우 **쉼표(,)를 이용하여 구분**한다.
- 사용자가 **잘못된 값을 입력**할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.

## ✅ 기능 명세

### 입력
- [X] 경주할 자동차 이름들을 담은 문자열을 입력할 수 있다.
- [X] 자동차는 두 대 이상 입력해야 한다.
- [X] 시도 횟수를 입력할 수 있다.
- [X] 시도 횟수는 양의 정수이어야 한다.

### 출력
- [X] 경주할 자동차 입력을 안내할 수 있다.
- [X] 시도 횟수 입력을 안내할 수 있다.
- [X] 각 시도마다 각 자동차의 이름과 전진한 횟수만큼의 붙임표(-)를 함께 알려준다.
- [X] 우승자를 출력할 수 있다. 우승자가 여러 명일 경우 쉼표(,)를 이용하여 우승자를 구분한다.

### 자동차
- [X] 자동차에 이름을 부여할 수 있다.
- [X] 자동차 이름은 5자 이하이어야 한다.
- [X] 자동차는 본인의 위치를 가질 수 있다.
- [X] 무작위 값이 4 이상인 경우 전진하고 아니면 멈춰있는다.

### 차고지
- [X] 쉼표(,)를 기준으로 각 자동차 이름을 구분할 수 있다.

### 경주
- [X] 시도 횟수만큼 n대의 자동차를 전진하거나 멈출 수 있다.
- [X] 우승자를 구할 수 있다.
- [X] 우승자는 한 명 이상일 수 있다.

### 무작위 값
- [X] 0에서 9 사이에서 무작위 값을 구할 수 있다.

### 예외
- [X] 사용자가 잘못된 값을 입력한 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션을 종료시킬 수 있다.
4 changes: 3 additions & 1 deletion src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package racingcar

import racingcar.Controller.RaceManager

fun main() {
// TODO: 프로그램 구현
RaceManager().startRace()
}
37 changes: 37 additions & 0 deletions src/main/kotlin/racingcar/Controller/RaceManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package racingcar.Controller

import racingcar.Model.Garage
import racingcar.Model.Race
import racingcar.Model.RandomNumber
import racingcar.Model.RandomNumberGenerator
import racingcar.view.InputView
import racingcar.view.OutputView

class RaceManager {
val inputView = InputView()
val outputView = OutputView()

fun startRace() {
val nameOfCars = inputView.getNameOfCars().toString()
val tryCounts = inputView.getTryCounts().toString().toInt()

playRace(nameOfCars, tryCounts)
}

private fun playRace(nameOfCars: String, tryCounts: Int) {
val carsInGarage = Garage(nameOfCars).carsInGarage
val randomNumberGenerator = RandomNumber
val race = Race(carsInGarage)

println()
outputView.printRoundMessage()
repeat(tryCounts) {
race.playOneRound(randomNumberGenerator)
outputView.printCurrentRound(carsInGarage)
println()
Comment on lines +26 to +31
Copy link

Choose a reason for hiding this comment

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

MVC 패턴을 사용하셔서 입출력에 관해서는 View에 책임을 부여하신 것 같아요. 그렇다면 RaceManager에서는 출력과 관련된 기능은 View로 분리 시키는 게 더 좋을 것 같아요!!

Copy link
Author

Choose a reason for hiding this comment

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

안그래도 개행 고민 중이었는데 분리하려면 확실히 하는 게 낫겠네요 감사합니다!

}

val winner = race.getWinner()
outputView.printWinner(winner)
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/racingcar/Model/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package racingcar.Model

class Car(val name: String) {
init {
require(name.length <= 5) { name + CANT_BE_LONGER_THAN_5 }
Copy link

Choose a reason for hiding this comment

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

차의 이름이 빈칸("")인 경우의 예외 처리가 필요할 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

아 그 생각은 못했네요!!

}

var position: Int = 0
private set

fun moveForward(randomValue: Int) {
if (randomValue >= MOVING_POINT) {
position++
}
}

companion object {
private const val CANT_BE_LONGER_THAN_5 = " -> 자동차 이름은 5자 이하이어야 합니다."
private const val MOVING_POINT = 4
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/racingcar/Model/Exception.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package racingcar.Model

object Exception {
Copy link

Choose a reason for hiding this comment

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

해당 오브젝트는 비즈니스 로직과 밀접한 데이터의 개념은 아니니 모델로 분리하는 게 아니라 유틸리티로 분리하는게 좋을 것 같아요

Copy link
Author

Choose a reason for hiding this comment

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

이거 고민했었는데 유틸 분리 방법이 있군요!

fun errorComesUpWith(errorMessage: String) {
throw IllegalArgumentException(errorMessage)
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/racingcar/Model/Garage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package racingcar.Model

private const val CAR_NAME_SPLIT_POINT = ','

class Garage(nameOfCars: String) {
val carsInGarage: List<Car> = nameOfCars.split(CAR_NAME_SPLIT_POINT)
.map { name -> Car(name.trim()) }
Comment on lines +6 to +7
Copy link

Choose a reason for hiding this comment

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

차량의 이름이 서로 같으면 안된다는 문제의 요구사항 명세가 있었는데 그 부분에 대한 구현이 아마 안된 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

헉 저 그 명세는 못봤었는데 그 예외도 있네요 이번주차에는 예외 사항 더 고민해봐야겠습니다!ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

오늘 내로 맞리뷰 달아두겠습니다!

}

Choose a reason for hiding this comment

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

차고라는 클래스명 센스 있네요.
trim으로 예외가 발생하지 않도록 처리하신 것도 좋은 것 같아요.

20 changes: 20 additions & 0 deletions src/main/kotlin/racingcar/Model/Race.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package racingcar.Model

class Race(carsInGarage: List<Car>) {
private val racingCars: List<Car> = carsInGarage

fun playOneRound(numberGenerator: RandomNumberGenerator): List<Car> {
racingCars.forEach { car ->
car.moveForward(numberGenerator.generateRandomNumber())
}

return racingCars
}

fun getWinner(): List<Car> {
val maxPosition = racingCars.maxBy { it.position }.position

return racingCars.filter { it.position == maxPosition }
}

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

import camp.nextstep.edu.missionutils.Randoms

private const val MIN_NUMBER = 0
private const val MAX_NUMBER = 9

fun interface RandomNumberGenerator {
fun generateRandomNumber(): Int
}

object RandomNumber : RandomNumberGenerator {
override fun generateRandomNumber(): Int {
return Randoms.pickNumberInRange(MIN_NUMBER, MAX_NUMBER)
}
}

Choose a reason for hiding this comment

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

랜덤 함수 하나도 유지보수와 확장에 신경쓰신 것 같아요.
하나 배워갑니다!

46 changes: 46 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package racingcar.view

import camp.nextstep.edu.missionutils.Console
import racingcar.Model.Exception

class InputView {
private val outputView = OutputView()
private val exception = Exception

fun getNameOfCars(): Any? {
outputView.enterNameOfCars()
val nameOfCars = Console.readLine()
return if (isNameOfCarsValid(nameOfCars)) {
nameOfCars
} else {
exception.errorComesUpWith(INVAID_CAR_NAMES_INPUT)
}
}

fun isNameOfCarsValid(nameOfCar: String?): Boolean {
if (nameOfCar != null) {
val regex = Regex(".+,.+")
return regex.containsMatchIn(nameOfCar)
} else
return false

Choose a reason for hiding this comment

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

아마 깜빡하신 것 같지만 여기 else만 {}가 안 붙어있어요!

Copy link
Author

Choose a reason for hiding this comment

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

와 대박 이걸 몰랐네요 감사합니다!

}

fun getTryCounts(): Any {
outputView.enterTryCounts()
val tryCounts = Console.readLine().toIntOrNull() ?: 0
return if (isTryCountsValid(tryCounts)) {
tryCounts
} else {
exception.errorComesUpWith(INVAID_TRY_COUNTS_INPUT)
}
}

private fun isTryCountsValid(tryCounts: Int): Boolean {
return tryCounts > 0
}

companion object {
private const val INVAID_CAR_NAMES_INPUT = "두 대 이상의 자동차들을 쉼표(,)를 기준으로 구분하여 입력해주세요."
private const val INVAID_TRY_COUNTS_INPUT = "양의 정수인 시도 횟수를 입력해주세요."
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/racingcar/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package racingcar.view

import racingcar.Model.Car

class OutputView {
fun enterNameOfCars() {
println(ENTER_NAME_OF_CARS)
}

fun enterTryCounts() {
println(ENTER_TRY_COUNTS)
}

fun printRoundMessage() {
println(ROUND_MESSAGE)
}

fun printCurrentRound(cars: List<Car>) {
for (car in cars) {
print("${car.name} : ")
repeat(car.position) {
print(ONE_STEP)
}
println()
}
}

fun printWinner(winners: List<Car>) {
val nameOfWinners = winners.joinToString(CAR_NAME_SPLIT_POINT) { it.name }
println(FINAL_WINNERS + nameOfWinners)
}

companion object {
private const val ENTER_NAME_OF_CARS = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
private const val ENTER_TRY_COUNTS = "시도할 횟수는 몇 회인가요?"
private const val ROUND_MESSAGE = "실행 결과"
private const val ONE_STEP = "-"
private const val FINAL_WINNERS = "최종 우승자: "
private const val CAR_NAME_SPLIT_POINT = ", "
}
}
87 changes: 87 additions & 0 deletions src/test/kotlin/modelTest/CarTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package modelTest

import camp.nextstep.edu.missionutils.Randoms
import camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest
import camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import racingcar.Model.Car

private const val CANT_BE_LONGER_THAN_5 = " -> 자동차 이름은 5자 이하이어야 합니다."

class CarTest {
@Test
fun `자동차에 이름을 부여할 수 있다`() {
assertSimpleTest {
//given
val nameOfCar = "hyun"

//when
val car = Car(nameOfCar)

//then
assertThat(car.name).isEqualTo(nameOfCar)
}
}

@Test
fun `자동차 이름은 5자 이하이어야 한다`() {
assertSimpleTest {
//given
val nameOfCar = "This_is_more_longer_than_5"

//when
val error = assertThrows<IllegalArgumentException> { Car(nameOfCar) }

//then
assertThat(error.message).isEqualTo(nameOfCar + CANT_BE_LONGER_THAN_5)
}
}

@CsvSource("0, 0", "4, 1", "9, 1")
@ParameterizedTest
fun `무작위 값이 4 이상인 경우 전진하고 아니면 멈춰있는다`(randomNumber: Int, expectedPosition: Int) {
assertRandomNumberInRangeTest(
{
//given
val car = Car("hyun")

//when
car.moveForward(randomNumber)

//then
assertThat(car.position).isEqualTo(expectedPosition)

},
MOVING_FORWARD, STOP
)
}

@Test
fun `자동차는 본인의 위치를 가질 수 있다`() {
assertRandomNumberInRangeTest(
{
//given
val car = Car("hyun")
val moveCounts = Randoms.pickNumberInRange(1, Int.MAX_VALUE)

//when
repeat(moveCounts) {
car.moveForward(MOVING_FORWARD)
}

//then
assertThat(car.position).isEqualTo(moveCounts)
},
MOVING_FORWARD, STOP
)
}

companion object {
private const val MOVING_FORWARD = 4
private const val STOP: Int = 3
}
}
Loading