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

[자동차 경주] 이지은 미션 제출합니다. #92

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
692eedc
docs($README.md): 구현할 기능 목록 정리
jieeeunnn Oct 28, 2024
6d589c9
feat(InputView.kt): 사용자 입력을 위한 InputView 클래스 생성
jieeeunnn Oct 28, 2024
b63d33e
feat(InputView.kt): 자동차 이름 입력 함수 구현
jieeeunnn Oct 28, 2024
67c6b08
feat(InputView.kt): 자동차 이름 빈값 유효성 검사 함수 구현
jieeeunnn Oct 28, 2024
ba42179
feat(InputView.kt): 자동차 이름 입력 시 구분자 검사 함수 구현
jieeeunnn Oct 28, 2024
db4006b
feat(InputView.kt): 자동차 이름을 구분자 기준으로 분리 함수 구현
jieeeunnn Oct 28, 2024
9d419aa
feat(InputView.kt): 각 자동차 이름 길이 검사 함수 구현
jieeeunnn Oct 28, 2024
7a0e5e8
feat(InputView.kt): 자동차 이름 중복 검사 구현
jieeeunnn Oct 28, 2024
b63257d
feat(InputView.kt): 자동차 이름 유효성 검사 함수 구현
jieeeunnn Oct 28, 2024
dfe1885
feat(InputView.kt): 자동차 이름 입력 함수에서 이름 반환
jieeeunnn Oct 28, 2024
4591953
feat(InputView.kt): 시도할 횟수를 입력받는 함수 구현
jieeeunnn Oct 28, 2024
6c2836f
feat(InputView.kt): 시도할 횟수를 정수로 변환해서 반환
jieeeunnn Oct 28, 2024
041df0d
feat(InputView.kt): 입력받은 횟수 유효성 검사 함수 구현
jieeeunnn Oct 28, 2024
2b65ab7
feat(Car.kt): 자동차의 상태와 동작을 관리하는 Car 클래스 생성
jieeeunnn Oct 28, 2024
1913e6e
feat(Car.kt): 자동차의 이동 거리를 증가시키는 move 함수 구현
jieeeunnn Oct 28, 2024
127ef0e
feat(Game.kt): 자동차 경주 게임의 상태를 관리하는 Game 클래스 생성
jieeeunnn Oct 28, 2024
421355e
feat(Game.kt): 자동차 리스트를 받아 게임을 플레이하는 함수 세팅
jieeeunnn Oct 28, 2024
53fb097
feat(Game.kt): 랜덤값에 따라 전진을 결정하는 함수 구현
jieeeunnn Oct 28, 2024
8278c49
feat(Game.kt): 자동차 리스트를 반환하는 함수 구현
jieeeunnn Oct 28, 2024
e138b07
feat(Game.kt): 우승자를 반환하는 함수 구현
jieeeunnn Oct 28, 2024
78ae6c7
feat(ResultView.kt): 결과를 보여줄 ResultView 클래스 생성
jieeeunnn Oct 28, 2024
f1f00a7
feat(ResultView.kt): 라운드 별 진행상황 출력 함수 구현
jieeeunnn Oct 28, 2024
7c7ea82
feat(ResultView.kt): 최종 우승자 출력 함수 구현
jieeeunnn Oct 28, 2024
2a3934b
feat(RacingGameController.kt): View와 Model을 관리할 Controller 클래스 구현
jieeeunnn Oct 28, 2024
099f569
feat(RacingGameController.kt): inputView의 함수를 통해 라운드 입력받기
jieeeunnn Oct 28, 2024
d68dd49
feat(RacingGameController.kt): 입력받은 round 횟수만큼 게임 진행
jieeeunnn Oct 28, 2024
6d3f45a
feat(RacingGameController.kt): 게임이 끝나면 최종 우승자 출력
jieeeunnn Oct 28, 2024
305bf6f
feat(Application.kt): controller를 통해 게임 시작
jieeeunnn Oct 28, 2024
e57a824
docs($README.md): 코드 구조 설명 추가
jieeeunnn Oct 28, 2024
a0aa107
test(CarNameInputTest.kt): 자동차 이름 입력 테스트 코드 작성
jieeeunnn Oct 28, 2024
87814f2
test(RoundInputTest.kt): 시도 횟수 입력 테스트 코드 작성
jieeeunnn Oct 28, 2024
9cd3d3e
test(GameTest.kt): 게임 진행 테스트 코드 작성
jieeeunnn Oct 28, 2024
2317482
fix(InputView.kt): 이름 최대 글자수 상수 선언
jieeeunnn Oct 28, 2024
1216c00
del(CarNameInputTest.kt): CarNameInputTest 클래스 삭제
jieeeunnn Oct 28, 2024
8cec194
del(RoundInputTest.kt): RoundInputTest 클래스 삭제
jieeeunnn Oct 28, 2024
57633b8
del(GameTest.kt): GameTest 클래스 삭제
jieeeunnn Oct 28, 2024
d02cf8b
feat(InputView.kt): 자동차 이름 숫자 유효성 검사 함수 구현
jieeeunnn 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
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
# kotlin-racingcar-precourse
# 프리코스 2주차 _ 자동차 경주

