diff --git a/README.md b/README.md index e71d7759f7..3069e0a6bc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,42 @@ 자동차 경주 미션 저장소 +## 기능 목록 정리 +- ### 입력받는 기능 + - [x] 자동차 이름을 입력받는 기능 + - 예외처리 + - [x] 쉼표를 기준으로 구분하여 입력을 받아야 한다. + - [x] 자동차 이름에는 중복을 허용하지 않는다. + - [x] 자동차 이동 시도횟수를 입력받는 기능 + - 예외처리 + - [x] 시도 횟수가 숫자인지 검증 + - [x] 최대 시도 횟수를 1~100회로 제한 +- ### 출력하는 기능 + - [x] 실행결과 문구를 출력하는 기능 + - [x] 자동차별 이동 횟수를 출력하는 기능 + - [x] 최종 우승자를 출력하는 기능 + +- ### 메인 기능 + - 자동차 경주 게임 + - [x] 전체 자동차가 경주하는 기능 + + - 전체 자동차 + - 예외처리 + - [x] 전체 자동차 이름에 중복이 있으면 예외처리 + - [x] 전체 자동차를 2~50대로 제한 + + - 자동차 + - [x] 자동차는 이름을 가진다. + - [x] 랜덤 값을 받아 전진 혹은 위치 유지 + - 예외처리 + - [x] 자동차의 이름은 1~5자만 가능하다. + + - 경주 결과 + - [x] 최종 우승 자동차 + - [x] 경주 기록 결과 + + - [x] 0~9사이의 랜덤 값을 생성하는 기능 + ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) diff --git a/src/main/java/racing/Application.java b/src/main/java/racing/Application.java new file mode 100644 index 0000000000..72a5cbfc80 --- /dev/null +++ b/src/main/java/racing/Application.java @@ -0,0 +1,13 @@ +package racing; + +import java.io.IOException; + +import racing.controller.RacingGameController; + +public class Application { + + public static void main(String[] args) throws IOException { + RacingGameController racingGameController = new RacingGameController(); + racingGameController.run(); + } +} diff --git a/src/main/java/racing/controller/RacingGameController.java b/src/main/java/racing/controller/RacingGameController.java new file mode 100644 index 0000000000..432d1efab5 --- /dev/null +++ b/src/main/java/racing/controller/RacingGameController.java @@ -0,0 +1,38 @@ +package racing.controller; + +import java.io.IOException; + +import racing.domain.RacingGame; +import racing.handler.InputHandler; +import racing.view.OutputView; + +public class RacingGameController { + + private final InputHandler inputHandler; + private final OutputView outputView; + + public RacingGameController() { + this.inputHandler = new InputHandler(); + this.outputView = new OutputView(); + } + + public void run() throws IOException { + String[] carNames = inputHandler.readCars(); + int movingTrial = inputHandler.readMovingTrial(); + + RacingGame racingGame = new RacingGame(carNames); + + outputView.printNotice(); + raceWithHistory(movingTrial, racingGame); + outputView.printWinner(racingGame.produceRacingResult().pickWinner()); + } + + private void raceWithHistory(int movingTrial, RacingGame racingGame) { + //TODO: 인덱스를 쓰지 않는데 개선할 방법 + for (int i = 0; i < movingTrial; i++) { + racingGame.race(); + + outputView.printRacingResult(racingGame.produceRacingResult().getHistory()); + } + } +} diff --git a/src/main/java/racing/domain/Car.java b/src/main/java/racing/domain/Car.java new file mode 100644 index 0000000000..71bd643dd7 --- /dev/null +++ b/src/main/java/racing/domain/Car.java @@ -0,0 +1,38 @@ +package racing.domain; + +public class Car { + + private static final int INITIAL_VALUE = 0; + private static final int MINIMUM_LENGTH_OF_CAR_NAME = 1; + private static final int MAXIMUM_LENGTH_OF_CAR_NAME = 5; + private static final String LENGTH_OF_CAR_NAME_ERROR = "[ERROR] 자동차이름의 길이는 1-5자까지 가능합니다."; + + private final String name; + private int position; + + public Car(String name) { + validateLengthOfName(name); + this.name = name; + this.position = INITIAL_VALUE; + } + + public void move(boolean isMovable) { + if (isMovable) { + this.position++; + } + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + private void validateLengthOfName(String name) { + if (name.length() < MINIMUM_LENGTH_OF_CAR_NAME || name.length() > MAXIMUM_LENGTH_OF_CAR_NAME) { + throw new IllegalArgumentException(LENGTH_OF_CAR_NAME_ERROR); + } + } +} diff --git a/src/main/java/racing/domain/CarGroup.java b/src/main/java/racing/domain/CarGroup.java new file mode 100644 index 0000000000..f369170a5d --- /dev/null +++ b/src/main/java/racing/domain/CarGroup.java @@ -0,0 +1,50 @@ +package racing.domain; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +//TODO: ERROR 패키지 분리 +public class CarGroup { + + private static final String DUPLICATED_CAR_NAME_ERROR = "[ERROR] 자동차 이름에는 중복이 허용되지 않습니다."; + private static final String RANGE_OF_CAR_GROUP_ERROR = "[ERROR] 자동차 대수는 2-50대 사이입니다."; + private static final int MINIMUM_NUMBER_OF_CARS = 2; + private static final int MAXIMUM_NUMBER_OF_CARS = 50; + + private final List cars; + + public CarGroup(String[] names){ + validateDuplicatedName(names); + validateNumberOfCars(names); + this.cars = setUp(names); + } + + public void race(boolean isMovable) { + for (Car car : cars) { + car.move(isMovable); + } + } + + public List getCars() { + return cars; + } + + private List setUp(String[] names) { + return Arrays.stream(names) + .map(Car::new) + .collect(Collectors.toList()); + } + + private void validateDuplicatedName(String[] names) { + if (names.length != Arrays.stream(names).distinct().count()) { + throw new IllegalArgumentException(DUPLICATED_CAR_NAME_ERROR); + } + } + + private void validateNumberOfCars(String[] names) { + if (names.length < MINIMUM_NUMBER_OF_CARS || names.length > MAXIMUM_NUMBER_OF_CARS){ + throw new IllegalArgumentException(RANGE_OF_CAR_GROUP_ERROR); + } + } +} diff --git a/src/main/java/racing/domain/RacingGame.java b/src/main/java/racing/domain/RacingGame.java new file mode 100644 index 0000000000..da7163c8a1 --- /dev/null +++ b/src/main/java/racing/domain/RacingGame.java @@ -0,0 +1,35 @@ +package racing.domain; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class RacingGame { + + private static final int MOVABLE_CONDITION = 4; + + private final CarGroup carGroup; + private final RandomNumberGenerator numberGenerator; + + public RacingGame(String[] names) { + this.carGroup = new CarGroup(names); + this.numberGenerator = new RandomNumberGenerator(); + } + + //TODO: 테스트 + public void race() { + carGroup.race(isMovable()); + } + + public RacingResult produceRacingResult() { + Map history = new LinkedHashMap<>(); + for (Car car : carGroup.getCars()) { + history.put(car.getName(), car.getPosition()); + } + + return new RacingResult(history); + } + + private boolean isMovable() { + return (numberGenerator.generate() >= MOVABLE_CONDITION); + } +} diff --git a/src/main/java/racing/domain/RacingResult.java b/src/main/java/racing/domain/RacingResult.java new file mode 100644 index 0000000000..bd75845e3c --- /dev/null +++ b/src/main/java/racing/domain/RacingResult.java @@ -0,0 +1,29 @@ +package racing.domain; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class RacingResult { + + private final Map history; + + public RacingResult(Map history) { + this.history = history; + } + + public Map getHistory() { + return history; + } + + public List pickWinner() { + Integer maxValue = Collections.max(history.values()); + + return history.entrySet() + .stream() + .filter(entry -> entry.getValue().equals(maxValue)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/racing/domain/RandomNumberGenerator.java b/src/main/java/racing/domain/RandomNumberGenerator.java new file mode 100644 index 0000000000..44b20a6239 --- /dev/null +++ b/src/main/java/racing/domain/RandomNumberGenerator.java @@ -0,0 +1,8 @@ +package racing.domain; + +public class RandomNumberGenerator { + + public int generate(){ + return (int)(Math.random() * 10); + } +} diff --git a/src/main/java/racing/handler/InputHandler.java b/src/main/java/racing/handler/InputHandler.java new file mode 100644 index 0000000000..8628a33144 --- /dev/null +++ b/src/main/java/racing/handler/InputHandler.java @@ -0,0 +1,60 @@ +package racing.handler; + +import java.io.IOException; +import java.util.regex.Pattern; + +import racing.view.InputView; + +public class InputHandler { + + private static final String COMMA = ","; + private static final Pattern REGEX = Pattern.compile("^[0-9]+$"); + private static final int MINIMUM_LENGTH_OF_MOVING_TRIAL = 1; + private static final int MAXIMUM_LENGTH_OF_MOVING_TRIAL = 100; + private static final String MOVING_TRIAL_NOT_INTEGER_ERROR = "[ERROR] 시도할 횟수는 숫자만 가능합니다."; + private static final String MOVING_TRIAL_RANGE_ERROR = "[ERROR] 시도할 횟수의 범위는 1이상 100이하만 가능합니다."; + + private final InputView inputView; + + public InputHandler() { + this.inputView = new InputView(); + } + + public String[] readCars() throws IOException { + try { + String inputName = inputView.readCarNames(); + String[] names = inputName.split(COMMA); + + return names; + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return readCars(); + } + } + + public int readMovingTrial() throws IOException { + try { + String input = inputView.readMovingTrial(); + validateInteger(input); + int movingTrial = Integer.parseInt(input); + validateTrialRange(movingTrial); + + return movingTrial; + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return readMovingTrial(); + } + } + + private void validateTrialRange(int movingTrial) { + if (movingTrial < MINIMUM_LENGTH_OF_MOVING_TRIAL || movingTrial > MAXIMUM_LENGTH_OF_MOVING_TRIAL) { + throw new IllegalArgumentException(MOVING_TRIAL_RANGE_ERROR); + } + } + + private void validateInteger(String movingTrial) { + if (!REGEX.matcher(movingTrial).matches()) { + throw new IllegalArgumentException(MOVING_TRIAL_NOT_INTEGER_ERROR); + } + } +} diff --git a/src/main/java/racing/util/InputUtil.java b/src/main/java/racing/util/InputUtil.java new file mode 100644 index 0000000000..dcc6cb81f5 --- /dev/null +++ b/src/main/java/racing/util/InputUtil.java @@ -0,0 +1,14 @@ +package racing.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class InputUtil { + + public static String readLine() throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); + + return bufferedReader.readLine(); + } +} diff --git a/src/main/java/racing/view/InputView.java b/src/main/java/racing/view/InputView.java new file mode 100644 index 0000000000..650d8d3b13 --- /dev/null +++ b/src/main/java/racing/view/InputView.java @@ -0,0 +1,23 @@ +package racing.view; + +import java.io.IOException; + +import racing.util.InputUtil; + +public class InputView { + + private static final String CAR_NAME_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."; + private static final String MOVING_TRIAL_INPUT_MESSAGE = "시도할 회수는 몇회인가요?"; + + public String readCarNames() throws IOException { + System.out.println(CAR_NAME_INPUT_MESSAGE); + + return InputUtil.readLine(); + } + + public String readMovingTrial() throws IOException { + System.out.println(MOVING_TRIAL_INPUT_MESSAGE); + + return InputUtil.readLine(); + } +} diff --git a/src/main/java/racing/view/OutputView.java b/src/main/java/racing/view/OutputView.java new file mode 100644 index 0000000000..119576062d --- /dev/null +++ b/src/main/java/racing/view/OutputView.java @@ -0,0 +1,26 @@ +package racing.view; + +import java.util.List; +import java.util.Map; + +public class OutputView { + private static final String RESULT_MESSAGE = "\n실행 결과"; + private static final String WINNER_MESSAGE = "%s가 최종 우승했습니다."; + + public void printNotice() { + System.out.println(RESULT_MESSAGE); + } + + public void printRacingResult(Map history) { + for (String name : history.keySet()) { + Integer positionValue = history.get(name); + System.out.println(name + " : " + "-".repeat(positionValue)); + } + System.out.println(); + } + + public void printWinner(List winners) { + String winnerNames = winners.toString(); + System.out.printf(WINNER_MESSAGE, winnerNames.substring(1, winnerNames.length()-1)); + } +} diff --git a/src/test/java/SetTest.java b/src/test/java/SetTest.java new file mode 100644 index 0000000000..16ea4d6f0f --- /dev/null +++ b/src/test/java/SetTest.java @@ -0,0 +1,41 @@ +import static org.assertj.core.api.Assertions.*; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +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.ValueSource; + +public class SetTest { + + private Set numbers; + + @BeforeEach + void setUp() { + numbers = new HashSet<>(); + numbers.add(1); + numbers.add(1); + numbers.add(2); + numbers.add(3); + } + + @Test + void hasThreeElements() { + assertThat(numbers.size()).isEqualTo(3); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3}) + void contains(int input) { + assertThat(numbers.contains(input)).isTrue(); + } + + @ParameterizedTest + @CsvSource(value = {"1:true", "2:true", "3:true", "4:false", "5:false"}, delimiter = ':') + void containsOneToFive(int element, boolean expected) { + assertThat(numbers.contains(element)).isEqualTo(expected); + } +} diff --git a/src/test/java/StringTest.java b/src/test/java/StringTest.java new file mode 100644 index 0000000000..f653bd75fe --- /dev/null +++ b/src/test/java/StringTest.java @@ -0,0 +1,48 @@ +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; + +public class StringTest { + + + + @Test + void split() { + String input = "1,2"; + + String[] numbers = input.split(","); + + assertThat(numbers).containsExactly("1", "2"); + } + + @Test + void substring() { + String input = "(1,2)"; + + String number = input.substring(1, 4); + + assertThat(number).isEqualTo("1,2"); + } + + @DisplayName("abc라는 문자열에서 charAt으로 0번째 값을 가져오면 a이다.") + @Test + void charAt() { + String input = "abc"; + + char alphabet = input.charAt(0); + + assertThat(alphabet).isEqualTo('a'); + } + + @DisplayName("charAt에서 문자열의 위치 값을 벗어난 인덱스가 주어졌을 때 StringIndexOutOfBoundsException이 발생한다.") + @Test + void charAtException() { + String input = "abc"; + + assertThatThrownBy(() -> input.charAt(3)) + .isInstanceOf(StringIndexOutOfBoundsException.class) + .hasMessageContaining("String index out of range"); + } +} diff --git a/src/test/java/racing/domain/CarGroupTest.java b/src/test/java/racing/domain/CarGroupTest.java new file mode 100644 index 0000000000..1149f7620d --- /dev/null +++ b/src/test/java/racing/domain/CarGroupTest.java @@ -0,0 +1,53 @@ +package racing.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CarGroupTest { + + @DisplayName("자동차 대수가 1대인 경우 예외가 발생한다.") + @Test + void throwExceptionWhenNumberOfCarsIsOne() { + String[] names = {"1"}; + + assertThatThrownBy(()-> new CarGroup(names)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("자동차 대수가 50대를 초과한 경우 예외가 발생한다.") + @Test + void throwExceptionWhenNumberOfCarsIsOverFifty() { + List names = new ArrayList<>(); + + for (int i = 0; i < 51; i++) { + names.add(i+""); + } + String[] fiftyNames = names.toArray(new String[51]); + + assertThatThrownBy(()-> new CarGroup(fiftyNames)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("중복된 자동차 이름이 존재하는 경우 예외가 발생한다.") + @Test + void throwExceptionWhenNameOfCarsIsDuplicate() { + String[] duplicateNames = {"1","1","3"}; + + assertThatThrownBy(()-> new CarGroup(duplicateNames)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("자동차 이름이 정상적으로 입력된 경우는 예외가 발생하지 않는다.") + @Test + void createCarGroup() { + String[] names = {"1","2","3"}; + + assertDoesNotThrow(() -> new CarGroup(names)); + } +} \ No newline at end of file diff --git a/src/test/java/racing/domain/CarTest.java b/src/test/java/racing/domain/CarTest.java new file mode 100644 index 0000000000..25faf3d232 --- /dev/null +++ b/src/test/java/racing/domain/CarTest.java @@ -0,0 +1,53 @@ +package racing.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.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; + +class CarTest { + + @DisplayName("자동차 이름의 길이가 1보다 작거나 5보다 큰 경우 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"123456", ""}) + void throwExceptionWhenLengthOfCarNameOutOfRange(String input) { + assertThatThrownBy(() -> new Car(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("자동차 이름의 길이가 1이상 5이하이면 예외가 발생하지 않는다.") + @ParameterizedTest + @ValueSource(strings = {"1", "12345"}) + void createCarWhenLengthOfCarNameIsValidate(String input) { + assertDoesNotThrow(() -> new Car(input)); + } + + @DisplayName("자동차의 move메소드는 true값을 인자로 받으면 전진한다.") + @Test + void moveWhenRandomNumberIsOverThree() { + //given + Car car = new Car("123"); + //when + car.move(true); + + //then + assertThat(car.getPosition()).isEqualTo(1); + } + + @DisplayName("자동차의 move메소드는 false값을 인자로 받는 경우 전진하지 않는다.") + @Test + void moveWhenRandomNumberIsUnderThree() { + //given + Car car = new Car("123"); + + //when + car.move(false); + + //then + assertThat(car.getPosition()).isEqualTo(0); + } +} \ No newline at end of file diff --git a/src/test/java/racing/domain/RacingResultTest.java b/src/test/java/racing/domain/RacingResultTest.java new file mode 100644 index 0000000000..128880a595 --- /dev/null +++ b/src/test/java/racing/domain/RacingResultTest.java @@ -0,0 +1,48 @@ +package racing.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RacingResultTest { + + @DisplayName("최종 우승 자동차 대수가 2대 이상인 경우를 확인한다.") + @Test + void checkWinner() { + //given + Map history = new HashMap<>(); + history.put("a",4); + history.put("b",2); + history.put("c",3); + history.put("d",4); + RacingResult racingResult = new RacingResult(history); + + //when + List winners = racingResult.pickWinner(); + + //then + assertThat(winners).isEqualTo(List.of("a","d")); + } + + @DisplayName("최종 우승 자동차 대수가 1대인 경우를 확인한다.") + @Test + void checkWinnerWhenWinnerIsOnlyOne() { + //given + Map history = new HashMap<>(); + history.put("a",4); + history.put("b",2); + history.put("c",3); + RacingResult racingResult = new RacingResult(history); + + //when + List winners = racingResult.pickWinner(); + + //then + assertThat(winners).isEqualTo(List.of("a")); + } +} \ No newline at end of file