-
Notifications
You must be signed in to change notification settings - Fork 11
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
[자동차 경주 step1] 양두영 미션 제출합니다. #19
base: FhRh
Are you sure you want to change the base?
Changes from all commits
643b604
1c60e62
76ee82c
1332d36
480295e
c6b78b0
ed68363
540ba0c
4a8fa93
0aae47d
7322a9a
469efd7
e76d519
128c5b2
1886873
c39982d
fbe849d
6da8a3d
21b64a2
a8747eb
b3dcfca
73495d0
ca514d0
30bb064
0bdeb43
a0631cf
77fff6b
7c200d5
075ae77
ea21bb6
5157d6a
be3bcb1
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,87 @@ | ||
# kotlin-racingcar | ||
# kotlin-racingcar | ||
|
||
초간단 자동차 경주 게임을 구현한다. | ||
|
||
- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. | ||
- 각 자동차에 이름을 부여할 수 있다. 자동차 이름은 5자를 초과할 수 없다. | ||
- 자동차 이름은 쉼표(,)를 기준으로 구분한다. | ||
- 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. | ||
- 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. | ||
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. | ||
- 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다. | ||
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. | ||
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. | ||
--- | ||
# 실행결과 | ||
```kotlin | ||
경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분). | ||
hoon,seong,choi | ||
시도할 횟수는 몇 회인가요? | ||
5 | ||
|
||
실행 결과 | ||
hoon : - | ||
seong : | ||
choi : - | ||
|
||
hoon : -- | ||
seong : - | ||
choi : -- | ||
|
||
hoon : --- | ||
seong : -- | ||
choi : --- | ||
|
||
hoon : ---- | ||
seong : --- | ||
choi : ---- | ||
|
||
hoon : ----- | ||
seong : ---- | ||
choi : ----- | ||
|
||
최종 우승자: hoon, choi | ||
``` | ||
--- | ||
# 기능 명세 | ||
### 주요 기능 | ||
|
||
1. 사용자가 자동차 이름 입력 | ||
2. 시도할 횟수 입력 | ||
3. 입력된 횟수만큼 각 자동차를 이동 | ||
4. 이동 종료 후 최종 우승자 출력 | ||
|
||
### 자동차 이름 입력 | ||
|
||
- [x] 이름 입력 안내 문구 출력 | ||
- [x] 사용자로부터 자동차 이름들 입력받는 기능 | ||
- [x] 이름을 쉼표 기준으로 구분하여 저장하는 기능 | ||
|
||
### 시도할 횟수 입력 | ||
|
||
- [x] 횟수 입력 안내 문구를 출력 | ||
- [x] 사용자로부터 라운드 횟수를 입력받는 기능 | ||
|
||
### 자동차 이동(게임 진행) | ||
|
||
- [x] "실행 결과" 문구 출력 | ||
- [x] 주사위를 사용해 1자리 난수 생성 기능 | ||
- [x] 난수값이 4 이상인 자동차의 위치를 1 증가시키는 기능 | ||
- [x] 각 라운드 별 실행 결과(자동차 위치) 출력하는 기능 | ||
- [x] 입력된 횟수만큼 라운드를 반복하는 기능 | ||
|
||
### 우승자 출력 | ||
|
||
- [x] 우승자(가장 멀리 이동한 자동차)를 판별하는 기능 | ||
- [x] 우승자 출력 기능 | ||
- [x] 공동 우승자의 경우 ","로 구분하여 출력해야 함 | ||
|
||
--- | ||
|
||
# 예외처리 | ||
|
||
- 자동차 이름 입력 시 유효한 입력이 아니라면 재 시도 | ||
- 입력된 이름들의 유효성 검증 | ||
- [x] 이름은 5자 이하만 가능 | ||
- [x] 중복된 이름 설정 불가 | ||
- [x] [a-zA-Z]+ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package racingcar.controller | ||
|
||
import racingcar.model.Car | ||
import racingcar.model.Cars | ||
import racingcar.model.RandomDice | ||
import racingcar.view.GameInputer | ||
import racingcar.view.GameAnnouncer | ||
|
||
class GameController { | ||
fun run(){ | ||
GameAnnouncer.askForCarNames() | ||
val carNames = GameInputer.getCarNames() | ||
val carList = carNames.map { name -> Car(name) } | ||
val cars = Cars(carList) | ||
|
||
GameAnnouncer.askForRounds() | ||
val rounds = GameInputer.getRounds() | ||
|
||
GameAnnouncer.printProcess() | ||
repeat(rounds){ | ||
processRound(cars) | ||
} | ||
|
||
val winners = resolveWinners(cars) | ||
GameAnnouncer.printWinners(winners) | ||
} | ||
|
||
private fun processRound(cars: Cars) { | ||
val carNum = cars.size | ||
for (i in 1..carNum) { | ||
if (RandomDice.rollDice() >= 4) { | ||
cars.moveCar(i) | ||
} | ||
} | ||
GameAnnouncer.printCarPositions(cars) | ||
} | ||
|
||
private fun resolveWinners(cars : Cars) : Cars{ | ||
val carList = cars.getCarList() | ||
val maxPosition = carList.maxOf { it.position } | ||
val winners = carList.filter { it.position == maxPosition } | ||
.map { it } | ||
return Cars(winners) | ||
} | ||
Comment on lines
+38
to
+44
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. private 함수는 테스트하기 힘들죠. 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. Cars 를 받아 Winners 를 구하는 책임을 누군가 가지는게 어떤가요! 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. 아하 심판 도메인을 구현하는 부분 참 좋은 생각인것 같습니다! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
package racingcar | ||
|
||
import racingcar.controller.GameController | ||
|
||
fun main() { | ||
// TODO: 프로그램 구현 | ||
val gameController = GameController() | ||
gameController.run() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package racingcar.model | ||
|
||
class Car (val name : String, position : Int = 0){ | ||
var position = position | ||
private set | ||
Comment on lines
+3
to
+5
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. private set 👍 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. 👍👍 |
||
init { | ||
require(name.isNotBlank()) { "이름은 공백일 수 없습니다." } | ||
require(name.matches(Regex("^[a-zA-Z]+$"))) { "이름은 영어 대소문자로 이루어져있어야 합니다." } | ||
require(name.length <= 5) { "이름은 5글자 이하여야 합니다." } | ||
require(position >= 0) { "위치는 0이상이어야 합니다." } | ||
} | ||
fun move(){ | ||
position++ | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package racingcar.model | ||
|
||
class Cars(private val carList:List<Car>) { | ||
val size: Int | ||
get() = carList.size | ||
|
||
init{ | ||
val carNames = carList.map {it.name}.toSet() | ||
require(carNames.size == carList.size) { "중복된 차 이름이 있습니다" } | ||
} | ||
|
||
fun getCarList():List<Car>{ | ||
return carList | ||
} | ||
|
||
fun moveCar(index : Int){ | ||
carList[index-1].move() | ||
} | ||
Comment on lines
+16
to
+18
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. 자동차를 움직일 수 있는 책임이 분산되어 있어요. Cars에서도, Cars 를 생성한 곳에서도 특정 index 의 Car 을 움직일 수 있는 형태입니다. 이는 사이드이펙트를 발생시킬 수 있는 구조로 보여요. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package racingcar.model | ||
|
||
import kotlin.random.Random | ||
|
||
object RandomDice { | ||
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. 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. object는 코틀린에서 특정 클래스를 싱글톤으로 사용하기 위한 키워드로 알고 있습니다. 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 rollDice():Int = Random.nextInt(0, 10) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package racingcar.view | ||
|
||
import racingcar.model.Car | ||
import racingcar.model.Cars | ||
|
||
object GameAnnouncer { | ||
private const val NAME_INPUT_DESCRIPTION = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)." | ||
private const val ROUNDS_INPUT_DESCRIPTION = "시도할 횟수는 몇 회인가요?" | ||
private const val PROCESSING_DESCRIPTION = "실행결과" | ||
|
||
fun askForCarNames(){ | ||
println(NAME_INPUT_DESCRIPTION) | ||
} | ||
fun askForRounds() { | ||
println(ROUNDS_INPUT_DESCRIPTION) | ||
} | ||
fun printCarPositions(cars : Cars){ | ||
cars.getCarList().forEach{printCarPosition(it)} | ||
println() | ||
} | ||
fun printWinners(winners : Cars) { | ||
val winnerList = winners.getCarList() | ||
val winnerNames = winnerList.joinToString(", ") { it.name } | ||
println("최종 우승자 : $winnerNames") | ||
} | ||
fun printProcess() { | ||
println(PROCESSING_DESCRIPTION) | ||
} | ||
|
||
fun printCarPosition(car : Car){ | ||
println(car.name+" : "+("-".repeat(car.position))) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package racingcar.view | ||
|
||
object GameInputer { | ||
fun getCarNames() : List<String>{ | ||
val inputString = readln() | ||
return inputString.split(",").map { it.trim() } | ||
} | ||
|
||
fun getRounds() : Int{ | ||
val input = readlnOrNull() | ||
val number = input?.toIntOrNull() ?: throw IllegalArgumentException("유효한 정수를 입력하지 않았습니다.") | ||
|
||
require(number < 0){throw IllegalArgumentException("음수는 허용되지 않습니다.")} | ||
return number | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package study.model | ||
|
||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Test | ||
import racingcar.model.Car | ||
import java.io.ByteArrayOutputStream | ||
import java.io.PrintStream | ||
|
||
class CarTest { | ||
@Test | ||
fun `자동차를 1칸 앞으로 움직인다`(){ | ||
//given | ||
val car = Car("apple") | ||
|
||
//when | ||
car.move() | ||
|
||
//then | ||
assertEquals(1,car.position) | ||
} | ||
@Test | ||
fun `자동차의 위치를 출력한다`(){ | ||
//given | ||
val expected = "apple : ---" | ||
val car = Car("apple",3) | ||
|
||
val outputStream = ByteArrayOutputStream() | ||
val originalOut = System.out | ||
System.setOut(PrintStream(outputStream)) | ||
|
||
//when | ||
car.printNowPosition() | ||
val result = outputStream.toString().trim() | ||
|
||
//then | ||
assertEquals(expected, result) | ||
System.setOut(originalOut) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package study.model | ||
|
||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Test | ||
import racingcar.model.Car | ||
import racingcar.model.Cars | ||
|
||
class CarsTest { | ||
|
||
@Test | ||
fun `자동차 이름 리스트로 자동차 객체들의 집합을 생성한다`(){ | ||
//given | ||
val car1 = Car("apple") | ||
val car2 = Car("banana") | ||
val car3 = Car("cherry") | ||
val expected = listOf(car1, car2, car3) | ||
|
||
//when | ||
val cars = Cars(listOf(car1,car2,car3)) | ||
|
||
//then | ||
val result = cars.getCarList() | ||
|
||
print(cars) | ||
assertEquals(expected, result) | ||
} | ||
|
||
@Test | ||
fun `특정 위치의 자동차를 한 칸 움직인다`(){ | ||
//given | ||
val expected = 1 | ||
val car1 = Car("apple") | ||
val car2 = Car("banana") | ||
val car3 = Car("cherry") | ||
val cars = Cars(listOf(car1,car2,car3)) | ||
|
||
//when | ||
cars.moveCar(1) | ||
|
||
//then | ||
val carList = cars.getCarList() | ||
assertEquals(carList[0].position, 1) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package study.utils | ||
|
||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.Assertions.assertTrue | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
import racingcar.view.GameInputer | ||
import java.io.ByteArrayInputStream | ||
import java.io.InputStream | ||
|
||
|
||
class GameInputerTest { | ||
private val gameInputer = GameInputer | ||
@Test | ||
fun `자동차 이름을 입력받아 쉼표 기준으로 구분한다`(){ | ||
//given | ||
val expected: List<String> = listOf("apple", "banana", "cherry") | ||
val input = "apple,banana,cherry" | ||
val simulatedInput: InputStream = ByteArrayInputStream(input.toByteArray()) | ||
System.setIn(simulatedInput) | ||
|
||
//when | ||
val result = gameInputer.getCarNames() | ||
|
||
//then | ||
assertThat(result).isEqualTo(expected) | ||
} | ||
|
||
@Test | ||
fun `라운드를 입력받는다`(){ | ||
//given | ||
val input = "5" | ||
val simulatedInput: InputStream = ByteArrayInputStream(input.toByteArray()) | ||
System.setIn(simulatedInput) | ||
|
||
//when | ||
val result = gameInputer.getRounds() | ||
|
||
//then | ||
assertTrue(result is Int,"변수가 정수여야 합니다.") | ||
} | ||
|
||
@Test | ||
fun `라운드가 잘못 입력될시, IllegalArgumentException을 발생시키고 프로그램을 종료한다`(){ | ||
//given | ||
val input = "종경" | ||
val simulatedInput: InputStream = ByteArrayInputStream(input.toByteArray()) | ||
System.setIn(simulatedInput) | ||
|
||
//when | ||
//then | ||
assertThrows<IllegalArgumentException> { | ||
gameInputer.getRounds() | ||
} | ||
} | ||
} |
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.
Controller 에 도메인 로직이 많아 보입니다. MVC 에서 비대해진 컨트롤러는 테스트하기 힘들고 재사용성이 떨어집니다. 관심사 분리를 통해 Model 과 View 를 연결해주는 책임만 가지도록 해보는건 어떤가요?
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.
저도 고민했던 문제지만 어떻게 해야 할지 여전히 잘 모르겠습니다 ㅠ
저는 컨트롤러가 게임의 사회자라고 생각하고 있습니다. 그리고 게임이 시작한 후에 사회자는 한 라운드마다 주사위를 굴리고 자동차를 이동 시키는 책임을 가지고 있다고 생각하는데, 이 일을 어떤 클래스에게 위임해야 할까요..?