-
Notifications
You must be signed in to change notification settings - Fork 201
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
[자동차 경주] 박지훈 미션 제출합니다. #219
base: main
Are you sure you want to change the base?
Changes from all commits
fcd6d04
c1b25aa
820021b
e40c436
2d31c07
c9e35fd
50864d5
f6c61c3
16d68fc
76ef2c2
ccab4a2
856129a
d3f668d
31f70de
1858222
aada946
62510db
0f9d531
c4e779a
50fe83d
a6ef238
2cdf58d
9221afe
49ea1f5
fe2f6de
7f01e40
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 |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# 자동차 경주 게임 | ||
|
||
## 기능 목록 | ||
|
||
- [x] 쉼표를 기준으로 경주할 자동차 이름을 입력 받는다. | ||
- [x] 잘못된 값 입력 시 에러를 발생시킨 후 애플리케이션을 종료한다. | ||
- [x] 입력된 값이 1자 이상 5자 이하이고 영문자인지 알 수 있다. | ||
- [x] 이름 중복 여부를 알 수 있다. | ||
- [x] 몇 번의 이동을 할 것인지(시도할 횟수)를 입력 받는다. | ||
- [x] 잘못된 값 입력 시 에러를 발생시킨 후 애플리케이션을 종료한다. | ||
- [x] 숫자 이외의 값 입력 여부를 알 수 있다. | ||
- [x] 0~9까지의 랜덤한 값을 생성한다. | ||
- [x] 랜덤한 값이 4 이상인지 알 수 있다. | ||
- [x] 자동차를 전진할 수 있다. | ||
- [x] 모든 차수의 실행 결과를 알 수 있도록 자동차의 전진 유무와 상관없이 자동차의 진행 상황을 출력한다. | ||
- [x] 게임 완료 후 우승자를 알 수 있다. | ||
- [x] 우승자가 여러 명일 경우 쉼표를 이용해 구분한다. | ||
|
||
## 기능 요구 사항 | ||
|
||
초간단 자동차 경주 게임을 구현한다. | ||
|
||
주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. | ||
각 자동차에 이름을 부여할 수 있다. | ||
전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. | ||
자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. | ||
사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. | ||
전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. | ||
자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. | ||
우승자는 한 명 이상일 수 있다. | ||
우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. | ||
사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
package racingcar | ||
|
||
import racingcar.controller.GameController | ||
|
||
fun main() { | ||
// TODO: 프로그램 구현 | ||
val controller = GameController() | ||
controller.run() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package racingcar.constant | ||
|
||
object Constants { | ||
|
||
const val MIN_NAME_LENGTH = 1 | ||
const val MAX_NAME_LENGTH = 5 | ||
const val MIN_ATTEMPT_COUNT = 0 | ||
|
||
const val START_INCLUSIVE = 0 | ||
const val END_INCLUSIVE = 9 | ||
|
||
const val MIN_FORWARD_THRESHOLD = 4 | ||
|
||
const val CAR_NAME_PROMPT = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" | ||
const val ATTEMPTS_PROMPT = "시도할 횟수는 몇 회인가요?" | ||
const val EXECUTION_RESULT_STRING = "실행 결과" | ||
const val FINAL_WINNER_STRING = "최종 우승자 : " | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package racingcar.controller | ||
|
||
import racingcar.constant.Constants.EXECUTION_RESULT_STRING | ||
import racingcar.domain.CarStatusUpdater | ||
import racingcar.domain.NumberGenerator | ||
import racingcar.domain.RoundExecutor | ||
import racingcar.util.Validator | ||
import racingcar.view.InputView | ||
import racingcar.view.RaceView | ||
import racingcar.view.ResultView | ||
|
||
class GameController { | ||
|
||
private val carStatus = mutableMapOf<String, String>() | ||
|
||
fun run() { | ||
val (carNames, attemptCount) = getUserInput() | ||
println() | ||
Validator(carNames, attemptCount) | ||
play(carNames, attemptCount.toInt()) | ||
printResult() | ||
} | ||
|
||
private fun getUserInput(): Pair<ArrayList<String>, String> { | ||
val input = InputView() | ||
return Pair(input.getCarName(), input.getNumberOfAttemps()) | ||
} | ||
|
||
private fun play(carNames: ArrayList<String>, attemptCount: Int) { | ||
println(EXECUTION_RESULT_STRING) | ||
|
||
val roundExecutor = RoundExecutor() | ||
roundExecutor.executeRounds(carNames, carStatus, attemptCount) | ||
} | ||
|
||
private fun printResult() { | ||
val resultView = ResultView() | ||
resultView.printWinner(carStatus) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package racingcar.domain | ||
|
||
class CarStatusUpdater { | ||
fun moveCarForward(carStatus: MutableMap<String, String>, carName: String, randomNumber: Int) { | ||
val judgment = Judgment() | ||
|
||
if (judgment.canMoveForward(randomNumber)) { | ||
carStatus[carName] = carStatus[carName] + "-" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package racingcar.domain | ||
|
||
import racingcar.constant.Constants.MIN_FORWARD_THRESHOLD | ||
|
||
class Judgment { | ||
fun canMoveForward(randomNumber: Int): Boolean { | ||
return randomNumber >= MIN_FORWARD_THRESHOLD | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package racingcar.domain | ||
|
||
import camp.nextstep.edu.missionutils.Randoms | ||
import racingcar.constant.Constants.START_INCLUSIVE | ||
import racingcar.constant.Constants.END_INCLUSIVE | ||
|
||
class NumberGenerator { | ||
fun createRandomNumber(): Int { | ||
return Randoms.pickNumberInRange(START_INCLUSIVE, END_INCLUSIVE) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package racingcar.domain | ||
|
||
import racingcar.view.RaceView | ||
|
||
class RoundExecutor() { | ||
fun executeRounds(carNames: ArrayList<String>, carStatus: MutableMap<String, String>, attemptCount: Int) { | ||
val numberGenerator = NumberGenerator() | ||
val carStatusUpdater = CarStatusUpdater() | ||
val raceView = RaceView() | ||
|
||
carStatus.clear() | ||
carStatus.putAll(carNames.associateWith { "" }) | ||
|
||
var currentCount = 0 | ||
var randomNumber: Int | ||
while (currentCount < attemptCount) { | ||
for (car in carStatus) { | ||
randomNumber = numberGenerator.createRandomNumber() | ||
carStatusUpdater.moveCarForward(carStatus, car.key, randomNumber) | ||
} | ||
raceView.printCarProgress(carStatus) | ||
currentCount++ | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package racingcar.util | ||
|
||
import racingcar.constant.Constants.MAX_NAME_LENGTH | ||
import racingcar.constant.Constants.MIN_ATTEMPT_COUNT | ||
import racingcar.constant.Constants.MIN_NAME_LENGTH | ||
|
||
class Validator(carNames: ArrayList<String>, attemptCount: String) { | ||
|
||
init { | ||
isValidLengthAndLetter(carNames) | ||
checkIfDuplicateNameExists(carNames) | ||
isPositiveInteger(attemptCount) | ||
} | ||
|
||
private fun isValidLengthAndLetter(carNames: ArrayList<String>) { | ||
carNames.forEach { carName -> | ||
if (carName.length !in MIN_NAME_LENGTH..MAX_NAME_LENGTH || !carName.all { it.isLetter() }) { | ||
throw IllegalArgumentException("Car names must be alphabetic and have a length of 1 to 5 characters.") | ||
} | ||
} | ||
} | ||
|
||
private fun checkIfDuplicateNameExists(carNames: ArrayList<String>) { | ||
val uniqueNames = carNames.toSet() | ||
if (uniqueNames.size != carNames.size) { | ||
throw IllegalArgumentException("Car names must not be duplicated.") | ||
} | ||
} | ||
Comment on lines
+23
to
+28
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. 아 중복 검사를 이렇게 간단하게 구현할 수 있네요. 배움 주셔서 감사합니다! |
||
|
||
private fun isPositiveInteger(attemptCount: String) { | ||
val attemptCountInt = try { | ||
attemptCount.toInt() | ||
} catch (e: NumberFormatException) { | ||
throw IllegalArgumentException("Attempt count must be a positive integer.") | ||
} | ||
|
||
if (attemptCountInt <= MIN_ATTEMPT_COUNT) { | ||
throw IllegalArgumentException("Attempt count must be a positive integer.") | ||
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. 매직넘버는 상수화를 통해 잘 관리해주셨는데 출력문들도 enum클래스나 constant로 통합하여 관리하는 것도 하나의 방법이 될 수 있을 것 같아요! 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. 이번 3주차 과제에서는 말씀해주신 enum 클래스를 한번 사용해보겠습니다! |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package racingcar.view | ||
|
||
import camp.nextstep.edu.missionutils.Console | ||
import racingcar.constant.Constants.ATTEMPTS_PROMPT | ||
import racingcar.constant.Constants.CAR_NAME_PROMPT | ||
|
||
class InputView { | ||
fun getCarName(): ArrayList<String> { | ||
println(CAR_NAME_PROMPT) | ||
return ArrayList(Console.readLine().split(",").map { it.trim() }) | ||
} | ||
|
||
fun getNumberOfAttemps(): String { | ||
println(ATTEMPTS_PROMPT) | ||
return Console.readLine() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package racingcar.view | ||
|
||
class RaceView { | ||
fun printCarProgress(carStatus: MutableMap<String, String>) { | ||
for (car in carStatus) { | ||
println(car.key + " : " + car.value) | ||
} | ||
println() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package racingcar.view | ||
|
||
import racingcar.constant.Constants.FINAL_WINNER_STRING | ||
|
||
class ResultView { | ||
fun printWinner(carStatus: MutableMap<String, String>) { | ||
val longestDistance = carStatus.values.maxByOrNull { it.length }?.length | ||
val finalWinner = carStatus.filter { it.value.length == longestDistance }.keys | ||
print(FINAL_WINNER_STRING + finalWinner.joinToString(", ")) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package racingcar.domain | ||
|
||
import org.junit.jupiter.api.Assertions.* | ||
import org.junit.jupiter.api.Test | ||
import racingcar.constant.Constants.MIN_FORWARD_THRESHOLD | ||
|
||
class CarStatusUpdaterTest { | ||
private val carStatusUpdater = CarStatusUpdater() | ||
|
||
@Test | ||
fun `moveCarForward는 난수가 경계값 이상일 때 자동차를 전진`() { | ||
val carStatus = mutableMapOf("CAR" to "") | ||
carStatusUpdater.moveCarForward(carStatus, "CAR", MIN_FORWARD_THRESHOLD) | ||
|
||
assertEquals("-", carStatus["CAR"]) | ||
} | ||
|
||
@Test | ||
fun `moveCarForward는 난수가 경계값 미만일 때 자동차를 정지`() { | ||
val carStatus = mutableMapOf("CAR" to "") | ||
carStatusUpdater.moveCarForward(carStatus, "CAR", MIN_FORWARD_THRESHOLD - 1) | ||
|
||
assertEquals("", carStatus["CAR"]) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package racingcar.domain | ||
|
||
import org.junit.jupiter.api.Assertions.* | ||
import org.junit.jupiter.api.Test | ||
import racingcar.constant.Constants.MIN_FORWARD_THRESHOLD | ||
|
||
class JudgmentTest { | ||
private val judgment = Judgment() | ||
|
||
@Test | ||
fun `난수가 경계값 이상일 때 전진 가능`() { | ||
assertTrue(judgment.canMoveForward(MIN_FORWARD_THRESHOLD)) | ||
assertTrue(judgment.canMoveForward(MIN_FORWARD_THRESHOLD + 1)) | ||
} | ||
|
||
@Test | ||
fun `난수가 경계값 미만일 때 전진 불가능`() { | ||
assertFalse(judgment.canMoveForward(MIN_FORWARD_THRESHOLD - 1)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package racingcar.domain | ||
|
||
import org.junit.jupiter.api.Assertions.* | ||
import org.junit.jupiter.api.Test | ||
|
||
class NumberGeneratorTest { | ||
@Test | ||
fun `난수가 0부터 9까지의 범위에 있다`() { | ||
val numberGenerator = NumberGenerator() | ||
for (i in 1..100) { | ||
val randomNumber = numberGenerator.createRandomNumber() | ||
assertTrue(randomNumber in 0..9) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package racingcar.util | ||
|
||
import org.junit.jupiter.api.Assertions.* | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.MethodSource | ||
import org.junit.jupiter.params.provider.ValueSource | ||
|
||
class ValidatorTest { | ||
|
||
companion object { | ||
@JvmStatic | ||
fun provideCarNames(): List<Array<String>> { | ||
return listOf( | ||
arrayOf("AB##", "ABC"), | ||
arrayOf("A1B2", "ABC"), | ||
arrayOf("A B", "ABC"), | ||
) | ||
} | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideCarNames") | ||
fun `자동차 이름에 문자 이외의 요소가 있을 때 예외 발생`(carName1: String, carName2: String) { | ||
val carNames = arrayListOf(carName1, carName2) | ||
val exception = assertThrows<IllegalArgumentException> { | ||
Validator(carNames, "5") | ||
} | ||
assertEquals("Car names must be alphabetic and have a length of 1 to 5 characters.", exception.message) | ||
} | ||
|
||
@Test | ||
fun `자동차 이름의 길이가 5보다 클 때 예외 발생`() { | ||
val carNames = arrayListOf("ABCDEF", "ABC") | ||
val exception = assertThrows<IllegalArgumentException> { | ||
Validator(carNames, "5") | ||
} | ||
assertEquals("Car names must be alphabetic and have a length of 1 to 5 characters.", exception.message) | ||
} | ||
|
||
@Test | ||
fun `자동차 이름이 중복될 때 예외 발생`() { | ||
val carNames = arrayListOf("ABC", "ABC") | ||
val exception = assertThrows<IllegalArgumentException> { | ||
Validator(carNames, "5") | ||
} | ||
assertEquals("Car names must not be duplicated.", exception.message) | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = ["0", "-1", "숫자가아님"]) | ||
fun `시도할 횟수가 양의 정수가 아닐 때 예외 발생`(count: String) { | ||
val carNames = arrayListOf("ABC", "DEF") | ||
val exception = assertThrows<IllegalArgumentException> { | ||
Validator(carNames, count) | ||
} | ||
assertEquals("Attempt count must be a positive integer.", exception.message) | ||
} | ||
|
||
@Test | ||
fun `자동차 이름으로 한글을 입력할 때`() { | ||
val carNames = arrayListOf("대한민국", "캐나다") | ||
assertDoesNotThrow { | ||
Validator(carNames, "5") | ||
} | ||
} | ||
} |
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.
하나의 함수에서 두 가지 예외 처리를 한번에 하는 것도 좋지만 isValidLength()와 isValidLetter()로 나누어 함수는 한 가지 일을 하게 한다는 피드백을 반영하고, 에러 메세지의 구분된 출력을 통해 입력하는 사용자로 하여금 어떤 입력이 잘못된 것인지 확실하게 표현해주는 것도 방법이 될 수 있을 것 같아요!
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.
길이 검사는 너무 간단하다 생각해 문자 검사와 함께 검사를 했는데, 너무 간단해서 이것을 하나의 기능으로 착각했던 것 같습니다... ' 함수는 한 가지 일을 하게 한다 ' 라는 피드백에도 어긋나고, 사용자 입장에서 봤을 때도 분명 구분된 출력이 더 편할 것 같아요 공감합니다! 해당 피드백에 더욱 신경 쓰며 코딩하게 해주셔서 감사합니다!