diff --git a/README.md b/README.md index 491aece1..301ad6d5 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# java-racingcar-precourse \ No newline at end of file +# java-racingcar-precourse + +## 기능 요구 사항 +초간단 자동차 경주 게임을 구현한다. + +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. + - 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. +- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. +- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. + +## 자동차 경주 기능 목록 +- [ ] 게임을 시작하면, 경주할 자동차를 생성할 수 있어야 한다. + - [ ] 자동차는 쉼표(,)를 기준으로 구분하여 생성된다. + - [ ] 자동차 이름은 5자 이하만 가능하다. + - [ ] 자동차 이름은 중복으로 사용될 수 없다. + - [ ] 자동차경주를 하기위해서는, 자동차는 최소 2대 이상 생성해야한다. + + +- [ ] 사용자는 몇 번의 이동을 할 것인지 입력할 수 있다. + - [ ] 이동할 횟수는 0이상의 '자연수'이어야 한다. + + +- [ ] 자동차는 전진 또는 멈출 수 있다. + - [ ] 0에서 9사이에서 무작위한 값이 4이상인 경우 전진한다. + - [ ] 0에서 9사이의 무작위한 값이 3이하인 경우 멈춘다. + + +- [ ] 자동차 경주를 확인할 수 있다. + - [ ] 경주하는 자동차 이름을 확인할 수 있다. + - [ ] 라운드마다 자동차가 전진했다면 `-`로 이동 거리를 출력한다. + + +- [ ] 자동차 경주 게임의 우승자를 확인할 수 있다. + - [ ] 단독 우승자의 경우 우승한 자동차를 출력한다. + - [ ] 공동 우승자의 경우 우승한 자동차 이름을 `, `로 구분지어서 출력한다. diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..43155e90 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,9 @@ +import controller.RacingGameController; + +public class Application { + public static void main(String[] args) { + RacingGameController racingGameController = new RacingGameController(); + racingGameController.startGame(); + } +} + diff --git a/src/main/java/controller/RacingGameController.java b/src/main/java/controller/RacingGameController.java new file mode 100644 index 00000000..0fcac56e --- /dev/null +++ b/src/main/java/controller/RacingGameController.java @@ -0,0 +1,43 @@ +package controller; + +import domain.RacingCars; +import domain.TrialNum; +import util.CreatingCar; +import util.RandomGeneratingMove; +import view.InputView; +import view.OutputView; + +public class RacingGameController { + private InputView inputView = new InputView(); + private OutputView outputView = new OutputView(); + private final RandomGeneratingMove randomGeneratingMove = new RandomGeneratingMove(); + public void startGame(){ + RacingCars racingCars = createRacingCars(); + TrialNum trialNum = getTryCount(); + + racing(racingCars, trialNum); + } + + private RacingCars createRacingCars(){ + outputView.printRequestCarNames(); + String[] carNames = inputView.inputCarNames(); + return CreatingCar.createCars(carNames); + } + + private TrialNum getTryCount(){ + outputView.printRequestTryCount(); + String inputTryCount = inputView.inputTryCount(); + return new TrialNum(inputTryCount); + } + + private void racing(RacingCars cars, TrialNum trialNum) { + outputView.printExecutionResult(); + + for (int i = 0; i < trialNum.getTrialNum(); i++) { + cars.moveAll(randomGeneratingMove); + outputView.printRoundByExecutionResults(cars); + } + outputView.printRacingCarWinnerResult(cars); + } + +} diff --git a/src/main/java/domain/Distance.java b/src/main/java/domain/Distance.java new file mode 100644 index 00000000..c1d28916 --- /dev/null +++ b/src/main/java/domain/Distance.java @@ -0,0 +1,9 @@ +package domain; + +public record Distance(int location) { + private static final int MOVE_VALUE = 1; + + public Distance increase(){ + return new Distance(location + MOVE_VALUE); + } +} diff --git a/src/main/java/domain/RacingCar.java b/src/main/java/domain/RacingCar.java new file mode 100644 index 00000000..e91d90e5 --- /dev/null +++ b/src/main/java/domain/RacingCar.java @@ -0,0 +1,36 @@ +package domain; + +public class RacingCar implements Comparable { + private static final int MOVE_CONDITION = 4; + + private RacingCarName racingCarName; + private Distance distance; + + public RacingCar(final String name, final int distance) { + this.racingCarName = new RacingCarName(name); + this.distance = new Distance(distance); + } + + public void move(final int num){ + if(num >= MOVE_CONDITION){ + this.distance = distance.increase(); + } + } + + public String getRacingCarName() { + return racingCarName.carName(); + } + + public Integer getDistance() { + return distance.location(); + } + + public boolean isSameDistance(RacingCar competeCar) { + return this.distance.location() == competeCar.distance.location(); + } + + @Override + public int compareTo(RacingCar competeCar) { + return this.distance.location() - competeCar.distance.location(); + } +} diff --git a/src/main/java/domain/RacingCarName.java b/src/main/java/domain/RacingCarName.java new file mode 100644 index 00000000..a5c21031 --- /dev/null +++ b/src/main/java/domain/RacingCarName.java @@ -0,0 +1,15 @@ +package domain; + +public record RacingCarName(String carName){ + private static final int CAR_NAME_LENGTH_MAX = 5; + + public RacingCarName { + isCarNameLengthValidate(carName); + } + + private void isCarNameLengthValidate(final String carName){ + if (carName.isEmpty() || carName.length() > CAR_NAME_LENGTH_MAX){ + throw new IllegalArgumentException("자동차이름은 1자리이상 5자 이하만 가능합니다."); + } + } +} diff --git a/src/main/java/domain/RacingCars.java b/src/main/java/domain/RacingCars.java new file mode 100644 index 00000000..8199fe87 --- /dev/null +++ b/src/main/java/domain/RacingCars.java @@ -0,0 +1,66 @@ +package domain; + +import util.RandomGeneratingMove; + +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class RacingCars { + private static final int MINIMUM_CAR_COUNT = 2; + private final List cars; + + public RacingCars(List cars) { + isDifferentCarName(cars); + minimumNumofCars(cars.size()); + this.cars = cars; + } + + public void moveAll(final RandomGeneratingMove randomGeneratingMove) { + for (final RacingCar car : cars) { + int engine = randomGeneratingMove.generateRandomInt(); + car.move(engine); + } + } + + private void isDifferentCarName(List cars) { + List carName = cars.stream() + .map(idx -> idx.getRacingCarName()) + .collect(Collectors.toList()); + + HashSet carNameDuplicateSet = new HashSet<>(carName); + + if (carNameDuplicateSet.size() != carName.size()) { + throw new IllegalArgumentException("자동차 이름을 중복으로 사용할 수 없습니다."); + } + } + + private void minimumNumofCars(int size) { + if (size < MINIMUM_CAR_COUNT) { + throw new IllegalArgumentException("자동차 경주를 위해서는 최소 2대 이상의 자동차를 만들어야 합니다."); + } + } + + public List getCars() { + return cars; + } + + public List findWinners(){ + RacingCar maxDistanceCar = findMaxDistanceCar(); + List winnerCarNameList = findSameDistanceList(maxDistanceCar); + return winnerCarNameList; + } + + private RacingCar findMaxDistanceCar() { + return cars.stream() + .max(RacingCar::compareTo) + .orElseThrow(() -> new IllegalArgumentException("차량 리스트가 비었습니다.")); + } + + private List findSameDistanceList(RacingCar maxDistanceCar) { + return cars.stream() + .filter(maxDistanceCar::isSameDistance) + .map(RacingCar::getRacingCarName) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/domain/TrialNum.java b/src/main/java/domain/TrialNum.java new file mode 100644 index 00000000..e5afa08b --- /dev/null +++ b/src/main/java/domain/TrialNum.java @@ -0,0 +1,25 @@ +package domain; + +public class TrialNum { + + private final int trialNum; + + public TrialNum(final String trialNum) { + isNaturalNumberValidate(trialNum); + this.trialNum = convertStringToInt(trialNum); + } + + private void isNaturalNumberValidate(String trialNum) { + if (!trialNum.matches("^[1-9]\\d*$")){ + throw new IllegalArgumentException("이동할 횟수는 자연수여야 한다."); + } + } + + private int convertStringToInt(String input) { + return Integer.parseInt(input); + } + + public int getTrialNum() { + return trialNum; + } +} diff --git a/src/main/java/util/CreatingCar.java b/src/main/java/util/CreatingCar.java new file mode 100644 index 00000000..9046961f --- /dev/null +++ b/src/main/java/util/CreatingCar.java @@ -0,0 +1,26 @@ +package util; + +import domain.RacingCar; +import domain.RacingCars; +import java.util.stream.Collectors; +import java.util.Arrays; +import java.util.List; + +public class CreatingCar { + private static final int START_LOCATION = 0; + + public static RacingCars createCars(final String[] carNames) { + List cars = makeCars(carNames); + return new RacingCars(cars); + } + + private static List makeCars(final String[] carNames) { + return Arrays.stream(carNames) + .map(CreatingCar::makeCar) + .collect(Collectors.toList()); + } + + private static RacingCar makeCar(final String carName) { + return new RacingCar(carName, START_LOCATION); + } +} diff --git a/src/main/java/util/RandomGeneratingMove.java b/src/main/java/util/RandomGeneratingMove.java new file mode 100644 index 00000000..833d29ac --- /dev/null +++ b/src/main/java/util/RandomGeneratingMove.java @@ -0,0 +1,20 @@ +package util; + +import java.util.Random; + +public class RandomGeneratingMove implements RandomGenerator { + + private static final int MIN_RANDOM_NUMBER = 0; + private static final int MAX_RANDOM_NUMBER = 9; + private Random random; + + public RandomGeneratingMove() { + this.random = new Random(); + } + + @Override + public int generateRandomInt() { + return random.nextInt((MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER + 1)) + MIN_RANDOM_NUMBER; + } +} + diff --git a/src/main/java/util/RandomGenerator.java b/src/main/java/util/RandomGenerator.java new file mode 100644 index 00000000..034f39d9 --- /dev/null +++ b/src/main/java/util/RandomGenerator.java @@ -0,0 +1,5 @@ +package util; + +public interface RandomGenerator { + int generateRandomInt(); +} diff --git a/src/main/java/util/SplitingCarName.java b/src/main/java/util/SplitingCarName.java new file mode 100644 index 00000000..8d4b25bd --- /dev/null +++ b/src/main/java/util/SplitingCarName.java @@ -0,0 +1,9 @@ +package util; + +public class SplitingCarName { + private static final String STRING_SPLITTER = ","; + + public static String[] splitCarName(String carNames) { + return carNames.split(STRING_SPLITTER); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..a1eec378 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,22 @@ +package view; + +import util.SplitingCarName; + +import java.util.Scanner; + +public class InputView { + private Scanner scanner; + + public InputView() { + this.scanner = new Scanner(System.in); + } + + public String[] inputCarNames(){ + String carNames = scanner.nextLine(); + return SplitingCarName.splitCarName(carNames); + } + + public String inputTryCount(){ + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..95b0a6a6 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,60 @@ +package view; + +import domain.RacingCar; +import domain.RacingCars; + +import java.util.List; + +public class OutputView { + + private static final String INPUT_CAR_NAMES = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String INPUT_TRY_COUNT = "시도할 회수는 몇회인가요?"; + private static final String NEW_LINE = "\n"; + private static final String EXECUTION_RESULT = "실행 결과"; + private static final String COLON_SIGN = " : "; + private static final String MOVE_SIGN = "-"; + private static final String FINAL_WINNER = "최종 우승자"; + private static final String COMMA_SIGN = ", "; + + public void printRequestCarNames(){ + System.out.println(INPUT_CAR_NAMES); + } + + public void printRequestTryCount(){ + System.out.println(INPUT_TRY_COUNT); + } + + public void printExecutionResult(){ + System.out.println(NEW_LINE + EXECUTION_RESULT); + } + + public void printRoundByExecutionResults(final RacingCars cars){ + StringBuilder resultByRound = new StringBuilder(); + for (RacingCar car : cars.getCars()) { + resultByRound.append(car.getRacingCarName()) + .append(COLON_SIGN) + .append(MOVE_SIGN.repeat(car.getDistance())) + .append(NEW_LINE); + } + System.out.println(resultByRound); + } + + public void printRacingCarWinnerResult(final RacingCars cars){ + List winners = cars.findWinners(); + StringBuilder winnerRacingCar = new StringBuilder(); + winnerRacingCar.append(FINAL_WINNER) + .append(COLON_SIGN); + + for (int i = 0; i < winners.size(); i++) { + winnerRacingCar.append(winners.get(i)); + if (isJointWinner(i, winners)){ + winnerRacingCar.append(COMMA_SIGN); + } + } + System.out.println(winnerRacingCar); + } + + private boolean isJointWinner(int idx, List winners) { + return idx < (winners.size() - 1); + } +} diff --git a/src/test/java/domain/DistanceTest.java b/src/test/java/domain/DistanceTest.java new file mode 100644 index 00000000..93144d6c --- /dev/null +++ b/src/test/java/domain/DistanceTest.java @@ -0,0 +1,22 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DistanceTest { + @DisplayName("자동차가 전진했다면, 위치를 변경(1증가)") + @Test + void carMoveSuccess() throws Exception{ + //given + int startLocation = 0; + Distance init = new Distance(startLocation); + + //when + Distance move = init.increase(); + + //then + assertThat(move.location()).isEqualTo(1); + } +} diff --git a/src/test/java/domain/RacingCarNameTest.java b/src/test/java/domain/RacingCarNameTest.java new file mode 100644 index 00000000..7109933e --- /dev/null +++ b/src/test/java/domain/RacingCarNameTest.java @@ -0,0 +1,36 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class RacingCarNameTest { + + @ParameterizedTest + @ValueSource(strings = {"yoni", "ini", "nini", "zzang"}) + @DisplayName("racing car 생성 test") + void test_name_constructor_success(String input) { + // when + RacingCarName racingCarName = new RacingCarName(input); + + // then + assertThat(racingCarName.carName()).isEqualTo(input); + } + + @ParameterizedTest + @ValueSource(strings = {"", "avante", "sonata"}) + @DisplayName("경주할 자동차 이름은 유효한 범위(1이상 5이하)를 가져야한다. ") + void setRacingCarNameLengthCorrect(String carName) { + // given + String expectedMessage = "자동차이름은 1자리이상 5자 이하만 가능합니다."; + + // when & then + assertThatThrownBy(() -> new RacingCarName(carName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(expectedMessage); + } +} diff --git a/src/test/java/domain/RacingCarTest.java b/src/test/java/domain/RacingCarTest.java new file mode 100644 index 00000000..278d2276 --- /dev/null +++ b/src/test/java/domain/RacingCarTest.java @@ -0,0 +1,85 @@ +package domain; + +import org.assertj.core.api.Assertions; +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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class RacingCarTest { + @DisplayName("자동차는 전진 또는 멈출 수 있다.") + @ParameterizedTest + @CsvSource({"4,1", "5,1", "6,1", + "0,0", "1,0", "3,0"}) + void carMoveOrStopByRandomNumber(int givenPower, int curLocation) { + // given + int startLocation = 0; + RacingCar car = createCar("ini", startLocation); + + // when + car.move(givenPower); + + // then + assertThat(car.getDistance()).isEqualTo(curLocation); + } + + @DisplayName("같은 위치의 자동차가 존재하는지 확인할 수 있다.") + @ParameterizedTest + @CsvSource({"2,2,true", "2,1,false"}) + void isSameDistanceOfCar(int myCarLocation, + int competeCarLocation, + boolean sameDistanceCheck) throws Exception { + //given + RacingCar myCar = createCar("ini", myCarLocation); + RacingCar competeCar = createCar("yoni", competeCarLocation); + + //when + boolean sameDistance = myCar.isSameDistance(competeCar); + + //then + assertThat(sameDistance).isEqualTo(sameDistanceCheck); + } + + @DisplayName("전진한 자동차들을 비교할 수 있다.") + @Test + void compareDistanceToCar() throws Exception { + + //given + String myCarName = "ini"; + String competeCarName = "yoni"; + RacingCar myCar = createCar(myCarName, 4); + RacingCar competeCar = createCar(competeCarName, 2); + + //when + int compare = myCar.compareTo(competeCar); + + //then + assertThat(compare).isGreaterThan(0); + } + + @DisplayName("자동차 경주 게임의 우승자를 확인할 수 있다.") + @Test + void findWinners() throws Exception { + //given + RacingCar myCar = createCar("ini", 3); + RacingCar competeCar1 = createCar("yoni", 2); + RacingCar competeCar2 = createCar("yuni", 3); + RacingCars cars = new RacingCars(List.of(myCar, competeCar1, competeCar2)); + + //when + List winners = cars.findWinners(); + + //then + Assertions.assertThat(winners).hasSize(2) + .containsExactly("ini", "yuni"); + } + + private static RacingCar createCar(String carName, int location) { + return new RacingCar(carName, location); + } +} diff --git a/src/test/java/domain/RacingCarsTest.java b/src/test/java/domain/RacingCarsTest.java new file mode 100644 index 00000000..7ce81e0e --- /dev/null +++ b/src/test/java/domain/RacingCarsTest.java @@ -0,0 +1,37 @@ +package domain; + +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +public class RacingCarsTest { + @DisplayName("자동차 이름을 중복으로 사용할 수 없습니다.") + @Test + void isDifferentCarName() throws Exception{ + //given + String racingCarName = "yoni"; + RacingCar car = new RacingCar(racingCarName, 1); + + List cars = List.of(car, car); + + //when //then + Assertions.assertThatThrownBy(() -> new RacingCars(cars)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름을 중복으로 사용할 수 없습니다."); + } + + @DisplayName("자동차 경주를 위해서는 최소 2대 이상의 자동차를 만들어야 합니다.") + @Test + void minimumCountOfCars() throws Exception{ + //given + String racingCarName = "ini"; + RacingCar car = new RacingCar(racingCarName, 1); + + List cars = List.of(car); + + //when //then + Assertions.assertThatThrownBy(() -> new RacingCars(cars)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 경주를 위해서는 최소 2대 이상의 자동차를 만들어야 합니다."); + } +} diff --git a/src/test/java/domain/TrialNumTest.java b/src/test/java/domain/TrialNumTest.java new file mode 100644 index 00000000..b29a9bcb --- /dev/null +++ b/src/test/java/domain/TrialNumTest.java @@ -0,0 +1,29 @@ +package domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class TrialNumTest { + @DisplayName("사용자는 몇 번의 이동을 할 것인지 입력할 수 있다.") + @Test + void inputUserTryCount() throws Exception{ + + String userInput = "5"; + TrialNum tryCount = new TrialNum(userInput); + + Assertions.assertThat(tryCount.getTrialNum()).isEqualTo(5); + } + + @DisplayName("이동할 횟수는 자연수여야 한다.") + @ParameterizedTest + @ValueSource(strings = {"0","ㄷㄷ","!@","-1","은"}) + void isNaturalNumberValidate(String input) throws Exception{ + + Assertions.assertThatThrownBy(() -> new TrialNum(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 횟수는 자연수여야 한다."); + } +} diff --git a/src/test/java/util/SplitingCarNameTest.java b/src/test/java/util/SplitingCarNameTest.java new file mode 100644 index 00000000..c0f7e5e6 --- /dev/null +++ b/src/test/java/util/SplitingCarNameTest.java @@ -0,0 +1,20 @@ +package util; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class SplitingCarNameTest { + @DisplayName("자동차는 쉼표(,)를 기준으로 구분하여 생성된다.") + @Test + void splitCarName() throws Exception{ + //given + String playerInput = "ini,yoni,nini,bye"; + SplitingCarName splitingCarName = new SplitingCarName(); + String[] strings = splitingCarName.splitCarName(playerInput); + + //when //then + assertThat(strings).hasSize(4) + .containsExactly(new String[]{"ini","yoni","nini","bye"}); + } +}