diff --git a/docs/README.md b/docs/README.md index e69de29bb2d..fd28e56ceb1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,59 @@ +# 구현 기능 목록 + +- [ ] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. + + +- [X] 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. + - [X] 자동차 이름은 쉼표(,)를 기준으로 구분 + - [X] 이름은 5자 이하만 가능 + - [X] null 이거나 공백 X + - [X] 자동차 이름이 같은 것이 있으면 안됨 + + +- [X] 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. + - [X] 1 이상의 정수 여야 함 + + +- [X] 전진하는 조건 : 0에서 9 사이에서 무작위 값을 구한 후, 무작위 값이 4 이상일 경우 + - [X] `Randoms.pickNumberInRange(0,9);` 사용 + + +- [ ] 자동차 경주 게임을 완료한 후 우승자를 알려준다. 우승자는 한 명 이상일 수 있다. + - [ ] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. + + +- [ ] 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다. + + + +--- +- 전체 흐름 예시 +``` +경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) +pobi,woni,jun +시도할 회수는 몇회인가요? +5 + +실행 결과 +pobi : - +woni : +jun : - + +pobi : -- +woni : - +jun : -- + +pobi : --- +woni : -- +jun : --- + +pobi : ---- +woni : --- +jun : ---- + +pobi : ----- +woni : ---- +jun : ----- + +최종 우승자 : pobi, jun +``` \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e7242..c1216493693 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,12 @@ package racingcar; +import camp.nextstep.edu.missionutils.Console; +import racingcar.controller.MainController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + MainController mainController = MainController.create(); + mainController.run(); + Console.close(); } } diff --git a/src/main/java/racingcar/constants/Constants.java b/src/main/java/racingcar/constants/Constants.java new file mode 100644 index 00000000000..2eb5eec561e --- /dev/null +++ b/src/main/java/racingcar/constants/Constants.java @@ -0,0 +1,7 @@ +package racingcar.constants; + +public class Constants { + public static final int MINIMUM_RANDOM_NUMBER = 0; + public static final int MAXIMUM_RANDOM_NUMBER = 9; + public static final int MOVE_THRESHOLD = 4; +} diff --git a/src/main/java/racingcar/controller/MainController.java b/src/main/java/racingcar/controller/MainController.java new file mode 100644 index 00000000000..f68619ebd49 --- /dev/null +++ b/src/main/java/racingcar/controller/MainController.java @@ -0,0 +1,63 @@ +package racingcar.controller; + +import racingcar.domain.Car; +import racingcar.domain.Cars; +import racingcar.domain.TotalRound; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; +import java.util.function.Supplier; + +public class MainController { + private final InputView inputView; + private final OutputView outputView; + private PlayController playController; + + private MainController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public static MainController create() { + return new MainController(InputView.getInstance(), OutputView.getInstance()); + } + + public void run() { + setup(); + playController.play(); + } + + private void setup() { + Cars cars = createCars(); + TotalRound totalRound = createTotalRound(); + playController = PlayController.of(outputView, cars, totalRound); + } + + private Cars createCars() { + return readUserInput(() -> { + List carNames = inputView.readCarNames(); + List validCars = carNames.stream() + .map(Car::from) + .toList(); + return Cars.from(validCars); + }); + } + + private TotalRound createTotalRound() { + return readUserInput(() -> { + int totalRound = inputView.readTotalRound(); + return TotalRound.from(totalRound); + }); + } + + private T readUserInput(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/controller/PlayController.java b/src/main/java/racingcar/controller/PlayController.java new file mode 100644 index 00000000000..6aa647f93c5 --- /dev/null +++ b/src/main/java/racingcar/controller/PlayController.java @@ -0,0 +1,50 @@ +package racingcar.controller; + +import racingcar.domain.Car; +import racingcar.domain.Cars; +import racingcar.domain.TotalRound; +import racingcar.dto.CarDto; +import racingcar.dto.CarsDto; +import racingcar.dto.WinnerNamesDto; +import racingcar.view.OutputView; + +import java.util.List; +import java.util.stream.IntStream; + +public class PlayController { + private final OutputView outputView; + private final Cars cars; + private final TotalRound totalRound; + + public PlayController(OutputView outputView, Cars cars, TotalRound totalRound) { + this.outputView = outputView; + this.cars = cars; + this.totalRound = totalRound; + } + + public static PlayController of(OutputView outputView, Cars cars, TotalRound totalRound) { + return new PlayController(outputView, cars, totalRound); + } + + public void play() { + outputView.printStartResult(); + IntStream.range(0, totalRound.getTotalRound()) + .forEach(i -> playRound()); + printWinners(); + } + + private void playRound() { + cars.play(); + List carDtos = cars.provideCars().stream() + .map(car -> CarDto.of(car.provideName(), car.providePosition())) + .toList(); + CarsDto carsDto = CarsDto.from(carDtos); + outputView.printResult(carsDto); + } + + private void printWinners() { + List winners = cars.decideWinner(); + WinnerNamesDto winnerNamesDto = WinnerNamesDto.from(winners); + outputView.printWinners(winnerNamesDto); + } +} \ 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 00000000000..c85c6073429 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,43 @@ +package racingcar.domain; + +import racingcar.utils.CarNamesValidator; +import racingcar.utils.RandomNumberGenerator; + +import static racingcar.constants.Constants.MOVE_THRESHOLD; + +public class Car { + private final String name; + private int position; + + private Car(String name) { + this.name = name; + } + + public static Car from(String name) { + CarNamesValidator.validateName(name); + return new Car(name); + } + + public void play() { + int randomNumber = RandomNumberGenerator.generate(); + moveOrStop(randomNumber); + } + + private void moveOrStop(int randomNumber) { + if (randomNumber >= MOVE_THRESHOLD) { + position++; + } + } + + public boolean isEqualPosition(int value) { + return position == value; + } + + public String provideName() { + return name; + } + + public int providePosition() { + return position; + } +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 00000000000..1c9f6c8f2f8 --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,34 @@ +package racingcar.domain; + +import java.util.List; + +public class Cars { + private final List cars; + + private Cars(List cars) { + this.cars = cars; + } + + public static Cars from(List cars) { + return new Cars(cars); + } + + public void play() { + cars.forEach(Car::play); + } + + public List decideWinner() { + int maxPosition = cars.stream() + .mapToInt(Car::providePosition) + .max() + .getAsInt(); + + return cars.stream() + .filter(car -> car.isEqualPosition(maxPosition)) + .toList(); + } + + public List provideCars() { + return List.copyOf(cars); + } +} diff --git a/src/main/java/racingcar/domain/TotalRound.java b/src/main/java/racingcar/domain/TotalRound.java new file mode 100644 index 00000000000..9a8d96666c7 --- /dev/null +++ b/src/main/java/racingcar/domain/TotalRound.java @@ -0,0 +1,20 @@ +package racingcar.domain; + +import racingcar.utils.TotalRoundValidator; + +public class TotalRound { + private final int totalRound; + + private TotalRound(int totalRound) { + this.totalRound = totalRound; + } + + public static TotalRound from(int totalRound) { + TotalRoundValidator.validatePositive(totalRound); + return new TotalRound(totalRound); + } + + public int getTotalRound() { + return totalRound; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/dto/CarDto.java b/src/main/java/racingcar/dto/CarDto.java new file mode 100644 index 00000000000..cae750ec510 --- /dev/null +++ b/src/main/java/racingcar/dto/CarDto.java @@ -0,0 +1,23 @@ +package racingcar.dto; + +public class CarDto { + private final String name; + private final int position; + + private CarDto(String name, int position) { + this.name = name; + this.position = position; + } + + public static CarDto of(String name, int position) { + return new CarDto(name, position); + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/racingcar/dto/CarsDto.java b/src/main/java/racingcar/dto/CarsDto.java new file mode 100644 index 00000000000..ee5a8726050 --- /dev/null +++ b/src/main/java/racingcar/dto/CarsDto.java @@ -0,0 +1,19 @@ +package racingcar.dto; + +import java.util.List; + +public class CarsDto { + private final List carDtos; + + private CarsDto(List carDtos) { + this.carDtos = carDtos; + } + + public static CarsDto from(List carDtos) { + return new CarsDto(carDtos); + } + + public List getCarDtos() { + return carDtos; + } +} diff --git a/src/main/java/racingcar/dto/WinnerNamesDto.java b/src/main/java/racingcar/dto/WinnerNamesDto.java new file mode 100644 index 00000000000..33df1e41402 --- /dev/null +++ b/src/main/java/racingcar/dto/WinnerNamesDto.java @@ -0,0 +1,32 @@ +package racingcar.dto; + +import racingcar.domain.Car; + +import java.util.List; + +public class WinnerNamesDto { + private final List names; + + private WinnerNamesDto(List names) { + this.names = names; + } + + public static WinnerNamesDto from(List cars) { + List names = cars.stream() + .map(Car::provideName) + .toList(); + return new WinnerNamesDto(names); + } + + public List getNames() { + return names; + } + + public String getNameByIndex(int index) { + return names.get(index); + } + + public int getSize() { + return names.size(); + } +} diff --git a/src/main/java/racingcar/exception/ErrorMessage.java b/src/main/java/racingcar/exception/ErrorMessage.java new file mode 100644 index 00000000000..50ab72bae9b --- /dev/null +++ b/src/main/java/racingcar/exception/ErrorMessage.java @@ -0,0 +1,21 @@ +package racingcar.exception; + +public enum ErrorMessage { + ERROR_CAPTION("[ERROR] "), + INVALID_CAR_NAMES("유효하지 않은 이름 형식 입니다."), + DUPLICATE_CAR_NAMES("서로 다른 이름을 입력해 주세요."), + INVALID_BLANK_CAR_NAME("유효하지 않은 이름 입니다."), + INVALID_CAR_NAME_LENGTH("이름은 5자 이하여야 합니다."), + NOT_NUMERIC_INPUT("숫자를 입력해 주세요."), + NOT_POSITIVE_INPUT("양수를 입력해 주세요."); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return ERROR_CAPTION.message + message; + } +} diff --git a/src/main/java/racingcar/utils/CarNamesValidator.java b/src/main/java/racingcar/utils/CarNamesValidator.java new file mode 100644 index 00000000000..23b114e7746 --- /dev/null +++ b/src/main/java/racingcar/utils/CarNamesValidator.java @@ -0,0 +1,54 @@ +package racingcar.utils; + +import org.junit.platform.commons.util.StringUtils; + +import java.util.HashSet; +import java.util.List; + +import static racingcar.exception.ErrorMessage.*; + +public class CarNamesValidator { + private static final String CAR_NAME_DELIMITER = ","; + private static final int CAR_NAME_MAXIMUM_SIZE = 5; + + public static List safeSplit(String input) { + validateEmpty(input); + validateStartsOrEndsWithDelimiter(input, CAR_NAME_DELIMITER); + return List.of(input.split(CAR_NAME_DELIMITER)); + } + + private static void validateEmpty(String input) { + if (StringUtils.isBlank(input)) { + throw new IllegalArgumentException(INVALID_CAR_NAMES.getMessage()); + } + } + + private static void validateStartsOrEndsWithDelimiter(String input, String delimiter) { + if (input.startsWith(delimiter) || input.endsWith(delimiter)) { + throw new IllegalArgumentException(INVALID_CAR_NAMES.getMessage()); + } + } + + public static void validateDuplicate(List input) { + if (input.size() != new HashSet<>(input).size()) { + throw new IllegalArgumentException(DUPLICATE_CAR_NAMES.getMessage()); + } + } + + public static void validateName(String name) { + validateNotNull(name); + validateLength(name); + } + + private static void validateNotNull(String name) { + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException(INVALID_BLANK_CAR_NAME.getMessage()); + } + } + + private static void validateLength(String name) { + if (name.length() > CAR_NAME_MAXIMUM_SIZE) { + throw new IllegalArgumentException(INVALID_CAR_NAME_LENGTH.getMessage()); + } + } +} diff --git a/src/main/java/racingcar/utils/RandomNumberGenerator.java b/src/main/java/racingcar/utils/RandomNumberGenerator.java new file mode 100644 index 00000000000..4d380d7e053 --- /dev/null +++ b/src/main/java/racingcar/utils/RandomNumberGenerator.java @@ -0,0 +1,12 @@ +package racingcar.utils; + +import camp.nextstep.edu.missionutils.Randoms; + +import static racingcar.constants.Constants.MAXIMUM_RANDOM_NUMBER; +import static racingcar.constants.Constants.MINIMUM_RANDOM_NUMBER; + +public class RandomNumberGenerator { + public static int generate() { + return Randoms.pickNumberInRange(MINIMUM_RANDOM_NUMBER, MAXIMUM_RANDOM_NUMBER); + } +} diff --git a/src/main/java/racingcar/utils/TotalRoundValidator.java b/src/main/java/racingcar/utils/TotalRoundValidator.java new file mode 100644 index 00000000000..58614081ae0 --- /dev/null +++ b/src/main/java/racingcar/utils/TotalRoundValidator.java @@ -0,0 +1,20 @@ +package racingcar.utils; + +import static racingcar.exception.ErrorMessage.NOT_NUMERIC_INPUT; +import static racingcar.exception.ErrorMessage.NOT_POSITIVE_INPUT; + +public class TotalRoundValidator { + public static int safeParseInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(NOT_NUMERIC_INPUT.getMessage()); + } + } + + public static void validatePositive(int value) { + if (value <= 0) { + throw new IllegalArgumentException(NOT_POSITIVE_INPUT.getMessage()); + } + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 00000000000..5bfa361ff0c --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,35 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; +import racingcar.utils.CarNamesValidator; +import racingcar.utils.TotalRoundValidator; + +import java.util.List; + +public class InputView { + private static final InputView instance = new InputView(); + private static final String START_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String TOTAL_ROUND_MESSAGE = "시도할 회수는 몇회인가요?"; + + + private InputView() { + } + + public static InputView getInstance() { + return instance; + } + + public List readCarNames() { + System.out.println(START_MESSAGE); + String input = Console.readLine(); + List carNamesInput = CarNamesValidator.safeSplit(input); + CarNamesValidator.validateDuplicate(carNamesInput); + return carNamesInput; + } + + public int readTotalRound() { + System.out.println(TOTAL_ROUND_MESSAGE); + String input = Console.readLine(); + return TotalRoundValidator.safeParseInt(input); + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 00000000000..afdb5322ed6 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,56 @@ +package racingcar.view; + +import racingcar.dto.CarDto; +import racingcar.dto.CarsDto; +import racingcar.dto.WinnerNamesDto; + +import java.util.List; + +public class OutputView { + private static final OutputView instance = new OutputView(); + private static final String START_RESULT_MESSAGE = "실행 결과"; + private static final String RESULT_FORMAT = "%s : %s"; + private static final String MOVE_SYMBOL = "-"; + private static final String WINNER_MESSAGE_FORMAT = "최종 우승자 : %s"; + private static final String WINNER_DELIMITER = ", "; + private static final int SINGLE_WINNER = 1; + + + private OutputView() { + } + + public static OutputView getInstance() { + return instance; + } + + public void printError(String errorMessage) { + System.out.println(errorMessage); + } + + public void printStartResult() { + printLine(); + System.out.println(START_RESULT_MESSAGE); + } + + private void printLine() { + System.out.println(); + } + + public void printResult(CarsDto carsDto) { + List carDtos = carsDto.getCarDtos(); + carDtos.forEach(this::printCarResult); + printLine(); + } + + private void printCarResult(CarDto car) { + String positionImage = MOVE_SYMBOL.repeat(car.getPosition()); + String resultMessage = String.format(RESULT_FORMAT, car.getName(), positionImage); + System.out.println(resultMessage); + } + + public void printWinners(WinnerNamesDto dto) { + String winnerNames = String.join(WINNER_DELIMITER, dto.getNames()); + String message = String.format(WINNER_MESSAGE_FORMAT, winnerNames); + System.out.println(message); + } +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 00000000000..d05cfb0f542 --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,27 @@ +package racingcar.domain; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +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 + void create() { + //given, when + Car car = Car.from("name"); + + //then + assertThat(car).isNotNull(); + } + + @ParameterizedTest(name = "[{index}] Car 의 이름이 ''{0}'' 이면 예외 발생한다.") + @ValueSource(strings = {"abcdefg", "", " "}) + void cannotCreateBonusNumber(String element) { + assertThatThrownBy(() -> Car.from(element)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/TotalRoundTest.java b/src/test/java/racingcar/domain/TotalRoundTest.java new file mode 100644 index 00000000000..3305a896d61 --- /dev/null +++ b/src/test/java/racingcar/domain/TotalRoundTest.java @@ -0,0 +1,25 @@ +package racingcar.domain; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; + +class TotalRoundTest { + @Test + void create() { + //given, when + TotalRound totalRound = TotalRound.from(1); + + //then + assertThat(totalRound).isNotNull(); + } + + @ParameterizedTest(name = "[{index}] {0} 이면 TotalRound 생성 시 예외 발생한다.") + @ValueSource(ints = {-100, 0}) + void exception(int element) { + assertThatThrownBy(() -> TotalRound.from(element)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file