diff --git a/README.md b/README.md index d0286c859f..8dc8799c7e 100644 --- a/README.md +++ b/README.md @@ -1 +1,115 @@ # java-racingcar-precourse + +## 기능 요구 사항 + +초간단 자동차 경주 게임을 구현한다. + +주어진 횟수 동안 `n`대의 자동차는 전진 또는 멈출 수 있다. + +각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. + +자동차 이름은 쉼표(`,`)를 기준으로 구분하며 이름은 5자 이하만 가능하다. + +사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. + +전진하는 조건은 `0`에서 `9` 사이에서 무작위 값을 구한 후 무작위 값이 `4` 이상일 경우이다. + +자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. + +우승자가 여러 명일 경우 쉼표(`,`)를 이용하여 구분한다. + +사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. + +### 실행 결과 예시 + +``` +경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) +pobi,woni,jun +시도할 횟수는 몇 회인가요? +5 + +실행 결과 +pobi : - +woni : +jun : - + +pobi : -- +woni : - +jun : -- + +pobi : --- +woni : -- +jun : --- + +pobi : ---- +woni : --- +jun : ---- + +pobi : ----- +woni : ---- +jun : ----- + +최종 우승자 : pobi, jun +``` + +--- +### 프로그래밍 요구 사항 + +indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. + +예를 들어 `while`문 안에 `if`문이 있으면 들여쓰기는 2이다. + +힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. + +3항 연산자를 쓰지 않는다. + +함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. + +`JUnit 5`와 `AssertJ`를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다. + +테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다. + +--- + +### 라이브러리 + +`camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다. + +`Random` 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다. + +사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용 + +--- +### 커밋 메시지 규칙 + +`(): ` + +``: 커밋의 종류 + +``` + - feat: 새로운 기능 + - fix: 버그 수정 + - docs: 문서 변경 + - style: 코드 형식 수정 (공백, 세미콜론 등) + - refactor: 리팩토링 (기능 변경 없음) + - test: 테스트 추가 또는 수정 + - chore: 기타 작업 (빌드, 도구 설정 등) +``` + +``: 변경이 발생한 파일이나 기능의 범위 + +``: 간결한 설명을 현재 시제로 작성, 첫 글자는 소문자로 시작하고, 끝에 점을 찍지 않는다. + +--- + +## 기능 구현 목록 + +_Bottom-up implementation_ + +0. [x] I/O 테스트 및 라이브러리 메소드 체크 +1. [x] n회, 수행 레이싱 참가 차량 1개에 대해서 구현 +2. [x] 1회, 수행 레이싱 참가 차량 k개에 대해서 구현 +3. [x] 관심사 분리 +4. [x] n회, 수행 레이싱 참가 차량 k개에 대해서 구현 +5. [x] 테스트 코드 작성 +6. [x] 유지 보수 및 최적화 diff --git a/src/main/java/racing/Application.java b/src/main/java/racing/Application.java new file mode 100644 index 0000000000..b38857e863 --- /dev/null +++ b/src/main/java/racing/Application.java @@ -0,0 +1,10 @@ +package racing; +import racing.game.RacingController; + +public class Application { + public static void main(String[] args) { + + RacingController racingController = new RacingController(); + racingController.startRacingGame(); + } +} diff --git a/src/main/java/racing/game/RacingController.java b/src/main/java/racing/game/RacingController.java new file mode 100644 index 0000000000..93791f9d7d --- /dev/null +++ b/src/main/java/racing/game/RacingController.java @@ -0,0 +1,50 @@ +package racing.game; + +import racing.game.domain.Car; +import racing.io.RacingInput; +import racing.io.RacingOutput; + +import java.util.ArrayList; +import java.util.List; + +public class RacingController { + + private final RacingService racingService = new RacingService(); + private final RacingOutput racingOutput = new RacingOutput(); + private final RacingInput racingInput = new RacingInput(); + + + private void startRound(List cars) { + cars.forEach(racingService::moveCarForward); + showRoundResult(cars); + } + + private void showRoundResult(List cars) { + racingOutput.displayCarPositions(cars); + } + + private void showWinner(List cars) { + List winnerNames = racingService.getWinnerNames(cars); + racingOutput.displayWinners(winnerNames); + } + + public List createCars(List carNames) { + ArrayList cars = new ArrayList<>(); + carNames.forEach(carName -> cars.add(new Car(carName))); + + return cars; + } + + public void startRacingGame() { + List carNames = racingInput.getCarNames(); + List cars = createCars(carNames); + int gameRound = racingInput.getGameRound(); + + racingOutput.displayStartMessage(); + for (int roundNum = 0; roundNum < gameRound; roundNum++) { + startRound(cars); + } + + showWinner(cars); + } +} diff --git a/src/main/java/racing/game/RacingService.java b/src/main/java/racing/game/RacingService.java new file mode 100644 index 0000000000..dd4ab91d7f --- /dev/null +++ b/src/main/java/racing/game/RacingService.java @@ -0,0 +1,35 @@ +package racing.game; + +import camp.nextstep.edu.missionutils.Randoms; +import racing.game.domain.Car; + +import java.util.List; +import java.util.stream.Collectors; + +public class RacingService { + private static final int MIN_POSITION = 0; + private static final int THRESHOLD_VALUE = 4; + + private boolean isForward() { + return Randoms.pickNumberInRange(0, 9) >= THRESHOLD_VALUE; + } + private int findMaxPosition(List cars) { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(MIN_POSITION); + } + + public void moveCarForward(Car car) { + if (isForward()) car.moveForward(); + } + + public List getWinnerNames(List cars) { + int maxPosition = findMaxPosition(cars); + + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .map(Car::getName) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/racing/game/domain/Car.java b/src/main/java/racing/game/domain/Car.java new file mode 100644 index 0000000000..d0a7ef889e --- /dev/null +++ b/src/main/java/racing/game/domain/Car.java @@ -0,0 +1,21 @@ +package racing.game.domain; + +public class Car { + private int position; + private final String name; + + public Car(String name) { + this.name = name; + } + + public void moveForward() { + position ++; + } + public int getPosition() { + return position; + } + public String getName() { + return name; + } + +} diff --git a/src/main/java/racing/io/RacingInput.java b/src/main/java/racing/io/RacingInput.java new file mode 100644 index 0000000000..050dc2f1c2 --- /dev/null +++ b/src/main/java/racing/io/RacingInput.java @@ -0,0 +1,51 @@ +package racing.io; + +import camp.nextstep.edu.missionutils.Console; +import racing.validation.InputValidator; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class RacingInput { + + private final InputValidator inputValidator = new InputValidator(); + + private List parseCarName(String carNameStr) { + + return Arrays.stream(carNameStr.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + } + + public List getCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String carNameStr = Console.readLine(); + List carNames = parseCarName(carNameStr); + inputValidator.validateCarNames(carNames); + + return carNames; + } + + + public int getGameRound() { + System.out.println("시도할 횟수는 몇 회인가요?"); + String gameRoundStr = Console.readLine(); + + int gameRound = parseGameRound(gameRoundStr); + + inputValidator.validateGameRound(gameRound); + + return gameRound; + } + + public int parseGameRound(String gameRoundStr) { + int gameRound; + try { + gameRound = Integer.parseInt(gameRoundStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("유효하지 않은 숫자 입력입니다."); + } + return gameRound; + } +} diff --git a/src/main/java/racing/io/RacingOutput.java b/src/main/java/racing/io/RacingOutput.java new file mode 100644 index 0000000000..96fa003a59 --- /dev/null +++ b/src/main/java/racing/io/RacingOutput.java @@ -0,0 +1,31 @@ +package racing.io; + +import racing.game.domain.Car; + +import java.util.List; + +public class RacingOutput { + + private String displayPosition(int repeatCnt) { + return "-".repeat(repeatCnt); + } + + private void displayCarPosition(Car car) { + System.out.println(car.getName() + " : " + displayPosition(car.getPosition())); + } + + public void displayStartMessage() { + System.out.println("\n실행 결과"); + } + public void displayCarPositions(List cars) { + for (Car car : cars) { + displayCarPosition(car); + } + System.out.println(); + } + + public void displayWinners(List winners) { + System.out.println("최종 우승자 : " + String.join(", ", winners)); + } + +} diff --git a/src/main/java/racing/validation/InputValidator.java b/src/main/java/racing/validation/InputValidator.java new file mode 100644 index 0000000000..4e9b1bf9de --- /dev/null +++ b/src/main/java/racing/validation/InputValidator.java @@ -0,0 +1,29 @@ +package racing.validation; + +import java.util.List; + +public class InputValidator { + public void validateCarNames(List carNames) { + + if (carNames.isEmpty()) { + throw new IllegalArgumentException("자동차 이름이 유효 하지않습니다."); + } + for (String carName : carNames) { + validateCarName(carName); + } + } + public void validateGameRound(int roundCnt) { + if (roundCnt < 1) { + throw new IllegalArgumentException("게임 횟수는 1이상 부터 가능합니다"); + } + } + + public void validateCarName(String carName) { + if (carName.isEmpty()) { + throw new IllegalArgumentException("자동차 이름이 유효 하지않습니다."); + } + if (carName.length() > 5) { + throw new IllegalArgumentException("자동차 이름은 5자 이하만 가능합니다."); + } + } +} diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java deleted file mode 100644 index a17a52e724..0000000000 --- a/src/main/java/racingcar/Application.java +++ /dev/null @@ -1,7 +0,0 @@ -package racingcar; - -public class Application { - public static void main(String[] args) { - // TODO: 프로그램 구현 - } -} diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java deleted file mode 100644 index 1d35fc33fe..0000000000 --- a/src/test/java/racingcar/ApplicationTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package racingcar; - -import camp.nextstep.edu.missionutils.test.NsTest; -import org.junit.jupiter.api.Test; - -import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; -import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class ApplicationTest extends NsTest { - private static final int MOVING_FORWARD = 4; - private static final int STOP = 3; - - @Test - void 기능_테스트() { - assertRandomNumberInRangeTest( - () -> { - run("pobi,woni", "1"); - assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi"); - }, - MOVING_FORWARD, STOP - ); - } - - @Test - void 예외_테스트() { - assertSimpleTest(() -> - assertThatThrownBy(() -> runException("pobi,javaji", "1")) - .isInstanceOf(IllegalArgumentException.class) - ); - } - - @Override - public void runMain() { - Application.main(new String[]{}); - } -} diff --git a/src/test/java/racingcar/InputValidatorTest.java b/src/test/java/racingcar/InputValidatorTest.java new file mode 100644 index 0000000000..24c79676a2 --- /dev/null +++ b/src/test/java/racingcar/InputValidatorTest.java @@ -0,0 +1,73 @@ +package racingcar; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.io.RacingInput; +import racing.validation.InputValidator; + +import java.util.List; + +public class InputValidatorTest { + private final InputValidator inputValidator = new InputValidator(); + private final RacingInput racingInput = new RacingInput(); + @Test + @DisplayName("유효 하지않은 입력 - 빈 자동차 이름 목록") + void 예외_테스트_0(){ + // given + List emptyCarNames = List.of(); + + // when + Assertions.assertThatThrownBy(() -> inputValidator.validateCarNames(emptyCarNames) + // then + ).isInstanceOf(IllegalArgumentException.class); + } + @Test + @DisplayName("유효 하지않은 자동차 이름 - 5자 초과") + void 예외_테스트1(){ + // given + String longCarName = "loooooooooong"; + + // when + Assertions.assertThatThrownBy(() -> inputValidator.validateCarName(longCarName)) + // then + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("유효 하지않은 자동차 이름 - 빈문자열") + void 예외_테스트_2(){ + // given + String invalidCarName = ""; + + // when + Assertions.assertThatThrownBy(() -> inputValidator.validateCarName(invalidCarName)) + // then + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("유효 하지않은 시도 횟수 - 0이하") + void 예외_테스트_3(){ + // given + int gameRound = 0; + + // when + Assertions.assertThatThrownBy(() -> inputValidator.validateGameRound(gameRound)) + // then + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("유효 하지않은 시도 횟수 - 문자") + void 예외_테스트_4(){ + // given + String gameRound = "a"; + + // when + Assertions.assertThatThrownBy(() -> racingInput.parseGameRound(gameRound)) + // then + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/racingcar/RacingTest.java b/src/test/java/racingcar/RacingTest.java new file mode 100644 index 0000000000..892c4f7ad5 --- /dev/null +++ b/src/test/java/racingcar/RacingTest.java @@ -0,0 +1,80 @@ +package racingcar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.game.RacingController; +import racing.game.RacingService; +import racing.game.domain.Car; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RacingTest { + private final RacingController racingController = new RacingController(); + private final RacingService racingService = new RacingService(); + + @Test + @DisplayName("전진 기능 : 자동차가 전진하는 경우") + void 기능_테스트_1 (){ + // given + Car car = new Car("test"); + int initPosition = car.getPosition(); + + // when + car.moveForward(); + + // then + assertThat(car.getPosition()).isEqualTo(initPosition + 1); + } + + @Test + @DisplayName("우승자 1명 선정 기능: 최대 위치를 가진 자동차 이름 반환") + void 기능_테스트_2(){ + // given + Car car1 = new Car("test1"); + Car car2 = new Car("test2"); + List cars = List.of(car1, car2); + + // when + car1.moveForward(); + List winners = racingService.getWinnerNames(cars); + + // then + assertThat(winners).containsExactly("test1"); + } + + @Test + @DisplayName("우승자 여러명 선정 기능: 최대 위치를 가진 자동차 이름 반환") + void 기능_테스트_3(){ + // given + Car car1 = new Car("test1"); + Car car2 = new Car("test2"); + Car car3 = new Car("test3"); + List cars = List.of(car1, car2, car3); + + // when + car1.moveForward(); + car2.moveForward(); + List winners = racingService.getWinnerNames(cars); + + // then + assertThat(winners).containsExactlyInAnyOrder("test1", "test2"); + } + + @Test + @DisplayName("자동차 생성 기능: 자동차 이름 리스트로 자동차 객체 생성") + void 기능_테스트_4(){ + // given + List carNames = List.of("test1", "test2", "test3"); + + // when + List cars = racingController.createCars(carNames); + + // then + assertThat(cars).hasSize(3); + assertThat(carNames).containsExactlyInAnyOrder("test1", "test2", "test3"); + } + + +}