diff --git a/README.md b/README.md index d0286c859f..2ca5b7ed9c 100644 --- a/README.md +++ b/README.md @@ -1 +1,39 @@ # java-racingcar-precourse + +## 기능 목록 + +### 1. 자동차 이름 입력 및 검증 +- [X] 자동차 이름은 쉼표(,) 기준으로 구분 +- [X] 이름은 최대 5자 +- [X] 잘못된 입력 시 `IllegalArgumentException` 발생 + +### 2. 자동차 이동 로직 +- [X] 0~9 사이 랜덤 값 >= 4일 때 전진 +- [X] 4 이하인 경우 정지 + +### 3. 위치(Position) VO +- [X] 초기 위치 = 0 +- [X] 이동 시 불변 객체 반환 +- [X] 위치 출력 시 `-` 문자를 사용하여 표현 + +### 4. Car 클래스 +- [X] 자동차 이름(Name VO)과 위치(Position VO) 관리 +- [X] move() 메서드에서 MoveStrategy를 통해 전진 여부 결정 + +### 5. Cars 컬렉션 +- [X] 여러 대의 Car를 관리 +- [X] 각 라운드마다 모든 자동차 이동 수행 +- [X] 우승자 계산 기능 + +### 6. 게임 진행 +- [X] 시도할 횟수만큼 라운드 반복 +- [X] 각 라운드 결과 출력 +- [X] 최종 우승자 출력 + +### 7. 입력/출력 +- [X] 사용자 입력 처리 + - 자동차 이름 + - 시도 횟수 +- [X] 실행 결과 출력 + - 각 자동차의 위치 + - 우승자가 여러 명일 경우 ','로 구분하여 공동 우승 표시 diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..ed2d63c3de 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,10 @@ package racingcar; +import racingcar.controller.RacingGame; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + RacingGame racingGame = new RacingGame(); + racingGame.run(); } } diff --git a/src/main/java/racingcar/controller/RacingGame.java b/src/main/java/racingcar/controller/RacingGame.java new file mode 100644 index 0000000000..073701ba62 --- /dev/null +++ b/src/main/java/racingcar/controller/RacingGame.java @@ -0,0 +1,40 @@ +package racingcar.controller; + +import racingcar.domain.Cars; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + +public class RacingGame { + + private final InputView inputView; + private final OutputView outputView; + + public RacingGame() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + } + + public void run() { + List carNames = inputView.inputCarNames(); + int roundCount = inputView.inputRoundCount(); + + Cars cars = new Cars(carNames); + + playRounds(cars, roundCount); + showWinners(cars); + } + + private void playRounds(Cars cars, int roundCount) { + for (int i = 0; i < roundCount; i++) { + cars.moveAll(); + outputView.printRoundResults(cars.getCars()); + } + } + + private void showWinners(Cars cars) { + List winners = cars.getWinners(); + outputView.printWinners(winners); + } +} \ 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..a9ebecc70a --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,33 @@ +package racingcar.domain; + +import racingcar.strategy.MoveStrategy; + +public class Car { + + private final Name name; + private Position position; + + public Car(String nameValue) { + name = new Name(nameValue); + position = new Position(); + } + + public void move(MoveStrategy strategy) { + if (strategy.isMovable()) { + position = position.move(); + } + } + + public int getPosition() { + return position.getValue(); + } + + public String getName() { + return name.getValue(); + } + + public String displayCar() { + return name.getValue() + " : " + position.displayPosition(); + } + +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000000..2ec543adec --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,45 @@ +package racingcar.domain; + +import racingcar.strategy.RandomNumberMoveStrategy; + +import java.util.List; + +public class Cars { + + private final List cars; + + public Cars(List names) { + this.cars = names.stream() + .map(Car::new) + .toList(); + } + + public int getSize() { + return cars.size(); + } + + public List getCars() { + return cars; + } + + public void moveAll() { + for (Car car : cars) { + car.move(new RandomNumberMoveStrategy()); + } + } + + public List getWinners() { + int maxPosition = findMaxPosition(); + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .map(Car::getName) + .toList(); + } + + private int findMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } +} diff --git a/src/main/java/racingcar/domain/Name.java b/src/main/java/racingcar/domain/Name.java new file mode 100644 index 0000000000..49f250d84d --- /dev/null +++ b/src/main/java/racingcar/domain/Name.java @@ -0,0 +1,33 @@ +package racingcar.domain; + +public class Name { + + private static final int MAX_NAME_LENGTH = 5; + private static final String EMPTY_NAME_MESSAGE = "자동차 이름은 빈 값일 수 없습니다."; + private static final String LENGTH_EXCEED_MESSAGE = "자동차 이름은 5자 이하만 가능합니다."; + + private final String value; + + public Name(String value) { + validate(value); + this.value = value.trim(); + } + + private void validate(String value) { + if (isNullOrEmpty(value)) { + throw new IllegalArgumentException(EMPTY_NAME_MESSAGE); + } + if (value.trim().length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException(LENGTH_EXCEED_MESSAGE); + } + } + + public String getValue() { + return value; + } + + private static boolean isNullOrEmpty(String str) { + return str == null || str.isBlank(); + } + +} diff --git a/src/main/java/racingcar/domain/Position.java b/src/main/java/racingcar/domain/Position.java new file mode 100644 index 0000000000..2040f5260f --- /dev/null +++ b/src/main/java/racingcar/domain/Position.java @@ -0,0 +1,39 @@ +package racingcar.domain; + +public class Position { + + private static final int INITIAL_POSITION = 0; + private static final int MOVE_DISTANCE = 1; + private static final String POSITION_SYMBOL = "-"; + private static final String NEGATIVE_POSITION_MESSAGE = "위치는 음수일 수 없습니다."; + + private final int value; + + public Position() { + this(INITIAL_POSITION); + } + + private Position(int value) { + validate(value); + this.value = value; + } + + public Position move() { + return new Position(value + MOVE_DISTANCE); + } + + public int getValue() { + return value; + } + + public String displayPosition() { + return POSITION_SYMBOL.repeat(value); + } + + private void validate(int value) { + if (value < 0) { + throw new IllegalArgumentException(NEGATIVE_POSITION_MESSAGE); + } + } + +} \ No newline at end of file diff --git a/src/main/java/racingcar/strategy/MoveStrategy.java b/src/main/java/racingcar/strategy/MoveStrategy.java new file mode 100644 index 0000000000..c48e5425c0 --- /dev/null +++ b/src/main/java/racingcar/strategy/MoveStrategy.java @@ -0,0 +1,6 @@ +package racingcar.strategy; + +@FunctionalInterface +public interface MoveStrategy { + boolean isMovable(); +} \ No newline at end of file diff --git a/src/main/java/racingcar/strategy/RandomNumberMoveStrategy.java b/src/main/java/racingcar/strategy/RandomNumberMoveStrategy.java new file mode 100644 index 0000000000..ab2bcc1eda --- /dev/null +++ b/src/main/java/racingcar/strategy/RandomNumberMoveStrategy.java @@ -0,0 +1,16 @@ +package racingcar.strategy; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomNumberMoveStrategy implements MoveStrategy { + + private static final int MIN_NUMBER = 0; + private static final int MAX_NUMBER = 9; + private static final int MOVE_THRESHOLD = 4; + + @Override + public boolean isMovable() { + int randomNumber = Randoms.pickNumberInRange(MIN_NUMBER, MAX_NUMBER); + return randomNumber >= MOVE_THRESHOLD; + } +} \ 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..cd97e41938 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,27 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.Arrays; +import java.util.List; + +public class InputView { + + private static final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String ROUND_COUNT_INPUT_MESSAGE = "시도할 횟수는 몇 회인가요?"; + private static final String DELIMITER = ","; + + public List inputCarNames() { + System.out.println(CAR_NAMES_INPUT_MESSAGE); + String input = Console.readLine(); + return Arrays.stream(input.split(DELIMITER)) + .map(String::trim) + .toList(); + } + + public int inputRoundCount() { + System.out.println(ROUND_COUNT_INPUT_MESSAGE); + String input = Console.readLine(); + return Integer.parseInt(input); + } +} \ 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..7d0ade4f63 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,24 @@ +package racingcar.view; + +import racingcar.domain.Car; + +import java.util.List; + +public class OutputView { + + private static final String RESULT_OUTPUT_MESSAGE = "\n실행 결과"; + private static final String WINNER_OUTPUT_MESSAGE = "최종 우승자 : "; + private static final String DELIMITER = ", "; + + public void printRoundResults(List cars) { + System.out.println(RESULT_OUTPUT_MESSAGE); + cars.forEach(car -> System.out.println(car.displayCar())); + System.out.println(); + } + + public void printWinners(List winners) { + String winner = String.join(DELIMITER, winners); + System.out.println(WINNER_OUTPUT_MESSAGE + winner); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000000..0012f0fd36 --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,73 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.strategy.MoveStrategy; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CarTest { + + @Test + @DisplayName("자동차는 이름과 위치를 가진다") + void createCar() { + Car car = new Car("pobi"); + assertEquals("pobi", car.getName()); + assertEquals(0, car.getPosition()); + } + + @Test + @DisplayName("랜덤 값이 4 미만이면 이동하지 않는다") + void stayWhenNumberLessThanFour() { + Car car = new Car("pobi"); + MoveStrategy notMovableStrategy = () -> false; + car.move(notMovableStrategy); + assertEquals(0, car.getPosition()); + } + + @Test + @DisplayName("랜덤 값이 4 이상이면 전진한다") + void moveWhenNumberGreaterThanOrEqualToFour() { + Car car = new Car("pobi"); + MoveStrategy movableStrategy = () -> true; + car.move(movableStrategy); + assertEquals(1, car.getPosition()); + } + + @Test + @DisplayName("여러 번 이동할 수 있다") + void moveMultipleTimes() { + Car car = new Car("pobi"); + + MoveStrategy movableStrategy = () -> true; + MoveStrategy notMovableStrategy = () -> false; + + car.move(movableStrategy); + car.move(notMovableStrategy); + car.move(movableStrategy); + car.move(movableStrategy); + + assertEquals(3, car.getPosition()); + } + + @Test + @DisplayName("초기 상태를 문자열로 출력하면 이름 : 으로 표시된다") + void displayInitialCarStatus() { + Car car = new Car("pobi"); + assertEquals("pobi : ", car.displayCar()); + } + + @Test + @DisplayName("자동차 상태를 이름 : - 으로 출력할 수 있다") + void displayCarStatus() { + Car car = new Car("pobi"); + MoveStrategy movableStrategy = () -> true; + + car.move(movableStrategy); + car.move(movableStrategy); + car.move(movableStrategy); + + assertEquals("pobi : ---", car.displayCar()); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/CarsTest.java b/src/test/java/racingcar/domain/CarsTest.java new file mode 100644 index 0000000000..73f3dd2075 --- /dev/null +++ b/src/test/java/racingcar/domain/CarsTest.java @@ -0,0 +1,21 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CarsTest { + + @Test + @DisplayName("자동차 이름 리스트로 Cars를 생성할 수 있다") + void createCars() { + List names = List.of("pobi", "woni", "jun"); + Cars cars = new Cars(names); + + assertEquals(3, cars.getSize()); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/NameTest.java b/src/test/java/racingcar/domain/NameTest.java new file mode 100644 index 0000000000..26d7026a16 --- /dev/null +++ b/src/test/java/racingcar/domain/NameTest.java @@ -0,0 +1,37 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +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.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NameTest { + + @ParameterizedTest + @ValueSource(strings = { "pobi", "jun", "woni" }) + @DisplayName("이름 생성하기") + void createName(String validName) { + Name name = new Name(validName); + assertEquals(validName, name.getValue()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " "}) + @DisplayName("빈 이름 또는 NULL 값이면 예외가 발생한다") + void validateEmptyName(String invalidName) { + assertThatThrownBy(() -> new Name(invalidName)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = { "abcdef", "123456" }) + @DisplayName("5자를 초과하는 이름이면 예외가 발생한다") + void validateNameLength(String invalidName) { + assertThatThrownBy(() -> new Name(invalidName)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/PositionTest.java b/src/test/java/racingcar/domain/PositionTest.java new file mode 100644 index 0000000000..6ad0c7bc5c --- /dev/null +++ b/src/test/java/racingcar/domain/PositionTest.java @@ -0,0 +1,47 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PositionTest { + + @Test + @DisplayName("초기 위치는 0이다") + void createInitialPosition() { + Position position = new Position(); + assertEquals(0, position.getValue()); + } + + @Test + @DisplayName("전진하면 위치가 1 증가한다") + void moveForward() { + Position position = new Position(); + Position movedPosition = position.move(); + assertEquals(1, movedPosition.getValue()); + } + + @Test + @DisplayName("여러 번 전진할 수 있다") + void moveMultipleTimes() { + Position position = new Position(); + Position movedPosition = position.move().move().move(); + assertEquals(3, movedPosition.getValue()); + } + + @Test + @DisplayName("초기 위치를 문자열로 변환하면 빈 문자열이다") + void displayInitialPosition() { + Position position = new Position(); + assertEquals("", position.displayPosition()); + } + + @Test + @DisplayName("위치를 문자열로 변환하면 '-'가 위치 값만큼 반복된다") + void displayPosition() { + Position position = new Position().move().move().move(); + assertEquals("---", position.displayPosition()); + } + +} \ No newline at end of file