## 자동차 경주

📍구현할 기능 목록
Copy link

Choose a reason for hiding this comment

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

어딘가에서 전해들은 팁인데 체크리스트로 요구사항을 명세하고 해당 기능을 구현한 후의 커밋에 readme의 체크리스트를 [x]로 수정하여 제출하는 방식을 권장한다고 했었던 것 같아요.
저도 이 내용을 저번 주차 이후에 듣고 적용해보았더니 요구사항을 실수로 놓치거나 하는 일이 안 생겨서 좋더라구요.
괜찮다는 방식이란 생각이 드시면 지은님께서도 한번 이 방식을 적용해보시면 좋을 것 같아요!!

Copy link
Author

Choose a reason for hiding this comment

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

이슈나 PR에서만 체크리스트 방식을 생각했었는데 그런 방식도 있겠군요! 공유 감사합니다 :)

1. 경주할 자동차 이름 입력받기
2. 입력받은 자동차 이름 유효성 검증

- 잘못된 값을 입력할 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션 종료
1. 이름을 5자 초과하여 입력한 경우
2. 빈 값을 입력한 경우
3. 쉼표가 아닌 구분자를 사용한 경우 (구분자를 쉼표(,)만 인식하도록 하여 하나의 문자열로 처리)
4. 자동차 이름이 중복된 경우

3. 시도할 횟수 입력받기
4. 입력받은 시도 횟수 유효성 검증

- 잘못된 값을 입력할 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션 종료
1. 음수를 입력한 경우
2. 0을 입력한 경우
3. 문자열을 입력한 경우
4. 입력한 수가 Int 범위를 벗어나는 경우
Copy link

Choose a reason for hiding this comment

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

integer 범위를 벗어나는 경우는 생각 못했네요!!


5. 0부터 9 사이의 랜덤값을 통해 전진 유무를 결정
6. 입력받은 시도 횟수만큼 반복
7. 실행 결과 출력
8. 전진 횟수가 가장 많은 자동차 판별
9. 최종 우승자 출력


## 코드 구조
MVC (모델 - 뷰 - 컨트롤러) 패턴을 사용하여 코드 구조 설계

### Model
1. Car
- 역할 : 자동차의 상태와 동작을 정의
- 주요 메서드
- `move()` : 자동차가 전진할 때 호출되는 메서드로 거리를 증가시킴
2. Game
- 역할 : 자동차 경주 게임의 상태를 관리
- 주요 메서드
- `playRound()` : 각 자동차가 이동하는 게임 라운드를 실행
- `getCars()` : 현재 게임에 참여하고 있는 자동차 리스트를 반환
- `getWinners()` : 현재까지의 라운드에서 가장 멀리 이동한 자동차를 찾아 그 이름을 리스트로 반환
- `isMoveForward()` : Ramdon값을 사용하여 자동차의 이동 여부를 결정

