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

[자동차 경주] 박지훈 미션 제출합니다. #219

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fcd6d04
docs: README.md 기능 목록 업데이트
bak40 Oct 28, 2023
c1b25aa
feat(racingcar): 뼈대 코드 생성
bak40 Oct 28, 2023
820021b
feat(racingcar): 경주할 자동차 이름 입력받기 구현
bak40 Oct 28, 2023
e40c436
feat(racingcar): 몇 번의 이동을 할 것인지 입력받기 구현
bak40 Oct 28, 2023
2d31c07
feat(racingcar): 사용자 입력값에 따른 에러발생 뼈대 코드 생성
bak40 Oct 28, 2023
c9e35fd
refactor(racingcar): Validator 클래스 함수의 접근제한자 수정
bak40 Oct 28, 2023
50864d5
feat(racingcar): 사용자 입력값에 따른 에러발생 기능 완성
bak40 Oct 28, 2023
f6c61c3
refactor(racingcar): 상수를 별도의 파일로 관리
bak40 Oct 29, 2023
16d68fc
feat(racingcar): 랜덤한 수 생성 기능 구현
bak40 Oct 29, 2023
76ef2c2
docs: README.md 기능 목록 업데이트
bak40 Oct 29, 2023
ccab4a2
feat(racingcar): 랜덤한 값이 4 이상인지 판단하는 기능 구현
bak40 Oct 29, 2023
856129a
feat(racingcar): 자동차 전진 기능 구현
bak40 Oct 29, 2023
d3f668d
feat(racingcar): 자동차 진행상황 출력 구현
bak40 Oct 29, 2023
31f70de
feat(racingcar): 최종 우승자 선택 기능 구현 완료
bak40 Oct 31, 2023
1858222
refactor(racingcar): 문자열 상수를 Constants로 이동
bak40 Oct 31, 2023
aada946
refactor(racingcar): isValidLengthAndAlphabetic에서 isValidLengthAndLet…
bak40 Oct 31, 2023
62510db
test(racingcar): 자동차 이름 및 시도 횟수 테스트 케이스 작성
bak40 Oct 31, 2023
0f9d531
test(racingcar): 난수 생성 테스트 케이스 작성
bak40 Nov 1, 2023
c4e779a
test(racingcar): 난수에 따른 자동차 동작 테스트
bak40 Nov 1, 2023
50fe83d
refactor(racingcar): 테스트 중복제거
bak40 Nov 1, 2023
a6ef238
refactor(racingcar): moveCarForward 함수 파라미터 추가
bak40 Nov 1, 2023
2cdf58d
test(racingcar): 난수에 따른 자동차 전진 정지 테스트
bak40 Nov 1, 2023
9221afe
refactor(racingcar): 자동차 이름 앞 뒤 공백 제거
bak40 Nov 1, 2023
49ea1f5
refactor(racingcar): ValidatorTest 테스트 케이스 감소
bak40 Nov 1, 2023
fe2f6de
test(refactor): 우승자 출력 테스트
bak40 Nov 1, 2023
7f01e40
refactor(racingcar): play 함수의 라운드 실행 로직 분리
bak40 Nov 1, 2023
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
32 changes: 32 additions & 0 deletions docs/README.md
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을 발생시킨 후 애플리케이션은 종료되어야 한다.
5 changes: 4 additions & 1 deletion 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.GameController

fun main() {
// TODO: 프로그램 구현
val controller = GameController()
controller.run()
}
18 changes: 18 additions & 0 deletions src/main/kotlin/racingcar/constant/Constants.kt
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 = "최종 우승자 : "
}
40 changes: 40 additions & 0 deletions src/main/kotlin/racingcar/controller/GameController.kt
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)
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/racingcar/domain/CarStatusUpdater.kt
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] + "-"
}
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/racingcar/domain/Judgment.kt
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
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/racingcar/domain/NumberGenerator.kt
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)
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/racingcar/domain/RoundExecutor.kt
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++
}
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/racingcar/util/Validator.kt
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.")
}
}
}
Comment on lines +15 to +21
Copy link

Choose a reason for hiding this comment

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

하나의 함수에서 두 가지 예외 처리를 한번에 하는 것도 좋지만 isValidLength()와 isValidLetter()로 나누어 함수는 한 가지 일을 하게 한다는 피드백을 반영하고, 에러 메세지의 구분된 출력을 통해 입력하는 사용자로 하여금 어떤 입력이 잘못된 것인지 확실하게 표현해주는 것도 방법이 될 수 있을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

길이 검사는 너무 간단하다 생각해 문자 검사와 함께 검사를 했는데, 너무 간단해서 이것을 하나의 기능으로 착각했던 것 같습니다... ' 함수는 한 가지 일을 하게 한다 ' 라는 피드백에도 어긋나고, 사용자 입장에서 봤을 때도 분명 구분된 출력이 더 편할 것 같아요 공감합니다! 해당 피드백에 더욱 신경 쓰며 코딩하게 해주셔서 감사합니다!


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
Copy link

Choose a reason for hiding this comment

The 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.")
Copy link

Choose a reason for hiding this comment

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

매직넘버는 상수화를 통해 잘 관리해주셨는데 출력문들도 enum클래스나 constant로 통합하여 관리하는 것도 하나의 방법이 될 수 있을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

이번 3주차 과제에서는 말씀해주신 enum 클래스를 한번 사용해보겠습니다!

}
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/racingcar/view/InputView.kt
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()
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/racingcar/view/RaceView.kt
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()
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/racingcar/view/ResultView.kt
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(", "))
}
}
25 changes: 25 additions & 0 deletions src/test/kotlin/racingcar/domain/CarStatusUpdaterTest.kt
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"])
}
}
20 changes: 20 additions & 0 deletions src/test/kotlin/racingcar/domain/JudgmentTest.kt
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))
}
}
15 changes: 15 additions & 0 deletions src/test/kotlin/racingcar/domain/NumberGeneratorTest.kt
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)
}
}
}
68 changes: 68 additions & 0 deletions src/test/kotlin/racingcar/util/ValidatorTest.kt
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")
}
}
}
Loading