-
Notifications
You must be signed in to change notification settings - Fork 56
[자동차 경주] 이지섭 미션 제출합니다. #57
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
base: main
Are you sure you want to change the base?
Changes from all commits
f3803a0
4d4a6d1
065286c
27584fd
72786bb
96ecb8c
432dfef
9fa34f0
22ae208
9493486
2a09f6a
0fa4713
ceb100b
3a809d9
bb7e5e1
231df92
1dcbe9b
4e5ec1e
16ca743
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,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 | ||
| ``` |
| 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 | ||
| } | ||
|
|
||
| fun printOutput(winners: List<Car>?) { | ||
|
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. 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()) { | ||
|
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. 반복문이 돌 때마다 isFinished()로 비교하는 것보다 또한 클래스 내부 메서드 호출 시에 |
||
| this.makeMove() | ||
| this.printRoundResult(cars) | ||
| this.increaseCurrentRound() | ||
| } | ||
| } | ||
|
|
||
| fun getWinners(): List<Car> { | ||
| check(this.isFinished()) { "게임이 아직 시작되지 않았습니다." } | ||
|
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. check를 통해 모든 라운드가 끝났는지 검사하시는 것 같습니다. 그런데, 이미 play 함수에서 isFinished를 사용해서 반복문을 검사하고 있는데 이 부분이 또 필요한 지 모르겠습니다.
Author
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.
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() | ||
| } | ||
| } | ||
| 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 { | ||
|
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. 일반적인 형식의 class로 만드시는 것보다 object class 형식으로 정의하시면 더 보기 좋은 코드가 될 거 같습니다! 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. InputView만 따로 view 폴더에 빼신 이유가 궁금합니다.
Author
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.
과제 마지막에 MVC를 조금이라도 적용해보려다 보니 입력 기능만 따로 빼게 되었습니다
Author
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 getInputCarNames(): String { | ||
| println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)") | ||
| val carNamesInput = Console.readLine() | ||
|
|
||
| return carNamesInput | ||
| } | ||
|
|
||
| fun getInputRound(): String { | ||
| println("시도할 횟수는 몇 회인가요?") | ||
| val roundInput = Console.readLine() | ||
|
|
||
| return roundInput | ||
| } | ||
| } | ||
| 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) | ||
| } | ||
| } |
| 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("숫자를 입력해주세요.") | ||
| } | ||
|
|
||
| } |
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.
빈 값을 입력하면 0으로 반환하도록 처리하셨는데,
이렇게 되면 게임 결과 계산 시 모든 유저가 우승으로 처리될 것 같습니다.
해당 케이스를 에러로 처리하지 않고 0을 반환하도록 하신 이유가 궁금합니다!
또한 가독성을 위해서 명시적으로 return을 사용하는 형태로 바꾸면 더 읽기 쉬울 것 같습니다!
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.
이건 제 요구사항 분석 과정에서 잘못된 판단이 들어간 것 같습니다. 말씀하신 대로 round 입력이 0이거나 들어오지 않을 경우에는 모종의 이유로 게임 개최는 했지만 진행할 수 없는 상황이라고 판단하고 모든 유저가 우승자가 되도록 구현했습니다.
지금 생각해보니 IllegalArgumentException을 발생시키거나 우승자가 없도록 하는 것이 더 올바른 구현일 것 같네요.