diff --git a/docs/Readme.md b/docs/Readme.md new file mode 100644 index 0000000000..e1df789ce7 --- /dev/null +++ b/docs/Readme.md @@ -0,0 +1,29 @@ +## 기능 구현 +- Car + - [x] 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. + - [x] 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. + - [x] 자동차의 개수는 2대 이상 + +- Cars + - [x] 자동차들을 추가할 수 있다. + - [x] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. + - [x] 자동차 상태를 문자열로 확인할 수 있다. + - [x] 우승 자동차를 확인할 수 찾을 수 있다. + +- InputView + - [x] 경주 할 자동차 이름을 입력한다(이름은 쉼표(,) 기준으로 구분) + - [x] 사용자는 몇 번의 이동을 할 것인지를 입력한다. + +- CreateRandomValue + - [x] 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. + +- OutputView + - [x] 실행 결과 출력 + - [x] 최종 결과 출력 -> 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. + - [x] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. + +- InputValidator + - [x] 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, + - [x] "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. + - [x] 쉼표로 구분 + - [x] 시도 횟수 숫자 확인 \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index b9ed0456a3..667ba1f98d 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,10 @@ package racingcar; +import racingcar.controller.RacingCarController; + public class Application { public static void main(String[] args) { - // TODO 구현 진행 + RacingCarController racingCarController = new RacingCarController(); + racingCarController.run(); } } diff --git a/src/main/java/racingcar/Car.java b/src/main/java/racingcar/Car.java deleted file mode 100644 index ab3df94921..0000000000 --- a/src/main/java/racingcar/Car.java +++ /dev/null @@ -1,12 +0,0 @@ -package racingcar; - -public class Car { - private final String name; - private int position = 0; - - public Car(String name) { - this.name = name; - } - - // 추가 기능 구현 -} diff --git a/src/main/java/racingcar/constants/ErrorMessage.java b/src/main/java/racingcar/constants/ErrorMessage.java new file mode 100644 index 0000000000..2e2cc1da59 --- /dev/null +++ b/src/main/java/racingcar/constants/ErrorMessage.java @@ -0,0 +1,20 @@ +package racingcar.constants; + +public enum ErrorMessage { + NAME_DIVISION_ERROR("이름은 쉼표(,) 기준으로 구분합니다."), + NAME_LENGTH_ERROR("자동차 이름이름은 5자 이하만 가능합니다."), + TRY_COUNT_ERROR("시도할 회수는 1회 이상입니다."), + INPUT_NUMERIC_ERROR("숫자가 아닙니다."); + + private static final String ERROR = "[ERROR]"; + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return ERROR + message; + } + +} diff --git a/src/main/java/racingcar/constants/ProgressMessage.java b/src/main/java/racingcar/constants/ProgressMessage.java new file mode 100644 index 0000000000..71c516b2d0 --- /dev/null +++ b/src/main/java/racingcar/constants/ProgressMessage.java @@ -0,0 +1,20 @@ +package racingcar.constants; + +public enum ProgressMessage { + + INPUT_CAR_NAMES("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"), + INPUT_TRY_COUNT("시도할 회수는 몇회인가요?"), + EXECUTE_RESULT("실행 결과"), + FINAL_WINNER("최종 우승자 : "); + + private final String message; + + ProgressMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return message; + } +} diff --git a/src/main/java/racingcar/controller/RacingCarController.java b/src/main/java/racingcar/controller/RacingCarController.java new file mode 100644 index 0000000000..019eec7a0a --- /dev/null +++ b/src/main/java/racingcar/controller/RacingCarController.java @@ -0,0 +1,35 @@ +package racingcar.controller; + +import racingcar.domain.Cars; +import racingcar.view.InputView; +import racingcar.view.OutputView; +import racingcar.service.RacingCarService; + +public class RacingCarController { + + private final InputView inputView; + private final OutputView outputView; + private final RacingCarService racingCarService; + + public RacingCarController() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + this.racingCarService = new RacingCarService(); + } + + public void run(){ + Cars cars = inputView.readCarNames(); + int tryCount = inputView.readTryCount(); + + outputView.printExecuteResult(); + changeCarsStatus(cars, tryCount); + outputView.printFinalWinner(cars); + } + + private void changeCarsStatus(Cars cars, int tryCount) { + for (int i = 0; i < tryCount; i++){ + racingCarService.moveCars(cars); + outputView.printCarsStatus(cars); + } + } +} diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..229a8a1888 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,42 @@ +package racingcar.domain; + +import racingcar.constants.ErrorMessage; + +public class Car { + private static final int MAX_NAME_LENGTH = 5; + private final String name; + private int position = 0; + + public Car(String name) { + validateCarNameLength(name); + this.name = name; + } + + // 추가 기능 구현 + public void validateCarNameLength(String name){ + if (name.length() > MAX_NAME_LENGTH){ + throw new IllegalArgumentException(ErrorMessage.NAME_LENGTH_ERROR.getMessage()); + } + } + + public void movePosition(){ + position++; + } + + public StringBuilder makeScreen(StringBuilder stringBuilder) { + stringBuilder.append(name).append(" : "); + for (int i = 0; i < position; i++){ + stringBuilder.append("-"); + } + stringBuilder.append("\n"); + return stringBuilder; + } + + public int getPosition() { + return position; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000000..b3a0147bef --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,61 @@ +package racingcar.domain; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class Cars { + + private static final int OVER_ONE_WINNER = 1; + private static final int ONLY_ONE_WINNER = 0; + private static final int DEFAULT_POSITION = 0; + private static final String DIVISION = ","; + private static final String WINNER_DELIMITER = ", "; + private final List cars = new ArrayList<>(); + + public void addCars(String name){ + Set carNames = Arrays.stream(name.split(DIVISION)).collect(Collectors.toSet()); + carNames.stream().map(Car::new).forEach(cars::add); + } + + public void moveCars(){ + cars.forEach(car -> { + if (CreateRandomValue.canMoveCar()) { + car.movePosition(); + } + }); + } + + public String executionResult(){ + return cars.stream() + .map(car -> car.makeScreen(new StringBuilder())) + .reduce(new StringBuilder(), StringBuilder::append) + .toString(); + } + + public String finalWinner(){ + List winCars = getWinCarNames(); + + if (winCars.size() > OVER_ONE_WINNER) { + return String.join(WINNER_DELIMITER, winCars); + } + return winCars.get(ONLY_ONE_WINNER); + } + + private List getWinCarNames() { + return cars.stream() + .filter(car -> car.getPosition() == maxPosition()) + .map(Car::getName) + .collect(Collectors.toList()); + } + + private int maxPosition(){ + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(DEFAULT_POSITION); + } + +} diff --git a/src/main/java/racingcar/domain/CreateRandomValue.java b/src/main/java/racingcar/domain/CreateRandomValue.java new file mode 100644 index 0000000000..d08f62d510 --- /dev/null +++ b/src/main/java/racingcar/domain/CreateRandomValue.java @@ -0,0 +1,15 @@ +package racingcar.domain; + + +import camp.nextstep.edu.missionutils.Randoms; + +public class CreateRandomValue { + + private static final int MIN_RANDOM_VALUE = 0; + private static final int MAX_RANDOM_VALUE = 9; + private static final int MIN_RANDOM_RANGE = 4; + public static boolean canMoveCar() { + int randomNumber = Randoms.pickNumberInRange(MIN_RANDOM_VALUE, MAX_RANDOM_VALUE); + return randomNumber >= MIN_RANDOM_RANGE; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/service/RacingCarService.java b/src/main/java/racingcar/service/RacingCarService.java new file mode 100644 index 0000000000..c510536e42 --- /dev/null +++ b/src/main/java/racingcar/service/RacingCarService.java @@ -0,0 +1,10 @@ +package racingcar.service; + +import racingcar.domain.Cars; + +public class RacingCarService { + + public void moveCars(Cars cars){ + cars.moveCars(); + } +} diff --git a/src/main/java/racingcar/view/InputValidator.java b/src/main/java/racingcar/view/InputValidator.java new file mode 100644 index 0000000000..a84d48c998 --- /dev/null +++ b/src/main/java/racingcar/view/InputValidator.java @@ -0,0 +1,35 @@ +package racingcar.view; + +import racingcar.constants.ErrorMessage; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +public class InputValidator { + + private static final int MIN_TRY_COUNT = 1; + private static final int MIN_NAME_SIZE = 2; + private static final String DIVISION = ","; + private static final String NUMERIC_REGEX = "^\\d*$"; + + public void validateContainDivision(String name) { + Set carNames = Arrays.stream(name.split(DIVISION)).collect(Collectors.toSet()); + if(carNames.size() < MIN_NAME_SIZE){ + throw new IllegalArgumentException(ErrorMessage.NAME_DIVISION_ERROR.getMessage()); + } + } + + public void validateTryCount(String count) { + validateNumeric(count); + if (Integer.parseInt(count) < MIN_TRY_COUNT){ + throw new IllegalArgumentException(ErrorMessage.TRY_COUNT_ERROR.getMessage()); + } + } + + private void validateNumeric(String count){ + if (!count.matches(NUMERIC_REGEX)){ + throw new IllegalArgumentException(ErrorMessage.INPUT_NUMERIC_ERROR.getMessage()); + } + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..2909a96081 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,42 @@ +package racingcar.view; + +import java.util.function.Supplier; +import camp.nextstep.edu.missionutils.Console; +import racingcar.domain.Cars; + +public class InputView { + + private final OutputView outputView = new OutputView(); + public final InputValidator inputValidator = new InputValidator(); + + public Cars readCarNames(){ + return read(() -> { + outputView.printInputCarNames(); + String names = Console.readLine(); + inputValidator.validateContainDivision(names); + Cars cars = new Cars(); + cars.addCars(names); + return cars; + }); + } + + public int readTryCount(){ + return read(() ->{ + outputView.printInputTryCount(); + String count = Console.readLine(); + outputView.printEnter(); + inputValidator.validateTryCount(count); + return Integer.parseInt(count); + }); + } + + private T read(Supplier supplier){ + while (true){ + try{ + return supplier.get(); + }catch (IllegalArgumentException e){ + outputView.printErrorMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..9e93298801 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,35 @@ +package racingcar.view; + +import racingcar.constants.ProgressMessage; +import racingcar.domain.Cars; + +public class OutputView { + public void printErrorMessage(String message) { + System.out.println(message); + } + + public void printInputCarNames(){ + System.out.println(ProgressMessage.INPUT_CAR_NAMES); + } + + public void printInputTryCount(){ + System.out.println(ProgressMessage.INPUT_TRY_COUNT); + } + + public void printExecuteResult(){ + System.out.println(ProgressMessage.EXECUTE_RESULT); + } + + public void printCarsStatus(Cars cars){ + System.out.println(cars.executionResult()); + } + + public void printFinalWinner(Cars cars){ + System.out.print(ProgressMessage.FINAL_WINNER); + System.out.println(cars.finalWinner()); + } + + public void printEnter(){ + System.out.println(); + } +} diff --git a/src/test/java/racingcar/CarsTest.java b/src/test/java/racingcar/CarsTest.java new file mode 100644 index 0000000000..911c3c6650 --- /dev/null +++ b/src/test/java/racingcar/CarsTest.java @@ -0,0 +1,41 @@ +package racingcar; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Cars; + +import static org.assertj.core.api.Assertions.*; + +class CarsTest { + + private Cars cars; + + @BeforeEach + void setUp() { + cars = new Cars(); + cars.addCars("pobi,woni,jun"); + cars.moveCars(); + } + + @Test + @DisplayName("자동차를 추가했을 때 차를 움직이면 실행결과에 이름이 나타난다.") + void showCarScreen(){ + cars.addCars("pobi,woni,jun"); + cars.moveCars(); + + String screen = cars.executionResult(); + + assertThat(screen).containsAnyOf("pobi :", "woni :", "jun :"); + } + + @Test + @DisplayName("게임에서 이긴 최종 우승자의 이름을 나타난다.") + void showWinCarNames(){ + cars.addCars("pobi,woni,jun"); + cars.moveCars(); + + String names = cars.finalWinner(); + assertThat(names).containsAnyOf("pobi", "woni", "jun"); + } +} diff --git a/src/test/java/racingcar/InputValidatorTest.java b/src/test/java/racingcar/InputValidatorTest.java new file mode 100644 index 0000000000..6fd2cb7969 --- /dev/null +++ b/src/test/java/racingcar/InputValidatorTest.java @@ -0,0 +1,47 @@ +package racingcar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.view.InputValidator; + +import static org.assertj.core.api.Assertions.*; + +class InputValidatorTest { + + private static final String ERROR = "[ERROR]"; + InputValidator inputValidator = new InputValidator(); + + @Test + @DisplayName("쉼표를 포함하여 입력값을 2명이상 입력하지 않았을 때 에러가 발생한다.") + void inputContainDivision(){ + assertThatThrownBy(() -> inputValidator.validateContainDivision("pobi,")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR); + } + + @Test + @DisplayName("자동차 이름이 5자를 초과하면 에러가 발생한다.") + void inputCarNamesLength(){ + assertThatThrownBy(() -> new Car("junsu12")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR); + } + + @Test + @DisplayName("시도할 횟수가 1회 이상이 아니라면 에러가 발생한다.") + void inputTryCount(){ + assertThatThrownBy(() -> inputValidator.validateTryCount("0")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR); + } + + @Test + @DisplayName("시도할 횟수가 숫자가 아니라면 에러가 발생한다.") + void inputTryCountNumeric(){ + assertThatThrownBy(() -> inputValidator.validateTryCount("a")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR); + } + +}