### View
1. InputView
- 역할 : 사용자로부터 입력을 받고, 해당 입력의 유효성을 검증
- 주요 메서드
- `inputCarNames()` : 사용자에게 자동차 이름을 입력받음
- `inputRounds()` : 사용자에게 시도할 라운드 수를 입력받음
- `validateEmptyInput()` : 입력이 비어있거나 공백만 포함된 경우 유효성 검사
- `validateSeparator()` : 입력 문자열이 유효한 쉼표 구분 형식인지 확인
- `validateCarNames()` : 자동차 이름의 유효성을 검사하는 메서드로, 이름의 길이와 중복 여부를 확인
- `validateNameLength()` : 자동차 이름의 길이가 1자 이상 5자 이하인지 확인
- `validateDuplicateNames()` : 자동차 이름 리스트에서 중복된 이름이 없는지 확인
- `splitAndTrimNames()` : 입력 문자열을 쉼표로 분리하고, 각 이름의 앞뒤 공백을 제거하여 리스트로 반환
- `parseRounds()` : 입력 문자열을 정수로 변환하고, 변환된 값이 유효한 라운드 수인지 확인
- `validatePositiveRounds()` : 라운드 수가 1 이상, 정수 범위 내에 있는지 확인
2. ResultView
- 역할 : 자동차 경주 게임의 결과를 출력
- 주요 메서드
- `printRoundResult()` : 각 자동차의 이름과 이동 거리를 출력
- `printWinners()` : 최종 우승자 목록을 출력

### Controller
1. RacingGameController
- 역할 : 자동차 경주 게임의 주요 로직을 관리하며 View와 Model 간의 상호작용을 조정
- 주요 메서드
- `startGame()` : 게임을 시작하는 메서드
7 changes: 5 additions & 2 deletions src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package racingcar

import racingcar.controller.RacingGameController

fun main() {
// TODO: 프로그램 구현
}
val controller = RacingGameController()
controller.startGame()
}
23 changes: 23 additions & 0 deletions src/main/kotlin/racingcar/controller/RacingGameController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.controller

import racingcar.model.Game
import racingcar.view.InputView
import racingcar.view.ResultView

