From f601b5d5a8db50524daa75b38c7ef81c770ba4ff Mon Sep 17 00:00:00 2001 From: Bae Han Jun Date: Sun, 21 Sep 2025 20:56:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +- docs/README.md | 25 +++++ src/main/java/racingcar/Application.java | 39 +++++++- src/main/java/racingcar/Car.java | 51 ++++++++++ src/main/java/racingcar/InputValidator.java | 46 +++++++++ src/main/java/racingcar/RacingGame.java | 62 +++++++++++++ src/test/java/racingcar/CarTest.java | 74 +++++++++++++++ .../java/racingcar/InputValidatorTest.java | 93 +++++++++++++++++++ src/test/java/racingcar/RacingGameTest.java | 54 +++++++++++ 9 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 src/main/java/racingcar/Car.java create mode 100644 src/main/java/racingcar/InputValidator.java create mode 100644 src/main/java/racingcar/RacingGame.java create mode 100644 src/test/java/racingcar/CarTest.java create mode 100644 src/test/java/racingcar/InputValidatorTest.java create mode 100644 src/test/java/racingcar/RacingGameTest.java diff --git a/build.gradle b/build.gradle index a89989b..6048069 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'application' } group 'camp.nextstep.edu' @@ -16,10 +17,14 @@ dependencies { java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } test { useJUnitPlatform() } + +application { + mainClass = 'racingcar.Application' +} diff --git a/docs/README.md b/docs/README.md index e69de29..ac737c6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,25 @@ +# 기능 목록 + +## 1. 자동차 관련 기능 +- [x] 자동차 이름 입력 및 검증 (5자 이하) +- [x] 자동차 위치 관리 +- [x] 자동차 전진 로직 (랜덤 값 4 이상일 때) +- [x] 자동차 상태 출력 (이름과 위치) + +## 2. 게임 로직 +- [x] 자동차 목록 생성 +- [x] 시도 횟수 입력 및 검증 +- [x] 각 라운드별 자동차 이동 처리 +- [x] 우승자 결정 (최대 위치의 자동차들) +- [x] 결과 출력 + +## 3. 입력/출력 +- [x] 자동차 이름 입력 (쉼표로 구분) +- [x] 시도 횟수 입력 +- [x] 각 라운드 결과 출력 +- [x] 최종 우승자 출력 + +## 4. 예외 처리 +- [x] 잘못된 자동차 이름 (5자 초과) +- [x] 잘못된 시도 횟수 (숫자가 아닌 경우) +- [x] IllegalArgumentException 발생 시 애플리케이션 종료 diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e..f7bada2 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,44 @@ package racingcar; +import camp.nextstep.edu.missionutils.Console; +import java.util.List; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + try { + List carNames = getCarNames(); + int attemptCount = getAttemptCount(); + + RacingGame game = new RacingGame(carNames); + playGame(game, attemptCount); + + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + throw e; + } + } + + private static List getCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String input = Console.readLine(); + return InputValidator.validateCarNames(input); + } + + private static int getAttemptCount() { + System.out.println("시도할 회수는 몇회인가요?"); + String input = Console.readLine(); + return InputValidator.validateAttemptCount(input); + } + + private static void playGame(RacingGame game, int attemptCount) { + System.out.println(); + System.out.println("실행 결과"); + + for (int i = 0; i < attemptCount; i++) { + game.playRound(); + game.printRoundResult(); + } + + game.printWinners(); } } diff --git a/src/main/java/racingcar/Car.java b/src/main/java/racingcar/Car.java new file mode 100644 index 0000000..27fc51d --- /dev/null +++ b/src/main/java/racingcar/Car.java @@ -0,0 +1,51 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Randoms; + +public class Car { + private static final int MOVE_THRESHOLD = 4; + private static final int RANDOM_MIN = 0; + private static final int RANDOM_MAX = 9; + private static final int MAX_NAME_LENGTH = 5; + + private final String name; + private int position; + + public Car(String name) { + validateName(name); + this.name = name; + this.position = 0; + } + + private void validateName(String name) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 비어있을 수 없습니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); + } + } + + public void move() { + int randomNumber = Randoms.pickNumberInRange(RANDOM_MIN, RANDOM_MAX); + if (randomNumber >= MOVE_THRESHOLD) { + position++; + } + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public String getPositionString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < position; i++) { + sb.append("-"); + } + return sb.toString(); + } +} diff --git a/src/main/java/racingcar/InputValidator.java b/src/main/java/racingcar/InputValidator.java new file mode 100644 index 0000000..db9cff4 --- /dev/null +++ b/src/main/java/racingcar/InputValidator.java @@ -0,0 +1,46 @@ +package racingcar; + +import java.util.Arrays; +import java.util.List; + +public class InputValidator { + + public static List validateCarNames(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException("자동차 이름을 입력해주세요."); + } + + List carNames = Arrays.asList(input.split(",", -1)); + for (String name : carNames) { + String trimmedName = name.trim(); + validateCarName(trimmedName); + } + + return carNames; + } + + private static void validateCarName(String name) { + if (name.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 비어있을 수 없습니다."); + } + if (name.length() > 5) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); + } + } + + public static int validateAttemptCount(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException("시도할 회수를 입력해주세요."); + } + + try { + int count = Integer.parseInt(input.trim()); + if (count <= 0) { + throw new IllegalArgumentException("시도할 회수는 1 이상이어야 합니다."); + } + return count; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("시도할 회수는 숫자여야 합니다."); + } + } +} diff --git a/src/main/java/racingcar/RacingGame.java b/src/main/java/racingcar/RacingGame.java new file mode 100644 index 0000000..55702c1 --- /dev/null +++ b/src/main/java/racingcar/RacingGame.java @@ -0,0 +1,62 @@ +package racingcar; + +import java.util.ArrayList; +import java.util.List; + +public class RacingGame { + private final List cars; + + public RacingGame(List carNames) { + this.cars = createCars(carNames); + } + + private List createCars(List carNames) { + List cars = new ArrayList<>(); + for (String name : carNames) { + cars.add(new Car(name)); + } + return cars; + } + + public void playRound() { + for (Car car : cars) { + car.move(); + } + } + + public void printRoundResult() { + for (Car car : cars) { + System.out.println(car.getName() + " : " + car.getPositionString()); + } + System.out.println(); + } + + public List getWinners() { + int maxPosition = getMaxPosition(); + List winners = new ArrayList<>(); + + for (Car car : cars) { + if (car.getPosition() == maxPosition) { + winners.add(car.getName()); + } + } + + return winners; + } + + private int getMaxPosition() { + int maxPosition = 0; + for (Car car : cars) { + if (car.getPosition() > maxPosition) { + maxPosition = car.getPosition(); + } + } + return maxPosition; + } + + public void printWinners() { + List winners = getWinners(); + System.out.print("최종 우승자 : "); + System.out.println(String.join(", ", winners)); + } +} diff --git a/src/test/java/racingcar/CarTest.java b/src/test/java/racingcar/CarTest.java new file mode 100644 index 0000000..ae65765 --- /dev/null +++ b/src/test/java/racingcar/CarTest.java @@ -0,0 +1,74 @@ +package racingcar; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; + +class CarTest { + + @Test + @DisplayName("자동차 이름이 5자를 초과하면 예외가 발생한다") + void validateName_TooLongName_ThrowsException() { + assertThatThrownBy(() -> new Car("abcdef")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 5자 이하여야 합니다."); + } + + @Test + @DisplayName("자동차 이름이 비어있으면 예외가 발생한다") + void validateName_EmptyName_ThrowsException() { + assertThatThrownBy(() -> new Car("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("자동차 이름이 null이면 예외가 발생한다") + void validateName_NullName_ThrowsException() { + assertThatThrownBy(() -> new Car(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("자동차 이름이 공백만 있으면 예외가 발생한다") + void validateName_BlankName_ThrowsException() { + assertThatThrownBy(() -> new Car(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("유효한 자동차 이름으로 생성할 수 있다") + void createCar_ValidName_Success() { + Car car = new Car("pobi"); + + assertThat(car.getName()).isEqualTo("pobi"); + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + @DisplayName("자동차 이름이 5자일 때 생성할 수 있다") + void createCar_FiveCharacterName_Success() { + Car car = new Car("abcde"); + + assertThat(car.getName()).isEqualTo("abcde"); + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + @DisplayName("자동차의 초기 위치는 0이다") + void getPosition_InitialPosition_IsZero() { + Car car = new Car("pobi"); + + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + @DisplayName("자동차의 초기 위치 문자열은 빈 문자열이다") + void getPositionString_InitialPosition_IsEmpty() { + Car car = new Car("pobi"); + + assertThat(car.getPositionString()).isEqualTo(""); + } +} diff --git a/src/test/java/racingcar/InputValidatorTest.java b/src/test/java/racingcar/InputValidatorTest.java new file mode 100644 index 0000000..e463c9e --- /dev/null +++ b/src/test/java/racingcar/InputValidatorTest.java @@ -0,0 +1,93 @@ +package racingcar; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import java.util.List; +import static org.assertj.core.api.Assertions.*; + +class InputValidatorTest { + + @Test + @DisplayName("유효한 자동차 이름들을 검증할 수 있다") + void validateCarNames_ValidNames_Success() { + List result = InputValidator.validateCarNames("pobi,woni,jun"); + + assertThat(result).containsExactly("pobi", "woni", "jun"); + } + + @Test + @DisplayName("자동차 이름이 비어있으면 예외가 발생한다") + void validateCarNames_EmptyInput_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateCarNames("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름을 입력해주세요."); + } + + @Test + @DisplayName("자동차 이름이 null이면 예외가 발생한다") + void validateCarNames_NullInput_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateCarNames(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름을 입력해주세요."); + } + + @Test + @DisplayName("자동차 이름 중 하나가 5자를 초과하면 예외가 발생한다") + void validateCarNames_TooLongName_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateCarNames("pobi,abcdef")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 5자 이하여야 합니다."); + } + + @Test + @DisplayName("자동차 이름 중 하나가 비어있으면 예외가 발생한다") + void validateCarNames_EmptyName_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateCarNames("pobi,,")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("유효한 시도 횟수를 검증할 수 있다") + void validateAttemptCount_ValidCount_Success() { + int result = InputValidator.validateAttemptCount("5"); + + assertThat(result).isEqualTo(5); + } + + @Test + @DisplayName("시도 횟수가 비어있으면 예외가 발생한다") + void validateAttemptCount_EmptyInput_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateAttemptCount("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도할 회수를 입력해주세요."); + } + + @Test + @DisplayName("시도 횟수가 null이면 예외가 발생한다") + void validateAttemptCount_NullInput_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateAttemptCount(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도할 회수를 입력해주세요."); + } + + @Test + @DisplayName("시도 횟수가 숫자가 아니면 예외가 발생한다") + void validateAttemptCount_NotNumber_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateAttemptCount("abc")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도할 회수는 숫자여야 합니다."); + } + + @Test + @DisplayName("시도 횟수가 0 이하면 예외가 발생한다") + void validateAttemptCount_ZeroOrNegative_ThrowsException() { + assertThatThrownBy(() -> InputValidator.validateAttemptCount("0")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도할 회수는 1 이상이어야 합니다."); + + assertThatThrownBy(() -> InputValidator.validateAttemptCount("-1")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도할 회수는 1 이상이어야 합니다."); + } +} diff --git a/src/test/java/racingcar/RacingGameTest.java b/src/test/java/racingcar/RacingGameTest.java new file mode 100644 index 0000000..8fe72d2 --- /dev/null +++ b/src/test/java/racingcar/RacingGameTest.java @@ -0,0 +1,54 @@ +package racingcar; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import java.util.List; +import static org.assertj.core.api.Assertions.*; + +class RacingGameTest { + + @Test + @DisplayName("자동차 목록으로 게임을 생성할 수 있다") + void createGame_ValidCarNames_Success() { + List carNames = List.of("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames); + + assertThat(game).isNotNull(); + } + + @Test + @DisplayName("게임을 한 라운드 진행할 수 있다") + void playRound_Success() { + List carNames = List.of("pobi", "woni"); + RacingGame game = new RacingGame(carNames); + + game.playRound(); + + // 라운드가 진행되었는지 확인 (구체적인 위치는 랜덤이므로 확인하지 않음) + assertThat(game).isNotNull(); + } + + @Test + @DisplayName("우승자를 결정할 수 있다") + void getWinners_SingleWinner_Success() { + List carNames = List.of("pobi", "woni"); + RacingGame game = new RacingGame(carNames); + + List winners = game.getWinners(); + + assertThat(winners).isNotEmpty(); + assertThat(winners).containsAnyOf("pobi", "woni"); + } + + @Test + @DisplayName("공동 우승자를 결정할 수 있다") + void getWinners_MultipleWinners_Success() { + List carNames = List.of("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames); + + List winners = game.getWinners(); + + assertThat(winners).isNotEmpty(); + assertThat(winners.size()).isGreaterThanOrEqualTo(1); + } +}