diff --git a/README.md b/README.md index d0286c859f..a22620ab1d 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ # java-racingcar-precourse +# 자동차 경주 +## 학습 목표 +- 여러 역할을 수행하는 큰 함수를 단일 역할을 수행하는 작은 함수로 분리한다. +- 테스트 도구를 사용하는 방법을 배우고 프로그램이 제대로 작동하는지 테스트한다. +- 1주 차 공통 피드백(디스코드 참고)을 최대한 반영한다. + +## 기능 요구 사항 +초간단 자동차 경주 게임을 구현한다. + +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. +- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. +- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다. + +## 프로그래밍 요구 사항 1 +- JDK 21 버전에서 실행 가능해야 한다. +- 프로그램 실행의 시작점은 Application의 main()이다. +- build.gradle 파일은 변경할 수 없으며, 제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다. +- 프로그램 종료 시 System.exit()를 호출하지 않는다. +- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. +- 기본적으로 Java Style Guide를 원칙으로 한다. + +## 프로그래밍 요구 사항 2 +- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. +- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. +- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. +- 3항 연산자를 쓰지 않는다. +- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. +- JUnit 5와 AssertJ를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다. + +## 기능 목록 +- [x] 사용자에게 자동차 이름 입력 받기 + - [x] null이라면? 공백이라면? (예외: 한글자 이상 이름 입력) +- [x] 입력 받은 이름을 `,` 기준으로 파싱 +- [x] 입력 받은 이름 예외 검사 + - [x] 5자 이상이라면? (예외: 5자 이하로만 이름 부여 가능) + - [x] 문자가 아닌 숫자나 `,`이외의 기호라면? (예외: 영문자만 입력 가능) + - [x] 빈 토큰이 있다면? (예외: 빈 이름은 허용되지 않음) +- [x] 사용자에게 이동 횟수 입력 받기 + - [x] null이라면? 공백이라면? (예외: 잘못된 숫자 입력) +- [x] 입력 받은 횟수 숫자로 변환 + - [x] 숫자가 아닌 값을 입력했다면? (예외: 숫자만 입력 가능) +- [x] 입력 받은 횟수 예외 검사 + - [x] 입력된 숫자가 0이라면? (예외: 1 이상의 숫자를 입력) +- [x] 자동차 수만큼 0-9 숫자 랜덤 돌리기 +- [x] 랜덤 결과로 +1 or 0 저장 +- [x] +1 or 0 만큼 전진 or 유지 +- [x] 사용자에게 과정 출력 +- [x] 최종 우승자 출력 \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..a15657ef7c 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,18 @@ package racingcar; +import java.util.List; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + String plyeNames = NameInputView.nameInput(); + List names = NameParser.split(plyeNames); + names = NameValidator.validate(names); + + String playCount = CountInputView.countInput(); + int count = CountParser.parse(playCount); + count = CountValidator.countValidate(count); + + RacingGame.run(names, count); + } } diff --git a/src/main/java/racingcar/CountInputView.java b/src/main/java/racingcar/CountInputView.java new file mode 100644 index 0000000000..fa506874f5 --- /dev/null +++ b/src/main/java/racingcar/CountInputView.java @@ -0,0 +1,19 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Console; + +public class CountInputView { + + private CountInputView() {} + + public static String countInput() { + System.out.println("시도할 횟수는 몇 회 인가요?"); + String countInput = Console.readLine(); + + if (countInput == null || countInput.isBlank()) { + throw new IllegalArgumentException("숫자를 입력해야 합니다."); + } + + return countInput; + } +} diff --git a/src/main/java/racingcar/CountParser.java b/src/main/java/racingcar/CountParser.java new file mode 100644 index 0000000000..7c4caf52ff --- /dev/null +++ b/src/main/java/racingcar/CountParser.java @@ -0,0 +1,14 @@ +package racingcar; + +public class CountParser { + + private CountParser() {} + + public static int parse(String countInput) { + try { + return Integer.parseInt(countInput); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("시도 횟수는 숫자만 입력 가능합니다."); + } + } +} diff --git a/src/main/java/racingcar/CountValidator.java b/src/main/java/racingcar/CountValidator.java new file mode 100644 index 0000000000..7be3a9b0b1 --- /dev/null +++ b/src/main/java/racingcar/CountValidator.java @@ -0,0 +1,14 @@ +package racingcar; + +public class CountValidator { + + private CountValidator() {} + + public static int countValidate(int countInput) { + if (countInput < 1) { + throw new IllegalArgumentException("1 이상의 시도 횟수를 입력해야 합니다."); + } + + return countInput; + } +} diff --git a/src/main/java/racingcar/MoveAction.java b/src/main/java/racingcar/MoveAction.java new file mode 100644 index 0000000000..f1065e2799 --- /dev/null +++ b/src/main/java/racingcar/MoveAction.java @@ -0,0 +1,27 @@ +package racingcar; + +import java.util.List; +import java.util.Map; + +public class MoveAction { + + private MoveAction() {} + + public static void printAction(List names, Mappositions, List moves) { + + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + int move = moves.get(i); + + if (move == 1) { + positions.put(name, positions.get(name) + 1); + } + } + + for (String name : names) { + int position = positions.get(name); + System.out.println(name + " : " + "-".repeat(position)); + } + System.out.println(); + } +} diff --git a/src/main/java/racingcar/MoveResult.java b/src/main/java/racingcar/MoveResult.java new file mode 100644 index 0000000000..14f87b2ec1 --- /dev/null +++ b/src/main/java/racingcar/MoveResult.java @@ -0,0 +1,23 @@ +package racingcar; + +import java.util.ArrayList; +import java.util.List; + +public class MoveResult { + + private MoveResult() {} + + public static List generateMoveResults(List results) { + List moves = new ArrayList<>(); + + for (Integer result : results) { + if (result < 4) { + moves.add(0); + continue; + } + moves.add(1); + } + + return moves; + } +} diff --git a/src/main/java/racingcar/NameInputView.java b/src/main/java/racingcar/NameInputView.java new file mode 100644 index 0000000000..436706b148 --- /dev/null +++ b/src/main/java/racingcar/NameInputView.java @@ -0,0 +1,19 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Console; + +public class NameInputView { + + private NameInputView() {} + + public static String nameInput() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String name = Console.readLine(); + + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("한 글자 이상 이름을 입력해야 합니다."); + } + + return name; + } +} diff --git a/src/main/java/racingcar/NameParser.java b/src/main/java/racingcar/NameParser.java new file mode 100644 index 0000000000..7edef6b360 --- /dev/null +++ b/src/main/java/racingcar/NameParser.java @@ -0,0 +1,15 @@ +package racingcar; + +import java.util.List; +import java.util.Arrays; + +public class NameParser { + + private NameParser() {} + + public static List split(String name) { + return Arrays.stream(name.split(",")) + .map(String::trim) + .toList(); + } +} diff --git a/src/main/java/racingcar/NameValidator.java b/src/main/java/racingcar/NameValidator.java new file mode 100644 index 0000000000..b2773f6d13 --- /dev/null +++ b/src/main/java/racingcar/NameValidator.java @@ -0,0 +1,32 @@ +package racingcar; + +import java.util.List; + +public class NameValidator { + + private NameValidator() {} + + public static List validate(List names) { + if (names.isEmpty()) { + throw new IllegalArgumentException("한 글자 이상 이름을 입력해야 합니다."); + } + + for (String name : names) { + if (name.length() > 5) { + throw new IllegalArgumentException("5자 이하로만 이름 부여가 가능합니다."); + } + + if (!name.matches("[a-zA-Z]+")) { + throw new IllegalArgumentException("영문자만 입력 가능합니다."); + } + + if (name.isBlank()) { + throw new IllegalArgumentException("빈 이름은 허용되지 않습니다."); + } + } + + return names; + } + + +} diff --git a/src/main/java/racingcar/RacingGame.java b/src/main/java/racingcar/RacingGame.java new file mode 100644 index 0000000000..5da7f92f86 --- /dev/null +++ b/src/main/java/racingcar/RacingGame.java @@ -0,0 +1,48 @@ +package racingcar; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RacingGame { + + public static void run(List names, int count) { + Map positions = initPositions(names); + System.out.println("\n실행 결과"); + + for (int i = 0; i < count; i++) { + playRound(names, positions); + } + + int max = 0; + for (Integer distance : positions.values()) { + if (distance > max) { + max = distance; + } + } + + List winners = new ArrayList<>(); + for (String name : positions.keySet()) { + if (positions.get(name) == max) { + winners.add(name); + } + } + + System.out.println("최종 우승자 : " + String.join(", " , winners)); + } + + private static Map initPositions(List names) { + Map positions = new LinkedHashMap<>(); + for (String name : names) { + positions.put(name, 0); + } + return positions; + } + + private static void playRound(List names, Map positions) { + List randomNumbers = RandomGenerator.numbersForOneRound(1, names); + List moves = MoveResult.generateMoveResults(randomNumbers); + MoveAction.printAction(names, positions, moves); + } +} diff --git a/src/main/java/racingcar/RandomGenerator.java b/src/main/java/racingcar/RandomGenerator.java new file mode 100644 index 0000000000..cdb6f83b7e --- /dev/null +++ b/src/main/java/racingcar/RandomGenerator.java @@ -0,0 +1,21 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; +import java.util.List; + +public class RandomGenerator { + + private RandomGenerator() {} + + public static List numbersForOneRound(int countInput, List names) { + List results = new ArrayList<>(names.size()); + + for (int i = 0; i < names.size(); i++) { + int number = Randoms.pickNumberInRange(0, 9); + results.add(number); + } + + return results; + } +} diff --git a/src/test/java/racingcar/NameValidatorTest.java b/src/test/java/racingcar/NameValidatorTest.java new file mode 100644 index 0000000000..c5ceb581d9 --- /dev/null +++ b/src/test/java/racingcar/NameValidatorTest.java @@ -0,0 +1,23 @@ +package racingcar; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class NameValidatorTest { + + @Test + void emptyList_throws() { + assertThatThrownBy(() -> NameValidator.validate(List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("한 글자 이상"); + } + + @Test + void blankElement_throws() { + assertThatThrownBy(() -> NameValidator.validate(List.of("pobi", " "))) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/racingcar/TryCountValidatorTest.java b/src/test/java/racingcar/TryCountValidatorTest.java new file mode 100644 index 0000000000..e8c81d2f61 --- /dev/null +++ b/src/test/java/racingcar/TryCountValidatorTest.java @@ -0,0 +1,17 @@ +package racingcar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class TryCountValidatorTest { + + @Test + void nonPositive_throws() { + assertThatThrownBy(() -> CountValidator.countValidate(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("1 이상"); + } + +} +