diff --git a/README.md b/README.md index 114c56b31e..2f8723853e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # kotlin-racingcar -###기능 요구 사항 +## 문자열 계산기 + +### 기능 요구 사항 - 사용자가 입력한 문자열 값에 따라 사칙 연산을 수행할 수 있는 계산기를 구현해야 한다. - 문자열 계산기는 사칙 연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다. - 예를 들어 "2 + 3 * 4 / 2"와 같은 문자열을 입력할 경우 (2 + 3) * 4 / 2 실행 결과인 10을 출력해야 한다. -###기능 구현 사항 +### 기능 구현 사항 - [X] 덧셈 연산을 할 수 있다. - [X] 뺄셈 연산을 할 수 있다. - [X] 곱셈 연산을 할 수 있다. @@ -14,5 +16,27 @@ - [X] 입력값이 사칙연산 기호가 아닌 경우 오류가 발생해야 한다. - [X] 사칙 연산을 모두 포함하는 기능을 구현해야 한다. -###프로그래밍 요구 사항 +### 프로그래밍 요구 사항 - 메서드가 너무 많은 일을 하지 않도록 분리하기 위해 노력한다. + +## 자동차 경주 + +### 기능 요구 사항 +초간단 자동차 경주 게임을 구현한다. +- 주어진 횟수 동안 N대의 자동차는 전진 또는 멈출 수 있다. +- 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. +- 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다. + +### 기능 구현 사항 +- [X] 사용자로부터 자동차 대수와 시도 횟수를 입력받는다. +- [X] 경주 게임은 입력값만큼의 자동차를 생성한다. +- [X] 경주 게임은 0에서 9 사이에서 무작위 값을 구한다. +- [X] 자동차는 무작위 값이 4 이상일 경우 전진한다. +- [X] 자동차는 무작위 값이 4 미만일 경우 정지한다. +- [X] 경주 게임은 모든 자동차에 대해 무작위 값을 구하여 이동시킨다. +- [X] 경주 게임의 결과를 결과 화면을 통해 출력한다. + +### 프로그래밍 요구 사항 +- [X] 모든 로직에 단위 테스트를 구현한다. +- [X] 핵심 로직을 구현하는 코드와 UI를 담당하는 로직(InputView, ResultView)을 구분한다. diff --git a/src/main/kotlin/racingcar/Car.kt b/src/main/kotlin/racingcar/Car.kt new file mode 100644 index 0000000000..310b9733db --- /dev/null +++ b/src/main/kotlin/racingcar/Car.kt @@ -0,0 +1,15 @@ +package racingcar + +class Car { + var position: Int = DEFAULT_POSITION + private set + + fun move(value: Int) { + if (value >= FORWARD_NUMBER) position++ + } + + companion object { + private const val DEFAULT_POSITION = 0 + private const val FORWARD_NUMBER = 4 + } +} diff --git a/src/main/kotlin/racingcar/InputView.kt b/src/main/kotlin/racingcar/InputView.kt new file mode 100644 index 0000000000..7a39670a98 --- /dev/null +++ b/src/main/kotlin/racingcar/InputView.kt @@ -0,0 +1,15 @@ +package racingcar + +class InputView { + companion object { + fun view(): Pair { + println("자동차 대수는 몇 대인가요?") + val numberOfCars = readLine()!!.toInt() + println("시도할 횟수는 몇 회인가요?") + val count = readLine()!!.toInt() + println("실행 결과") + + return numberOfCars to count + } + } +} diff --git a/src/main/kotlin/racingcar/Main.kt b/src/main/kotlin/racingcar/Main.kt new file mode 100644 index 0000000000..4f63fb803c --- /dev/null +++ b/src/main/kotlin/racingcar/Main.kt @@ -0,0 +1,13 @@ +package racingcar + +fun main() { + val racingGame = RacingGame() + + val (numberOfCars, count) = InputView.view() + racingGame.set(numberOfCars, count) + + for (i in 1..count) { + racingGame.run() + ResultView.view(racingGame.carList) + } +} diff --git a/src/main/kotlin/racingcar/RacingGame.kt b/src/main/kotlin/racingcar/RacingGame.kt new file mode 100644 index 0000000000..8298430b32 --- /dev/null +++ b/src/main/kotlin/racingcar/RacingGame.kt @@ -0,0 +1,37 @@ +package racingcar + +import kotlin.random.Random + +class RacingGame { + lateinit var carList: List + private set + lateinit var randomNumberList: Array + private set + private var times: Int = 0 + + fun set(numberOfCars: Int, count: Int) { + val carMList: MutableList = mutableListOf() + for (i in 1..numberOfCars) { + carMList.add(Car()) + } + carList = carMList.toList() + randomNumberList = Array(count) { IntArray(numberOfCars) } + } + + fun run() { + for (i in carList.indices) { + val randomNumber = random() + carList[i].move(randomNumber) + randomNumberList[times][i] = randomNumber + } + times++ + } + + private fun random(): Int { + return Random.nextInt(MAX_RANDOM_NUMBER) + } + + companion object { + private const val MAX_RANDOM_NUMBER: Int = 9 + } +} diff --git a/src/main/kotlin/racingcar/ResultView.kt b/src/main/kotlin/racingcar/ResultView.kt new file mode 100644 index 0000000000..9c063fd6cc --- /dev/null +++ b/src/main/kotlin/racingcar/ResultView.kt @@ -0,0 +1,13 @@ +package racingcar + +class ResultView { + companion object { + fun view(cars: List) { + for (car in cars) { + if (car.position == 0) println("x") + else println("-".repeat(car.position)) + } + println() + } + } +} diff --git a/src/main/kotlin/racingcar/feedback/Car.kt b/src/main/kotlin/racingcar/feedback/Car.kt new file mode 100644 index 0000000000..f8b18e9924 --- /dev/null +++ b/src/main/kotlin/racingcar/feedback/Car.kt @@ -0,0 +1,37 @@ +package racingcar.feedback + +// 상수를 최상위 수준과 동반 객체 중 무엇으로 정의하느냐는 상수를 사용하는 클래스의 범위에 따라 다르다! +// 만약에 상수를 최상위 수준에서 선언하면 코틀린은 CarKt 이라는 클래스 파일을 생성하여 모아둔다. +// private const val ... + +object Numbers { + // 최상위 수준으로 선언했을 때의 단점은 개발자가 직접 상수 이름을 알아야 한다는 것이다. + // 그래서 object 안에 상수를 선언해서 사용하기도 한다. + const val MAXIMUM_NAME_LENGTH: Int = 5 +} + +class Car(val name: String, position: Int = DEFAULT_POSITION) { + var position: Int = position + private set // 커스텀 게터와 세터는 생성자에서 바로 사용할 수 없다! + /* + private var _position: Int = position + val position: Int + get() = _position + */ + + init { + require(name.length <= Numbers.MAXIMUM_NAME_LENGTH) { "자동차 이름은 5글자를 넘길 수 없습니다." } + } + + fun move() { + position++ + } + + companion object { + private const val DEFAULT_POSITION: Int = 0 + + @JvmField + val DEFAULT_CAR: Car = Car("") + fun of(name: String): Car = Car(name) + } +} diff --git a/src/main/kotlin/racingcar/feedback/Numbers.kt b/src/main/kotlin/racingcar/feedback/Numbers.kt new file mode 100644 index 0000000000..a611ce485a --- /dev/null +++ b/src/main/kotlin/racingcar/feedback/Numbers.kt @@ -0,0 +1,19 @@ +@file:JvmName("NumberUtils") + +package racingcar.feedback + +const val FORWARD_NUMBER: Int = 4 + +fun calculate(text: String?): Int { + require(!text.isNullOrBlank()) { "입력값이 null 또는 빈 문자열일 수 없습니다." } + run(!text.isNullOrBlank()) { "예외가 발생했습니다." } // 커스텀 예외에 대해서 직접 만들어 써보자! + // ... + return 0 +} + +fun run(value: Boolean, lazyMessage: () -> Any) { + if (!value) { + val message = lazyMessage() + throw RuntimeException(message.toString()) + } +} diff --git a/src/test/kotlin/racingcar/CarTest.kt b/src/test/kotlin/racingcar/CarTest.kt new file mode 100644 index 0000000000..69e5bc9b72 --- /dev/null +++ b/src/test/kotlin/racingcar/CarTest.kt @@ -0,0 +1,33 @@ +package racingcar + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class CarTest { + @ParameterizedTest + @ValueSource(ints = [4, 5, 6]) + fun `자동차는 무작위 값이 4 이상일 경우 전진한다`(input: Int) { + // given + val car = Car() + + // when + car.move(input) + + // then + assertThat(car.position).isEqualTo(1) + } + + @ParameterizedTest + @ValueSource(ints = [1, 2, 3]) + fun `자동차는 무작위 값이 4 미만일 경우 정지한다`(input: Int) { + // given + val car = Car() + + // when + car.move(input) + + // then + assertThat(car.position).isEqualTo(0) + } +} diff --git a/src/test/kotlin/racingcar/RacingGameTest.kt b/src/test/kotlin/racingcar/RacingGameTest.kt new file mode 100644 index 0000000000..05fa79f611 --- /dev/null +++ b/src/test/kotlin/racingcar/RacingGameTest.kt @@ -0,0 +1,37 @@ +package racingcar + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class RacingGameTest { + @Test + fun `경주 게임은 입력값만큼의 자동차를 생성한다`() { + // given + val racingGame = RacingGame() + + // when + racingGame.set(3, 5) + + // then + assertThat(racingGame.carList.size).isEqualTo(3) + } + + @Test + fun `경주 게임은 모든 자동차에 대해 무작위 값을 구하여 이동시킨다`() { + // given + val racingGame = RacingGame() + racingGame.set(3, 1) + + // when + racingGame.run() + + // then + val cars = racingGame.carList + val numbers = racingGame.randomNumberList[0] + + for (i in cars.indices) { + if (numbers[i] >= 4) assertThat(cars[i].position).isEqualTo(1) + else assertThat(cars[i].position).isEqualTo(0) + } + } +} diff --git a/src/test/kotlin/racingcar/feedback/CarTest.kt b/src/test/kotlin/racingcar/feedback/CarTest.kt new file mode 100644 index 0000000000..a38708e7a3 --- /dev/null +++ b/src/test/kotlin/racingcar/feedback/CarTest.kt @@ -0,0 +1,42 @@ +package racingcar.feedback + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Test + +class CarTest { + @Test + fun `자동차를 생성한다`() { + val car = Car("jason", 0) + assertThat(car.name).isEqualTo("jason") + assertThat(car.position).isEqualTo(0) + } + + @Test + fun `기본 인자를 활용하여 자동차를 생성한다`() { + val car = Car("jason") + assertThat(car.name).isEqualTo("jason") + assertThat(car.position).isEqualTo(0) + } + + @Test + fun `자동차의 이름이 5글자를 초과하면 오류를 발생시킨다`() { + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + Car("가나다라마바사") + } + } + + @Test + fun `팩토리 함수를 이용하여 자동차를 생성한다`() { + val car = Car.of("jason") + assertThat(car.name).isEqualTo("jason") + assertThat(car.position).isEqualTo(0) + } + + @Test + fun `정적 변수를 사용한다`() { + val car = Car.DEFAULT_CAR + assertThat(car.name).isEqualTo("") + assertThat(car.position).isEqualTo(0) + } +} diff --git a/src/test/kotlin/racingcar/feedback/JavaTest.java b/src/test/kotlin/racingcar/feedback/JavaTest.java new file mode 100644 index 0000000000..c01ce9788a --- /dev/null +++ b/src/test/kotlin/racingcar/feedback/JavaTest.java @@ -0,0 +1,13 @@ +package racingcar.feedback; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class NumberUtilsTest { + @Test + void 코틀린상수() { + assertThat(NumberUtils.FORWARD_NUMBER).isEqualTo(4); + Car defaultCar = Car.DEFAULT_CAR; + } +} diff --git a/src/test/kotlin/study/PersonKoTest.kt b/src/test/kotlin/study/PersonKoTest.kt index 4475df756b..023eef3466 100644 --- a/src/test/kotlin/study/PersonKoTest.kt +++ b/src/test/kotlin/study/PersonKoTest.kt @@ -1,8 +1,8 @@ package study import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe class PersonKoTest : StringSpec({ "이름 붙인 인자" {