Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# kotlin-racingcar-precourse

# 자동차 경주

## 구현할 기능 목록

1. 자동차 이름 입력 기능
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 중복된 이름은 허용하지 않는다.

2. 시도 횟수 입력 기능
- 시도 횟수는 0 이상의 정수여야 한다.
- 음수나 숫자가 아닌 값이 입력되면 예외 처리를 한다.

3. 자동차 이동 기능
- 각 자동차는 0부터 9까지의 랜덤 숫자를 생성한다.
- 생성된 숫자가 4 이상일 경우 자동차는 한 칸 전진한다.
- 생성된 숫자가 4 미만일 경우 자동차는 움직이지 않는다.
- 각 자동차의 현재 위치를 화면에 출력한다.
- 이동 결과는 시도 횟수만큼 반복 출력한다.

4. 우승자 결정 기능
- 가장 멀리 이동한 자동차가 우승자이다.
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
20 changes: 18 additions & 2 deletions src/main/kotlin/racingcar/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
package racingcar

fun main() {
// TODO: 프로그램 구현
}
val inputHandler = InputHandler()
val outputHandler = OutputHandler()
val car = Car()

val carNames = inputHandler.getCarNames()
car.addCar(carNames)
val raceCount = inputHandler.getRaceCount()

outputHandler.printRaceMessage()
repeat(raceCount) {
car.moveCar()
outputHandler.printRaceResult(car.carsPosition)
}

val maxPosition = car.carsPosition.values.maxOrNull() ?: 0
val winners = car.carsPosition.filter { it.value == maxPosition }.keys.toList()
outputHandler.printWinners(winners)
}
22 changes: 22 additions & 0 deletions src/main/kotlin/racingcar/Car.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package racingcar

import camp.nextstep.edu.missionutils.Randoms

