diff --git a/docs/README.md b/docs/README.md index e69de29..4b8696a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,50 @@ +# 구현할 기능 목록 + +## Model + +### Domain +- RacingCar (하나의 레이싱 카) + - 차 이름 출력을 위한 getter + - 차 위치 이동 메서드 +- AllRacingCars (모든 레이싱 카들) + - 최종 우승자 반환 메서드 +- FullGame (전체 경기) +- Game (단일 경기) +- GameRecords (경기 기록) +- FinalWinners (최종 우승자) + - 최종 우승자 계산 메서드 + - 최종 우승자 출력을 위한 getter + +### Service +- RacingCarService (객체 생성과 메서드 호출 담당, 없애고 싶다.) + +1. 컨트롤러로부터 전달 받은 자동차 이름으로 RacingCar 객체 생성 +2. RacingCar 객체들로 AllRacingCars 객체 생성 +2. AllRacingCars 객체와 컨트롤러로부터 전달 받은 시도 횟수로 FullGame 객체 생성 +3. FullGame 객체 내에서 GameResult 생성 +4. AllGameResults의 각 GameResult가 반환한 우승자들을 모아 FinalWinners 객체 생성 + - 각 GameResult 객체 내에서 하이픈 개수 비교 +6. GameResult에서 단일 경기 우승자 Winners 객체로 반환 +7. 반환한 Winners 객체들을 WinnerCounts로 생성 +8. WinnerCounts에서 최종 우승자 FinalWinners로 생성 및 반환 + + +## View + +### Input +1. 경주할 자동차 이름 입력 받기 +2. 시도할 횟수 입력 받기 + +### Output +1. 각 차수별 실행 결과 출력 하기 +2. 단독 우승자 안내 문구 출력 하기 or 공동 우승자 안내 문구 출력 하기 + +## Controller +1. 뷰에서 입력 받은 경주할 자동차 이름 파싱 및 검증 +2. 서비스 객체에 자동차 이름 전달 +3. 뷰에서 입력 받은 시도 횟수 파싱 및 검증 +4. 서비스 객체에 시도 횟수 전달 +5. 서비스 객체로부터 AllGameResults 객체 받아 와서 변환 +6. 변환한 AllGameResults 뷰에게 전달 +7. 서비스 객체로부터 FinalWinners 받아 와서 변환 +8. 변환한 FinalWinners 뷰에게 전달 diff --git a/src/main/java/racingcar/AppConfig.java b/src/main/java/racingcar/AppConfig.java new file mode 100644 index 0000000..d531e3a --- /dev/null +++ b/src/main/java/racingcar/AppConfig.java @@ -0,0 +1,22 @@ +package racingcar; + +import racingcar.controller.Controller; +import racingcar.view.InputView; +import racingcar.view.OutputView; +import racingcar.view.TextInputView; +import racingcar.view.TextOutputView; + +public final class AppConfig { + + public Controller controller() { + return new Controller(inputView(), outputView()); + } + + private InputView inputView() { + return new TextInputView(); + } + + private OutputView outputView() { + return new TextOutputView(); + } +} diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e..4261695 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,11 @@ package racingcar; +import racingcar.controller.Controller; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + AppConfig appConfig = new AppConfig(); + Controller controller = appConfig.controller(); + controller.run(); } } diff --git a/src/main/java/racingcar/common/ErrorMessage.java b/src/main/java/racingcar/common/ErrorMessage.java new file mode 100644 index 0000000..cd21ee0 --- /dev/null +++ b/src/main/java/racingcar/common/ErrorMessage.java @@ -0,0 +1,11 @@ +package racingcar.common; + +public class ErrorMessage { + private static final String ERROR_MESSAGE = "[ERROR]"; + + public static final String ERROR_EMPTY_RACING_CAR_NAME = ERROR_MESSAGE + "입력된 자동차 이름이 없습니다."; + public static final String ERROR_SINGLE_RACING_CAR_NAME = ERROR_MESSAGE + "한 대의 자동차로는 경기할 수 없습니다."; + public static final String ERROR_EMPTY_GAME_COUNT = ERROR_MESSAGE + "입력된 경기 횟수가 없습니다."; + public static final String ERROR_GAME_COUNT_FORMAT = ERROR_MESSAGE + "경기 횟수는 정수여야 합니다."; + public static final String ERROR_TOO_SMALL_GAME_COUNT = ERROR_MESSAGE + "시도 횟수는 0보다 커야 합니다."; +} diff --git a/src/main/java/racingcar/controller/Controller.java b/src/main/java/racingcar/controller/Controller.java new file mode 100644 index 0000000..ed80d02 --- /dev/null +++ b/src/main/java/racingcar/controller/Controller.java @@ -0,0 +1,79 @@ +package racingcar.controller; + +import static racingcar.common.ErrorMessage.ERROR_EMPTY_GAME_COUNT; +import static racingcar.common.ErrorMessage.ERROR_EMPTY_RACING_CAR_NAME; +import static racingcar.common.ErrorMessage.ERROR_GAME_COUNT_FORMAT; + +import java.util.List; +import racingcar.model.domain.AllRacingCars; +import racingcar.model.domain.FullGame; +import racingcar.model.domain.GameRecords; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class Controller { + + public static final String DELIMITER = ","; + public static final String NUMERIC_REGEX = "\\d+"; + + private final InputView inputView; + private final OutputView outputView; + + public Controller(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + AllRacingCars allRacingCars = createAllRacingCars(); + + FullGame fullGame = createFullGame(allRacingCars); + + getGameRecords(fullGame); + + getFinalWinners(allRacingCars); + } + + private void getFinalWinners(AllRacingCars allRacingCars) { + outputView.outputFinalWinners(allRacingCars.getFinalWinners().toDto()); + } + + private void getGameRecords(FullGame fullGame) { + GameRecords gameRecords = fullGame.playAllRounds(); + outputView.outputGameRecords(gameRecords.toDto()); + } + + private FullGame createFullGame(AllRacingCars allRacingCars) { + String inputCount = inputView.inputGameCount(); + validateInputCount(inputCount); + int gameCount = Integer.parseInt(inputCount); + return FullGame.of(allRacingCars, gameCount); + } + + private AllRacingCars createAllRacingCars() { + String inputNames = inputView.inputRacingCarNames(); + validateInputNames(inputNames); + List carNames = splitCarNames(inputNames); + return AllRacingCars.from(carNames); + } + + private void validateInputCount(String inputCount) { + if (inputCount.trim().isEmpty() || inputCount == null) { + throw new IllegalArgumentException(ERROR_EMPTY_GAME_COUNT); + } + if (!inputCount.matches(NUMERIC_REGEX)) { + throw new IllegalArgumentException(ERROR_GAME_COUNT_FORMAT); + } + } + + private static List splitCarNames(String racingCarNames) { + return List.of(racingCarNames.split(DELIMITER)); + } + + private static void validateInputNames(String inputNames) { + if (inputNames.trim().isEmpty() || inputNames == null) { + throw new IllegalArgumentException(ERROR_EMPTY_RACING_CAR_NAME); + } + } + +} diff --git a/src/main/java/racingcar/model/domain/AllRacingCars.java b/src/main/java/racingcar/model/domain/AllRacingCars.java new file mode 100644 index 0000000..29b84eb --- /dev/null +++ b/src/main/java/racingcar/model/domain/AllRacingCars.java @@ -0,0 +1,62 @@ +package racingcar.model.domain; + +import static racingcar.common.ErrorMessage.ERROR_SINGLE_RACING_CAR_NAME; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + + +public final class AllRacingCars { + + public static final int MIN_CAR_COUNT = 2; + + private final List cars; + + private AllRacingCars(List cars) { + validate(cars); + this.cars = cars; + } + + public static AllRacingCars from(List carNames) { + return new AllRacingCars(convertToRacingCar(carNames)); + } + + public Round playOneRound() { + return Round.from(moveAllRacingCars()); + } + + public FinalWinners getFinalWinners() { + return FinalWinners.of(cars, calculateMaxPosition()); + } + + private void validate(List cars) { + if (cars.size() < MIN_CAR_COUNT) { + throw new IllegalArgumentException(ERROR_SINGLE_RACING_CAR_NAME); + } + } + + private static List convertToRacingCar(List carNames) { + return carNames.stream().map(RacingCar::from).toList(); + } + + private int calculateMaxPosition() { + return cars.stream() + .mapToInt(RacingCar::getPosition) + .max() + .orElse(0); + } + + private Map moveAllRacingCars() { + return cars.stream() + .map(car -> { + car.move(); + return car; + }) + .collect(Collectors.toMap( + Function.identity(), + RacingCar::getPosition + )); + } +} diff --git a/src/main/java/racingcar/model/domain/FinalWinners.java b/src/main/java/racingcar/model/domain/FinalWinners.java new file mode 100644 index 0000000..3cdb6dc --- /dev/null +++ b/src/main/java/racingcar/model/domain/FinalWinners.java @@ -0,0 +1,30 @@ +package racingcar.model.domain; + +import java.util.List; +import java.util.stream.Collectors; +import racingcar.model.dto.FinalWinnersDto; + +public final class FinalWinners { + + private final List finalWinners; + + private FinalWinners(final List finalWinners) { + this.finalWinners = finalWinners; + } + + public static FinalWinners of(List cars, int maxPosition) { + List result = calculateFinalWinners(cars, maxPosition); + return new FinalWinners(result); + } + + private static List calculateFinalWinners(List cars, int maxPosition) { + return cars.stream() + .filter(car -> car.isSamePosition(maxPosition)) + .collect(Collectors.toList()); + } + + public FinalWinnersDto toDto() { + return FinalWinnersDto.from(finalWinners.stream().map(RacingCar::getName).toList()); + } + +} diff --git a/src/main/java/racingcar/model/domain/FullGame.java b/src/main/java/racingcar/model/domain/FullGame.java new file mode 100644 index 0000000..26c0462 --- /dev/null +++ b/src/main/java/racingcar/model/domain/FullGame.java @@ -0,0 +1,39 @@ +package racingcar.model.domain; + +import java.util.HashMap; +import java.util.Map; +import racingcar.common.ErrorMessage; + +public final class FullGame { + + public static final int START_ROUND = 1; + public static final int MIN_GAME_COUNT = 0; + + private final AllRacingCars allRacingCars; + private final int gameCount; + + private FullGame(AllRacingCars allRacingCars, int gameCount) { + validate(gameCount); + this.allRacingCars = allRacingCars; + this.gameCount = gameCount; + } + + public static FullGame of(AllRacingCars allRacingCars, int gameCount) { + return new FullGame(allRacingCars, gameCount); + } + + public GameRecords playAllRounds() { + Map gameResult = new HashMap<>(); + for (int roundCount = START_ROUND; roundCount <= gameCount; roundCount++) { + Round round = allRacingCars.playOneRound(); + gameResult.put(roundCount, round); + } + return GameRecords.from(gameResult); + } + + private void validate(int gameCount) { + if (gameCount <= MIN_GAME_COUNT) { + throw new IllegalArgumentException(ErrorMessage.ERROR_TOO_SMALL_GAME_COUNT); + } + } +} diff --git a/src/main/java/racingcar/model/domain/GameRecords.java b/src/main/java/racingcar/model/domain/GameRecords.java new file mode 100644 index 0000000..fc28c3c --- /dev/null +++ b/src/main/java/racingcar/model/domain/GameRecords.java @@ -0,0 +1,29 @@ +package racingcar.model.domain; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import racingcar.model.dto.GameResultDto; +import racingcar.model.dto.RoundResultDto; + +public final class GameRecords { + + private final Map gameRecords; + + private GameRecords(Map gameRecords) { + this.gameRecords = gameRecords; + } + + public static GameRecords from(Map gameRecords) { + return new GameRecords(gameRecords); + } + + public GameResultDto toDto() { + Map result = gameRecords.entrySet().stream() + .collect(Collectors.toMap( + Entry::getKey, + entry -> entry.getValue().toDto() + )); + return GameResultDto.from(result); + } +} diff --git a/src/main/java/racingcar/model/domain/RacingCar.java b/src/main/java/racingcar/model/domain/RacingCar.java new file mode 100644 index 0000000..573bd8d --- /dev/null +++ b/src/main/java/racingcar/model/domain/RacingCar.java @@ -0,0 +1,39 @@ +package racingcar.model.domain; + +import camp.nextstep.edu.missionutils.Randoms; + +final class RacingCar { + + public static final int MIN_NUMBER = 0; + public static final int MAX_NUMBER = 9; + public static final int JUDGE_NUMBER = 4; + + private final String name; + private int position = 0; + + private RacingCar(String name) { + this.name = name; + } + + public static RacingCar from(String name) { + return new RacingCar(name); + } + + public void move() { + if (Randoms.pickNumberInRange(MIN_NUMBER, MAX_NUMBER) > JUDGE_NUMBER) { + position++; + } + } + + public boolean isSamePosition(int otherPosition) { + return this.position == otherPosition; + } + + public String getName() { + return this.name; + } + + public Integer getPosition() { + return position; + } +} diff --git a/src/main/java/racingcar/model/domain/Round.java b/src/main/java/racingcar/model/domain/Round.java new file mode 100644 index 0000000..a694013 --- /dev/null +++ b/src/main/java/racingcar/model/domain/Round.java @@ -0,0 +1,28 @@ +package racingcar.model.domain; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import racingcar.model.dto.RoundResultDto; + +final class Round { + + private final Map round; + + private Round(Map round) { + this.round = round; + } + + public static Round from(Map round) { + return new Round(round); + } + + public RoundResultDto toDto() { + Map result = round.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().getName(), + Entry::getValue + )); + return RoundResultDto.from(result); + } +} diff --git a/src/main/java/racingcar/model/dto/FinalWinnersDto.java b/src/main/java/racingcar/model/dto/FinalWinnersDto.java new file mode 100644 index 0000000..837f582 --- /dev/null +++ b/src/main/java/racingcar/model/dto/FinalWinnersDto.java @@ -0,0 +1,20 @@ +package racingcar.model.dto; + +import java.util.List; + +public final class FinalWinnersDto { + + private final List winners; + + private FinalWinnersDto(List winners) { + this.winners = winners; + } + + public List getWinners() { + return winners; + } + + public static FinalWinnersDto from(List winners) { + return new FinalWinnersDto(winners); + } +} diff --git a/src/main/java/racingcar/model/dto/GameResultDto.java b/src/main/java/racingcar/model/dto/GameResultDto.java new file mode 100644 index 0000000..97b1676 --- /dev/null +++ b/src/main/java/racingcar/model/dto/GameResultDto.java @@ -0,0 +1,20 @@ +package racingcar.model.dto; + +import java.util.Map; + +public final class GameResultDto { + + private final Map gameResult; + + private GameResultDto(Map gameResult) { + this.gameResult = gameResult; + } + + public Map getGameResult() { + return gameResult; + } + + public static GameResultDto from(Map gameResult) { + return new GameResultDto(gameResult); + } +} diff --git a/src/main/java/racingcar/model/dto/RoundResultDto.java b/src/main/java/racingcar/model/dto/RoundResultDto.java new file mode 100644 index 0000000..5629623 --- /dev/null +++ b/src/main/java/racingcar/model/dto/RoundResultDto.java @@ -0,0 +1,20 @@ +package racingcar.model.dto; + +import java.util.Map; + +public final class RoundResultDto { + + private final Map roundResult; + + private RoundResultDto(Map roundResult) { + this.roundResult = roundResult; + } + + public Map getRoundResult() { + return roundResult; + } + + public static RoundResultDto from(Map roundResult) { + return new RoundResultDto(roundResult); + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000..9d20a11 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,9 @@ +package racingcar.view; + +public interface InputView { + + String inputRacingCarNames(); + + String inputGameCount(); + +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000..f126091 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,11 @@ +package racingcar.view; + +import racingcar.model.dto.FinalWinnersDto; +import racingcar.model.dto.GameResultDto; + +public interface OutputView { + + void outputGameRecords(GameResultDto gameResultDto); + + void outputFinalWinners(FinalWinnersDto finalWinnersDto); +} diff --git a/src/main/java/racingcar/view/TextInputView.java b/src/main/java/racingcar/view/TextInputView.java new file mode 100644 index 0000000..5d3b594 --- /dev/null +++ b/src/main/java/racingcar/view/TextInputView.java @@ -0,0 +1,21 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; + +public final class TextInputView implements InputView { + + public static final String INPUT_RACING_CARS_NAME_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + public static final String INPUT_GAME_COUNT_MESSAGE = "시도할 회수는 몇회인가요?"; + + @Override + public String inputRacingCarNames() { + System.out.println(INPUT_RACING_CARS_NAME_MESSAGE); + return Console.readLine(); + } + + @Override + public String inputGameCount() { + System.out.println(INPUT_GAME_COUNT_MESSAGE); + return Console.readLine(); + } +} diff --git a/src/main/java/racingcar/view/TextOutputView.java b/src/main/java/racingcar/view/TextOutputView.java new file mode 100644 index 0000000..42b93d0 --- /dev/null +++ b/src/main/java/racingcar/view/TextOutputView.java @@ -0,0 +1,51 @@ +package racingcar.view; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import racingcar.model.dto.FinalWinnersDto; +import racingcar.model.dto.GameResultDto; +import racingcar.model.dto.RoundResultDto; + +public final class TextOutputView implements OutputView { + + public static final String GAME_RECORDS_MESSAGE = "실행 결과"; + public static final String FINAL_WINNERS_MESSAGE = "최종 우승자 : "; + public static final String EXPRESS_POSITION = "-"; + public static final int START_ROUND = 1; + public static final int MIN_WINNERS_COUNT = 1; + + @Override + public void outputGameRecords(GameResultDto gameResultDto) { + System.out.println(GAME_RECORDS_MESSAGE); + + Map gameResult = gameResultDto.getGameResult(); + + for (int round = START_ROUND; round <= gameResult.size(); round++) { + RoundResultDto roundResult = gameResult.get(round); + Map carPositions = roundResult.getRoundResult(); + + for (Entry entry : carPositions.entrySet()) { + String carName = entry.getKey(); + int position = entry.getValue(); + System.out.println(carName + " : " + EXPRESS_POSITION.repeat(position)); + } + System.out.println(); + } + } + + @Override + public void outputFinalWinners(FinalWinnersDto finalWinnersDto) { + System.out.print(FINAL_WINNERS_MESSAGE); + + List winners = finalWinnersDto.getWinners(); + int winnersCount = winners.size(); + + for (int i = MIN_WINNERS_COUNT; i <= winnersCount; i++) { + System.out.print(winners.get(i-1)); + if (i < winnersCount) { + System.out.print(", "); + } + } + } +}