diff --git a/README.md b/README.md index 491aece1..d6b229a3 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ -# java-racingcar-precourse \ No newline at end of file +# java-racingcar-precourse + +## 기능 요구 사항 +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. +- 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. +- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. +- 사용자가 잘못된 값을 입력할 경우 를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다 + 시 받는다. +- IllegalArgumentException이 아닌 IllegalArgumentException , IllegalStateException 등과 같은 명확한 유형을 처리한다. + +## 구현 기능 목록 +### model +자동차 +- [X] 자동차 전진 + +게임 +- [X] 경주 참여 자동차 추가 +- [X] 시도 횟수 설정 +- [X] 경주 자동차 이동 +- [X] 현재 실행 횟수 증가 +- [X] 현재 실행 횟수와 시도 횟수 비교 +- [X] 최대 위치 얻기 +- [X] 우승자 정보 얻기 + +사용자 입력 유효성 검사 +- [X] 자동차 이름 입력 유효성 검사 +- [X] 시도 횟수 입력 유효성 검사 + +### view +입력 +- [X] 자동차 이름 입력 +- [X] 시도 횟수 입력 + +출력 +- [X] 실행 결과 출력 +- [X] 우승자 출력 + +### controller +- [X] 자동차 이름 유효성 검사 및 입력 받기 +- [X] 시도 횟수 유효성 검사 및 입력 받기 +- [X] 경주 자동차 등록 +- [X] 시도 횟수 설정 +- [X] 경주 진행 (입력 처리, 경주 시작, 우승자 출력) + +### utils +- [X] 자동차 이름 parser +- [X] 시도 횟수 parser +- [X] 난수 생성기 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 20a92c9e..bb9dd494 100644 --- a/build.gradle +++ b/build.gradle @@ -23,3 +23,8 @@ dependencies { test { useJUnitPlatform() } + +/* 제 PC에서는 추가하지 않으면 실행되지 않아서 추가했습니다. */ +tasks.withType(JavaCompile){ + options.encoding = "UTF-8" +} \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java new file mode 100644 index 00000000..6afd456d --- /dev/null +++ b/src/main/java/racingcar/Application.java @@ -0,0 +1,10 @@ +package racingcar; + +import racingcar.controller.Controller; + +public class Application { + public static void main(String[] args) { + Controller controller = new Controller(); + controller.run(); + } +} diff --git a/src/main/java/racingcar/controller/Controller.java b/src/main/java/racingcar/controller/Controller.java new file mode 100644 index 00000000..bb5f0acf --- /dev/null +++ b/src/main/java/racingcar/controller/Controller.java @@ -0,0 +1,76 @@ +package racingcar.controller; + +import racingcar.model.Car; +import racingcar.model.Game; +import racingcar.model.Validator; +import racingcar.utils.Parser; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + +import static racingcar.utils.Parser.parseTrialNumber; + +public class Controller { + private final Validator validator = new Validator(); + private final InputView inputView = new InputView(); + + public void run(){ + play(createGame()); + } + + private Game createGame(){ + Game game = new Game(); + registerRaceCar(game); + setTrial(game); + return game; + } + + private void play(Game game){ + OutputView.printHead(); + while(!game.isGameEnd()){ + game.moveForward(); + game.increaseTrialNumber(); + OutputView.printScore(game.getRaceCars()); + } + OutputView.printWinners(game.getWinners()); + } + + private void registerRaceCar(Game game){ + game.setRaceCars(Parser.parseCarName(getCarNamesUserInput())); + } + + private void setTrial(Game game){ + game.setTrial(Parser.parseTrialNumber(getTrialNumberUserInput())); + } + + private String getCarNamesUserInput() { + String input = inputView.requestCarNamesMessage(); + return checkValidationCarNames(input); + } + + private String checkValidationCarNames(String input) { + try { + validator.checkCarsName(input); + return input; + } catch (IllegalArgumentException e) { + System.out.println("[ERROR] " + e.getMessage()); + return getCarNamesUserInput(); + } + } + + private String getTrialNumberUserInput() { + String input = inputView.requestTrialNumberMessage(); + return checkValidationTrialNumber(input); + } + + private String checkValidationTrialNumber(String input) { + try { + validator.checkTrialNumber(input); + return input; + } catch (IllegalArgumentException e) { + System.out.println("[ERROR] " + e.getMessage()); + return getTrialNumberUserInput(); + } + } +} diff --git a/src/main/java/racingcar/model/Car.java b/src/main/java/racingcar/model/Car.java new file mode 100644 index 00000000..29a2f980 --- /dev/null +++ b/src/main/java/racingcar/model/Car.java @@ -0,0 +1,26 @@ +package racingcar.model; + +public class Car { + private String name; + private int position; + private static final int FORWARD_CONDITION = 4; + + public Car (String name){ + this.name = name; + this.position = 0; + } + + public String getName(){ + return name; + } + + public int getPosition(){ + return position; + } + + public void move(int condition){ + if(condition>=FORWARD_CONDITION){ + this.position += 1; + } + } +} diff --git a/src/main/java/racingcar/model/Game.java b/src/main/java/racingcar/model/Game.java new file mode 100644 index 00000000..d20a0667 --- /dev/null +++ b/src/main/java/racingcar/model/Game.java @@ -0,0 +1,67 @@ +package racingcar.model; + +import racingcar.utils.RandomNumberGenerator; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Game { + private final List raceCars; + private static int trial; + private int trialNumber = 0; + + public Game(){ + Game.trial = 0; + raceCars = new ArrayList<>(); + } + + public List getRaceCars(){ + return raceCars; + } + + public int getTrial(){ + return trial; + } + + public int getTrialNumber(){ + return trialNumber; + } + + public void setRaceCars(String[] carsName){ + for (String s : carsName) { + raceCars.add(new Car(s)); + } + } + + public void setTrial(int trial){ + Game.trial = trial; + } + + public void moveForward(){ + for (Car car : raceCars){ + car.move(RandomNumberGenerator.generate()); + } + } + + public void increaseTrialNumber(){ + this.trialNumber += 1; + } + + public Boolean isGameEnd(){ + return trial <= this.trialNumber; + } + + public List getWinners() { + return raceCars.stream() + .filter(c -> c.getPosition() == getMaxPosition(raceCars)) + .collect(Collectors.toList()); + } + + public int getMaxPosition(List cars){ + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } +} diff --git a/src/main/java/racingcar/model/Validator.java b/src/main/java/racingcar/model/Validator.java new file mode 100644 index 00000000..22ed0f37 --- /dev/null +++ b/src/main/java/racingcar/model/Validator.java @@ -0,0 +1,34 @@ +package racingcar.model; + +import java.util.HashSet; +import java.util.Set; + +public class Validator { + public void checkCarsName(String carsName){ + if (carsName.endsWith(",")) { + throw new IllegalArgumentException("입력 문자열의 마지막 문자가 ','일 수 없습니다."); + } + String[] names = carsName.split(","); + Set carSet = new HashSet<>(); + for (String name : names) { + if (name.trim().isEmpty() || name.length() > 5) { + throw new IllegalArgumentException("자동차 이름은 1 ~ 5글자로 입력해주세요."); + } + if (!carSet.add(name)) { + throw new IllegalArgumentException("자동차 이름은 중복될 수 없습니다."); + } + } + } + + public void checkTrialNumber(String trialNumberInput){ + int trialNumber; + try{ + trialNumber = Integer.parseInt(trialNumberInput); + } catch (NumberFormatException e){ + throw new IllegalArgumentException("1 이상 2,147,483,647 이하의 자연수를 입력해주세요."); + } + if(trialNumber < 1){ + throw new IllegalArgumentException("1 이상 2,147,483,647 이하의 자연수를 입력해주세요."); + } + } +} diff --git a/src/main/java/racingcar/utils/Parser.java b/src/main/java/racingcar/utils/Parser.java new file mode 100644 index 00000000..cc52ab25 --- /dev/null +++ b/src/main/java/racingcar/utils/Parser.java @@ -0,0 +1,11 @@ +package racingcar.utils; + +public class Parser { + public static String[] parseCarName(String input){ + return input.split(","); + } + + public static int parseTrialNumber(String input){ + return Integer.parseInt(input); + } +} diff --git a/src/main/java/racingcar/utils/RandomNumberGenerator.java b/src/main/java/racingcar/utils/RandomNumberGenerator.java new file mode 100644 index 00000000..daaa0ac6 --- /dev/null +++ b/src/main/java/racingcar/utils/RandomNumberGenerator.java @@ -0,0 +1,10 @@ +package racingcar.utils; + +import java.util.Random; + +public class RandomNumberGenerator { + public static int generate(){ + Random random = new Random(); + return random.nextInt(10); + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 00000000..b5447fa3 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,25 @@ +package racingcar.view; + +import java.util.Scanner; + +public class InputView { + private final Scanner scanner; + + public InputView() { + this.scanner = new Scanner(System.in); + } + + public String requestCarNamesMessage() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + return getUserInput(); + } + + public String requestTrialNumberMessage() { + System.out.println("시도할 횟수는 몇회인가요?"); + return getUserInput(); + } + + private String getUserInput() { + return scanner.nextLine(); + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 00000000..47f1b90a --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,41 @@ +package racingcar.view; + +import racingcar.model.Car; + +import java.util.List; + +public class OutputView { + public static void printHead(){ + System.out.println(); + System.out.println("실행 결과"); + } + + public static void printScore(List cars){ + for(Car car : cars) { + printCarScore(car); + } + System.out.println(); + } + + public static void printWinners(List winners){ + StringBuilder winnerNames = new StringBuilder("최종 우승자 : "); + for(Car car : winners){ + winnerNames.append(car.getName()).append(", "); + } + int length = winnerNames.length(); + String result = winnerNames.substring(0, length - 2); + System.out.print(result); + } + + public static void printCarScore(Car car){ + System.out.print(car.getName()+" : "); + printCarPosition(car); + System.out.println(); + } + + public static void printCarPosition(Car car){ + for(int i=0; i validator.checkCarsName(testInput1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("입력 문자열의 마지막 문자가 ','일 수 없습니다."); + + assertThatThrownBy(() -> validator.checkCarsName(testInput2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 1 ~ 5글자로 입력해주세요."); + + assertThatThrownBy(() -> validator.checkCarsName(testInput3)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 중복될 수 없습니다."); + + assertThatThrownBy(() -> validator.checkCarsName(testInput4)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 1 ~ 5글자로 입력해주세요."); + } + + @Test + @DisplayName("시도 횟수 유효 Test") + void 시도_횟수_유효_TEST(){ + //given + String testNum1 = "2147483699"; + String testNum2 = "-1"; + + //when & then + assertThatThrownBy(() -> validator.checkTrialNumber(testNum1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("1 이상 2,147,483,647 이하의 자연수를 입력해주세요."); + + assertThatThrownBy(() -> validator.checkTrialNumber(testNum2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("1 이상 2,147,483,647 이하의 자연수를 입력해주세요."); + } + +}