diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..6cf87569bc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +## 기능 목록 + +- [x] 이름 입력 안내 출력 +- [x] 이름 입력 + - [x] `,`로 이름을 나누어 입력하지 않은 경우 예외 발생 + - [x] 이름이 5글자가 넘을 경우 예외 발생 +- [x] 횟수 입력 안내 출력 +- [x] 횟수 입력 + - [x] 숫자로 입력하지 않은 경우 예외 발생 + - [x] 0이하의 숫자를 입력할 경우 예외 발생 +- [x] 입력받은 횟수만큼 자동차 이동 + - [x] 랜덤으로 숫자를 뽑는다. + - [x] 뽑은 숫자가 4이상일 경우 이동한다. +- [x] 진행 내용 출력 + - [x] 달린 만큼 `-`를 출력한다 + - [x] `이름: --` 형식으로 출력한다. +- [x] 최종 결과 + - [x] 가장 멀리 나간 자동차가 무엇인지 구한다. +- [x] 최종 우승자 출력 + - [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/constant/ExceptionMessage.java b/src/main/java/racingcar/constant/ExceptionMessage.java new file mode 100644 index 0000000000..261ad70f0e --- /dev/null +++ b/src/main/java/racingcar/constant/ExceptionMessage.java @@ -0,0 +1,21 @@ +package racingcar.constant; + +public enum ExceptionMessage { + + INVALID_LENGTH("이름은 5글자 이하로 입력해야합니다."), + INCORRECT_DELIMITER("','로 구분하여 입력해야합니다."), + NOT_INTEGER("숫자로 입력해야 합니다."), + INVALID_TRY_COUNT_RANGE("0이상의 숫자를 입력해야 합니다."); + + private static final String PREFIX = "[ERROR] "; + private final String message; + + ExceptionMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return PREFIX + message; + } +} diff --git a/src/main/java/racingcar/constant/OutputMessage.java b/src/main/java/racingcar/constant/OutputMessage.java new file mode 100644 index 0000000000..72bc42ed35 --- /dev/null +++ b/src/main/java/racingcar/constant/OutputMessage.java @@ -0,0 +1,20 @@ +package racingcar.constant; + +public enum OutputMessage { + READ_NAME("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"), + READ_TRY_COUNT("시도할 회수는 몇회인가요?"), + RACING_RESULT("%s : %s"), + RESULT_MESSAGE("실행 결과"), + WINNER("최종 우승자 : "); + + private final String message; + + OutputMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return message; + } +} diff --git a/src/main/java/racingcar/constant/PrintElement.java b/src/main/java/racingcar/constant/PrintElement.java new file mode 100644 index 0000000000..6e584e7758 --- /dev/null +++ b/src/main/java/racingcar/constant/PrintElement.java @@ -0,0 +1,17 @@ +package racingcar.constant; + +public enum PrintElement { + + NEW_LINE("\n"), + SEPARATOR(", "); + + private final String element; + + PrintElement(String element) { + this.element = element; + } + + public String getElement() { + return element; + } +} diff --git a/src/main/java/racingcar/constant/RandomNumber.java b/src/main/java/racingcar/constant/RandomNumber.java new file mode 100644 index 0000000000..a417e7cd5e --- /dev/null +++ b/src/main/java/racingcar/constant/RandomNumber.java @@ -0,0 +1,18 @@ +package racingcar.constant; + +public enum RandomNumber { + + START_RANGE(0), + END_RANGE(9), + CAN_MOVE_NUMBER(4); + + private final int value; + + RandomNumber(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/racingcar/controller/RacingCarController.java b/src/main/java/racingcar/controller/RacingCarController.java new file mode 100644 index 0000000000..8abe87797c --- /dev/null +++ b/src/main/java/racingcar/controller/RacingCarController.java @@ -0,0 +1,42 @@ +package racingcar.controller; + +import racingcar.domain.Names; +import racingcar.domain.TryCount; +import racingcar.service.CarService; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class RacingCarController { + + private final OutputView outputView = new OutputView(); + private final InputView inputView = new InputView(); + private CarService carService; + + public void run() { + carService = new CarService(readNames()); + TryCount tryCount = readTryCount(); + startRacing(tryCount); + } + + private Names readNames() { + outputView.printCarNames(); + Names names = inputView.readNames(); + outputView.printNewLine(); + return names; + } + + private TryCount readTryCount() { + outputView.printTryCount(); + TryCount tryCount = inputView.readTryCount(); + outputView.printNewLine(); + return tryCount; + } + + private void startRacing(TryCount tryCount) { + outputView.printResultMessage(); + for (int i = 0; i < tryCount.getValue(); i++) { + outputView.printRacingResult(carService.race()); + } + outputView.printWinner(carService.getWinner()); + } +} diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..0b3cf1799a --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,33 @@ +package racingcar.domain; + +import racingcar.constant.OutputMessage; + +public class Car { + private final Name name; + private final Position position; + + public Car(Name name) { + this.name = name; + this.position = new Position(); + } + + public void race() { + position.move(); + } + + public boolean isWinner(int maxPosition) { + return position.isSame(maxPosition); + } + + public String getRacingResult() { + return String.format(OutputMessage.RACING_RESULT.toString(), name.getValue(), position.getValue()); + } + + public Position getPosition() { + return position; + } + + public Name 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..7e8ecf91ee --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,49 @@ +package racingcar.domain; + +import racingcar.constant.PrintElement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class Cars { + + private final List cars = new ArrayList<>(); + + public Cars(Names names) { + names.getNames() + .forEach(name -> cars.add(new Car(name))); + } + + public void race() { + cars.forEach(Car::race); + } + + public String getRacingResult() { + StringBuilder stringBuilder = new StringBuilder(); + cars.forEach(car -> + stringBuilder.append(car.getRacingResult()) + .append(PrintElement.NEW_LINE.getElement()) + ); + return stringBuilder.toString(); + } + + public Names getWinner() { + int maxPosition = calculateMaxPosition(); + List winner = cars.stream() + .filter(car -> car.isWinner(maxPosition)) + .map(Car::getName) + .collect(Collectors.toList()); + return new Names(winner); + } + + private int calculateMaxPosition() { + List positions = cars.stream() + .map(Car::getPosition) + .mapToInt(Position::getPosition) + .boxed() + .collect(Collectors.toList()); + return Collections.max(positions); + } +} diff --git a/src/main/java/racingcar/domain/Name.java b/src/main/java/racingcar/domain/Name.java new file mode 100644 index 0000000000..31d1fa4966 --- /dev/null +++ b/src/main/java/racingcar/domain/Name.java @@ -0,0 +1,24 @@ +package racingcar.domain; + +import racingcar.constant.ExceptionMessage; + +public class Name { + + private static final int MAX_LENGTH = 5; + private final String value; + + public Name(String value) { + validateName(value); + this.value = value; + } + + private void validateName(String value) { + if (value.length() > MAX_LENGTH) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_LENGTH.toString()); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/racingcar/domain/Names.java b/src/main/java/racingcar/domain/Names.java new file mode 100644 index 0000000000..7d909601e5 --- /dev/null +++ b/src/main/java/racingcar/domain/Names.java @@ -0,0 +1,27 @@ +package racingcar.domain; + +import racingcar.constant.PrintElement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; + +public class Names { + + private final List element; + + public Names(List names) { + this.element = new ArrayList<>(names); + } + + public String getElements() { + StringJoiner stringJoiner = new StringJoiner(PrintElement.SEPARATOR.getElement()); + element.forEach(name -> stringJoiner.add(name.getValue())); + return stringJoiner.toString(); + } + + public List getNames() { + return Collections.unmodifiableList(element); + } +} diff --git a/src/main/java/racingcar/domain/Position.java b/src/main/java/racingcar/domain/Position.java new file mode 100644 index 0000000000..57af03ee82 --- /dev/null +++ b/src/main/java/racingcar/domain/Position.java @@ -0,0 +1,42 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import racingcar.constant.RandomNumber; + +public class Position { + + private static final String RACING_LINE = "-"; + private int value = 0; + + public void move() { + if (canMove()) { + value += 1; + } + } + + private boolean canMove() { + return pickRandomNumber() >= RandomNumber.CAN_MOVE_NUMBER.getValue(); + } + + private int pickRandomNumber() { + int start = RandomNumber.START_RANGE.getValue(); + int end = RandomNumber.END_RANGE.getValue(); + return Randoms.pickNumberInRange(start, end); + } + + public boolean isSame(int maxPosition) { + return value == maxPosition; + } + + public int getPosition() { + return value; + } + + public String getValue() { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < value; i++) { + stringBuilder.append(RACING_LINE); + } + return stringBuilder.toString(); + } +} diff --git a/src/main/java/racingcar/domain/TryCount.java b/src/main/java/racingcar/domain/TryCount.java new file mode 100644 index 0000000000..1be714fecf --- /dev/null +++ b/src/main/java/racingcar/domain/TryCount.java @@ -0,0 +1,25 @@ +package racingcar.domain; + +import racingcar.constant.ExceptionMessage; + +public class TryCount { + + private static final int MIN_VALUE= 1; + + private final int value; + + public TryCount(int value) { + validateRange(value); + this.value = value; + } + + public void validateRange(int value) { + if (value < MIN_VALUE) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_TRY_COUNT_RANGE.toString()); + } + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/racingcar/service/CarService.java b/src/main/java/racingcar/service/CarService.java new file mode 100644 index 0000000000..aea164b3ed --- /dev/null +++ b/src/main/java/racingcar/service/CarService.java @@ -0,0 +1,22 @@ +package racingcar.service; + +import racingcar.domain.Cars; +import racingcar.domain.Names; + +public class CarService { + + private final Cars cars; + + public CarService(Names names) { + this.cars = new Cars(names); + } + + public String race() { + cars.race(); + return cars.getRacingResult(); + } + + public Names getWinner() { + return cars.getWinner(); + } +} diff --git a/src/main/java/racingcar/validator/InputValidator.java b/src/main/java/racingcar/validator/InputValidator.java new file mode 100644 index 0000000000..e820573088 --- /dev/null +++ b/src/main/java/racingcar/validator/InputValidator.java @@ -0,0 +1,25 @@ +package racingcar.validator; + +import racingcar.constant.ExceptionMessage; + +import java.util.regex.Pattern; + +public class InputValidator { + + private static final String DELIMITER_REGEXP = "^[a-zA-Zㄱ-힣0-9,]*$"; + private static final String NUMBER_REGEXP = "^\\d*$"; + + public void validateDelimiter(String input) { + if (!Pattern.matches(DELIMITER_REGEXP, input)) { + ExceptionMessage exceptionMessage = ExceptionMessage.INCORRECT_DELIMITER; + throw new IllegalArgumentException(exceptionMessage.toString()); + } + } + + public void validateIsNumber(String input) { + if (!Pattern.matches(NUMBER_REGEXP, input)) { + ExceptionMessage exceptionMessage = ExceptionMessage.NOT_INTEGER; + throw new IllegalArgumentException(exceptionMessage.toString()); + } + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..30f376a662 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,47 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; +import racingcar.domain.Name; +import racingcar.domain.Names; +import racingcar.domain.TryCount; +import racingcar.validator.InputValidator; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class InputView { + + private static final String NAMES_REGEX = ","; + + private final InputValidator inputValidator = new InputValidator(); + + public Names readNames() { + return attemptedInput(() -> { + String input = Console.readLine(); + inputValidator.validateDelimiter(input); + List names = Arrays.stream(input.split(NAMES_REGEX)) + .map(Name::new) + .collect(Collectors.toList()); + return new Names(names); + }); + } + + public TryCount readTryCount() { + return attemptedInput(() -> { + String input = Console.readLine(); + inputValidator.validateIsNumber(input); + return new TryCount(Integer.parseInt(input)); + }); + } + + private T attemptedInput(Supplier supplier) { + try { + return supplier.get(); + } catch (IllegalArgumentException exception) { + System.out.println(exception.getMessage()); + return supplier.get(); + } + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..a202e7f01c --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,31 @@ +package racingcar.view; + +import racingcar.constant.OutputMessage; +import racingcar.domain.Names; + +public class OutputView { + + public void printCarNames() { + System.out.println(OutputMessage.READ_NAME); + } + + public void printTryCount() { + System.out.println(OutputMessage.READ_TRY_COUNT); + } + + public void printResultMessage() { + System.out.println(OutputMessage.RESULT_MESSAGE); + } + + public void printRacingResult(String result) { + System.out.println(result); + } + + public void printWinner(Names names) { + System.out.println(OutputMessage.WINNER + names.getElements()); + } + + public void printNewLine() { + System.out.println(); + } +} diff --git a/src/test/java/racingcar/domain/InputValidatorTest.java b/src/test/java/racingcar/domain/InputValidatorTest.java new file mode 100644 index 0000000000..6512ba2257 --- /dev/null +++ b/src/test/java/racingcar/domain/InputValidatorTest.java @@ -0,0 +1,44 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.validator.InputValidator; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class InputValidatorTest { + + private static final String ERROR_MESSAGE = "[ERROR]"; + + private final InputValidator inputValidator = new InputValidator(); + + @DisplayName("콤마이외의 특수문자가 있을 경우 예외 발생") + @Test + void specialCharactersException() { + String input = "po|pi"; + + assertThatThrownBy(() -> inputValidator.validateDelimiter(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @DisplayName("공백이 존재할 경우 예외 발생") + @Test + void spaceException() { + String input = "po, pi"; + + assertThatThrownBy(() -> inputValidator.validateDelimiter(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @DisplayName("숫자가 아닐 경우 예외 발생") + @Test + void nameLengthException() { + String input = "o"; + + assertThatThrownBy(() -> inputValidator.validateIsNumber(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +} diff --git a/src/test/java/racingcar/domain/NameTest.java b/src/test/java/racingcar/domain/NameTest.java new file mode 100644 index 0000000000..f81b6c66cd --- /dev/null +++ b/src/test/java/racingcar/domain/NameTest.java @@ -0,0 +1,21 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NameTest { + + private static final String ERROR_MESSAGE = "[ERROR]"; + + @DisplayName("이름의 길이가 5가 넘어갈 경우 예외가 발생한다.") + @Test + void nameLengthException() { + String name = "김연진김연진"; + + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +} diff --git a/src/test/java/racingcar/domain/TryCountTest.java b/src/test/java/racingcar/domain/TryCountTest.java new file mode 100644 index 0000000000..49ffeb0859 --- /dev/null +++ b/src/test/java/racingcar/domain/TryCountTest.java @@ -0,0 +1,21 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TryCountTest { + + private static final String ERROR_MESSAGE = "[ERROR]"; + + @DisplayName("시도 횟수에 0이하의 숫자를 입력할 경우 예외 발생") + @Test + void tryCountException() { + int count = 0; + + assertThatThrownBy(() -> new TryCount(count)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +}