From 361cbb7915ba0047c06ec7d406cd91d4be1410d8 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 13:54:59 +0900 Subject: [PATCH 01/19] docs(README): add feature list for racing car --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d0286c859f..7216466133 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# java-racingcar-precourse +## 기능 구현 목록 + +### 1. 입력 기능 +- [ ] 자동차 이름 입력 받기 +- [ ] 시도 횟수 입력 받기 + +### 2. 입력 검증 기능 +**자동차 이름 검증** +- [ ] 입력값이 null이거나 빈 문자열인 경우 예외 처리 +- [ ] 자동차 이름이 5자를 초과하는 경우 예외 처리 +- [ ] 자동차 이름이 빈 문자열인 경우 예외 처리 +- [ ] 자동차 이름이 공백만 있는 경우 예외 처리 + +**시도 횟수 검증** +- [ ] 입력값이 null이거나 빈 문자열인 경우 예외 처리 +- [ ] 숫자가 아닌 값 입력 시 예외 처리 +- [ ] 0 이하의 값 입력 시 예외 처리 +- [ ] 정수 범위를 벗어나는 값 입력 시 예외 처리 + +### 3. 입력 파싱 기능 +- [ ] 쉼표(,)를 기준으로 자동차 이름 분리 +- [ ] 입력받은 시도 횟수를 정수로 변환 + +### 4. 자동차 기능 +- [ ] 자동차 객체 생성 (이름 저장) +- [ ] 자동차의 현재 위치 저장 +- [ ] 자동차 전진 기능 +- [ ] 자동차의 현재 위치 조회 + +### 5. 게임 진행 기능 +- [ ] 게임 라운드 반복 실행 (n회) +- [ ] 각 라운드마다 모든 자동차에 대해 이동 시도 +- [ ] 0~9 사이의 무작위 값 생성 +- [ ] 무작위 값이 4 이상일 경우 자동차 전진 + +### 6. 우승자 판별 기능 +- [ ] 모든 자동차 중 최대 이동 거리 찾기 +- [ ] 최대 이동 거리를 가진 자동차들을 우승자로 선정 + +### 7. 출력 기능 +- [ ] 각 라운드 후 모든 자동차의 이름과 위치 출력 + - [ ] 자동차 위치를 `-` 기호로 표시 +- [ ] 우승자 이름 출력 (단독 우승자) +- [ ] 우승자 이름 출력 (공동 우승자, 쉼표로 구분) + +--- + From a6913f990e413b21f9c2c55512de4c0e58824671 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 21:53:31 +0900 Subject: [PATCH 02/19] test(ApplicationTest): add various tests for racing car functionality --- src/test/java/racingcar/ApplicationTest.java | 85 ++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java index 1d35fc33fe..df596a5256 100644 --- a/src/test/java/racingcar/ApplicationTest.java +++ b/src/test/java/racingcar/ApplicationTest.java @@ -1,6 +1,7 @@ package racingcar; import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; @@ -13,6 +14,7 @@ class ApplicationTest extends NsTest { private static final int STOP = 3; @Test + @DisplayName("단독 우승자 기능 테스트") void 기능_테스트() { assertRandomNumberInRangeTest( () -> { @@ -24,6 +26,32 @@ class ApplicationTest extends NsTest { } @Test + @DisplayName("공동 우승자 기능 테스트") + void 공동_우승자_테스트() { + assertRandomNumberInRangeTest( + () -> { + run("pobi,woni,jun", "1"); + assertThat(output()).contains("pobi : -", "woni : -", "jun : ", "최종 우승자 : pobi, woni"); + }, + MOVING_FORWARD, MOVING_FORWARD, STOP + ); + } + + @Test + @DisplayName("여러 라운드 실행 테스트") + void 여러_라운드_테스트() { + assertRandomNumberInRangeTest( + () -> { + run("pobi,woni", "5"); + assertThat(output()).contains("pobi :", "woni :", "최종 우승자"); + }, + MOVING_FORWARD, STOP, MOVING_FORWARD, STOP, MOVING_FORWARD, STOP, + MOVING_FORWARD, STOP, MOVING_FORWARD, STOP + ); + } + + @Test + @DisplayName("자동차 이름이 5자를 초과하면 예외 발생") void 예외_테스트() { assertSimpleTest(() -> assertThatThrownBy(() -> runException("pobi,javaji", "1")) @@ -31,6 +59,63 @@ class ApplicationTest extends NsTest { ); } + @Test + @DisplayName("자동차 이름이 빈 문자열이면 예외 발생") + void 빈_이름_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,,woni", "1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("시도 횟수가 0이면 예외 발생") + void 시도_횟수_0_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "0")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("시도 횟수가 음수면 예외 발생") + void 시도_횟수_음수_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "-1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("시도 횟수가 숫자가 아니면 예외 발생") + void 시도_횟수_문자_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "abc")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("자동차 이름이 null이면 예외 발생") + void 자동차_이름_null_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("", "5")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("단일 자동차로 게임 실행") + void 단일_자동차_테스트() { + assertRandomNumberInRangeTest( + () -> { + run("pobi", "3"); + assertThat(output()).contains("pobi :", "최종 우승자 : pobi"); + }, + MOVING_FORWARD, MOVING_FORWARD, MOVING_FORWARD + ); + } + @Override public void runMain() { Application.main(new String[]{}); From 8704960729900989b9bc5cac4cc2e6186c513848 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 21:53:37 +0900 Subject: [PATCH 03/19] test(RacingGame): add unit tests for racing game functionality --- .../java/racingcar/domain/RacingGameTest.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 src/test/java/racingcar/domain/RacingGameTest.java diff --git a/src/test/java/racingcar/domain/RacingGameTest.java b/src/test/java/racingcar/domain/RacingGameTest.java new file mode 100644 index 0000000000..5f602705b9 --- /dev/null +++ b/src/test/java/racingcar/domain/RacingGameTest.java @@ -0,0 +1,165 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RacingGameTest { + + @Test + @DisplayName("게임 생성 시 자동차들이 정상적으로 저장된다") + void createGameWithCars() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + + // when + RacingGame game = new RacingGame(carNames); + + // then + assertThat(game.getCars()).hasSize(3); + } + + @Test + @DisplayName("라운드 실행 시 모든 자동차가 이동을 시도한다") + void playOneRound() { + // given + List carNames = Arrays.asList("pobi", "woni"); + RacingGame game = new RacingGame(carNames); + + // when + game.playRound(); + + // then + List cars = game.getCars(); + assertThat(cars).allMatch(car -> car.getPosition() >= 0); + } + + @Test + @DisplayName("여러 라운드를 실행할 수 있다") + void playMultipleRounds() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames); + int rounds = 5; + + // when + for (int i = 0; i < rounds; i++) { + game.playRound(); + } + + // then + List cars = game.getCars(); + assertThat(cars).allMatch(car -> car.getPosition() >= 0); + } + + @Test + @DisplayName("우승자를 판별할 수 있다 - 단독 우승") + void findSingleWinner() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames); + + // 특정 자동차만 전진시키기 (테스트를 위해 직접 조작) + List cars = game.getCars(); + cars.get(0).move(); // pobi 전진 + cars.get(0).move(); // pobi 전진 + + // when + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactly("pobi"); + } + + @Test + @DisplayName("우승자를 판별할 수 있다 - 공동 우승") + void findMultipleWinners() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames); + + // 두 자동차를 같은 거리로 전진시키기 + List cars = game.getCars(); + cars.get(0).move(); // pobi + cars.get(0).move(); // pobi + cars.get(1).move(); // woni + cars.get(1).move(); // woni + cars.get(2).move(); // jun + + // when + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); + } + + @Test + @DisplayName("모든 자동차가 같은 위치면 모두 우승자이다") + void findAllWinnersWhenTied() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames); + + // 모든 자동차를 같은 거리로 + List cars = game.getCars(); + cars.forEach(car -> { + car.move(); + car.move(); + }); + + // when + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactlyInAnyOrder("pobi", "woni", "jun"); + } + + @Test + @DisplayName("한 대의 자동차만 있어도 게임을 진행할 수 있다") + void playGameWithSingleCar() { + // given + List carNames = List.of("pobi"); + RacingGame game = new RacingGame(carNames); + + // when + game.playRound(); + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactly("pobi"); + } + + @Test + @DisplayName("게임 시작 시 모든 자동차의 위치는 0이다") + void initialPositionIsZero() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + + // when + RacingGame game = new RacingGame(carNames); + + // then + List cars = game.getCars(); + assertThat(cars).allMatch(car -> car.getPosition() == 0); + } + + @Test + @DisplayName("현재 자동차 목록을 조회할 수 있다") + void getCars() { + // given + List carNames = Arrays.asList("pobi", "woni"); + RacingGame game = new RacingGame(carNames); + + // when + List cars = game.getCars(); + + // then + assertThat(cars).hasSize(2); + assertThat(cars.get(0).getName()).isEqualTo("pobi"); + assertThat(cars.get(1).getName()).isEqualTo("woni"); + } +} \ No newline at end of file From f6e8f0bd4d9d3a69d89a74f235ca168596a30215 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 21:53:44 +0900 Subject: [PATCH 04/19] test(InputView): add unit tests for input parsing functionality --- .../java/racingcar/view/InputViewTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/test/java/racingcar/view/InputViewTest.java diff --git a/src/test/java/racingcar/view/InputViewTest.java b/src/test/java/racingcar/view/InputViewTest.java new file mode 100644 index 0000000000..343f633cf1 --- /dev/null +++ b/src/test/java/racingcar/view/InputViewTest.java @@ -0,0 +1,97 @@ +package racingcar.view; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class InputViewTest { + + @Test + @DisplayName("쉼표로 구분된 자동차 이름을 파싱한다") + void parseCarNames() { + // given + String input = "pobi,woni,jun"; + + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).containsExactly("pobi", "woni", "jun"); + } + + @Test + @DisplayName("자동차 이름이 하나만 있어도 파싱된다") + void parseSingleCarName() { + // given + String input = "pobi"; + + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).containsExactly("pobi"); + } + + @ParameterizedTest + @CsvSource({ + "'pobi,woni,jun', 3", + "'pobi,woni', 2", + "'pobi', 1", + "'a,b,c,d,e', 5" + }) + @DisplayName("여러 개의 자동차 이름을 정확히 파싱한다") + void parseMultipleCarNames(String input, int expectedSize) { + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).hasSize(expectedSize); + } + + @Test + @DisplayName("시도 횟수 문자열을 정수로 변환한다") + void parseRoundCount() { + // given + String input = "5"; + + // when + int roundCount = InputView.parseRoundCount(input); + + // then + assertThat(roundCount).isEqualTo(5); + } + + @ParameterizedTest + @CsvSource({ + "1, 1", + "5, 5", + "10, 10", + "100, 100" + }) + @DisplayName("다양한 시도 횟수를 정확히 파싱한다") + void parseVariousRoundCounts(String input, int expected) { + // when + int roundCount = InputView.parseRoundCount(input); + + // then + assertThat(roundCount).isEqualTo(expected); + } + + @Test + @DisplayName("자동차 이름 사이의 공백은 제거된다") + void trimSpacesInCarNames() { + // given + String input = "pobi, woni, jun"; + + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).containsExactly("pobi", "woni", "jun"); + } +} \ No newline at end of file From d4e70581cd600f7b239c05caa363df22c2780d4f Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 21:53:48 +0900 Subject: [PATCH 05/19] test(CarTest): add unit tests for car creation and movement functionality --- src/test/java/racingcar/domain/CarTest.java | 128 ++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/test/java/racingcar/domain/CarTest.java diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000000..32c65b674d --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,128 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CarTest { + + @Test + @DisplayName("자동차 생성 시 이름이 정상적으로 저장된다") + void createCarWithName() { + // given + String name = "pobi"; + + // when + Car car = new Car(name); + + // then + assertThat(car.getName()).isEqualTo(name); + } + + @Test + @DisplayName("자동차 생성 시 초기 위치는 0이다") + void createCarWithInitialPosition() { + // given + String name = "pobi"; + + // when + Car car = new Car(name); + + // then + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + @DisplayName("자동차가 전진하면 위치가 1 증가한다") + void moveForward() { + // given + Car car = new Car("pobi"); + int initialPosition = car.getPosition(); + + // when + car.move(); + + // then + assertThat(car.getPosition()).isEqualTo(initialPosition + 1); + } + + @Test + @DisplayName("자동차가 여러 번 전진하면 위치가 누적된다") + void moveForwardMultipleTimes() { + // given + Car car = new Car("pobi"); + + // when + car.move(); + car.move(); + car.move(); + + // then + assertThat(car.getPosition()).isEqualTo(3); + } + + @Test + @DisplayName("자동차 이름이 5자를 초과하면 예외가 발생한다") + void createCarWithNameLongerThan5() { + // given + String name = "pobi12"; + + // when & then + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("자동차 이름이 null이거나 빈 문자열이면 예외가 발생한다") + void createCarWithNullOrEmptyName(String name) { + // when & then + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", " "}) + @DisplayName("자동차 이름이 공백만 있으면 예외가 발생한다") + void createCarWithBlankName(String name) { + // when & then + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @Test + @DisplayName("다른 자동차와 위치를 비교할 수 있다") + void comparePosition() { + // given + Car car1 = new Car("pobi"); + Car car2 = new Car("woni"); + + car1.move(); + car1.move(); + car2.move(); + + // when & then + assertThat(car1.getPosition()).isGreaterThan(car2.getPosition()); + } + + @Test + @DisplayName("자동차 이름은 5자까지 가능하다") + void createCarWithName5Characters() { + // given + String name = "pobi1"; + + // when + Car car = new Car(name); + + // then + assertThat(car.getName()).isEqualTo(name); + } +} \ No newline at end of file From c7e2ea1b76056be8801947d983d5abbbe4e4ba24 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 21:53:54 +0900 Subject: [PATCH 06/19] test(InputValidator): add unit tests for car name and round count validation --- .../validator/InputValidatorTest.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/test/java/racingcar/validator/InputValidatorTest.java diff --git a/src/test/java/racingcar/validator/InputValidatorTest.java b/src/test/java/racingcar/validator/InputValidatorTest.java new file mode 100644 index 0000000000..c6f5b9983f --- /dev/null +++ b/src/test/java/racingcar/validator/InputValidatorTest.java @@ -0,0 +1,141 @@ +package racingcar.validator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class InputValidatorTest { + + @Test + @DisplayName("정상적인 자동차 이름들을 검증한다") + void validateValidCarNames() { + // given + String carNames = "pobi,woni,jun"; + + // when & then + assertThatCode(() -> InputValidator.validateCarNames(carNames)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("자동차 이름 입력이 null이거나 빈 문자열이면 예외가 발생한다") + void validateNullOrEmptyCarNames(String carNames) { + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @Test + @DisplayName("자동차 이름 중 5자를 초과하는 이름이 있으면 예외가 발생한다") + void validateCarNameLongerThan5() { + // given + String carNames = "pobi,javaji,woni"; + + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @ValueSource(strings = {"pobi,,woni", ",pobi,woni", "pobi,woni,", "pobi, ,woni"}) + @DisplayName("자동차 이름 중 빈 이름이 있으면 예외가 발생한다") + void validateCarNamesWithEmpty(String carNames) { + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @ValueSource(strings = {"pobi, ,woni", "pobi, ,jun"}) + @DisplayName("자동차 이름 중 공백만 있는 이름이 있으면 예외가 발생한다") + void validateCarNamesWithBlank(String carNames) { + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @Test + @DisplayName("자동차 이름이 하나만 있어도 정상적으로 검증된다") + void validateSingleCarName() { + // given + String carNames = "pobi"; + + // when & then + assertThatCode(() -> InputValidator.validateCarNames(carNames)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("정상적인 시도 횟수를 검증한다") + void validateValidRoundCount() { + // given + String roundCount = "5"; + + // when & then + assertThatCode(() -> InputValidator.validateRoundCount(roundCount)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("시도 횟수가 null이거나 빈 문자열이면 예외가 발생한다") + void validateNullOrEmptyRoundCount(String roundCount) { + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @ParameterizedTest + @ValueSource(strings = {"abc", "1.5", "일", "1a", " "}) + @DisplayName("시도 횟수가 숫자가 아니면 예외가 발생한다") + void validateNonNumericRoundCount(String roundCount) { + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @ParameterizedTest + @ValueSource(strings = {"0", "-1", "-100"}) + @DisplayName("시도 횟수가 0 이하이면 예외가 발생한다") + void validateRoundCountLessThanOrEqualToZero(String roundCount) { + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @Test + @DisplayName("시도 횟수가 정수 범위를 벗어나면 예외가 발생한다") + void validateRoundCountOutOfIntegerRange() { + // given + String roundCount = "9999999999999999999"; + + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @Test + @DisplayName("시도 횟수가 10이면 정상적으로 검증된다") + void validateRoundCountOne() { + // given + String roundCount = "10"; + + // when & then + assertThatCode(() -> InputValidator.validateRoundCount(roundCount)) + .doesNotThrowAnyException(); + } +} \ No newline at end of file From 6056a210e1fc8bb4a039d39d086a4bf61f15643b Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 21:54:09 +0900 Subject: [PATCH 07/19] test(OutputView): add unit tests for car position and winner formatting --- .../java/racingcar/view/OutputViewTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/test/java/racingcar/view/OutputViewTest.java diff --git a/src/test/java/racingcar/view/OutputViewTest.java b/src/test/java/racingcar/view/OutputViewTest.java new file mode 100644 index 0000000000..d47f51f673 --- /dev/null +++ b/src/test/java/racingcar/view/OutputViewTest.java @@ -0,0 +1,97 @@ +package racingcar.view; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutputViewTest { + + @Test + @DisplayName("자동차 위치를 - 기호로 포맷팅한다") + void formatCarPosition() { + // given + String carName = "pobi"; + int position = 3; + + // when + String result = OutputView.formatCarStatus(carName, position); + + // then + assertThat(result).isEqualTo("pobi : ---"); + } + + @ParameterizedTest + @CsvSource({ + "pobi, 0, 'pobi : '", + "pobi, 1, 'pobi : -'", + "pobi, 2, 'pobi : --'", + "pobi, 5, 'pobi : -----'" + }) + @DisplayName("다양한 위치를 정확히 포맷팅한다") + void formatVariousPositions(String carName, int position, String expected) { + // when + String result = OutputView.formatCarStatus(carName, position); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("단독 우승자를 포맷팅한다") + void formatSingleWinner() { + // given + List winners = List.of("pobi"); + + // when + String result = OutputView.formatWinners(winners); + + // then + assertThat(result).isEqualTo("최종 우승자 : pobi"); + } + + @Test + @DisplayName("공동 우승자를 쉼표로 구분하여 포맷팅한다") + void formatMultipleWinners() { + // given + List winners = Arrays.asList("pobi", "jun"); + + // when + String result = OutputView.formatWinners(winners); + + // then + assertThat(result).isEqualTo("최종 우승자 : pobi, jun"); + } + + @Test + @DisplayName("세 명 이상의 공동 우승자도 포맷팅한다") + void formatThreeWinners() { + // given + List winners = Arrays.asList("pobi", "woni", "jun"); + + // when + String result = OutputView.formatWinners(winners); + + // then + assertThat(result).isEqualTo("최종 우승자 : pobi, woni, jun"); + } + + @Test + @DisplayName("위치가 0인 자동차는 - 없이 이름만 출력한다") + void formatCarWithZeroPosition() { + // given + String carName = "pobi"; + int position = 0; + + // when + String result = OutputView.formatCarStatus(carName, position); + + // then + assertThat(result).isEqualTo("pobi : "); + } +} \ No newline at end of file From f8c9348680bc8423df479ebc861f256e00a5f6b4 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 22:42:08 +0900 Subject: [PATCH 08/19] feat(Car): implement car creation and save current position --- README.md | 4 ++-- src/main/java/racingcar/domain/Car.java | 31 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/main/java/racingcar/domain/Car.java diff --git a/README.md b/README.md index 7216466133..57515a6b8d 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ - [ ] 입력받은 시도 횟수를 정수로 변환 ### 4. 자동차 기능 -- [ ] 자동차 객체 생성 (이름 저장) -- [ ] 자동차의 현재 위치 저장 +- [x] 자동차 객체 생성 (이름 저장) +- [x] 자동차의 현재 위치 저장 - [ ] 자동차 전진 기능 - [ ] 자동차의 현재 위치 조회 diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..131842db57 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,31 @@ +package racingcar.domain; + +public class Car { + private static final int MAX_NAME_LENGTH = 5; + private static final int INITIAL_POSITION = 0; + + private final String name; + private int position; + + public Car(String name) { + validateName(name); + this.name = name; + this.position = INITIAL_POSITION; + } + + private void validateName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다."); + } + if (name.isBlank()) { + throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); + } + } + + public String getName() { + return name; + } +} \ No newline at end of file From 7478b658e142fd10252f165ba7344af1124027f5 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 22:42:50 +0900 Subject: [PATCH 09/19] feat(Car): add moveForward and getPosition methods --- README.md | 4 ++-- src/main/java/racingcar/domain/Car.java | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57515a6b8d..e96aa5f78d 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ ### 4. 자동차 기능 - [x] 자동차 객체 생성 (이름 저장) - [x] 자동차의 현재 위치 저장 -- [ ] 자동차 전진 기능 -- [ ] 자동차의 현재 위치 조회 +- [x] 자동차 전진 기능 +- [x] 자동차의 현재 위치 조회 ### 5. 게임 진행 기능 - [ ] 게임 라운드 반복 실행 (n회) diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java index 131842db57..71d56917f0 100644 --- a/src/main/java/racingcar/domain/Car.java +++ b/src/main/java/racingcar/domain/Car.java @@ -28,4 +28,12 @@ private void validateName(String name) { public String getName() { return name; } + + public int getPosition() { + return position; + } + + public void moveForward() { + position++; + } } \ No newline at end of file From 49b33428bda2963d049047710414125b56f59dc1 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:06:25 +0900 Subject: [PATCH 10/19] test(test): add stub to build test --- .../controller/RacingGameController.java | 12 ++++++++++++ .../racingcar/validator/InputValidator.java | 15 +++++++++++++++ src/main/java/racingcar/view/InputView.java | 19 +++++++++++++++++++ src/main/java/racingcar/view/OutputView.java | 19 +++++++++++++++++++ src/test/java/racingcar/domain/CarTest.java | 14 +++++++------- .../java/racingcar/domain/RacingGameTest.java | 18 +++++++++--------- 6 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 src/main/java/racingcar/controller/RacingGameController.java create mode 100644 src/main/java/racingcar/validator/InputValidator.java create mode 100644 src/main/java/racingcar/view/InputView.java create mode 100644 src/main/java/racingcar/view/OutputView.java diff --git a/src/main/java/racingcar/controller/RacingGameController.java b/src/main/java/racingcar/controller/RacingGameController.java new file mode 100644 index 0000000000..8071ded0ee --- /dev/null +++ b/src/main/java/racingcar/controller/RacingGameController.java @@ -0,0 +1,12 @@ +package racingcar.controller; + +public class RacingGameController { + + public RacingGameController() { + // TODO: 구현 예정 + } + + public void run() { + // TODO: 구현 예정 + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/validator/InputValidator.java b/src/main/java/racingcar/validator/InputValidator.java new file mode 100644 index 0000000000..bab653f5e5 --- /dev/null +++ b/src/main/java/racingcar/validator/InputValidator.java @@ -0,0 +1,15 @@ +package racingcar.validator; + +public class InputValidator { + + private InputValidator() { + } + + public static void validateCarNames(String carNames) { + // TODO: 구현 예정 + } + + public static void validateRoundCount(String roundCount) { + // TODO: 구현 예정 + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..a6e9bf06f1 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,19 @@ +package racingcar.view; + +import java.util.List; + +public class InputView { + + private InputView() { + } + + public static List parseCarNames(String input) { + // TODO: 구현 예정 + return null; + } + + public static int parseRoundCount(String input) { + // TODO: 구현 예정 + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..c94ddace60 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,19 @@ +package racingcar.view; + +import java.util.List; + +public class OutputView { + + private OutputView() { + } + + public static String formatCarStatus(String carName, int position) { + // TODO: 구현 예정 + return null; + } + + public static String formatWinners(List winners) { + // TODO: 구현 예정 + return null; + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java index 32c65b674d..c757da6feb 100644 --- a/src/test/java/racingcar/domain/CarTest.java +++ b/src/test/java/racingcar/domain/CarTest.java @@ -45,7 +45,7 @@ void moveForward() { int initialPosition = car.getPosition(); // when - car.move(); + car.moveForward(); // then assertThat(car.getPosition()).isEqualTo(initialPosition + 1); @@ -58,9 +58,9 @@ void moveForwardMultipleTimes() { Car car = new Car("pobi"); // when - car.move(); - car.move(); - car.move(); + car.moveForward(); + car.moveForward(); + car.moveForward(); // then assertThat(car.getPosition()).isEqualTo(3); @@ -105,9 +105,9 @@ void comparePosition() { Car car1 = new Car("pobi"); Car car2 = new Car("woni"); - car1.move(); - car1.move(); - car2.move(); + car1.moveForward(); + car1.moveForward(); + car2.moveForward(); // when & then assertThat(car1.getPosition()).isGreaterThan(car2.getPosition()); diff --git a/src/test/java/racingcar/domain/RacingGameTest.java b/src/test/java/racingcar/domain/RacingGameTest.java index 5f602705b9..7e7d21ebed 100644 --- a/src/test/java/racingcar/domain/RacingGameTest.java +++ b/src/test/java/racingcar/domain/RacingGameTest.java @@ -65,8 +65,8 @@ void findSingleWinner() { // 특정 자동차만 전진시키기 (테스트를 위해 직접 조작) List cars = game.getCars(); - cars.get(0).move(); // pobi 전진 - cars.get(0).move(); // pobi 전진 + cars.get(0).moveForward(); // pobi 전진 + cars.get(0).moveForward(); // pobi 전진 // when List winners = game.getWinners(); @@ -84,11 +84,11 @@ void findMultipleWinners() { // 두 자동차를 같은 거리로 전진시키기 List cars = game.getCars(); - cars.get(0).move(); // pobi - cars.get(0).move(); // pobi - cars.get(1).move(); // woni - cars.get(1).move(); // woni - cars.get(2).move(); // jun + cars.get(0).moveForward(); // pobi + cars.get(0).moveForward(); // pobi + cars.get(1).moveForward(); // woni + cars.get(1).moveForward(); // woni + cars.get(2).moveForward(); // jun // when List winners = game.getWinners(); @@ -107,8 +107,8 @@ void findAllWinnersWhenTied() { // 모든 자동차를 같은 거리로 List cars = game.getCars(); cars.forEach(car -> { - car.move(); - car.move(); + car.moveForward(); + car.moveForward(); }); // when From 0084494ef0d3f346dfe21613d40230840591a899 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:18:48 +0900 Subject: [PATCH 11/19] feat(InputView): implement car names and round count input methods --- README.md | 8 +++---- src/main/java/racingcar/view/InputView.java | 26 +++++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e96aa5f78d..4c56a15b76 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ## 기능 구현 목록 ### 1. 입력 기능 -- [ ] 자동차 이름 입력 받기 -- [ ] 시도 횟수 입력 받기 +- [x] 자동차 이름 입력 받기 +- [x] 시도 횟수 입력 받기 ### 2. 입력 검증 기능 **자동차 이름 검증** @@ -18,8 +18,8 @@ - [ ] 정수 범위를 벗어나는 값 입력 시 예외 처리 ### 3. 입력 파싱 기능 -- [ ] 쉼표(,)를 기준으로 자동차 이름 분리 -- [ ] 입력받은 시도 횟수를 정수로 변환 +- [x] 쉼표(,)를 기준으로 자동차 이름 분리 +- [x] 입력받은 시도 횟수를 정수로 변환 ### 4. 자동차 기능 - [x] 자동차 객체 생성 (이름 저장) diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java index a6e9bf06f1..c481ca51c8 100644 --- a/src/main/java/racingcar/view/InputView.java +++ b/src/main/java/racingcar/view/InputView.java @@ -1,19 +1,37 @@ package racingcar.view; +import camp.nextstep.edu.missionutils.Console; + +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; public class InputView { + private static final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String ROUND_COUNT_INPUT_MESSAGE = "시도할 횟수는 몇 회인가요?"; private InputView() { } + public static List readCarNames() { + System.out.println(CAR_NAMES_INPUT_MESSAGE); + String input = Console.readLine(); + return parseCarNames(input); + } + public static List parseCarNames(String input) { - // TODO: 구현 예정 - return null; + return Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + } + + public static int readRoundCount() { + System.out.println(ROUND_COUNT_INPUT_MESSAGE); + String input = Console.readLine(); + return parseRoundCount(input); } public static int parseRoundCount(String input) { - // TODO: 구현 예정 - return 0; + return Integer.parseInt(input); } } \ No newline at end of file From 48b95f561cb98f26884ee302367a81a94154d650 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:26:09 +0900 Subject: [PATCH 12/19] feat(InputValidator): implement car names and round count validation methods --- README.md | 16 ++++---- .../racingcar/validator/InputValidator.java | 41 ++++++++++++++++++- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4c56a15b76..30726a4973 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ ### 2. 입력 검증 기능 **자동차 이름 검증** -- [ ] 입력값이 null이거나 빈 문자열인 경우 예외 처리 -- [ ] 자동차 이름이 5자를 초과하는 경우 예외 처리 -- [ ] 자동차 이름이 빈 문자열인 경우 예외 처리 -- [ ] 자동차 이름이 공백만 있는 경우 예외 처리 +- [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리 +- [x] 자동차 이름이 5자를 초과하는 경우 예외 처리 +- [x] 자동차 이름이 빈 문자열인 경우 예외 처리 +- [x] 자동차 이름이 공백만 있는 경우 예외 처리 **시도 횟수 검증** -- [ ] 입력값이 null이거나 빈 문자열인 경우 예외 처리 -- [ ] 숫자가 아닌 값 입력 시 예외 처리 -- [ ] 0 이하의 값 입력 시 예외 처리 -- [ ] 정수 범위를 벗어나는 값 입력 시 예외 처리 +- [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리 +- [x] 숫자가 아닌 값 입력 시 예외 처리 +- [x] 0 이하의 값 입력 시 예외 처리 +- [x] 정수 범위를 벗어나는 값 입력 시 예외 처리 ### 3. 입력 파싱 기능 - [x] 쉼표(,)를 기준으로 자동차 이름 분리 diff --git a/src/main/java/racingcar/validator/InputValidator.java b/src/main/java/racingcar/validator/InputValidator.java index bab653f5e5..c3185104c0 100644 --- a/src/main/java/racingcar/validator/InputValidator.java +++ b/src/main/java/racingcar/validator/InputValidator.java @@ -1,15 +1,52 @@ package racingcar.validator; public class InputValidator { + private static final int MAX_NAME_LENGTH = 5; private InputValidator() { } public static void validateCarNames(String carNames) { - // TODO: 구현 예정 + if (carNames == null || carNames.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다."); + } + + String[] names = carNames.split(",", -1); + for (String name : names) { + validateSingleCarName(name.trim()); + } + } + + private static void validateSingleCarName(String name) { + if (name.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 빈 문자열일 수 없습니다."); + } + if (name.isBlank()) { + throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); + } } public static void validateRoundCount(String roundCount) { - // TODO: 구현 예정 + if (roundCount == null || roundCount.isEmpty()) { + throw new IllegalArgumentException("시도 횟수는 null이거나 빈 문자열일 수 없습니다."); + } + + if (roundCount.isBlank()) { + throw new IllegalArgumentException("시도 횟수는 공백일 수 없습니다."); + } + + int count; + try { + count = Integer.parseInt(roundCount); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("시도 횟수는 숫자여야 합니다."); + } + + if (count <= 0) { + throw new IllegalArgumentException("시도 횟수는 0보다 커야 합니다."); + } } } \ No newline at end of file From 61e8be3dc6dcee9e2fd8a34af5aac0417110c562 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:30:00 +0900 Subject: [PATCH 13/19] feat(OutputView): add feature to print each round result and final winner car --- README.md | 8 +++--- src/main/java/racingcar/view/OutputView.java | 30 +++++++++++++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 30726a4973..b7551232f9 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,10 @@ - [ ] 최대 이동 거리를 가진 자동차들을 우승자로 선정 ### 7. 출력 기능 -- [ ] 각 라운드 후 모든 자동차의 이름과 위치 출력 - - [ ] 자동차 위치를 `-` 기호로 표시 -- [ ] 우승자 이름 출력 (단독 우승자) -- [ ] 우승자 이름 출력 (공동 우승자, 쉼표로 구분) +- [x] 각 라운드 후 모든 자동차의 이름과 위치 출력 + - [x] 자동차 위치를 `-` 기호로 표시 +- [x] 우승자 이름 출력 (단독 우승자) +- [x] 우승자 이름 출력 (공동 우승자, 쉼표로 구분) --- diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java index c94ddace60..0e44641a5e 100644 --- a/src/main/java/racingcar/view/OutputView.java +++ b/src/main/java/racingcar/view/OutputView.java @@ -3,17 +3,39 @@ import java.util.List; public class OutputView { + private static final String RESULT_MESSAGE = "실행 결과"; + private static final String WINNER_PREFIX = "최종 우승자 : "; + private static final String POSITION_SYMBOL = "-"; + private static final String CAR_STATUS_FORMAT = "%s : %s"; + private static final String WINNER_DELIMITER = ", "; private OutputView() { } + public static void printResultMessage() { + System.out.println(); + System.out.println(RESULT_MESSAGE); + } + + public static void printCarStatus(String carName, int position) { + System.out.println(formatCarStatus(carName, position)); + } + public static String formatCarStatus(String carName, int position) { - // TODO: 구현 예정 - return null; + String positionDisplay = POSITION_SYMBOL.repeat(position); + return String.format(CAR_STATUS_FORMAT, carName, positionDisplay); + } + + public static void printRoundResult() { + System.out.println(); + } + + public static void printWinners(List winners) { + System.out.println(); + System.out.println(formatWinners(winners)); } public static String formatWinners(List winners) { - // TODO: 구현 예정 - return null; + return WINNER_PREFIX + String.join(WINNER_DELIMITER, winners); } } \ No newline at end of file From f8ed21d95103ec4c159750a1f0b80b7ede478803 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:40:44 +0900 Subject: [PATCH 14/19] feat(RacingGame): implement basic racing game structure and round play logic --- README.md | 2 +- .../java/racingcar/domain/RacingGame.java | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/main/java/racingcar/domain/RacingGame.java diff --git a/README.md b/README.md index b7551232f9..c8ac0317a7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ### 5. 게임 진행 기능 - [ ] 게임 라운드 반복 실행 (n회) -- [ ] 각 라운드마다 모든 자동차에 대해 이동 시도 +- [x] 각 라운드마다 모든 자동차에 대해 이동 시도 - [ ] 0~9 사이의 무작위 값 생성 - [ ] 무작위 값이 4 이상일 경우 자동차 전진 diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java new file mode 100644 index 0000000000..e9a9676399 --- /dev/null +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -0,0 +1,34 @@ +package racingcar.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class RacingGame { + private final List cars; + + public RacingGame(List carNames) { + this.cars = carNames.stream() + .map(Car::new) + .collect(Collectors.toList()); + } + + public List getCars() { + return new ArrayList<>(cars); + } + + public void playRound() { + for (Car car : cars) { + tryMove(car); + } + } + + private void tryMove(Car car) { + // TODO: 무작위 값 생성 및 이동 판단 로직 구현 예정 + } + + public List getWinners() { + // TODO: 구현 예정 + return null; + } +} \ No newline at end of file From 97fed3bdaeae129accfeffd0aecba45926491057 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:45:06 +0900 Subject: [PATCH 15/19] feat(RacingGame): implement random movement logic for cars --- README.md | 4 ++-- src/main/java/racingcar/domain/RacingGame.java | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c8ac0317a7..1d8cc5fbae 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ ### 5. 게임 진행 기능 - [ ] 게임 라운드 반복 실행 (n회) - [x] 각 라운드마다 모든 자동차에 대해 이동 시도 -- [ ] 0~9 사이의 무작위 값 생성 -- [ ] 무작위 값이 4 이상일 경우 자동차 전진 +- [x] 0~9 사이의 무작위 값 생성 +- [x] 무작위 값이 4 이상일 경우 자동차 전진 ### 6. 우승자 판별 기능 - [ ] 모든 자동차 중 최대 이동 거리 찾기 diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java index e9a9676399..3544a44deb 100644 --- a/src/main/java/racingcar/domain/RacingGame.java +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -1,10 +1,16 @@ package racingcar.domain; +import camp.nextstep.edu.missionutils.Randoms; + import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class RacingGame { + private static final int RANDOM_NUMBER_MIN = 0; + private static final int RANDOM_NUMBER_MAX = 9; + private static final int MOVE_FORWARD_THRESHOLD = 4; + private final List cars; public RacingGame(List carNames) { @@ -16,7 +22,7 @@ public RacingGame(List carNames) { public List getCars() { return new ArrayList<>(cars); } - + public void playRound() { for (Car car : cars) { tryMove(car); @@ -24,7 +30,10 @@ public void playRound() { } private void tryMove(Car car) { - // TODO: 무작위 값 생성 및 이동 판단 로직 구현 예정 + int randomNumber = Randoms.pickNumberInRange(RANDOM_NUMBER_MIN, RANDOM_NUMBER_MAX); + if (randomNumber >= MOVE_FORWARD_THRESHOLD) { + car.moveForward(); + } } public List getWinners() { From 21cc2f5a3251ddcc2dcf8c3cdb71a88c9c066c48 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:50:04 +0900 Subject: [PATCH 16/19] feat(RacingGame): add rounds parameter and implement play method for multiple rounds --- README.md | 2 +- .../java/racingcar/domain/RacingGame.java | 12 ++++++++-- .../java/racingcar/domain/RacingGameTest.java | 22 +++++++++---------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1d8cc5fbae..21a5ced14f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - [x] 자동차의 현재 위치 조회 ### 5. 게임 진행 기능 -- [ ] 게임 라운드 반복 실행 (n회) +- [x] 게임 라운드 반복 실행 (n회) - [x] 각 라운드마다 모든 자동차에 대해 이동 시도 - [x] 0~9 사이의 무작위 값 생성 - [x] 무작위 값이 4 이상일 경우 자동차 전진 diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java index 3544a44deb..255a1e70fe 100644 --- a/src/main/java/racingcar/domain/RacingGame.java +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -12,17 +12,25 @@ public class RacingGame { private static final int MOVE_FORWARD_THRESHOLD = 4; private final List cars; + private final int rounds; - public RacingGame(List carNames) { + public RacingGame(List carNames, int rounds) { this.cars = carNames.stream() .map(Car::new) .collect(Collectors.toList()); + this.rounds = rounds; } public List getCars() { return new ArrayList<>(cars); } - + + public void play() { + for (int i = 0; i < rounds; i++) { + playRound(); + } + } + public void playRound() { for (Car car : cars) { tryMove(car); diff --git a/src/test/java/racingcar/domain/RacingGameTest.java b/src/test/java/racingcar/domain/RacingGameTest.java index 7e7d21ebed..bbcb955c01 100644 --- a/src/test/java/racingcar/domain/RacingGameTest.java +++ b/src/test/java/racingcar/domain/RacingGameTest.java @@ -17,7 +17,7 @@ void createGameWithCars() { List carNames = Arrays.asList("pobi", "woni", "jun"); // when - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 5); // then assertThat(game.getCars()).hasSize(3); @@ -28,7 +28,7 @@ void createGameWithCars() { void playOneRound() { // given List carNames = Arrays.asList("pobi", "woni"); - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 1); // when game.playRound(); @@ -43,13 +43,11 @@ void playOneRound() { void playMultipleRounds() { // given List carNames = Arrays.asList("pobi", "woni", "jun"); - RacingGame game = new RacingGame(carNames); int rounds = 5; + RacingGame game = new RacingGame(carNames, rounds); // when - for (int i = 0; i < rounds; i++) { - game.playRound(); - } + game.play(); // then List cars = game.getCars(); @@ -61,7 +59,7 @@ void playMultipleRounds() { void findSingleWinner() { // given List carNames = Arrays.asList("pobi", "woni", "jun"); - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 5); // 특정 자동차만 전진시키기 (테스트를 위해 직접 조작) List cars = game.getCars(); @@ -80,7 +78,7 @@ void findSingleWinner() { void findMultipleWinners() { // given List carNames = Arrays.asList("pobi", "woni", "jun"); - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 5); // 두 자동차를 같은 거리로 전진시키기 List cars = game.getCars(); @@ -102,7 +100,7 @@ void findMultipleWinners() { void findAllWinnersWhenTied() { // given List carNames = Arrays.asList("pobi", "woni", "jun"); - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 5); // 모든 자동차를 같은 거리로 List cars = game.getCars(); @@ -123,7 +121,7 @@ void findAllWinnersWhenTied() { void playGameWithSingleCar() { // given List carNames = List.of("pobi"); - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 1); // when game.playRound(); @@ -140,7 +138,7 @@ void initialPositionIsZero() { List carNames = Arrays.asList("pobi", "woni", "jun"); // when - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 5); // then List cars = game.getCars(); @@ -152,7 +150,7 @@ void initialPositionIsZero() { void getCars() { // given List carNames = Arrays.asList("pobi", "woni"); - RacingGame game = new RacingGame(carNames); + RacingGame game = new RacingGame(carNames, 5); // when List cars = game.getCars(); From 3d4d054c219731114214d26461a28fc162b97fdd Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:52:59 +0900 Subject: [PATCH 17/19] feat(RacingGame): implement winner determination logic based on maximum position --- README.md | 4 ++-- src/main/java/racingcar/domain/RacingGame.java | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 21a5ced14f..78f9b35a0a 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ - [x] 무작위 값이 4 이상일 경우 자동차 전진 ### 6. 우승자 판별 기능 -- [ ] 모든 자동차 중 최대 이동 거리 찾기 -- [ ] 최대 이동 거리를 가진 자동차들을 우승자로 선정 +- [x] 모든 자동차 중 최대 이동 거리 찾기 +- [x] 최대 이동 거리를 가진 자동차들을 우승자로 선정 ### 7. 출력 기능 - [x] 각 라운드 후 모든 자동차의 이름과 위치 출력 diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java index 255a1e70fe..79cfbde435 100644 --- a/src/main/java/racingcar/domain/RacingGame.java +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -45,7 +45,17 @@ private void tryMove(Car car) { } public List getWinners() { - // TODO: 구현 예정 - return null; + int maxPosition = findMaxPosition(); + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .map(Car::getName) + .collect(Collectors.toList()); + } + + private int findMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); } } \ No newline at end of file From 0a93e13d94c544743f61628cd5a80d74d7a9e252 Mon Sep 17 00:00:00 2001 From: sun Date: Mon, 27 Oct 2025 23:56:12 +0900 Subject: [PATCH 18/19] feat: integrate application --- src/main/java/racingcar/Application.java | 5 +- .../controller/RacingGameController.java | 49 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..dd6bf24561 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,10 @@ package racingcar; +import racingcar.controller.RacingGameController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + RacingGameController controller = new RacingGameController(); + controller.run(); } } diff --git a/src/main/java/racingcar/controller/RacingGameController.java b/src/main/java/racingcar/controller/RacingGameController.java index 8071ded0ee..e1592cd3a1 100644 --- a/src/main/java/racingcar/controller/RacingGameController.java +++ b/src/main/java/racingcar/controller/RacingGameController.java @@ -1,12 +1,53 @@ package racingcar.controller; +import racingcar.domain.Car; +import racingcar.domain.RacingGame; +import racingcar.validator.InputValidator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + public class RacingGameController { - public RacingGameController() { - // TODO: 구현 예정 + public void run() { + List carNames = readAndValidateCarNames(); + int roundCount = readAndValidateRoundCount(); + + RacingGame game = new RacingGame(carNames, roundCount); + + OutputView.printResultMessage(); + playGameWithOutput(game, roundCount); + + List winners = game.getWinners(); + OutputView.printWinners(winners); } - public void run() { - // TODO: 구현 예정 + private List readAndValidateCarNames() { + List carNames = InputView.readCarNames(); + String input = String.join(",", carNames); + InputValidator.validateCarNames(input); + return carNames; + } + + private int readAndValidateRoundCount() { + int roundCount = InputView.readRoundCount(); + InputValidator.validateRoundCount(String.valueOf(roundCount)); + return roundCount; + } + + private void playGameWithOutput(RacingGame game, int roundCount) { + for (int i = 0; i < roundCount; i++) { + game.playRound(); + printRoundResult(game); + } + } + + private void printRoundResult(RacingGame game) { + List cars = game.getCars(); + for (Car car : cars) { + OutputView.printCarStatus(car.getName(), car.getPosition()); + } + OutputView.printRoundResult(); } } \ No newline at end of file From 0ecfc06c7f1cd4b5d14c8b1925927410f45f5c7a Mon Sep 17 00:00:00 2001 From: sun Date: Tue, 28 Oct 2025 00:02:20 +0900 Subject: [PATCH 19/19] fix: delete enter when print result --- src/main/java/racingcar/view/OutputView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java index 0e44641a5e..19688f57b2 100644 --- a/src/main/java/racingcar/view/OutputView.java +++ b/src/main/java/racingcar/view/OutputView.java @@ -31,7 +31,6 @@ public static void printRoundResult() { } public static void printWinners(List winners) { - System.out.println(); System.out.println(formatWinners(winners)); }