diff --git a/README.md b/README.md index a2b6ed1440..275abf01fe 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,87 @@ -# java-racingcar-precourse # 미션 - 자동차 경주 +## 문제 요구사항 분석 -## 🔍 진행 방식 +주제 : 임의의 갯수의 자동차의 랜덤한 움직임 결과를 출력하는 프로그램 -- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다. -- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다. -- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. +**자동차** -## 📮 미션 제출 방법 +- 이름 조건 + - 영어 + - 5글자 이하 +- 움직임 종류 + - 전진 + - 멈춤 +- 움직임 조건 + - 0~9까지의 랜덤 숫자 중 4이상 -- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다. - - GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고해 제출한다. -- GitHub에 미션을 제출한 후 [우아한테크코스 지원](https://apply.techcourse.co.kr) 사이트에 접속하여 프리코스 과제를 제출한다. - - 자세한 방법은 [제출 가이드](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 참고 - - **Pull Request만 보내고 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.** +**게임** -## 🚨 과제 제출 전 체크 리스트 - 0점 방지 +- 실행 조건 + - n개의 자동차 이름 입력 + - t번의 움직임 횟수 입력 +- 종료 조건 + - t번의 움직임이 모두 이루어짐 +- 우승 조건 + - 가장 많이 움직인 자동차 + - 중복 가능 -- 기능 구현을 모두 정상적으로 했더라도 **요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리**한다. -- 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는지 확인한다. -- **테스트가 실패할 경우 0점으로 처리**되므로, 반드시 확인 후 제출한다. +**입력** -### 테스트 실행 가이드 +- 첫 번째 줄 입력 + - 영어 + - 5글자 이하 + - 쉼표 구분 +- 두 번째 줄 입력 + - 숫자 -- 터미널에서 `java -version`을 실행하여 Java 버전이 17인지 확인한다. - Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 17로 실행되는지 확인한다. -- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고, - Windows 사용자의 경우 `gradlew.bat clean test` 또는 `./gradlew.bat clean test` 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다. +**출력** -``` -BUILD SUCCESSFUL in 0s -``` +- 실행 시 + - 움직임이 실행 될 때 마다 결과를 출력 +- 종료 시 + - 가장 많이 움직인 자동차의 이름을 출력 + - 중복 시, 쉼표로 구분 ---- +## 서비스 흐름 구조 기획 -## 🚀 기능 요구 사항 +Untitled-diagram-Mermaid-Chart-2025-09-16-052424-1 +## 기능 구현 체크리스트 -초간단 자동차 경주 게임을 구현한다. +### console -- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. -- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. -- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. -- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. -- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. -- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. -- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. -- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. +주요 기능 : 입력 값 담당 -### 입출력 요구 사항 +- [X] 입력 값 받기 +- [X] 검증기를 통한 입력 값 검증 과정 추가 +- [X] 입력 값 저장 -#### 입력 +### validator -- 경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분) +주요 기능 : 입력 값 검증 -``` -pobi,woni,jun -``` +- 이름 검증 + - [X] 종류 : 영어 + - [X] 글자 수 : 1~5 + - [X] 제한 : 구분자 (쉼표) +- 횟수 검증 + - [X] 종류 : 숫자 -- 시도할 회수 +### moveCar -``` -5 -``` +주요 기능 : 자동차의 이동 거리 계산 및 출력 -#### 출력 +- 이동 거리 계산 + - [X] 난수 생성 + - [X] 차량 별 이동 거리 저장 +- 이동 거리 출력 + - [X] 차량 별 이동 거리 출력 +- 우승자 출력 + - [X] 전체 차량 이동 거리 비교 -- 각 차수별 실행 결과 +### utils -``` -pobi : -- -woni : ---- -jun : --- -``` +주요 기능 : 상수 저장 -- 단독 우승자 안내 문구 - -``` -최종 우승자 : pobi -``` - -- 공동 우승자 안내 문구 - -``` -최종 우승자 : pobi, jun -``` - -#### 실행 결과 예시 - -``` -경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) -pobi,woni,jun -시도할 회수는 몇회인가요? -5 - -실행 결과 -pobi : - -woni : -jun : - - -pobi : -- -woni : - -jun : -- - -pobi : --- -woni : -- -jun : --- - -pobi : ---- -woni : --- -jun : ---- - -pobi : ----- -woni : ---- -jun : ----- - -최종 우승자 : pobi, jun -``` - ---- - -## 🎯 프로그래밍 요구 사항 - -- JDK 17 버전에서 실행 가능해야 한다. **JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.** -- 프로그램 실행의 시작점은 `Application`의 `main()`이다. -- `build.gradle` 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다. -- [Java 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/master/styleguide/java) 가이드를 준수하며 프로그래밍한다. -- 프로그램 종료 시 `System.exit()`를 호출하지 않는다. -- 프로그램 구현이 완료되면 `ApplicationTest`의 모든 테스트가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.** -- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다. - -### 추가된 요구 사항 - -- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. - - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. - - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. -- 3항 연산자를 쓰지 않는다. -- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. -- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다. - - 테스트 도구 사용법이 익숙하지 않다면 `test/java/study`를 참고하여 학습한 후 테스트를 구현한다. - -### 라이브러리 - -- JDK에서 제공하는 Random 및 Scanner API 대신 `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다. - - Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다. - - 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다. - -#### 사용 예시 - -- 0에서 9까지의 정수 중 한 개의 정수 반환 - -```java -Randoms.pickNumberInRange(0,9); -``` - ---- - -## ✏️ 과제 진행 요구 사항 - -- 미션은 [java-racingcar-7](https://github.com/woowacourse-precourse/java-racingcar-7) 저장소를 Fork & Clone해 시작한다. -- **기능을 구현하기 전 `docs/README.md`에 구현할 기능 목록을 정리**해 추가한다. -- **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다. - - [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다. -- 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고한다. +- 입력 값 안내 메시지 +- 입력 값 오류 메시지 +- 정규 표현식 \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..df2a237a78 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,11 @@ package racingcar; +import racingcar.runner.RaceRunner; + public class Application { - public static void main(String[] args) { - // TODO: 프로그램 구현 - } + + public static void main(String[] args) { + new RaceRunner().run(); + } + } diff --git a/src/main/java/racingcar/Application2.java b/src/main/java/racingcar/Application2.java new file mode 100644 index 0000000000..4e35336458 --- /dev/null +++ b/src/main/java/racingcar/Application2.java @@ -0,0 +1,63 @@ +//package racingcar; +// +//import camp.nextstep.edu.missionutils.Console; +//import camp.nextstep.edu.missionutils.Randoms; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; +//import java.util.regex.Pattern; +//import racingcar.utils.constant.Constant; +// +//public class Application2 { +// +// public static void main(String[] args) { +// // TODO: 프로그램 구현 +// +// // Console 부분 +// System.out.println(Constant.REQUIRE_CAR_NAMES); +// String[] cars = Console.readLine().split(","); +// for (String car : cars) { +// if (!Pattern.matches(Constant.REGEX_CAR_NAME, car)) { +// throw new IllegalArgumentException(Constant.CAR_NAME_ERROR); +// } +// } +// System.out.println(Constant.REQUIRE_TRY_NUMBER); +// String tryNumber = Console.readLine(); +// if (!Pattern.matches(Constant.REGEX_TRY_NUMBER, tryNumber)) { +// throw new IllegalArgumentException(Constant.TRY_NUMBER_ERROR); +// } +// int tryNumberInt = Integer.parseInt(tryNumber); +// +// //moveCar 부분 +// HashMap carInfo = new HashMap<>(); +// for(String car : cars){ +// carInfo.put(car,0); +// } +// while(tryNumberInt > 0) { +// for(String car : cars){ +// int currentPosition = carInfo.get(car); +// int randomNumber = Randoms.pickNumberInRange(0,9); +// if(randomNumber >=4) { +// currentPosition++; +// carInfo.put(car,currentPosition); +// } +// System.out.println(car + " : " + "-".repeat(currentPosition)); +// } +// System.out.println(); +// tryNumberInt--; +// } +// // 우승자 출력 +// int maxDistance = carInfo.values() +// .stream() +// .max(Integer::compareTo) +// .orElse(0); // 최댓값 구하기 +// +// List winners = carInfo.entrySet() +// .stream() +// .filter(entry -> entry.getValue() == maxDistance) // 최댓값과 같은 차량 필터링 +// .map(Map.Entry::getKey) // 차량 이름만 추출 +// .toList(); +// System.out.println("우승자 : " + String.join(",", winners)); +// } +// +//} diff --git a/src/main/java/racingcar/controller/RaceController.java b/src/main/java/racingcar/controller/RaceController.java new file mode 100644 index 0000000000..cdafbb612a --- /dev/null +++ b/src/main/java/racingcar/controller/RaceController.java @@ -0,0 +1,27 @@ +package racingcar.controller; + + +import camp.nextstep.edu.missionutils.Console; +import racingcar.utils.Validator; +import racingcar.utils.constant.Message; + +public class RaceController { + + public String[] inputCarNames() { + System.out.println(Message.REQUIRE_CAR_NAMES); + String[] input = Console.readLine().split(","); + String[] cars = new String[input.length]; + for(int i = 0 ; i < input.length ; i++) { + cars[i] = input[i].trim(); + } + Validator.validateCarName(cars); + return cars; + } + + public int inputTryNumber() { + System.out.println(Message.REQUIRE_TRY_NUMBER); + String tryNumber = Console.readLine(); + Validator.validateTryNumber(tryNumber); + return Integer.parseInt(tryNumber); + } +} diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..09d51354f4 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,22 @@ +package racingcar.domain; + +public class Car { + private String name; + private int distance; + + public Car(String name, int distance) { + this.name = name; + this.distance = distance; + } + public String getName(){ + return name; + } + + public int getDistance(){ + return distance; + } + + public void updateDistance(){ + distance += 1; + } +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000000..a309b7f840 --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,18 @@ +package racingcar.domain; + +public class Cars { + + private Car[] cars; + + public Cars(String[] carNames) { + this.cars = new Car[carNames.length]; + for (int i = 0; i < carNames.length; i++) { + cars[i] = new Car(carNames[i],0); + } + } + + public Car[] getCars(){ + return cars; + } + +} diff --git a/src/main/java/racingcar/domain/Race.java b/src/main/java/racingcar/domain/Race.java new file mode 100644 index 0000000000..97aa12d2a7 --- /dev/null +++ b/src/main/java/racingcar/domain/Race.java @@ -0,0 +1,33 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class Race { + + public void moveCar(Car[] carArray) { + for (Car car : carArray) { + int randomNumber = Randoms.pickNumberInRange(0, 9); + if (randomNumber >= 4) { + car.updateDistance(); + } + } + } + + public static List findWinner(Cars cars) { + Car[] carArray = cars.getCars(); + Arrays.sort(carArray, Comparator.comparingInt(Car::getDistance).reversed()); + + List winnerNames = new ArrayList<>(); + int maxDistance = carArray[0].getDistance(); + for (Car car : carArray) { + if (car.getDistance() == maxDistance) { + winnerNames.add(car.getName()); + } + } + return winnerNames; + } +} diff --git a/src/main/java/racingcar/runner/RaceRunner.java b/src/main/java/racingcar/runner/RaceRunner.java new file mode 100644 index 0000000000..1cbf2af3a1 --- /dev/null +++ b/src/main/java/racingcar/runner/RaceRunner.java @@ -0,0 +1,26 @@ +package racingcar.runner; + +import java.util.List; +import racingcar.controller.RaceController; +import racingcar.domain.Cars; +import racingcar.domain.Race; +import racingcar.service.RaceService; +import racingcar.view.RaceView; + +public class RaceRunner { + + public void run() { + RaceController raceController = new RaceController(); + String[] carNames = raceController.inputCarNames(); + int tryNumber = raceController.inputTryNumber(); + + Cars cars = new Cars(carNames); + + RaceService raceService = new RaceService(); + raceService.startRace(tryNumber, cars); + + List winners = Race.findWinner(cars); + RaceView raceView = new RaceView(); + raceView.printWinner(winners); + } +} diff --git a/src/main/java/racingcar/service/RaceService.java b/src/main/java/racingcar/service/RaceService.java new file mode 100644 index 0000000000..ac1d48f576 --- /dev/null +++ b/src/main/java/racingcar/service/RaceService.java @@ -0,0 +1,19 @@ +package racingcar.service; + +import racingcar.domain.Car; +import racingcar.domain.Cars; +import racingcar.domain.Race; +import racingcar.view.RaceView; + +public class RaceService { + + public void startRace(int tryNumber, Cars cars) { + Race race = new Race(); + Car[] carArray = cars.getCars(); + while (tryNumber > 0) { + race.moveCar(carArray); + RaceView.printProgress(carArray); + tryNumber--; + } + } +} diff --git a/src/main/java/racingcar/utils/Validator.java b/src/main/java/racingcar/utils/Validator.java new file mode 100644 index 0000000000..486a6fbf9a --- /dev/null +++ b/src/main/java/racingcar/utils/Validator.java @@ -0,0 +1,27 @@ +package racingcar.utils; + +import java.util.regex.Pattern; +import racingcar.utils.constant.Message; +import racingcar.utils.constant.Regex; + +public class Validator { + private Validator() {} + + public static void validateCarName(String[] cars) { + if(cars == null || cars.length == 0) { + throw new IllegalArgumentException(Message.ERROR_CAR_NAME); + } + for (String car : cars) { + if (!Pattern.matches(Regex.CAR_NAME, car)) { + throw new IllegalArgumentException(Message.ERROR_CAR_NAME); + } + } + } + + public static void validateTryNumber(String tryNumber) { + if (!Pattern.matches(Regex.TRY_NUMBER, tryNumber)) { + throw new IllegalArgumentException(Message.ERROR_TRY_NUMBER); + } + } + +} diff --git a/src/main/java/racingcar/utils/constant/Message.java b/src/main/java/racingcar/utils/constant/Message.java new file mode 100644 index 0000000000..fcddb80557 --- /dev/null +++ b/src/main/java/racingcar/utils/constant/Message.java @@ -0,0 +1,9 @@ +package racingcar.utils.constant; + +public class Message { + public final static String REQUIRE_CAR_NAMES = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + public final static String REQUIRE_TRY_NUMBER = "시도할 회수는 몇회인가요?"; + public final static String ERROR_CAR_NAME = "차량 이름 양식 오류"; + public final static String ERROR_TRY_NUMBER = "시도 회수 양식 오류"; + +} diff --git a/src/main/java/racingcar/utils/constant/Regex.java b/src/main/java/racingcar/utils/constant/Regex.java new file mode 100644 index 0000000000..56a98bf602 --- /dev/null +++ b/src/main/java/racingcar/utils/constant/Regex.java @@ -0,0 +1,6 @@ +package racingcar.utils.constant; + +public class Regex { + public final static String CAR_NAME = "^[a-z]{1,5}$"; + public final static String TRY_NUMBER = "^[0-9]$"; +} diff --git a/src/main/java/racingcar/view/RaceView.java b/src/main/java/racingcar/view/RaceView.java new file mode 100644 index 0000000000..3d488c1e13 --- /dev/null +++ b/src/main/java/racingcar/view/RaceView.java @@ -0,0 +1,18 @@ +package racingcar.view; + +import java.util.List; +import racingcar.domain.Car; + +public class RaceView { + + public static void printProgress(Car[] carArray) { + for (Car car : carArray) { + System.out.println(car.getName() + " : " + "-".repeat(car.getDistance())); + } + System.out.println(); + } + + public void printWinner(List winners) { + System.out.println("최종 우승자 : " + String.join(", ", winners)); + } +} diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java index 1d35fc33fe..8e033b488d 100644 --- a/src/test/java/racingcar/ApplicationTest.java +++ b/src/test/java/racingcar/ApplicationTest.java @@ -2,11 +2,14 @@ import camp.nextstep.edu.missionutils.test.NsTest; import org.junit.jupiter.api.Test; +import racingcar.controller.RaceController; +import racingcar.domain.Race; 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; +import static org.junit.jupiter.api.Assertions.*; class ApplicationTest extends NsTest { private static final int MOVING_FORWARD = 4; @@ -22,6 +25,31 @@ class ApplicationTest extends NsTest { MOVING_FORWARD, STOP ); } + @Test + void 입력_예외_테스트_입력값_없음(){ + assertSimpleTest(()-> + assertThatThrownBy(() -> run("","")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 입력_예외_테스트_차량이름_입력(){ + assertSimpleTest(()-> + assertThatThrownBy(() -> run("포비,워니,준","4")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 입력_예외_테스트_횟수_입력(){ + assertSimpleTest(()-> + assertThatThrownBy(() -> run("pobi,woni,jun","two")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test void 예외_테스트() { @@ -31,6 +59,11 @@ class ApplicationTest extends NsTest { ); } + @Test + void 기능_테스트_컨트롤러_input(){ + RaceController rc = new RaceController(); + } + @Override public void runMain() { Application.main(new String[]{});