diff --git a/README.md b/README.md new file mode 100644 index 00000000..2f462c58 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# 자동차 경주 - 초간단 애플리케이션 + +## [1] 움직이는 자동차 + +- 자동차에 관한 요구사항 + - [x] 자동차는 이름을 가지고 있다. + - [x] 자동차는 움직일 수 있다. + - [x] 0에서 9 사이의 random 값을 구해 그 값이 **4 이상이면 전진**하고, **3 이하 값이면 멈춘다**. + +--- + +## [2] 우승 자동차 구하기 + +- 기능 요구사항 + - [x] n대의 자동차 참여 가능 + - [x] 주어진 횟수동안 n대의 자동차는 전진 또는 멈출 수 있다. + - [x] 이동 조건은 앞의 [움직이는 자동차]와 동일 + - [x] 경주 게임 완료 후 우승자를 구할 수 있다. + - [x] 우승자는 한 명 이상일 수 있다. + +--- + +## [3] 게임 실행 + +- 기능 요구사항 + - [x] 자동차 및 게임 방법은 앞의 [움직이는 자동차], [우승 자동차 구하기]와 동일 + - [x] 사용자로부터 `이름`, `게임횟수` 를 입력받는다. + - [x] 자동차 이름은 쉼표(,) 기준으로 구분하며, 이름은 5자 이하만 가능하다. + - [x] 메인 메서드를 추가하여 실행 가능하게 만든다. + +--- + +## [4] 리팩터링 + +- 요구사항 + - [x] 단위 테스트를 구현한다. + +--- + +- 코드 작성 요구사항 + - [x] 자동차가 움직이는 기능이 의도대로 되는지 테스트한다. + - [x] 자바 코드 컨벤션 `Java Style Guide` 원칙으로 프로그래밍한다. + - [x] 3항 연산자를 쓰지 않는다. + - [x] else 예약어를 쓰지 않는다. + - [x] switch/case도 사용하지 않는다. + - [x] 함수(또는 메서드) 길이가 15라인을 넘어가지 않도록 구현한다. + - [x] 함수(또는 메서드)가 한 가지 일만 하도록 한다. diff --git a/src/main/java/App.java b/src/main/java/App.java new file mode 100644 index 00000000..3000b3c5 --- /dev/null +++ b/src/main/java/App.java @@ -0,0 +1,10 @@ +import controller.RacingGameController; + +public class App { + public static void main(String[] args) { + RacingGameController racingGameController = new RacingGameController(); + + racingGameController.startRacingGame(); + + } +} diff --git a/src/main/java/controller/RacingGameController.java b/src/main/java/controller/RacingGameController.java new file mode 100644 index 00000000..358b39c9 --- /dev/null +++ b/src/main/java/controller/RacingGameController.java @@ -0,0 +1,45 @@ +package controller; + +import domain.Car; +import domain.CarNameParser; +import domain.CarRaceGame; +import java.util.List; +import view.inputView; +import view.outputView; + +public class RacingGameController { + + public void startRacingGame() { + String carNames = inputView.enterCarNames(); + List cars = CarNameParser.parseCarName(carNames); + final CarRaceGame carRaceGame = new CarRaceGame(cars); + + int round = inputView.enterRoundNumber(); + + outputView.printGameResultTitle(); + + printGameRounds(carRaceGame, round, cars); + + printWinners(carRaceGame); + } + + private void printGameRounds(CarRaceGame carRaceGame, int round, List cars) { + carRaceGame.validateRoundNumber(round); + + for (int i = 0; i < round; i++) { + + carRaceGame.playOneRound(); + System.out.println(); + outputView.printRoundResult(cars); + } + } + + + private void printWinners(CarRaceGame carRaceGame) { + + List winnersName = carRaceGame.getWinnerNames(); + + outputView.printGameWinners(winnersName); + + } +} diff --git a/src/main/java/domain/Car.java b/src/main/java/domain/Car.java new file mode 100644 index 00000000..16a01c54 --- /dev/null +++ b/src/main/java/domain/Car.java @@ -0,0 +1,42 @@ +package domain; + +import domain.generator.NumberGenerator; + +public class Car { + + private final String name; + private int position; + private final NumberGenerator generator; + private static final int MOVE_THRESHOLD = 4; + + public Car(String name, NumberGenerator generator) { + validateName(name); + this.name = name.trim(); + this.position = 0; + this.generator = generator; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + private void validateName(String name) { + + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("이름은 빈 값이 될 수 없습니다."); + } + if (name.length() > 5) { + throw new IllegalArgumentException("이름은 5자 이하만 가능합니다."); + } + } + + public void movePosition() { + if (generator.generate() >= MOVE_THRESHOLD ) { + position++; + } + } +} diff --git a/src/main/java/domain/CarNameParser.java b/src/main/java/domain/CarNameParser.java new file mode 100644 index 00000000..19e410af --- /dev/null +++ b/src/main/java/domain/CarNameParser.java @@ -0,0 +1,35 @@ +package domain; + +import domain.generator.RandomNumberGenerator; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class CarNameParser { + + public static final String CAR_NAME_DELIMITER = ","; + + public static List parseCarName(String carNames) { + + List carNameList = Arrays.stream(carNames.split(CAR_NAME_DELIMITER)) + .map(String::trim) + .collect(Collectors.toList()); + + validateDuplicateCarName(carNameList); + + return carNameList.stream() + .map(name -> new Car(name.trim(), new RandomNumberGenerator())) + .collect(Collectors.toList()); + } + + private static void validateDuplicateCarName(List carNameList) { + + Set uniqueCarNames = new HashSet<>(carNameList); + + if (uniqueCarNames.size() != carNameList.size()) { + throw new IllegalArgumentException("중복된 자동차 이름은 입력할 수 없습니다."); + } + } +} diff --git a/src/main/java/domain/CarRaceGame.java b/src/main/java/domain/CarRaceGame.java new file mode 100644 index 00000000..249c7354 --- /dev/null +++ b/src/main/java/domain/CarRaceGame.java @@ -0,0 +1,57 @@ +package domain; + +import java.util.List; + +public class CarRaceGame { + + private final List cars; + + public CarRaceGame(List cars) { + this.cars = cars; + } + + public void validateRoundNumber(int gameRound) { + if (gameRound < 1 ) { + throw new IllegalArgumentException("라운드는 한 번 이상 진행되어야 합니다."); + } + } + + public void playOneRound() { + for (Car car : cars) { + car.movePosition(); + } + } + + private int getMaxDistance() { + int maxDistance = cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElseThrow(); + + return maxDistance; + } + + private List getWinners(int maxDistance) { + return cars.stream() + .filter(car -> car.getPosition() == maxDistance) + .map(Car::getName) + .toList(); + } + + public void playRacingGame(int gameRound) { + validateRoundNumber(gameRound); + + for (int i=0; i < gameRound;i++) { + playOneRound(); + } + } + + public List getWinnerNames() { + List winners = getWinners(getMaxDistance()); + + if (winners == null) { + throw new IllegalStateException("게임이 실행되지 않았습니다."); + } + return winners; + } +} diff --git a/src/main/java/domain/generator/NumberGenerator.java b/src/main/java/domain/generator/NumberGenerator.java new file mode 100644 index 00000000..8904aa58 --- /dev/null +++ b/src/main/java/domain/generator/NumberGenerator.java @@ -0,0 +1,5 @@ +package domain.generator; + +public interface NumberGenerator { + int generate(); +} diff --git a/src/main/java/domain/generator/RandomNumberGenerator.java b/src/main/java/domain/generator/RandomNumberGenerator.java new file mode 100644 index 00000000..10398566 --- /dev/null +++ b/src/main/java/domain/generator/RandomNumberGenerator.java @@ -0,0 +1,13 @@ +package domain.generator; + +import java.util.Random; + +public class RandomNumberGenerator implements NumberGenerator { + + private static final Random RANDOM = new Random(); + + @Override + public int generate( ) { + return RANDOM.nextInt( 10 ); + } +} diff --git a/src/main/java/view/inputView.java b/src/main/java/view/inputView.java new file mode 100644 index 00000000..bea263cb --- /dev/null +++ b/src/main/java/view/inputView.java @@ -0,0 +1,18 @@ +package view; + +import java.util.Scanner; + +public class inputView { + + private static final Scanner scanner = new Scanner(System.in); + + public static String enterCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + return scanner.nextLine(); + } + + public static int enterRoundNumber() { + System.out.println("시도할 회수는 몇회인가요?"); + return scanner.nextInt(); + } +} diff --git a/src/main/java/view/outputView.java b/src/main/java/view/outputView.java new file mode 100644 index 00000000..c1ed5b0d --- /dev/null +++ b/src/main/java/view/outputView.java @@ -0,0 +1,26 @@ +package view; + +import domain.Car; + +import java.util.List; + +public class outputView { + + private static final String CAR_POSITION_EXPRESSION = "-"; + private static final String WINNER_NAMES_DELIMITER = ","; + + public static void printGameResultTitle() { + System.out.print("\n실행 결과"); + } + + public static void printRoundResult(List cars) { + for (Car car : cars) { + System.out.println(car.getName() + ": " + CAR_POSITION_EXPRESSION.repeat(car.getPosition())); + } + System.out.println(); + } + + public static void printGameWinners(List winners) { + System.out.println(String.join(WINNER_NAMES_DELIMITER, winners) + "가 최종 우승했습니다."); + } +} diff --git a/src/test/java/domain/CarNameParserTest.java b/src/test/java/domain/CarNameParserTest.java new file mode 100644 index 00000000..606374cc --- /dev/null +++ b/src/test/java/domain/CarNameParserTest.java @@ -0,0 +1,59 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class CarNameParserTest { + + @Nested + @DisplayName("입력된 문자열은 구분자로 나누어 차의 이름이 된다.") + class ParseCarNameTest { + + @Test + @DisplayName("공백없이 들어왔을 경우") + public void parseCarName_문자열을입력하면_차이름리스트가생성된다() { + + // given + String carNames = "차1,차2,차3"; + + // when + List cars = CarNameParser.parseCarName(carNames); + + // then + assertThat(cars).extracting(Car::getName).containsExactly("차1", "차2", "차3"); + + } + + @Test + @DisplayName("공백 포함해 들어왔을 경우") + public void parseCarName_구분자기준으로공백이포함된경우_공백제거후차이름리스트가생성된다() { + + // given + String carNames = "차1 , 차2,차3 "; + + // when + List cars = CarNameParser.parseCarName(carNames); + + // then + assertThat(cars).extracting(Car::getName).containsExactly("차1", "차2", "차3"); + + } + } + + @Test + @DisplayName("입력된 차들의 이름은 중복이 없어야 한다.") + public void parseCarName_중복된차이름_예외처리() { + // given + CarNameParser carNameParser = new CarNameParser(); + String carNames = "차1,차1,차3"; + + assertThatThrownBy(() -> carNameParser.parseCarName(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("중복된 자동차 이름은 입력할 수 없습니다."); + } +} diff --git a/src/test/java/domain/CarRaceGameTest.java b/src/test/java/domain/CarRaceGameTest.java new file mode 100644 index 00000000..d740fffe --- /dev/null +++ b/src/test/java/domain/CarRaceGameTest.java @@ -0,0 +1,111 @@ +package domain; + +import domain.generator.FixedNumberGenerator; +import domain.generator.NumberGenerator; +import domain.generator.RandomNumberGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.ArrayList; + +public class CarRaceGameTest { + + private List createCars(NumberGenerator... generators) { + List cars = new ArrayList<>(); + for (int i = 0; i < generators.length; i++) { + cars.add(new Car("차" + (i + 1), generators[i])); + } + return cars; + } + + @Nested + @DisplayName("게임 라운드는 1 이상의 정수만 입력할 수 있다.") + class CarRaceGameRoundNumberTest { + + @ParameterizedTest + @ValueSource(ints = {0, -4}) + void playRacingGame_게임라운드0이하_예외처리(int gameRound) { + List cars = createCars( + new RandomNumberGenerator(), + new RandomNumberGenerator(), + new RandomNumberGenerator() + ); + + CarRaceGame game = new CarRaceGame(cars); + + assertThatThrownBy(() -> game.playRacingGame(gameRound)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("라운드는 한 번 이상 진행되어야 합니다."); + } + } + + @Nested + @DisplayName("게임이 정상적으로 진행된 경우, 결과도 정상적으로 출력되어야 한다.") + class CarRaceGamePrintResultTest { + + @Test + void playRacingGame_우승자한명_정상출력() { + List cars = createCars( + new FixedNumberGenerator(9, 6, 4, 2, 5), + new FixedNumberGenerator(0, 2, 2, 1, 3), + new FixedNumberGenerator(1, 2, 5, 0, 9) + ); + + CarRaceGame game = new CarRaceGame(cars); + game.playRacingGame(5); + + assertThat(game.getWinnerNames()).containsOnly("차1"); + } + + @Test + void playRacingGame_우승자두명_정상출력() { + List cars = createCars( + new FixedNumberGenerator(1, 8, 7), + new FixedNumberGenerator(0, 9, 2), + new FixedNumberGenerator(7, 2, 5) + ); + + CarRaceGame game = new CarRaceGame(cars); + game.playRacingGame(3); + + assertThat(game.getWinnerNames()).containsOnly("차1", "차3"); + } + + @Test + void playRacingGame_참여자모두가우승자인경우_정상출력() { + List cars = createCars( + new FixedNumberGenerator(3, 1, 7, 9), + new FixedNumberGenerator(0, 4, 2, 8), + new FixedNumberGenerator(7, 2, 5, 1) + ); + + CarRaceGame game = new CarRaceGame(cars); + game.playRacingGame(4); + + assertThat(game.getWinnerNames()).containsOnly("차1", "차2", "차3"); + + } + + @Test + void playRacingGame_모든참여자가전진하지못한경우_정상출력() { + List cars = createCars( + new FixedNumberGenerator(3, 0, 2, 1), + new FixedNumberGenerator(0, 3, 1, 1), + new FixedNumberGenerator(0, 1, 0, 1) + ); + + CarRaceGame game = new CarRaceGame(cars); + game.playRacingGame(4); + + assertThat(game.getWinnerNames()).containsOnly("차1", "차2", "차3"); + + } + } +} diff --git a/src/test/java/domain/CarTest.java b/src/test/java/domain/CarTest.java new file mode 100644 index 00000000..8da45a57 --- /dev/null +++ b/src/test/java/domain/CarTest.java @@ -0,0 +1,116 @@ +package domain; + +import domain.generator.FixedNumberGenerator; +import domain.generator.NumberGenerator; +import domain.generator.RandomNumberGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CarTest { + + @Nested + @DisplayName("Car 객체가 생성된다.") + class CarCreateTest{ + + @Test + void Car_객체생성() { + + // given + NumberGenerator generator = new RandomNumberGenerator(); + Car car = new Car("붕붕이", generator); + + // when + assertThat(car.getName()).isEqualTo("붕붕이"); + assertThat(car.getPosition()).isEqualTo(0); + } + } + + @Nested + @DisplayName("Car 객체의 이름으로 입력된 값을 검증한다.") + class CarNameTest{ + + @Test + void Car_이름중복제거() { + + String carName = " 붕붕이"; + // given + NumberGenerator generator = new RandomNumberGenerator(); + Car car = new Car(carName, generator); + + assertThat(car.getName()).isEqualTo(carName.trim()); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void Car_이름빈값일경우_예외처리(String name) { + + NumberGenerator generator = new RandomNumberGenerator(); + + assertThatThrownBy(() -> new Car(name,generator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름은 빈 값이 될 수 없습니다."); + } + + @Test + void Car_이름null값인경우_예외처리() { + + String name = null; + NumberGenerator generator = new RandomNumberGenerator(); + + assertThatThrownBy(() -> new Car(name,generator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름은 빈 값이 될 수 없습니다."); + } + } + + @Nested + @DisplayName("Car 객체가 요구사항에 맞게 이동한다.") + class CarPositionTest{ + + @Test + void getRandomNumber_랜덤숫자기준충족() { + + NumberGenerator generator = new RandomNumberGenerator(); + + Car car = new Car("차1",generator); + assertThat(generator.generate()) + .isGreaterThanOrEqualTo( 0) + .isLessThanOrEqualTo( 9); + } + + @Test + void movePosition_랜덤숫자4이상일때_1이동() { + + NumberGenerator generator = new FixedNumberGenerator(5); + + // given + Car car = new Car("차2", generator); + + // when + car.movePosition(); + + // then + assertThat(car.getPosition()).isEqualTo(1); + } + + @Test + void movePosition_랜덤숫자4미만일때_정지() { + + // given + NumberGenerator generator = new FixedNumberGenerator(2); + Car car = new Car("차3", generator); + + // when + car.movePosition(); + + // then + assertThat(car.getPosition()).isEqualTo(0); + } + } +} diff --git a/src/test/java/domain/generator/FixedNumberGenerator.java b/src/test/java/domain/generator/FixedNumberGenerator.java new file mode 100644 index 00000000..e6404d07 --- /dev/null +++ b/src/test/java/domain/generator/FixedNumberGenerator.java @@ -0,0 +1,16 @@ +package domain.generator; + +public class FixedNumberGenerator implements NumberGenerator { + private final int[] numbers; + private int index = 0; + + public FixedNumberGenerator(int... numbers) { + this.numbers = numbers; + } + + @Override + public int generate() { + return numbers[index++]; + } + +}