From 8b13b9e8d3edd4d3004257cb23de1c223ad63370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 06:50:47 +0900 Subject: [PATCH 1/8] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=ED=95=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20README.md=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/README.md b/docs/README.md index e69de29bb..f49c96c28 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,49 @@ +## 구현할 기능 목록 + +### 게임 시작 + +- 자동차 이름 입력 +- 시도 횟수 입력 +- 경주 중 현황 출력 +- 우승자들 결정 +- 우승자들 출력 + +### 자동차 이름 입력 + +- 경주 자동차 이름 입력 메시지 출력(`경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)`) +- 쉼표(,)를 기준으로 구분 +- 이름은 5자 이하만 가능 +- 자동차 이름으로 객체 생성 후 `List`에 담아서 반환 +- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료 + +### 시도 횟수 입력 + +- 시도 횟수 입력 메시지 출력(`시도할 횟수는 몇 회인가요?`) +- 입력받은 횟수 반환 +- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료 + +### 경주 중 현황 + +- 경기 결과 메시지 출력(`실행 결과`) +- 횟수별 경기 진행 +- 횟수별 경주 중 현황 출력 + +### 경기 진행 + +- 매개 변수로 받은 자동차 목록을 순회 +- `pickNumberInRange()`를 활용하여 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우는 전진, 아니면 멈춤 + +### 경주 중 현황 출력 + +- 매개 변수로 받은 자동차 목록을 순회하며 자동차 전진의 결과를 출력, 전진 거리는 `-`으로 표시(ex. `pobi : ---`) + +### 우승자들 결정 + +- 매개 변수로 받은 자동차 목록 중 자동차 주행 거리의 최대값을 구하기 +- 자동차 목록에서 최대값과 주행거리가 같은 자동차 이름을 `List`에 담아서 반환 + +### 우승자들 출력 + +- 우승자 메시지 출력(`최종 우승자 :`) +- 매개 변수로 받은 우승자들 목록 출력 +- 우승자는 한 명 이상일 수 있음 \ No newline at end of file From 073537bae9ff6ff642c48d3c6bfbfc4a7e703fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 06:57:37 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20Car=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/racingcar/Car.kt | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/kotlin/racingcar/Car.kt diff --git a/src/main/kotlin/racingcar/Car.kt b/src/main/kotlin/racingcar/Car.kt new file mode 100644 index 000000000..438c9e4b6 --- /dev/null +++ b/src/main/kotlin/racingcar/Car.kt @@ -0,0 +1,22 @@ +package racingcar + +class Car(name: String) { + private val name: String + private var distance: String + + init { + require(name.length in 1 until 6) { "이름은 1자 이상, 5자 이하만 가능합니다." } + this.name = name + this.distance = "" + } + + fun drive() { + this.distance = distance.plus('-') + } + + fun getName(): String = name + + fun getDistance(): String = distance + + fun getDistanceLength(): Int = distance.length +} \ No newline at end of file From 6b3492c49467ff7f4a51172c7818323165ab6d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 06:58:03 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/racingcar/Application.kt | 63 +++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index 0d8f3a79d..ae609f07e 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -1,5 +1,66 @@ package racingcar +import camp.nextstep.edu.missionutils.Console +import camp.nextstep.edu.missionutils.Randoms + fun main() { - // TODO: 프로그램 구현 + startGame() +} + +fun startGame() { + val carList = inputCarName() + val tryNumber = inputTryNumber() + + printRacing(carList, tryNumber) + + val winnerNameList = decideWinnerNameList(carList) + printWinner(winnerNameList) } + +fun inputCarName(): List { + println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)") + return with(Console.readLine().split(",")) { + this.map { Car(it) } + } +} + +fun inputTryNumber(): Int { + println("시도할 횟수는 몇 회인가요?") + return Console.readLine().apply { + require(this.isNotEmpty() && Regex("[0-9]+").matches(this)) { "시도할 횟수는 숫자만 입력해야만 합니다." } + }.toInt() +} + +fun printRacing(carList: List, tryNumber: Int) { + println("\n실행 결과") + + for (round in 0 until tryNumber) { + raceCar(carList) + printCurrentRace(carList) + } +} + +fun raceCar(carList: List) { + for (car in carList) { + val randomNumber = Randoms.pickNumberInRange(0, 9) + if (randomNumber >= 4) { + car.drive() + } + } +} + +fun printCurrentRace(carList: List) { + for (car in carList) { + println("${car.getName()} : ${car.getDistance()}") + } + println() +} + +fun decideWinnerNameList(carList: List): List { + val max = carList.maxOf { it.getDistanceLength() } + return carList.filter { it.getDistanceLength() == max }.map { it.getName() } +} + +fun printWinner(winnerList: List) { + print("최종 우승자 : ${winnerList.joinToString(", ")}") +} \ No newline at end of file From b43d031a19cbe66d070f09b02caee19951c94198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 22:54:09 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20=EA=B0=84=ED=8E=B8=ED=95=9C=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=82=AC=EC=9A=A9=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Car.kt: - name은 val형식으로 private을 붙일 필요 없어서 제거 - 캡술화를 위해 외부에 접근이 불가능한 _distance와 접근 가능한 distance로 변수 분리 - getDistanceLength()함수를 distanceLength 변수로 변경 - equals()와 hashCode함수() 정의 --- src/main/kotlin/racingcar/Car.kt | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/racingcar/Car.kt b/src/main/kotlin/racingcar/Car.kt index 438c9e4b6..b55fcce3b 100644 --- a/src/main/kotlin/racingcar/Car.kt +++ b/src/main/kotlin/racingcar/Car.kt @@ -1,22 +1,34 @@ package racingcar class Car(name: String) { - private val name: String - private var distance: String + val name: String + private var _distance: String + val distance: String + get() = this._distance + val distanceLength: Int + get() = distance.length init { require(name.length in 1 until 6) { "이름은 1자 이상, 5자 이하만 가능합니다." } this.name = name - this.distance = "" + this._distance = "" } fun drive() { - this.distance = distance.plus('-') + this._distance = _distance.plus('-') } - fun getName(): String = name - - fun getDistance(): String = distance + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Car) return false + if (name != other.name) return false + if (distance != other.distance) return false + return true + } - fun getDistanceLength(): Int = distance.length + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + distance.hashCode() + return result + } } \ No newline at end of file From f8994ceefa7bc4d8690d302c36a70e476c6fca17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 22:57:13 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20=EC=9B=90=ED=99=9C=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application.kt: - 원활한 테스트 코드 작성을 위해 모든 함수를 람다를 활용하여 매개변수로 전달하는 방식으로 변경 - raceCar()에서 자동차의 전진 또는 멈춤을 수행하는 함수를 driveCar()로 분리 --- src/main/kotlin/racingcar/Application.kt | 44 +++++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt index ae609f07e..53a9c2faa 100644 --- a/src/main/kotlin/racingcar/Application.kt +++ b/src/main/kotlin/racingcar/Application.kt @@ -4,61 +4,63 @@ import camp.nextstep.edu.missionutils.Console import camp.nextstep.edu.missionutils.Randoms fun main() { - startGame() + startGame(Console::readLine, Randoms::pickNumberInRange) } -fun startGame() { - val carList = inputCarName() - val tryNumber = inputTryNumber() +fun startGame(readLine: () -> String, pickNumberInRange: (Int, Int) -> Int) { + val carList = inputCarName(readLine) + val tryNumber = inputTryNumber(readLine) - printRacing(carList, tryNumber) + printRacing(carList, tryNumber, pickNumberInRange) val winnerNameList = decideWinnerNameList(carList) printWinner(winnerNameList) } -fun inputCarName(): List { +fun inputCarName(readLine: () -> String): List { println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)") - return with(Console.readLine().split(",")) { - this.map { Car(it) } - } + return readLine().split(",").map { Car(it.trim()) } } -fun inputTryNumber(): Int { +fun inputTryNumber(readLine: () -> String): Int { println("시도할 횟수는 몇 회인가요?") - return Console.readLine().apply { + return readLine().apply { require(this.isNotEmpty() && Regex("[0-9]+").matches(this)) { "시도할 횟수는 숫자만 입력해야만 합니다." } }.toInt() } -fun printRacing(carList: List, tryNumber: Int) { +fun printRacing(carList: List, tryNumber: Int, pickNumberInRange: (Int, Int) -> Int) { println("\n실행 결과") for (round in 0 until tryNumber) { - raceCar(carList) + raceCar(carList, pickNumberInRange) printCurrentRace(carList) } } -fun raceCar(carList: List) { +fun raceCar(carList: List, pickNumberInRange: (Int, Int) -> Int) { for (car in carList) { - val randomNumber = Randoms.pickNumberInRange(0, 9) - if (randomNumber >= 4) { - car.drive() - } + val randomNumber = pickNumberInRange(0, 9) + driveCar(car, randomNumber) + } +} + +fun driveCar(car: Car, randomNumber: Int) { + if (randomNumber >= 4) { + car.drive() } } fun printCurrentRace(carList: List) { for (car in carList) { - println("${car.getName()} : ${car.getDistance()}") + println("${car.name} : ${car.distance}") } println() } fun decideWinnerNameList(carList: List): List { - val max = carList.maxOf { it.getDistanceLength() } - return carList.filter { it.getDistanceLength() == max }.map { it.getName() } + val max = carList.maxOf { it.distanceLength } + return carList.filter { it.distanceLength == max }.map { it.name } } fun printWinner(winnerList: List) { From c49b89f4fce576cbcd819794e39ec7d877c3af63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 22:58:32 +0900 Subject: [PATCH 6/8] =?UTF-8?q?docs:=20README.md=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 함수 분리로 인한 기능 목록 추가 - 자동차 객체 추가 - 글자 크기 수정 --- docs/README.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index f49c96c28..c0ee1256b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -## 구현할 기능 목록 +# 구현할 기능 목록 ### 게임 시작 @@ -12,7 +12,6 @@ - 경주 자동차 이름 입력 메시지 출력(`경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)`) - 쉼표(,)를 기준으로 구분 -- 이름은 5자 이하만 가능 - 자동차 이름으로 객체 생성 후 `List`에 담아서 반환 - 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료 @@ -31,7 +30,11 @@ ### 경기 진행 - 매개 변수로 받은 자동차 목록을 순회 -- `pickNumberInRange()`를 활용하여 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우는 전진, 아니면 멈춤 +- `pickNumberInRange()`를 활용하여 0에서 9 사이에서 무작위 값을 구하기 + +### 자동차 전진 또는 멈춤 + +- 매개 변수로 받은 랜덤 숫자가 4 이상일 경우는 전진, 아니면 멈춤 ### 경주 중 현황 출력 @@ -46,4 +49,16 @@ - 우승자 메시지 출력(`최종 우승자 :`) - 매개 변수로 받은 우승자들 목록 출력 -- 우승자는 한 명 이상일 수 있음 \ No newline at end of file +- 우승자는 한 명 이상일 수 있음 + +--- + +# 객체 + +### 자동차 + +- `name` : String 타입으로 자동차 이름 + - 이름은 1자 이상, 5자 이하만 가능 +- `distance` : 자동차가 간 거리를 `-`로 표현한 String +- `distanceLength` : `distance`의 길이 +- `drive()` : `distance`에 `-` 추가 \ No newline at end of file From af4e7922d1af151f95158c8c6019e7837fe3d5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 23:01:05 +0900 Subject: [PATCH 7/8] =?UTF-8?q?test:=20CarTest.kt=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Car class 객체 테스트 코드 작성 - name 유효성 검사 - 전진 함수 --- src/test/kotlin/racingcar/CarTest.kt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/test/kotlin/racingcar/CarTest.kt diff --git a/src/test/kotlin/racingcar/CarTest.kt b/src/test/kotlin/racingcar/CarTest.kt new file mode 100644 index 000000000..bef6dbf16 --- /dev/null +++ b/src/test/kotlin/racingcar/CarTest.kt @@ -0,0 +1,20 @@ +package racingcar + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CarTest { + @Test + fun `name 1자 미만 5자 초과 예외 처리`() { + assertThrows { Car("123456") } + } + + @Test + fun `drive 실행 시 distance 1증가`() { + val car = Car("test") + val initDistanceLength = car.distanceLength + car.drive() + assertThat(car.distanceLength).isEqualTo(initDistanceLength + 1) + } +} \ No newline at end of file From e247e863f4b23864716254689424a2dfe54d1a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=95=84=EB=A6=84?= Date: Wed, 1 Nov 2023 23:02:29 +0900 Subject: [PATCH 8/8] =?UTF-8?q?test:=20RacingGameTest.kt=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Application.kt에서 구현한 함수에 대한 테스트 코드 작성 --- src/test/kotlin/racingcar/RacingGameTest.kt | 147 ++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/test/kotlin/racingcar/RacingGameTest.kt diff --git a/src/test/kotlin/racingcar/RacingGameTest.kt b/src/test/kotlin/racingcar/RacingGameTest.kt new file mode 100644 index 000000000..c05b265fe --- /dev/null +++ b/src/test/kotlin/racingcar/RacingGameTest.kt @@ -0,0 +1,147 @@ +package racingcar + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import java.io.PrintStream + +class RacingGameTest { + private lateinit var standardOut: PrintStream + private lateinit var captor: OutputStream + + @BeforeEach + fun init() { + standardOut = System.out + captor = ByteArrayOutputStream() + System.setOut(PrintStream(captor)) + } + + @AfterEach + fun printOutput() { + System.setOut(standardOut) + println(output()) + } + + fun output(): String { + return captor.toString().trim { it <= ' ' } + } + + @Test + fun `inputCarName 정상 입력`() { + val input = "a,b,c" + val carList = input.split(",").map { Car(it.trim()) } + assertThat(inputCarName { input }).isEqualTo(carList) + } + + @Test + fun `inputCarName 빈칸 입력 예외 발생`() { + val input = "" + assertThrows { inputCarName { input } } + } + + @Test + fun `inputCarName 5자 이상 입력 예외 발생`() { + val input = "hello,abc,wrongName" + assertThrows { inputCarName { input } } + } + + @Test + fun `inputTryNumber 정상 입력`() { + val input = "5" + assertThat(inputTryNumber { input }).isEqualTo(5) + } + + @Test + fun `inputTryNumber 빈칸 입력 예외 발생`() { + val input = "" + assertThrows { inputTryNumber { input } } + } + + @Test + fun `inputTryNumber 문자 입력 예외 발생`() { + val input = "a" + assertThrows { inputTryNumber { input } } + } + + @Test + fun `printRacing 결과 출력`() { + val carList = listOf(Car("a"), Car("b"), Car("c")) + val tryNumber = 1 + val pickNumberInRange = { _: Int, _: Int -> 4 } + printRacing(carList, tryNumber, pickNumberInRange) + for (car in carList) { + val expectedOutput = "${car.name} : ${"-".repeat(tryNumber)}" + assertThat(output()).contains(expectedOutput) + } + } + + @Test + fun `raceCar 정상 처리`() { + val carList = listOf(Car("a"), Car("b"), Car("c")) + val pickNumberInRange = { _: Int, _: Int -> 4 } + raceCar(carList, pickNumberInRange) + for (car in carList) { + assertThat(car.distanceLength).isEqualTo(1) + } + } + + @Test + fun `driveCar 랜덤 숫자 4이상 정상 처리`() { + val car = Car("a") + val randomNumber = 4 + driveCar(car, randomNumber) + assertThat(car.distanceLength).isEqualTo(1) + } + + @Test + fun `driveCar 랜덤 숫자 4미만 정상 처리`() { + val car = Car("a") + val randomNumber = 2 + driveCar(car, randomNumber) + assertThat(car.distanceLength).isEqualTo(0) + } + + @Test + fun `printCurrentRace 결과 출력`() { + val carList = listOf(Car("a"), Car("b"), Car("c")) + printCurrentRace(carList) + assertThat(output()).contains("a : ", "b : ", "c :") + } + + @Test + fun `decideWinnerNameList 한 명 정상 처리`() { + val carList = listOf(Car("a"), Car("b"), Car("c")) + carList[0].drive() + val expectList = listOf("a") + assertThat(decideWinnerNameList(carList)).isEqualTo(expectList) + } + + @Test + fun `decideWinnerNameList 여러 명 정상 처리`() { + val carList = listOf(Car("a"), Car("b"), Car("c")) + carList[0].drive() + carList[1].drive() + val expectList = listOf("a", "b") + assertThat(decideWinnerNameList(carList)).isEqualTo(expectList) + } + + @Test + fun `printWinner 한 명 결과 출력`() { + val winnerList = listOf("a") + printWinner(winnerList) + val expectedOutput = "최종 우승자 : a" + assertThat(output()).contains(expectedOutput) + } + + @Test + fun `printWinner 여러 명 결과 출력`() { + val winnerList = listOf("a", "b") + printWinner(winnerList) + val expectedOutput = "최종 우승자 : a, b" + assertThat(output()).contains(expectedOutput) + } +} \ No newline at end of file