diff --git a/README.md b/README.md index d0286c859f..466d0d7856 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ -# java-racingcar-precourse +# πŸš— μžλ™μ°¨ κ²½μ£Ό – κΈ°λŠ₯ μš”κ΅¬ 사항 (컀밋 λ‹¨μœ„) + +## 1. μž…λ ₯ 처리 +- [ ] μžλ™μ°¨ 이름 μž…λ ₯λ°›κΈ° + - [ ] μ½˜μ†”μ—μ„œ ν•œ 쀄 μž…λ ₯으둜 이름 λͺ©λ‘μ„ λ°›λŠ”λ‹€ (예: `pobi,woni,jun`). + - [ ] 이름 κ΅¬λΆ„μžλŠ” μ‰Όν‘œ(,)만 ν—ˆμš©ν•œλ‹€. + - [ ] 각 이름은 5자 μ΄ν•˜μ—¬μ•Ό ν•œλ‹€ β†’ μ•„λ‹ˆλ©΄ `IllegalArgumentException`. + - [ ] 빈 이름(예: `pobi,,jun`) λ˜λŠ” 곡백만 μž…λ ₯된 이름이 있으면 `IllegalArgumentException`. +- [ ] 이동 횟수 μž…λ ₯λ°›κΈ° + - [ ] μ½˜μ†”μ—μ„œ ν•œ 쀄 μž…λ ₯으둜 μ •μˆ˜λ₯Ό λ°›λŠ”λ‹€ (예: `5`). + - [ ] μ •μˆ˜κ°€ μ•„λ‹Œ 값이면 `IllegalArgumentException`. + - [ ] 음수/0 λ“± μœ νš¨ν•˜μ§€ μ•Šμ€ μ‹œλ„ 횟수면 `IllegalArgumentException`. + +--- + +## 2. μžλ™μ°¨ μƒνƒœ μ΄ˆκΈ°ν™” +- [ ] μž…λ ₯된 μ΄λ¦„λ“€λ‘œ μžλ™μ°¨ 리슀트λ₯Ό μƒμ„±ν•œλ‹€. +- [ ] 각 μžλ™μ°¨μ˜ 초기 이동거리 = 0 으둜 μ„€μ •ν•œλ‹€. + +--- + +## 3. λΌμš΄λ“œ μ§„ν–‰ (μ‹œλ„ 횟수만큼 반볡) +- [ ] λ§€ λΌμš΄λ“œμ—μ„œ λͺ¨λ“  μžλ™μ°¨μ— λŒ€ν•΄ λ‹€μŒμ„ μˆ˜ν–‰ν•œλ‹€. + - [ ] λ¬΄μž‘μœ„ μ •μˆ˜ 0~9 ν•˜λ‚˜λ₯Ό λ½‘λŠ”λ‹€ (`Randoms.pickNumberInRange(0, 9)` μ‚¬μš©). + - [ ] 값이 4 이상이면 ν•΄λ‹Ή μžλ™μ°¨μ˜ 이동거리λ₯Ό +1 ν•œλ‹€. + - [ ] 값이 3 μ΄ν•˜λ©΄ μ΄λ™ν•˜μ§€ μ•ŠλŠ”λ‹€. +- [ ] λΌμš΄λ“œ μ’…λ£Œ μ‹œ κ²°κ³Ό 좜λ ₯ + - [ ] 각 μžλ™μ°¨μ— λŒ€ν•΄ `이름 : ----` ν˜•μ‹μœΌλ‘œ ν˜„μž¬κΉŒμ§€μ˜ 이동거리λ₯Ό λ§‰λŒ€(`-`)둜 좜λ ₯ν•œλ‹€. + - [ ] 좜λ ₯은 μžλ™μ°¨ μž…λ ₯ μˆœμ„œλ₯Ό μœ μ§€ν•œλ‹€. + - [ ] λ§€ λΌμš΄λ“œ 좜λ ₯ 사이에 빈 쀄 없이 μ˜ˆμ‹œμ™€ λ™μΌν•œ ν˜•μ‹μœΌλ‘œ λˆ„μ  κ²°κ³Όλ₯Ό 보여쀀닀. + - 예) + ``` + pobi : -- + woni : --- + jun : - + ``` + +--- + +## 4. 우승자 νŒλ³„ 및 좜λ ₯ +- [ ] λͺ¨λ“  λΌμš΄λ“œ μ’…λ£Œ ν›„ μ΅œλŒ€ 이동거리λ₯Ό κ΅¬ν•œλ‹€. +- [ ] μ΅œλŒ€ 이동거리와 κ°™μ•„μ§„ μžλ™μ°¨λ“€μ„ 우승자 λͺ©λ‘μœΌλ‘œ λ§Œλ“ λ‹€. +- [ ] μš°μŠΉμžκ°€ μ—¬λŸ¬ λͺ…일 수 있으며, 이름을 μ‰Όν‘œ(,) 둜 μ—°κ²°ν•΄ 좜λ ₯ν•œλ‹€. +- [ ] 좜λ ₯ ν˜•μ‹μ€ μ •ν™•νžˆ λ‹€μŒκ³Ό κ°™λ‹€. + - [ ] 단독 우승: `μ΅œμ’… 우승자 : pobi` + - [ ] 곡동 우승: `μ΅œμ’… 우승자 : pobi, jun` + +--- + +## 5. μ˜ˆμ™Έ 처리 +- [ ] μž…λ ₯ ν˜•μ‹μ΄ 잘λͺ»λœ 경우 μ¦‰μ‹œ `IllegalArgumentException` λ°œμƒ. +- [ ] μ˜ˆμ™Έ λ°œμƒ ν›„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ’…λ£Œν•œλ‹€. diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..3a853e0280 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,10 @@ package racingcar; +import racingcar.controller.RacingController; + public class Application { public static void main(String[] args) { - // TODO: ν”„λ‘œκ·Έλž¨ κ΅¬ν˜„ + RacingController racingController = new RacingController(); + racingController.start(); } } diff --git a/src/main/java/racingcar/controller/RacingController.java b/src/main/java/racingcar/controller/RacingController.java new file mode 100644 index 0000000000..464887fcae --- /dev/null +++ b/src/main/java/racingcar/controller/RacingController.java @@ -0,0 +1,34 @@ +package racingcar.controller; + +import racingcar.domain.AttemptCount; +import racingcar.domain.Car; +import racingcar.domain.Cars; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + +public class RacingController { + private final InputView inputView = new InputView(); + private final OutputView outputView = new OutputView(); + + public void start(){ + List carNames = inputView.readCarNames(); + int attemptCountInput = inputView.readAttemptCount(); + + AttemptCount attemptCount = new AttemptCount(attemptCountInput); + + List carList = carNames.stream() + .map(Car::new) + .toList(); + + Cars cars = new Cars(carList); + + outputView.printStartMessage(); + for (int i = 0; i < attemptCount.getValue(); i++) { + cars.moveAll(); + outputView.printRoundResult(cars); + } + outputView.printWinners(cars.getWinners()); + } +} diff --git a/src/main/java/racingcar/domain/AttemptCount.java b/src/main/java/racingcar/domain/AttemptCount.java new file mode 100644 index 0000000000..b2de347251 --- /dev/null +++ b/src/main/java/racingcar/domain/AttemptCount.java @@ -0,0 +1,22 @@ +package racingcar.domain; + +public class AttemptCount { + private static final int MIN_COUNT = 1; + + private final int value; + + public AttemptCount(int value) { + validateCount(value); + this.value = value; + } + + private void validateCount(int count) { + if (count < MIN_COUNT) { + throw new IllegalArgumentException("μ‹œλ„ νšŸμˆ˜λŠ” 1 이상이어야 ν•©λ‹ˆλ‹€."); + } + } + + public int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..39b24f8fd8 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,35 @@ +package racingcar.domain; + +public class Car { + private static final int MAX_NAME_LENGTH = 5; + private static final int FORWARD_THRESHOLD = 4; + + private final String name; + private int position = 0; + + public Car(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("μžλ™μ°¨ 이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("μžλ™μ°¨ 이름은 5자 μ΄ν•˜λ§Œ κ°€λŠ₯ν•©λ‹ˆλ‹€."); + } + } + + public void move(int randomNumber) { + if (randomNumber >= FORWARD_THRESHOLD) position++; + } + + public String getName() { return name; } + + public int getPosition() { return position; } + + public String status() { + return name + " : " + "-".repeat(position); + } +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000000..dd09701644 --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,59 @@ +package racingcar.domain; + +import racingcar.util.RandomNumberGenerator; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class Cars { + private static final int RANDOM_MIN = 0; + private static final int RANDOM_MAX = 9; + + private final List cars; + + public Cars(List cars) { + validateNoDuplicateNames(cars); + this.cars = List.copyOf(cars); + } + + private void validateNoDuplicateNames(List cars) { + long uniqueNameCount = cars.stream() + .map(Car::getName) + .distinct() + .count(); + + if (uniqueNameCount != cars.size()) { + throw new IllegalArgumentException("μžλ™μ°¨ 이름은 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + } + + public void moveAll() { + for (Car car : cars) { + int random = RandomNumberGenerator.generateInRange(RANDOM_MIN, RANDOM_MAX); + car.move(random); + } + } + + public List getCars() { + return Collections.unmodifiableList(cars); + } + + public List getWinners() { + int maxPosition = findMaxPosition(); + return findCarsWithPosition(maxPosition); + } + + private int findMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } + + private List findCarsWithPosition(int position) { + return cars.stream() + .filter(car -> car.getPosition() == position) + .toList(); + } +} diff --git a/src/main/java/racingcar/util/RandomNumberGenerator.java b/src/main/java/racingcar/util/RandomNumberGenerator.java new file mode 100644 index 0000000000..6bc74b8304 --- /dev/null +++ b/src/main/java/racingcar/util/RandomNumberGenerator.java @@ -0,0 +1,11 @@ +package racingcar.util; + +import camp.nextstep.edu.missionutils.Randoms; + +public final class RandomNumberGenerator { + private RandomNumberGenerator() {} + + public static int generateInRange(int start, int end) { + return Randoms.pickNumberInRange(start, end); + } +} \ 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..9fcd17d5e5 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,42 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.Arrays; +import java.util.List; + +public class InputView { + public List readCarNames(){ + System.out.println("κ²½μ£Όν•  μžλ™μ°¨ 이름을 μž…λ ₯ν•˜μ„Έμš”.(이름은 μ‰Όν‘œ(,) κΈ°μ€€μœΌλ‘œ ꡬ뢄)"); + String input = Console.readLine(); + + List carNames = Arrays.stream(input.split(",")) + .map(String::trim) + .toList(); + + validateNotEmpty(carNames); + + return carNames; + } + + private void validateNotEmpty(List carNames) { + if (carNames.isEmpty()) { + throw new IllegalArgumentException("μžλ™μ°¨ 이름을 μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + if (carNames.stream().allMatch(String::isEmpty)) { + throw new IllegalArgumentException("μžλ™μ°¨ 이름을 μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + } + + public int readAttemptCount(){ + System.out.println("μ‹œλ„ν•  νšŸμˆ˜λŠ” λͺ‡ νšŒμΈκ°€μš”?"); + String input = Console.readLine(); + + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("μ‹œλ„ νšŸμˆ˜λŠ” μ •μˆ˜λ§Œ μž…λ ₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..d701e8d536 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,25 @@ +package racingcar.view; + +import racingcar.domain.Car; +import racingcar.domain.Cars; + +import java.util.List; +import java.util.stream.Collectors; + +public class OutputView { + public void printStartMessage() { + System.out.println("\nμ‹€ν–‰ κ²°κ³Ό"); + } + + public void printRoundResult(Cars cars) { + cars.getCars().forEach(car -> System.out.println(car.status())); + System.out.println(); + } + + public void printWinners(List winners) { + String winnerNames = winners.stream() + .map(Car::getName) + .collect(Collectors.joining(", ")); + System.out.println("μ΅œμ’… 우승자 : " + winnerNames); + } +} diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java index 1d35fc33fe..c354adfc2e 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; @@ -12,22 +13,87 @@ class ApplicationTest extends NsTest { private static final int MOVING_FORWARD = 4; private static final int STOP = 3; + @DisplayName("정상 μž…λ ₯ μ‹œ, κ²°κ³Όλ₯Ό 좜λ ₯ν•˜κ³  우승자λ₯Ό μ•ˆλ‚΄ν•œλ‹€") @Test - void κΈ°λŠ₯_ν…ŒμŠ€νŠΈ() { + void 정상_ν”Œλ‘œμš°_ν…ŒμŠ€νŠΈ() { assertRandomNumberInRangeTest( - () -> { - run("pobi,woni", "1"); - assertThat(output()).contains("pobi : -", "woni : ", "μ΅œμ’… 우승자 : pobi"); - }, - MOVING_FORWARD, STOP + () -> { + run("pobi,woni", "1"); + assertThat(output()) + .contains("μ‹€ν–‰ κ²°κ³Ό") + .contains("pobi : -") + .contains("woni : ") + .contains("μ΅œμ’… 우승자 : pobi"); + }, + MOVING_FORWARD, STOP ); } + @DisplayName("μžλ™μ°¨ 이름이 5자 초과면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") @Test - void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ() { + void 이름_길이_초과_μ˜ˆμ™Έ() { assertSimpleTest(() -> - assertThatThrownBy(() -> runException("pobi,javaji", "1")) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> runException("pobi,javaji", "1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("μžλ™μ°¨ 이름에 곡백만 μ‘΄μž¬ν•˜λ©΄ μ˜ˆμ™Έ λ°œμƒ") + @Test + void 이름_곡백_μ˜ˆμ™Έ() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi, ,jun", "2")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("빈 이름 μž…λ ₯ μ‹œ μ˜ˆμ™Έ λ°œμƒ") + @Test + void 빈_이름_μ˜ˆμ™Έ() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(",,", "5")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("μ€‘λ³΅λœ μžλ™μ°¨ 이름 μž…λ ₯ μ‹œ μ˜ˆμ™Έ λ°œμƒ") + @Test + void 쀑볡_이름_μ˜ˆμ™Έ() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,pobi,jun", "3")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("μ‹œλ„ νšŸμˆ˜κ°€ 음수면 μ˜ˆμ™Έ λ°œμƒ") + @Test + void 음수_μ‹œλ„νšŸμˆ˜_μ˜ˆμ™Έ() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "-1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("μ‹œλ„ νšŸμˆ˜κ°€ μˆ«μžκ°€ μ•„λ‹ˆλ©΄ μ˜ˆμ™Έ λ°œμƒ") + @Test + void μˆ«μžμ•„λ‹Œ_μž…λ ₯_μ˜ˆμ™Έ() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "abc")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("μš°μŠΉμžκ°€ μ—¬λŸ¬ λͺ…일 경우 μ‰Όν‘œλ‘œ κ΅¬λΆ„ν•˜μ—¬ μ•ˆλ‚΄ν•œλ‹€") + @Test + void 곡동_우승자_ν…ŒμŠ€νŠΈ() { + assertRandomNumberInRangeTest( + () -> { + run("pobi,woni", "1"); + String output = output(); + assertThat(output) + .contains("μ΅œμ’… 우승자 :"); // 곡동 μ—¬λΆ€λŠ” 값에 따라 맀번 λ‹¬λΌμ§ˆ 수 μžˆμ–΄ ν˜•μ‹λ§Œ 확인 + }, + MOVING_FORWARD, MOVING_FORWARD ); }