class Car(private val randomProvider: () -> Int = { Randoms.pickNumberInRange(0, 9) }) {
val carsPosition: MutableMap<String, Int> = mutableMapOf()

fun addCar(carNames: List<String>) {
for (name in carNames) {
carsPosition[name] = 0
}
}

fun moveCar() {
for (name in carsPosition.keys) {
val randomNumber = randomProvider()
if (randomNumber >= 4) {
carsPosition[name] = carsPosition[name]!! + 1
}
}
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/racingcar/InputHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar

import camp.nextstep.edu.missionutils.Console

class InputHandler {
private val inputValidator = InputValidator()
fun getCarNames(): List<String> {
println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")

val names = Console.readLine()
val carNames = names.split(",").map { it.trim() }

return inputValidator.validateCarNames(carNames)
}

fun getRaceCount(): Int {
println("시도할 횟수는 몇 회인가요?")

val input = Console.readLine()

return inputValidator.validateRaceCount(input)
}
}
37 changes: 37 additions & 0 deletions src/main/kotlin/racingcar/InputValidator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package racingcar

class InputValidator {
fun validateCarNames(carNames: List<String>): List<String> {
if (carNames.isEmpty() || carNames.any { it.isBlank() }) {
throw IllegalArgumentException("자동차 이름은 비어있을 수 없습니다.")
}

carNames.forEach { name ->
if (name.length > 5) {
throw IllegalArgumentException("자동차 이름 '$name'은 5자를 초과할 수 없습니다.")
}
if (name.isEmpty()) {
throw IllegalArgumentException("자동차 이름은 빈 문자열일 수 없습니다.")
}
}

val uniqueNames = carNames.toSet()
if (uniqueNames.size != carNames.size) {
throw IllegalArgumentException("자동차 이름은 중복될 수 없습니다.")
}

return carNames
}

fun validateRaceCount(input: String): Int {
return try {
val raceCount = input.toInt()
if (raceCount <= 0) {
throw IllegalArgumentException("시도 횟수는 양의 정수여야 합니다.")
}
raceCount
} catch (e: NumberFormatException) {
throw IllegalArgumentException("시도 횟수는 정수여야 합니다.")
}
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/racingcar/OutputHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package racingcar

class OutputHandler {

fun printRaceMessage() {
println()
println("실행 결과")
}

fun printRaceResult(carsPosition: Map<String, Int>) {
for ((name, position) in carsPosition) {
val progress = "-".repeat(position)
println("$name : $progress")
}
println()
}

fun printWinners(winners: List<String>) {
val winnerNames = winners.joinToString(", ")
println("최종 우승자 : $winnerNames")
}
}
30 changes: 30 additions & 0 deletions src/test/kotlin/racingcar/CarTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package racingcar

import org.junit.jupiter.api.Test
import org.assertj.core.api.Assertions.assertThat

class CarTest {
@Test
fun `랜덤 값이 4 이상이면 자동차 위치가 증가한다`() {
val car = Car { 4 } // 항상 4 반환
car.addCar(listOf("car1", "car2"))

car.moveCar()

assertThat(car.carsPosition).containsExactlyInAnyOrderEntriesOf(
mapOf("car1" to 1, "car2" to 1)
)
}

@Test
fun `랜덤 값이 4 미만이면 자동차 위치가 증가하지 않는다`() {
val car = Car { 3 } // 항상 3 반환
car.addCar(listOf("car1", "car2"))

car.moveCar()

assertThat(car.carsPosition).containsExactlyInAnyOrderEntriesOf(
mapOf("car1" to 0, "car2" to 0)
)
}
}
107 changes: 107 additions & 0 deletions src/test/kotlin/racingcar/InputValidatorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package racingcar

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class InputValidatorTest {

private lateinit var validator: InputValidator

@BeforeEach
fun setUp() {
validator = InputValidator()
}

@Test
fun `유효한 자동차 이름 리스트는 정상적으로 반환된다`() {
val carNames = listOf("car1", "car2", "car3")

val result = validator.validateCarNames(carNames)

assertThat(result).isEqualTo(carNames)
}

@Test
fun `빈 자동차 이름 리스트는 예외를 발생시킨다`() {
val carNames = emptyList<String>()

assertThatThrownBy { validator.validateCarNames(carNames) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("자동차 이름은 비어있을 수 없습니다.")
}

@Test
fun `공백이 포함된 자동차 이름은 예외를 발생시킨다`() {
val carNames = listOf("car1", " ", "car3")

assertThatThrownBy { validator.validateCarNames(carNames) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("자동차 이름은 비어있을 수 없습니다.")
}

@Test
fun `5자를 초과하는 자동차 이름은 예외를 발생시킨다`() {
val carNames = listOf("car1", "toolong", "car3")

assertThatThrownBy { validator.validateCarNames(carNames) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("자동차 이름 'toolong'은 5자를 초과할 수 없습니다.")
}

@Test
fun `중복된 자동차 이름은 예외를 발생시킨다`() {
val carNames = listOf("car1", "car1", "car2")

assertThatThrownBy { validator.validateCarNames(carNames) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("자동차 이름은 중복될 수 없습니다.")
}

@Test
fun `유효한 입력은 정수로 변환되어 반환된다`() {
val input = "5"

val result = validator.validateRaceCount(input)

assertThat(result).isEqualTo(5)
}

@Test
fun `0 이하의 입력은 예외를 발생시킨다`() {
val inputZero = "0"
val inputNegative = "-1"

assertThatThrownBy { validator.validateRaceCount(inputZero) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("시도 횟수는 양의 정수여야 합니다.")

assertThatThrownBy { validator.validateRaceCount(inputNegative) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("시도 횟수는 양의 정수여야 합니다.")
}

@Test
fun `정수가 아닌 입력은 예외를 발생시킨다`() {
val inputNonNumeric = "abc"
val inputDecimal = "1.5"

assertThatThrownBy { validator.validateRaceCount(inputNonNumeric) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("시도 횟수는 정수여야 합니다.")

assertThatThrownBy { validator.validateRaceCount(inputDecimal) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("시도 횟수는 정수여야 합니다.")
}

@Test
fun `빈 문자열 입력은 예외를 발생시킨다`() {
val input = ""

assertThatThrownBy { validator.validateRaceCount(input) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("시도 횟수는 정수여야 합니다.")
}
}
52 changes: 52 additions & 0 deletions src/test/kotlin/racingcar/OutputHandlerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package racingcar

import org.junit.jupiter.api.Test
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import java.io.ByteArrayOutputStream
import java.io.PrintStream

class OutputHandlerTest {
private lateinit var outputHandler: OutputHandler
private lateinit var outputStream: ByteArrayOutputStream

@BeforeEach
fun setUp() {
outputHandler = OutputHandler()
outputStream = ByteArrayOutputStream()
System.setOut(PrintStream(outputStream))
}

@Test
fun `자동차 이름과 현재 진행 상태를 출력한다`() {
val carsPosition = mapOf("car1" to 2, "car2" to 0, "car3" to 3)
val lineSeparator = System.lineSeparator()

outputHandler.printRaceResult(carsPosition)

val expectedOutput = "car1 : --${lineSeparator}car2 : ${lineSeparator}car3 : ---${lineSeparator}${lineSeparator}"
assertThat(outputStream.toString()).isEqualTo(expectedOutput)
}

@Test
fun `printWinners는 단일 우승자 이름을 출력한다`() {
val winners = listOf("car1")
val lineSeparator = System.lineSeparator()

outputHandler.printWinners(winners)

val expectedOutput = "최종 우승자 : car1${lineSeparator}"
assertThat(outputStream.toString()).isEqualTo(expectedOutput)
}

@Test
fun `printWinners는 여러 우승자 이름을 쉼표로 구분하여 출력한다`() {
val winners = listOf("car1", "car2", "car3")
val lineSeparator = System.lineSeparator()

outputHandler.printWinners(winners)

val expectedOutput = "최종 우승자 : car1, car2, car3${lineSeparator}"
assertThat(outputStream.toString()).isEqualTo(expectedOutput)
}
}