class RacingGameController(
private val inputView: InputView = InputView(),
private val resultView: ResultView = ResultView(),
private val racingGame: Game = Game(inputView.inputCarNames())
) {
fun startGame() {
val rounds = inputView.inputRounds()

repeat(rounds) {
racingGame.playRound()
resultView.printRoundResult(racingGame.getCars())
}

val winners = racingGame.getWinners()
resultView.printWinners(winners)
}
Comment on lines +12 to +22
Copy link

Choose a reason for hiding this comment

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

이렇게 보니 컨트롤러의 책임을 최소화하여 뷰와 도메인의 중재자 역할만 하게 하는 것도 전체적인 흐름을 파악하는데에 어려움이 전혀 없네요.

제가 작성한 것보다 훨씬 구조를 파악하기 쉽고 책임 분리가 잘 된 객체지향적인 구현인 것 같습니다.

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

class Car(val name: String) {
var distance: Int = 0
private set

fun move() {
distance++
}
}
35 changes: 35 additions & 0 deletions src/main/kotlin/racingcar/model/Game.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package racingcar.model

import camp.nextstep.edu.missionutils.Randoms

class Game(carNames: List<String>) {
private val cars = carNames.map { Car(it) }

fun playRound() {
cars.forEach { car ->
if (isMoveForward()) {
car.move()
}
}
}

private fun isMoveForward(): Boolean {
return Randoms.pickNumberInRange(MIN_MOVE_THRESHOLD, MAX_MOVE_THRESHOLD) >= MOVE_THRESHOLD
}

fun getCars(): List<Car> {
return cars
}

fun getWinners(): List<String> {
val maxDistance = cars.maxOf { it.distance }

return cars.filter { it.distance == maxDistance }.map { it.name }
Comment on lines +25 to +27

Choose a reason for hiding this comment

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

바로 리턴해서 코드가 간결하게 표현하는 점도 좋지만, 이렇게 변수를 선언 후 값을 할당해서 리턴하는 방법을 통하면 좀 더 명확해지고, 디버깅시 이점이 있다고 합니다~!😀 아마도 취향차이이지 않으까 싶기도 하네요~

Suggested change
val maxDistance = cars.maxOf { it.distance }
return cars.filter { it.distance == maxDistance }.map { it.name }
val winners = cars.filter { it.distance == maxDistance }.map { it.name }
return winners

Copy link
Author

Choose a reason for hiding this comment

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

간결한 코드들은 바로 리턴해주는 습관이 있었는데 값을 할당하면 이런 장점들이 있군요 ㅎㅎ 이런 부분도 더 신경을 써봐야겠네요
감사합니다 !

}

companion object {
const val MOVE_THRESHOLD = 4
const val MIN_MOVE_THRESHOLD = 0
const val MAX_MOVE_THRESHOLD = 9
}
}
91 changes: 91 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package racingcar.view

import camp.nextstep.edu.missionutils.Console

class InputView {
Copy link

Choose a reason for hiding this comment

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

View는 사용자에게 보여지는 화면의 기능만을 해야한다고 생각하기에 Validator와 Parser의 기능의 경우 다른 클래스로 분리하는 것이 좋을 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

다음 주차엔 조금 더 세부적으로 나눠보는 연습을 해야겠네요 ㅎㅎ 피드백 감사합니다 !


fun inputCarNames(): List<String> {
println(MESSAGE_ENTER_CAR_NAMES)
val input = Console.readLine().orEmpty()
validateEmptyInput(input)
validateSeparator(input)

val names = splitAndTrimNames(input)
validateCarNames(names)

return names
}

fun inputRounds(): Int {
println(MESSAGE_ENTER_ROUNDS)
val input = Console.readLine().orEmpty()
println()

return parseRounds(input)
}

private fun validateEmptyInput(input: String) {
if (input.isBlank()) throw IllegalArgumentException(ERROR_EMPTY_INPUT)
}

private fun validateSeparator(input: String) {
val names = input.split(COMMA).map { it.trim() }
require(names.isNotEmpty() && names.all { it.isNotBlank() }) {
ERROR_INVALID_SEPARATOR
}
}

private fun splitAndTrimNames(input: String): List<String> {
return input.split(COMMA).map { it.trim() }
}

private fun validateCarNames(names: List<String>) {
validateNameLength(names)
validateDuplicateNames(names)
validateNumericNames(names)
}

private fun validateNameLength(names: List<String>) {
require(names.all { it.isNotBlank() && it.length <= MAX_NAME_LENGTH }) {
ERROR_NAME_LENGTH
}
}

private fun validateDuplicateNames(names: List<String>) {
require(names.distinct().size == names.size) {
ERROR_DUPLICATE_NAMES
}
}

private fun validateNumericNames(names: List<String>) {
require(names.none { it.all { char -> char.isDigit() } }) {
ERROR_NUMERIC_NAMES
}
}


private fun parseRounds(input: String): Int {
val rounds = input.toIntOrNull() ?: throw IllegalArgumentException(ERROR_INVALID_NUMBER)
validatePositiveRounds(rounds)

return rounds
}

private fun validatePositiveRounds(rounds: Int) {
require(rounds in 1..Int.MAX_VALUE) { ERROR_NON_POSITIVE_ROUNDS }
}

companion object {
const val MESSAGE_ENTER_CAR_NAMES = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
const val MESSAGE_ENTER_ROUNDS = "시도할 횟수는 몇 회인가요?"
const val ERROR_EMPTY_INPUT = "자동차 이름을 입력해야 합니다."
const val ERROR_INVALID_SEPARATOR = "자동차 이름은 쉼표(,)로 구분해야 합니다."
const val ERROR_NAME_LENGTH = "자동차 이름은 1자 이상 5자 이하여야 합니다."
const val ERROR_DUPLICATE_NAMES = "자동차 이름은 중복될 수 없습니다."
const val ERROR_INVALID_NUMBER = "이동 횟수는 숫자로 입력해야 합니다."
const val ERROR_NON_POSITIVE_ROUNDS = "입력한 이동 횟수가 범위를 벗어났습니다."
const val ERROR_NUMERIC_NAMES = "자동차 이름은 숫자일 수 없습니다."
const val COMMA = ","
const val MAX_NAME_LENGTH = 5
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/racingcar/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package racingcar.view

import racingcar.model.Car

class ResultView {

fun printRoundResult(cars: List<Car>) {
cars.forEach { car ->
println("${car.name} : ${"-".repeat(car.distance)}")
}
println()
}

fun printWinners(winners: List<String>) {
println("$MESSAGE_WINNERS: ${winners.joinToString(", ")}")
}

companion object {
const val MESSAGE_WINNERS = "최종 우승자"
}
}