diff --git a/MISSION.md b/MISSION.md new file mode 100644 index 0000000..f12e2db --- /dev/null +++ b/MISSION.md @@ -0,0 +1,44 @@ +# 자동차 경주 미션 + +## 구현 + +- 어플리케이션 +- 유저 +- 자동차 객체 + - 자동차 일급 컬렉션 + - 자동차 생성자 +- 결과 +- 입 출력 핸들러 + +### 내용 + +#### 어플리케이션 +1. 유저에게 게임을 진행 할 자동차 목록을 입력 받는다. +2. 자동차가 전진 할 수 있는 횟수를 지정한다. +3. 자동차 경주가 진행 됨에 따라 자동차가 얼만큼 전진 했는지 알려준다. +4. 자동차 경주 결과를 유저에게 알려준다. +5. 게임 진행 여부를 입력 받는다. + +#### 유저 +1. 유저는 게임을 진행 할 자동차의 이름을 입력한다. +2. 유저는 전진 할 수 있는 기회를 설정한다. +3. 게임이 종료 되면 다시 진행 하거나 종료 할 수 있다. + +#### 자동차 +1. 자동차는 이름과 위치를 갖는 객체이다. +2. 자동차의 이름은 비어있거나 5글자를 초과 할 수 없다. +3. 경주가 진행 됨에 따라 전진 하거나 그대로 멈춰있어야한다. + +- 자동차 일급 컬렉션 + 1. 자동차 목록을 관리한다. + +- 자동차 생성자 + 1. 문자열을 입력 받아 자동차 일급 컬렉션을 생성한다. + +#### 결과 +1. 자동차 경주가 끝나면 경기 결과를 반환한다. + + +#### 입출력 핸들러 +1. 유저에게 입력 받은 내용을 반환한다. +2. 유저에게 출력 할 내용을 표현한다. diff --git a/build.gradle b/build.gradle index 8172fb7..6b428de 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation "org.junit.jupiter:junit-jupiter:5.7.2" testImplementation "org.assertj:assertj-core:3.19.0" + testImplementation "org.mockito:mockito-junit-jupiter:5.2.0" } test { diff --git a/src/main/java/CarRaceApplication.java b/src/main/java/CarRaceApplication.java new file mode 100644 index 0000000..155557e --- /dev/null +++ b/src/main/java/CarRaceApplication.java @@ -0,0 +1,15 @@ +import application.CarRace; +import config.CarRaceConfig; +import io.ConsoleInputHandler; + +public class CarRaceApplication { + + private static final ConsoleInputHandler inputHandler = new ConsoleInputHandler(); + + public static void main(String[] args) { + CarRaceConfig config = inputHandler.userInputGameConfig(); + CarRace carRaceGame = CarRace.from(config); + carRaceGame.raceStart(); + } + +} diff --git a/src/main/java/application/CarRace.java b/src/main/java/application/CarRace.java new file mode 100644 index 0000000..583cc50 --- /dev/null +++ b/src/main/java/application/CarRace.java @@ -0,0 +1,65 @@ +package application; + +import config.CarRaceConfig; +import domain.car.CarFactory; +import domain.car.Cars; +import domain.result.Result; +import domain.result.Results; +import io.ConsoleOutputHandler; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import util.RandomUtil; + +public class CarRace { + + public static final int BOUND = 9; + private static final ConsoleOutputHandler outputHandler = new ConsoleOutputHandler(); + private final RandomUtil randomUtil = new RandomUtil(); + private final Cars cars; + private final int lap; + + + public static CarRace from(CarRaceConfig config) { + Cars cars = CarFactory.from(config.getCarNames()); + return new CarRace(cars, config.getLabCount()); + } + + private CarRace(Cars cars, int lap) { + this.cars = cars; + this.lap = lap; + } + + public void raceStart() { + Results raceResult = Results.from(IntStream.range(1, lap + 1) + .mapToObj(this::runOneLab) + .peek(outputHandler::printScreenCarRaceResult) + .collect(Collectors.toList())); + + outputHandler.printRaceResult(raceResult.getWinner()); + } + + + private Result runOneLab(int runningLab) { + return Result.of(runningLab, Cars.from( + cars.getCars() + .stream() + .peek(car -> car.moveForward(getRunOneLapMoveNumber())) + .collect(Collectors.toList()) + ) + ); + + } + + public Cars getCars() { + return cars; + } + + public int getLap() { + return lap; + } + + public int getRunOneLapMoveNumber() { + return randomUtil.getRandomNumber(BOUND); + } + +} diff --git a/src/main/java/config/CarRaceConfig.java b/src/main/java/config/CarRaceConfig.java new file mode 100644 index 0000000..2ff0a67 --- /dev/null +++ b/src/main/java/config/CarRaceConfig.java @@ -0,0 +1,24 @@ +package config; + +public class CarRaceConfig { + + private final String carNames; + private final int labCount; + + private CarRaceConfig(String carNames, int labCount) { + this.carNames = carNames; + this.labCount = labCount; + } + + public static CarRaceConfig of(String carNames, int labCount) { + return new CarRaceConfig(carNames, labCount); + } + + public String getCarNames() { + return carNames; + } + + public int getLabCount() { + return labCount; + } +} diff --git a/src/main/java/domain/car/Car.java b/src/main/java/domain/car/Car.java new file mode 100644 index 0000000..bb8ebd8 --- /dev/null +++ b/src/main/java/domain/car/Car.java @@ -0,0 +1,86 @@ +package domain.car; + +import exception.GameException; +import java.util.Objects; +import messages.ErrorMessage; + +public class Car { + + private static final int CAR_NAME_LIMIT_LENGTH = 5; + private static final int MOVE_FORWARD_THRESOLD = 4; + private final String carName; + private int position; + + public static Car from(String carName) { + return new Car(carName, 0); + } + + public static Car from(String carName, int position) { + return new Car(carName, position); + } + + public void moveForward(int moveThresold) { + if (canMove(moveThresold)) { + this.position++; + } + } + + private boolean canMove(int moveThresold) { + if (validateMoveThresold(moveThresold)) { + throw new GameException(ErrorMessage.invalidCarMoveThresold); + } + + return moveThresold >= MOVE_FORWARD_THRESOLD; + } + + private boolean validateMoveThresold(int moveThresold) { + return moveThresold < 0 || moveThresold > 9; + } + + public int getPosition() { + return position; + } + + public String getCarName() { + return carName; + } + + private Car(String carName, int position) { + if (doesCarNameLengthZeroOrGreaterThanLimit(carName)) { + throw new GameException(ErrorMessage.invalidCarName); + } + + this.carName = carName; + this.position = position; + } + + private static boolean doesCarNameLengthZeroOrGreaterThanLimit(String carName) { + return isNullOrEmpty(carName) || isGreaterThanLimit(carName); + } + + private static boolean isGreaterThanLimit(String carName) { + return carName.length() > CAR_NAME_LIMIT_LENGTH; + } + + private static boolean isNullOrEmpty(String carName) { + return carName == null || carName.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Car car = (Car) o; + return position == car.position && Objects.equals(carName, car.carName); + } + + @Override + public int hashCode() { + return Objects.hash(carName, position); + } + +} diff --git a/src/main/java/domain/car/CarFactory.java b/src/main/java/domain/car/CarFactory.java new file mode 100644 index 0000000..8fcd27a --- /dev/null +++ b/src/main/java/domain/car/CarFactory.java @@ -0,0 +1,36 @@ +package domain.car; + +import exception.GameException; +import java.util.List; +import java.util.stream.Collectors; +import messages.ErrorMessage; + +public class CarFactory { + + private final List carNames; + + public static Cars from(String carNames) { + return new CarFactory(carNames).createCars(); + + } + + private CarFactory(String carNames) { + if (carNames == null || carNames.isEmpty()) { + throw new GameException(ErrorMessage.invalidCarName); + } + + this.carNames = splitByComma(carNames); + } + + private List splitByComma(String carNameString) { + return List.of(carNameString.split(",")); + } + + private Cars createCars() { + return Cars.from(carNames.stream() + .map(String::trim) + .map(Car::from) + .collect(Collectors.toList())); + } + +} diff --git a/src/main/java/domain/car/Cars.java b/src/main/java/domain/car/Cars.java new file mode 100644 index 0000000..8b05a2e --- /dev/null +++ b/src/main/java/domain/car/Cars.java @@ -0,0 +1,39 @@ +package domain.car; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Cars { + + private final List cars; + + public static Cars from(List cars) { + return new Cars(cars); + } + + public List getCars() { + return new ArrayList<>(cars); + } + + private Cars(List cars) { + this.cars = cars; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Cars cars1 = (Cars) o; + return Objects.equals(cars, cars1.cars); + } + + @Override + public int hashCode() { + return Objects.hashCode(cars); + } +} diff --git a/src/main/java/domain/result/Result.java b/src/main/java/domain/result/Result.java new file mode 100644 index 0000000..3241234 --- /dev/null +++ b/src/main/java/domain/result/Result.java @@ -0,0 +1,47 @@ +package domain.result; + +import domain.car.Car; +import domain.car.Cars; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Result { + + private final int runningLap; + private final Cars cars; + + public static Result of(int runningLap, Cars cars) { + return new Result(runningLap, cars); + } + + public List getResult() { + return cars.getCars().stream() + .map(car -> car.getCarName() + " : " + convertPositionNumberToEmoji(car)) + .collect(Collectors.toList()); + } + + public int getRunningLap() { + return runningLap; + } + + public List getCars() { + return new ArrayList<>(cars.getCars()); + } + + private Result(int runningLap, Cars cars) { + this.runningLap = runningLap; + this.cars = cars; + } + + private String convertPositionNumberToEmoji(Car car) { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < car.getPosition(); i++) { + sb.append("-"); + } + + return String.valueOf(sb); + } + +} diff --git a/src/main/java/domain/result/Results.java b/src/main/java/domain/result/Results.java new file mode 100644 index 0000000..d5cd09a --- /dev/null +++ b/src/main/java/domain/result/Results.java @@ -0,0 +1,41 @@ +package domain.result; + +import domain.car.Car; +import exception.GameException; +import java.util.List; +import java.util.stream.Collectors; +import messages.ErrorMessage; + +public class Results { + + private final List results; + + public static Results from(List results) { + return new Results(results); + } + + public Winners getWinner() { + + List cars = getFinalScore().getCars(); + int maxPosition = cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElseThrow(() -> new GameException(ErrorMessage.CarsNotFound)); + + List winners = cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .collect(Collectors.toList()); + + return Winners.from(winners.stream().map(Car::getCarName).collect(Collectors.toList())); + } + + private Results(List results) { + this.results = results; + } + + private Result getFinalScore() { + return results.get(results.size() - 1); + } + + +} diff --git a/src/main/java/domain/result/Winners.java b/src/main/java/domain/result/Winners.java new file mode 100644 index 0000000..5009f6d --- /dev/null +++ b/src/main/java/domain/result/Winners.java @@ -0,0 +1,20 @@ +package domain.result; + +import java.util.List; + +public class Winners { + + private final List winners; + + public static Winners from(List winners) { + return new Winners(winners); + } + + public List getWinners() { + return winners; + } + + private Winners(List winners) { + this.winners = winners; + } +} diff --git a/src/main/java/exception/GameException.java b/src/main/java/exception/GameException.java new file mode 100644 index 0000000..fa979d6 --- /dev/null +++ b/src/main/java/exception/GameException.java @@ -0,0 +1,8 @@ +package exception; + +public class GameException extends RuntimeException { + + public GameException(String message) { + super(message); + } +} diff --git a/src/main/java/io/ConsoleInputHandler.java b/src/main/java/io/ConsoleInputHandler.java new file mode 100644 index 0000000..a2f7db7 --- /dev/null +++ b/src/main/java/io/ConsoleInputHandler.java @@ -0,0 +1,21 @@ +package io; + +import config.CarRaceConfig; +import java.util.Scanner; +import messages.GameMessage; + +public class ConsoleInputHandler { + + Scanner scanner = new Scanner(System.in); + + public CarRaceConfig userInputGameConfig() { + System.out.println(GameMessage.userInputCarNames); + String carNames = scanner.nextLine(); + + System.out.println(GameMessage.userInputFinalLabs); + int finalLab = scanner.nextInt(); + + return CarRaceConfig.of(carNames, finalLab); + } + +} diff --git a/src/main/java/io/ConsoleOutputHandler.java b/src/main/java/io/ConsoleOutputHandler.java new file mode 100644 index 0000000..e44d64f --- /dev/null +++ b/src/main/java/io/ConsoleOutputHandler.java @@ -0,0 +1,22 @@ +package io; + +import domain.result.Result; +import domain.result.Winners; +import messages.GameMessage; + +public class ConsoleOutputHandler { + + public void printScreenCarRaceResult(Result result) { + System.out.println("Lab: " + result.getRunningLap()); + result.getResult().forEach(System.out::println); + } + + public void printRaceResult(Winners winners) { + System.out.println(getWinnerNames(winners)); + } + + private String getWinnerNames(Winners winners) { + return String.join(", ", winners.getWinners()) + GameMessage.finalWinnerSuffix; + } + +} diff --git a/src/main/java/messages/ErrorMessage.java b/src/main/java/messages/ErrorMessage.java new file mode 100644 index 0000000..563376c --- /dev/null +++ b/src/main/java/messages/ErrorMessage.java @@ -0,0 +1,9 @@ +package messages; + +public class ErrorMessage { + + public static final String invalidCarName = "자동차 이름은 비어있거나 5글자를 초과 할 수 없습니다."; + public static final String invalidCarMoveThresold = "자동차를 전진 시키기 위해 0과 9사이의 숫자만 입력 해야합니다."; + public static final String CarsNotFound = "자동차 목록을 찾을 수 없습니다."; + +} diff --git a/src/main/java/messages/GameMessage.java b/src/main/java/messages/GameMessage.java new file mode 100644 index 0000000..40da5ac --- /dev/null +++ b/src/main/java/messages/GameMessage.java @@ -0,0 +1,9 @@ +package messages; + +public class GameMessage { + + public static final String userInputCarNames = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."; + public static final String userInputFinalLabs = "시도할 회수는 몇회인가요?"; + public static final String finalWinnerSuffix = "가 최종 우승했습니다."; + +} diff --git a/src/main/java/util/RandomUtil.java b/src/main/java/util/RandomUtil.java new file mode 100644 index 0000000..fa6a06d --- /dev/null +++ b/src/main/java/util/RandomUtil.java @@ -0,0 +1,14 @@ +package util; + +import java.util.Random; + +public class RandomUtil { + + private final Random randomGenerator = new Random(); + + public int getRandomNumber(int bound) { + return randomGenerator.nextInt(bound); + } + + +} diff --git a/src/test/java/CarRaceTest.java b/src/test/java/CarRaceTest.java new file mode 100644 index 0000000..0d60323 --- /dev/null +++ b/src/test/java/CarRaceTest.java @@ -0,0 +1,40 @@ +import static org.assertj.core.api.Assertions.*; + +import application.CarRace; +import config.CarRaceConfig; +import domain.car.CarFactory; +import domain.car.Cars; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class CarRaceTest { + + static CarRace carRace; + + @BeforeAll + public static void setUp() { + carRace = CarRace.from(CarRaceConfig.of("test1, test2", 2)); + } + + @Test + @DisplayName("유저가 입력한 자동차 이름을 객체로 반환한다") + void test_유저_입력_자동차_생성() { + Cars cars = CarFactory.from("test1, test2"); + assertThat(carRace.getCars()).isEqualTo(cars); + assertThat(carRace.getLap()).isEqualTo(2); + + } + + + @Test + @DisplayName("자동차가 움직여야 하는 숫자를 반환한다") + void test_자동차_전진할_숫자_반환() { + CarRace mockCarRace = Mockito.mock(carRace.getClass()); + Mockito.when(mockCarRace.getRunOneLapMoveNumber()).thenReturn(4); + + assertThat(mockCarRace.getRunOneLapMoveNumber()).isEqualTo(4); + } + +} diff --git a/src/test/java/config/CarRaceConfigTest.java b/src/test/java/config/CarRaceConfigTest.java new file mode 100644 index 0000000..1c7bf13 --- /dev/null +++ b/src/test/java/config/CarRaceConfigTest.java @@ -0,0 +1,23 @@ +package config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CarRaceConfigTest { + + @Test + @DisplayName("사용자가 자동차 이름을 입력 한 값을 상태로 관리한다 ") + void test_게임_설정값_차_이름_검증() { + CarRaceConfig carRaceConfig = CarRaceConfig.of("a,b", 5); + assertThat(carRaceConfig.getCarNames()).isEqualTo("a,b"); + } + + @Test + @DisplayName("사용자가 자동차 이름을 입력 한 값을 상태로 관리한다 ") + void test_게임_설정값_진행_횟수_검증() { + CarRaceConfig carRaceConfig = CarRaceConfig.of("a,b", 5); + assertThat(carRaceConfig.getLabCount()).isEqualTo(5); + } +} diff --git a/src/test/java/domain/car/CarFactoryTest.java b/src/test/java/domain/car/CarFactoryTest.java new file mode 100644 index 0000000..9c89744 --- /dev/null +++ b/src/test/java/domain/car/CarFactoryTest.java @@ -0,0 +1,19 @@ +package domain.car; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CarFactoryTest { + + @Test + @DisplayName("자동차 이름을 ','로 구분하여 입력 하면 자동차 객체의 배열로 반환한다") + void test_자동차_이름_분리() { + String given = "a, b"; + Cars fixture = Cars.from(List.of(Car.from("a"), Car.from("b"))); + assertThat(CarFactory.from(given)).isEqualTo(fixture); + } + +} diff --git a/src/test/java/domain/car/CarTest.java b/src/test/java/domain/car/CarTest.java new file mode 100644 index 0000000..52abfbc --- /dev/null +++ b/src/test/java/domain/car/CarTest.java @@ -0,0 +1,59 @@ +package domain.car; + +import static org.assertj.core.api.Assertions.*; + +import exception.GameException; +import messages.ErrorMessage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +public class CarTest { + + @ParameterizedTest + @DisplayName("자동차 이름은 5글자를 초과 할 수 없다") + @NullAndEmptySource + @ValueSource(strings = {"testabc"}) + void test_자동차_단일_객체_생성(String given) { + + assertThatThrownBy(() -> { + Car.from(given); + }) + .isInstanceOf(GameException.class) + .hasMessage(ErrorMessage.invalidCarName); + } + + @ParameterizedTest + @DisplayName("자동차가 전진 하기 위해 0과 9의 숫자만 입력한다") + @ValueSource(ints = {-1, 10}) + void test_자동차_전진_메서드_인자_검증(int move) { + + Car car = Car.from("test"); + assertThatThrownBy(() -> { + car.moveForward(move); + }) + .isInstanceOf(GameException.class) + .hasMessage(ErrorMessage.invalidCarMoveThresold); + } + + @ParameterizedTest + @DisplayName("자동차는 4 이상 나올 시 앞으로 한 칸 전진한다") + @CsvSource(value = {"0,0", "3,0", "4,1", "9,1"}) + void test_자동차_전진_메서드_검증(int move, int position) { + Car car = Car.from("test"); + car.moveForward(move); + assertThat(car.getPosition()).isEqualTo(position); + } + + @Test + @DisplayName("자동차는 전진 하면 객체의 위치 값이 1 증가한다") + void test_자동차_전진_후_위치값_검증() { + Car car = Car.from("test"); + assertThat(car.getPosition()).isEqualTo(0); + car.moveForward(4); + assertThat(car.getPosition()).isEqualTo(1); + } +}