diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index b9ed045..fa7fbda 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,11 @@ package racingcar; +import racingcar.controller.RacingController; + public class Application { public static void main(String[] args) { // TODO 구현 진행 + RacingController racingController = new RacingController(); + racingController.run(); } } diff --git a/src/main/java/racingcar/Car.java b/src/main/java/racingcar/Car.java deleted file mode 100644 index ab3df94..0000000 --- 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/controller/RacingController.java b/src/main/java/racingcar/controller/RacingController.java new file mode 100644 index 0000000..84a9f6c --- /dev/null +++ b/src/main/java/racingcar/controller/RacingController.java @@ -0,0 +1,47 @@ +package racingcar.controller; + +import racingcar.controller.util.ExceptionHandler; +import racingcar.dto.CarStatusDTO; +import racingcar.model.RacingGame; +import racingcar.model.RandomNumberGenerator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + +public class RacingController { + private final InputView inputView = new InputView(); + private final OutputView outputView = new OutputView(); + private final RacingGame racingGame = new RacingGame(new RandomNumberGenerator()); + + public void run() { + try { + repeatUntilGetLegalAnswer(this::enrollCarToRace); + repeatUntilGetLegalAnswer(this::moveCarsByCount); + showWinners(); + } catch (Exception e) { + e.printStackTrace(); + outputView.printErrorMessage(e.getMessage()); + } + } + + private void repeatUntilGetLegalAnswer(Runnable runnable) { + ExceptionHandler.retryForIllegalArgument(runnable, outputView::printErrorMessage); + } + + private void enrollCarToRace() { + List carNames = inputView.inputCarNames(); + racingGame.enrollCars(carNames); + } + + private void moveCarsByCount() { + int moveCount = inputView.inputMoveCount(); + List carStatuses = racingGame.repeatMovingCars(moveCount); // moveCount만큼 반복 이동 + outputView.printGameResult(carStatuses); + } + + private void showWinners() { + List winnerNames = racingGame.findWinners(); + outputView.printWinners(winnerNames); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/controller/util/ExceptionHandler.java b/src/main/java/racingcar/controller/util/ExceptionHandler.java new file mode 100644 index 0000000..5e2419c --- /dev/null +++ b/src/main/java/racingcar/controller/util/ExceptionHandler.java @@ -0,0 +1,19 @@ +package racingcar.controller.util; + +import java.util.function.Consumer; + +public class ExceptionHandler { + private ExceptionHandler() { + } + + public static void retryForIllegalArgument(Runnable runnable, Consumer exceptionMessageHandling) { + while (true) { + try { + runnable.run(); + return; + } catch (IllegalArgumentException e) { + exceptionMessageHandling.accept(e.getMessage()); + } + } + } +} diff --git a/src/main/java/racingcar/dto/CarDTO.java b/src/main/java/racingcar/dto/CarDTO.java new file mode 100644 index 0000000..160e08e --- /dev/null +++ b/src/main/java/racingcar/dto/CarDTO.java @@ -0,0 +1,27 @@ +package racingcar.dto; + +public class CarDTO { + private final String name; + private final int position; + + public CarDTO(String name, int position) { + this.name = name; + this.position = position; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + @Override + public String toString() { + return "CarDTO{" + + "name='" + name + '\'' + + ", position=" + position + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/dto/CarStatusDTO.java b/src/main/java/racingcar/dto/CarStatusDTO.java new file mode 100644 index 0000000..2f080b5 --- /dev/null +++ b/src/main/java/racingcar/dto/CarStatusDTO.java @@ -0,0 +1,15 @@ +package racingcar.dto; + +import java.util.List; + +public class CarStatusDTO { + private final List cars; + + public CarStatusDTO(List cars) { + this.cars = cars; + } + + public List getCars() { + return cars; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/model/NumberGenerator.java b/src/main/java/racingcar/model/NumberGenerator.java new file mode 100644 index 0000000..25aa8e4 --- /dev/null +++ b/src/main/java/racingcar/model/NumberGenerator.java @@ -0,0 +1,6 @@ +package racingcar.model; + +@FunctionalInterface // 1개의 추상 메소드를 갖는 인터페이스 +public interface NumberGenerator { + int make(); +} \ No newline at end of file diff --git a/src/main/java/racingcar/model/RacingGame.java b/src/main/java/racingcar/model/RacingGame.java new file mode 100644 index 0000000..3f258ee --- /dev/null +++ b/src/main/java/racingcar/model/RacingGame.java @@ -0,0 +1,52 @@ +package racingcar.model; + +import racingcar.dto.CarDTO; +import racingcar.dto.CarStatusDTO; +import racingcar.model.domain.Car; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class RacingGame { + private final List racingCars = new ArrayList<>(); + private final NumberGenerator numberGenerator; + + public RacingGame(NumberGenerator numberGenerator) { + this.numberGenerator = numberGenerator; + } + + public void enrollCars(List carNames) { + carNames.forEach(name -> racingCars.add(new Car(name))); + } + + public List repeatMovingCars(int moveCount) { + List carStatuses = new ArrayList<>(); + for (int count = 0; count < moveCount; count++) + carStatuses.add(new CarStatusDTO(moveCars())); + return carStatuses; + } + + public List moveCars() { + racingCars.forEach(car -> car.move(numberGenerator.make())); + return racingCars.stream() + .map(Car::to) + .collect(Collectors.toList()); + } + + public List findWinners() { + List winners = new ArrayList<>(); + for (Car car : racingCars) { + if (findAnyHeadCar().compareTo(car) == 0) + winners.add(car.getName()); + } + return winners; + } + + private Car findAnyHeadCar() { + List sortedCars = new ArrayList<>(racingCars); + Collections.sort(sortedCars); // Car 클래스의 compareTo()로 인해 position에 따라 많이 전진한 순대로 정렬 + return sortedCars.get(0); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/model/RandomNumberGenerator.java b/src/main/java/racingcar/model/RandomNumberGenerator.java new file mode 100644 index 0000000..62be458 --- /dev/null +++ b/src/main/java/racingcar/model/RandomNumberGenerator.java @@ -0,0 +1,11 @@ +package racingcar.model; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomNumberGenerator implements NumberGenerator { + + @Override + public int make() { + return Randoms.pickNumberInRange(0, 9); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/model/domain/Car.java b/src/main/java/racingcar/model/domain/Car.java new file mode 100644 index 0000000..d38308c --- /dev/null +++ b/src/main/java/racingcar/model/domain/Car.java @@ -0,0 +1,42 @@ +package racingcar.model.domain; + +import racingcar.dto.CarDTO; + +public class Car implements Comparable { + private final String name; + private int position = 0; + + public Car(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (name.length() > 5) + throw new IllegalArgumentException("자동차 이름이 다섯 글자를 초과합니다."); + } + + public void move(int number) { + validateNumber(number); + if (number >= 4) + position++; + } + + public void validateNumber(int number) { + if (number < 0 || number > 9) + throw new IllegalArgumentException("전진하는 조건은 0에서 9 사이의 숫자일 때입니다."); + } + + public String getName() { + return name; + } + + public CarDTO to() { + return new CarDTO(name, position); + } + + @Override + public int compareTo(Car anotherCar) { + return anotherCar.position - this.position; + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000..cbc322d --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,21 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; +import racingcar.view.utils.FormatParser; +import racingcar.view.utils.NumberParser; + +import java.util.List; + +public class InputView { + public List inputCarNames() { + System.out.println("경주할 자동차 이름들을 입력하세요. (이름은 쉼표(,)로 구분함)"); + String line = Console.readLine(); // 콘솔로부터 값 읽기 + return FormatParser.split(line, ","); // ',' 단위로 자른 공백 제거한 입력한 자동차 이름들의 목록 + } + + public int inputMoveCount() { + System.out.println("몇 번 시도할지 입력하세요. (숫자)"); + String line = Console.readLine(); + return NumberParser.parseDigit(line); // 입력값을 정수로 변환 + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000..c5dcbfc --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,34 @@ +package racingcar.view; + +import racingcar.dto.CarDTO; +import racingcar.dto.CarStatusDTO; +import racingcar.view.utils.FormatParser; + +import java.util.List; + +public class OutputView { + public void printGameResult(List carStatuses) { // 경주 결과 자동차들 각각 상태 출력 + System.out.println("실행 결과"); + carStatuses.forEach(this::printCarStatuses); + } + + private void printCarStatuses(CarStatusDTO carStatus) { + List cars = carStatus.getCars(); + cars.forEach(this::printCar); + System.out.println(); + } + + private void printCar(CarDTO carDTO) { + System.out.printf("%s : %s" + System.lineSeparator(), carDTO.getName(), + FormatParser.make(carDTO.getPosition(), "-")); // lineSeparator: 개행문자 + } + + public void printWinners(List winnerNames) { + System.out.printf("최종 우승자 : %s" + System.lineSeparator(), + FormatParser.join(winnerNames, ", ")); + } + + public void printErrorMessage(String message) { + System.out.printf("[ERROR] %s" + System.lineSeparator(), message); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/view/utils/FormatParser.java b/src/main/java/racingcar/view/utils/FormatParser.java new file mode 100644 index 0000000..ec61b4c --- /dev/null +++ b/src/main/java/racingcar/view/utils/FormatParser.java @@ -0,0 +1,53 @@ +package racingcar.view.utils; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class FormatParser { + private FormatParser() { + } + + public static List split(String line, String delimiter) { + validateWrongFormat(line); + String[] values = line.split(delimiter); // 문자열 배열 + List parsedValue = Arrays.stream(values) + .map(String::trim) // trim: 문자열 공백 제거 + .collect(Collectors.toList()); + validateEmpty(parsedValue); + return parsedValue; + } + private static void validateWrongFormat(String line) { + if (hasWrongFormat(line)) { + throw new IllegalArgumentException("입력값의 형식이 잘못되었습니다."); + } + } + + private static boolean hasWrongFormat(String line) { + if (line.isEmpty()) { + return true; + } + return line.startsWith(",") || line.endsWith(","); + } + + private static void validateEmpty(List values) { + if (hasEmptyValue(values)) + throw new IllegalArgumentException("입력값에 빈 문자열이 있습니다."); + } + + private static boolean hasEmptyValue(List values) { + return values.stream() + .anyMatch(String::isEmpty); // anyMatch(): 최소한 한 개의 요소가 주어진 조건에 만족하는지 조사하는 함수 & '::': 메소드 레퍼런스. 람다식을 더욱 간결하게 해준다. + } + + public static String join(List values, String delimiter) { + return String.join(delimiter, values); + } + + public static String make(int count, String unit) { + String units = ""; + for (int i = 0; i < count; i++) + units = units + unit.charAt(0); + return units; + } +} diff --git a/src/main/java/racingcar/view/utils/NumberParser.java b/src/main/java/racingcar/view/utils/NumberParser.java new file mode 100644 index 0000000..e990a60 --- /dev/null +++ b/src/main/java/racingcar/view/utils/NumberParser.java @@ -0,0 +1,21 @@ +package racingcar.view.utils; + +public class NumberParser { + private NumberParser() {} + + public static int parseDigit(String value) { + int number = parseInteger(value); // 입력값을 정수로 변환 + if (number <= 0) + throw new IllegalArgumentException("입력값이 자연수가 아닙니다."); + + return number; + } + + public static int parseInteger(String value) { + try { + return Integer.parseInt(value); // 숫자형의 문자열을 인자 값으로 받으면 십진수의 Integer 형으로 변환해줌 + } catch (NumberFormatException e) { + throw new IllegalArgumentException("입력값이 숫자가 아닙니다."); + } + } +} diff --git "a/\352\270\260\353\212\245\353\252\251\353\241\235.md" "b/\352\270\260\353\212\245\353\252\251\353\241\235.md" new file mode 100644 index 0000000..650bd4d --- /dev/null +++ "b/\352\270\260\353\212\245\353\252\251\353\241\235.md" @@ -0,0 +1,36 @@ +## 💌 [미션 - 자동차 경주 게임] 기능 목록 + +### 도메인 구성 요소 + +- 자동차 + - 이름 + - 위치 +- 모든 자동차 목록 +- 자동차가 이동하기 위한 값 생성기 +- 시도 횟수 +- 게임 결과 + - 우승한 자동차 이름 모음 (중복 가능) + +### 도메인 로직을 위한 기능 + +1. 경주할 모든 자동차의 이름을 저장하는 기능 + - [ ] `예외발생` : 이름의 길이가 5자 초과 +2. 랜덤 숫자 생성 기능 +3. 자동차가 전달 받은 숫자에 따라 이동하는 기능 + - [ ] `예외발생`: 전달 받은 숫자가 범위 밖 +4. 자동차 간 위치를 비교해 값이 가장 큰, 1~n 명의 우승자를 정하는 기능 +5. 전달 받은 횟수만큼 자동차를 이동하는 기능 + + +### UI 로직을 위한 기능 + +1. 경주할 모든 자동차 이름을 형식에 맞게 입력 받는 기능 + - [ ] `예외발생` : 빈 문자열 + - [ ] `예외발생` : 입력값 형식에 맞지 않음 +2. 시도할 횟수를 입력 받는 기능 + - [ ] `예외발생` : 자연수가 아닌 값 +3. 차수별 실행 결과 출력 +4. 우승자 안내 문구 출력 +5. 이름 형식을 쉼표 구분 <-> 리스트로 파싱 +6. 에러 메시지 출력 +7. 잘못된 값 입력 시 그부분부터 재입력 \ No newline at end of file