-
Notifications
You must be signed in to change notification settings - Fork 108
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
base: main
Are you sure you want to change the base?
Changes from all commits
2a204a3
96c138b
0a23b5e
900f4ab
2a369f7
763a5a1
325eeff
f31e983
f90b595
8a3ca40
4654d6c
a066cfe
5dd39f9
1e479c5
fe67521
e086501
79115c0
9c94695
c451088
196bdfd
04aeb7d
6122a1c
603e152
ca9c7c5
99ec1d2
5c4598a
abcca82
7d88d81
76a4be5
994d58d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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`을 발생시킨 후 애플리케이션을 종료시킬 수 있다. |
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() | ||
} |
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() | ||
} | ||
|
||
val winner = race.getWinner() | ||
outputView.printWinner(winner) | ||
} | ||
} |
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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 차의 이름이 빈칸("")인 경우의 예외 처리가 필요할 것 같습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package racingcar.Model | ||
|
||
object Exception { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 오브젝트는 비즈니스 로직과 밀접한 데이터의 개념은 아니니 모델로 분리하는 게 아니라 유틸리티로 분리하는게 좋을 것 같아요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 고민했었는데 유틸 분리 방법이 있군요! |
||
fun errorComesUpWith(errorMessage: String) { | ||
throw IllegalArgumentException(errorMessage) | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 차량의 이름이 서로 같으면 안된다는 문제의 요구사항 명세가 있었는데 그 부분에 대한 구현이 아마 안된 것 같아요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 헉 저 그 명세는 못봤었는데 그 예외도 있네요 이번주차에는 예외 사항 더 고민해봐야겠습니다!ㅎㅎ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오늘 내로 맞리뷰 달아두겠습니다! |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 차고라는 클래스명 센스 있네요. |
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 } | ||
} | ||
|
||
} |
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) | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 랜덤 함수 하나도 유지보수와 확장에 신경쓰신 것 같아요. |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아마 깜빡하신 것 같지만 여기 else만 {}가 안 붙어있어요! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = "양의 정수인 시도 횟수를 입력해주세요." | ||
} | ||
} |
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 = ", " | ||
} | ||
} |
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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MVC 패턴을 사용하셔서 입출력에 관해서는 View에 책임을 부여하신 것 같아요. 그렇다면 RaceManager에서는 출력과 관련된 기능은 View로 분리 시키는 게 더 좋을 것 같아요!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안그래도 개행 고민 중이었는데 분리하려면 확실히 하는 게 낫겠네요 감사합니다!