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

[자동차 경주 step1] 양두영 미션 제출합니다. #19

Open
wants to merge 32 commits into
base: FhRh
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
643b604
feat : readme 작성
Due-IT Aug 6, 2024
1c60e62
feat : 게임 흐름 코드 추가
Due-IT Aug 6, 2024
76ee82c
feat : 게임 준비 컨트롤러 코드 작성
Due-IT Aug 6, 2024
1332d36
feat : 게임 준비 안내문구 출력 코드 작성
Due-IT Aug 6, 2024
480295e
feat : 게임 준비 입력기 코드 작성
Due-IT Aug 6, 2024
c6b78b0
feat : Cars객체 생성
Due-IT Aug 6, 2024
ed68363
test : 자동차 이름을 입력받아 쉼표 기준으로 구분한다
Due-IT Aug 6, 2024
540ba0c
fix : 한글 메서드 역따옴표 사용 및 출력테스트는 삭제
Due-IT Aug 6, 2024
4a8fa93
test : 라운드 입력과 예외처리에 대한 테스트 코드 작성
Due-IT Aug 6, 2024
0aae47d
docs : readme에 수행한 작업을 체크
Due-IT Aug 6, 2024
7322a9a
test : 자동차 객체 집합 생성 테스트 코드 작성
Due-IT Aug 6, 2024
469efd7
feat : 자동차 집합 생성 코드 작성
Due-IT Aug 6, 2024
e76d519
test : 자동차의 위치를 출력한다.
Due-IT Aug 6, 2024
128c5b2
feat : 이동 및 위치 출력 코드 작성
Due-IT Aug 6, 2024
1886873
test : 주사위를 굴려 한자리 정수를 반환한다
Due-IT Aug 6, 2024
c39982d
feat : 주사위를 굴려 1~9사이의 정수를 반환한다
Due-IT Aug 6, 2024
fbe849d
test : 특정 위치의 자동차를 한 칸 움직인다
Due-IT Aug 6, 2024
6da8a3d
feat : 테스트 코드 통과 완료
Due-IT Aug 6, 2024
21b64a2
feat : cars에 속한 car들의 위치 출력
Due-IT Aug 6, 2024
a8747eb
fix : Cars를 일급 컬렉션 정의 규칙에 맞게 수정
Due-IT Aug 6, 2024
b3dcfca
feat&test : 우승한 차들의 이름을 출력한다.
Due-IT Aug 6, 2024
73495d0
fix : 오로지 생성자만을 사용한 Cars객체 생성 및 전체적인 기능 작성 완료
Due-IT Aug 6, 2024
ca514d0
docs : 작업한 내용 readme 적용
Due-IT Aug 6, 2024
30bb064
refactor : mvc패턴을 고려하여 리팩토링
Due-IT Aug 9, 2024
0bdeb43
refactor : GameInputer 위치 변경
Due-IT Aug 9, 2024
a0631cf
feat : 입력값 검증 기능 추가
Due-IT Aug 9, 2024
77fff6b
docs : README 수정
Due-IT Aug 9, 2024
7c200d5
refactor : cars는 우승에 대한 개념과 출력에 대해 모른다.
Due-IT Aug 15, 2024
075ae77
refactor : 우승자는 컨트롤러가 결정한다.
Due-IT Aug 15, 2024
ea21bb6
fix : position은 외부에서 변경할 수 없다.
Due-IT Aug 15, 2024
5157d6a
refactor : require를 통해 입력값을 검사한다.
Due-IT Aug 15, 2024
be3bcb1
refactor : 중복검사 최적화
Due-IT Aug 15, 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
88 changes: 87 additions & 1 deletion README.md
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]+
45 changes: 45 additions & 0 deletions src/main/kotlin/racingcar/controller/GameController.kt
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)
}
}
Comment on lines +29 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

Controller 에 도메인 로직이 많아 보입니다. MVC 에서 비대해진 컨트롤러는 테스트하기 힘들고 재사용성이 떨어집니다. 관심사 분리를 통해 Model 과 View 를 연결해주는 책임만 가지도록 해보는건 어떤가요?

Copy link
Author

@Due-IT Due-IT Aug 15, 2024

Choose a reason for hiding this comment

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

저도 고민했던 문제지만 어떻게 해야 할지 여전히 잘 모르겠습니다 ㅠ
저는 컨트롤러가 게임의 사회자라고 생각하고 있습니다. 그리고 게임이 시작한 후에 사회자는 한 라운드마다 주사위를 굴리고 자동차를 이동 시키는 책임을 가지고 있다고 생각하는데, 이 일을 어떤 클래스에게 위임해야 할까요..?

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

Choose a reason for hiding this comment

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

private 함수는 테스트하기 힘들죠.
이 부분도 심판 도메인 모델로 분리하면 관심사도 분리되고 및 테스트 용이성도 증가할 것 같아요.!

Copy link
Contributor

Choose a reason for hiding this comment

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

Cars 를 받아 Winners 를 구하는 책임을 누군가 가지는게 어떤가요!

Copy link
Author

Choose a reason for hiding this comment

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

아하 심판 도메인을 구현하는 부분 참 좋은 생각인것 같습니다!

}
5 changes: 4 additions & 1 deletion src/main/kotlin/racingcar/main.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 gameController = GameController()
gameController.run()
}
16 changes: 16 additions & 0 deletions src/main/kotlin/racingcar/model/Car.kt
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
Copy link
Contributor

Choose a reason for hiding this comment

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

private set 👍

Copy link
Author

Choose a reason for hiding this comment

The 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++
}

}
19 changes: 19 additions & 0 deletions src/main/kotlin/racingcar/model/Cars.kt
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
Copy link
Contributor

Choose a reason for hiding this comment

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

자동차를 움직일 수 있는 책임이 분산되어 있어요. Cars에서도, Cars 를 생성한 곳에서도 특정 index 의 Car 을 움직일 수 있는 형태입니다. 이는 사이드이펙트를 발생시킬 수 있는 구조로 보여요.

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

import kotlin.random.Random

object RandomDice {
Copy link
Contributor

Choose a reason for hiding this comment

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

object 와 class 의 차이는 무엇일까요?

Copy link
Author

Choose a reason for hiding this comment

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

object는 코틀린에서 특정 클래스를 싱글톤으로 사용하기 위한 키워드로 알고 있습니다.
class와 달리 어디서든 사용되는 주사위는 같을 것으로 예상되어 싱글톤을 적용하였습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

확장에 유연하지 않아 이렇게 사용한다면 랜덤 함수를 직접 호출하는 것과 크게 다르지 않아보여요

fun rollDice():Int = Random.nextInt(0, 10)
}
33 changes: 33 additions & 0 deletions src/main/kotlin/racingcar/view/GameAnnouncer.kt
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)))
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/racingcar/view/GameInputer.kt
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
}
}
39 changes: 39 additions & 0 deletions src/test/kotlin/study/model/CarTest.kt
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)
}
}
44 changes: 44 additions & 0 deletions src/test/kotlin/study/model/CarsTest.kt
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)
}
}
56 changes: 56 additions & 0 deletions src/test/kotlin/study/utils/GameInputerTest.kt
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()
}
}
}
Loading