diff --git a/README.md b/README.md index d0286c859f..2393588db1 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ # java-racingcar-precourse + + + +## Constraints +- **JDK 21** 기준, 엔트리포인트: `racingcar.Application.main`. +- **mission-utils** 라이브러리의 `Console`, `Randoms` API만 사용 가능. +- Indent 깊이 **최대 2단계**, 삼항 연산자 금지. -> 메소드를 최대한 쪼개면 뎁스 낮출 수 있음 +- [Java Style Guide](https://github.com/woowacourse/woowacourse-docs/blob/main/styleguide/java) 준수. +- `build.gradle` 수정 금지, 외부 라이브러리 추가 금지. + +## Architecture +- **Application** + - 프로그램의 시작점. 입력 → 게임 실행 → 출력 순서로 흐름 제어. +- **InputParser** + - `parseNames(String)` → `List` (빈 문자열, 5자 초과 이름 검증) + - `parseRounds(String)` → `int` (양의 정수 검증) +- **Car** + - `name`, `position`, `move()` 보유. +- **MoveRules** (Functional Interface) + - `boolean shouldMove()` 정의. + - **RandomMoveRule**: `Randoms.pickNumberInRange(0,9) >= 4` 일 때 true 반환. +- **RacingGame** + - `play(List cars, int rounds, MoveRules rule)` + - 주어진 라운드 수만큼 반복하며 position 업데이트. +- **ResultView** + - `printRound(List)` : `"name : ---"` 형태 출력. + - `printWinners(List)` : `"최종 우승자 : pobi, jun"` 출력. +- **WinnerFinder** + - `findWinners(List)` → `List` (최대 position 기준 복수 우승자 처리) + +> 목적: 입력, 게임 로직, 규칙, 출력 단계를 완전히 분리하여 **작은 메서드** 중심으로 설계. + +## Feautres (커밋 단위) +이름 입력/검증 +시도 횟수 입력/검증 +이동 로직 (≥4 시 전진) +n라운드 실행 & 위치 업데이트 +라운드 단위 출력 +우승자 계산/출력 +입력 예외 처리 (IllegalArgumentException) +단일 책임화 및 indent ≤ 2 유지 (리팩토링) +테스트 (JUnit5 + AssertJ) +g \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..43fdb9cb97 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,131 @@ package racingcar; +import camp.nextstep.edu.missionutils.Console; +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.ArrayList; +import java.util.List; + public class Application { + public static void main(String[] args) { - // TODO: 프로그램 구현 + List names = readNames(); + int rounds = readRounds(); + + RacingGame game = new RacingGame(names, rounds); + + printExecutionHeader(); + runRounds(game.getCars(), rounds); +// printFinalResult(game.getCars()); + printWinners(game.getCars()); + } + + // 사용자 입력 - 이름, 시도 횟수 + private static List readNames() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String namesLine = Console.readLine(); + return InputParser.parseNames(namesLine); + } + + private static int readRounds() { + System.out.println("시도할 횟수는 몇 회인가요?"); + String roundsLine = Console.readLine(); + return InputParser.parseRounds(roundsLine); + } + + // 게임 과정 조회 + private static void printExecutionHeader() { + System.out.println(); + System.out.println("실행 결과"); + } + + private static void runRounds(List cars, int rounds) { + for (int r = 1; r <= rounds; r = r + 1) { + playOneRound(cars); + printRound(cars); + System.out.println(); + } + } + + private static void playOneRound(List cars) { + for (Car car : cars) { + boolean shouldMove = drawAndDecide(); + if (shouldMove) { + car.move(); + } + } + } + + private static boolean drawAndDecide() { + int value = Randoms.pickNumberInRange(0, 9); + if (value >= 4) { + return true; + } + return false; + } + + private static void printRound(List cars) { + for (Car car : cars) { + System.out.print(car.getName()); + System.out.print(" : "); + printDashes(car.getPosition()); + System.out.println(); + } + } + + private static void printDashes(int count) { + for (int i = 0; i < count; i = i + 1) { + System.out.print("-"); + } + } + + private static void printFinalResult(List cars) { + System.out.println("==== GAME RESULT ===="); + for (Car car : cars) { + System.out.print(car.getName()); + System.out.print(" : "); + printDashes(car.getPosition()); + System.out.println(); + } + System.out.println("====================="); + } + + private static void printWinners(List cars) { + int max = findMaxPosition(cars); + List winnerNames = collectWinners(cars, max); + System.out.println("최종 우승자 : " + joinByCommaAndSpace(winnerNames)); + } + + // ---------- helpers ---------- + private static int findMaxPosition(List cars) { + int max = 0; + for (Car car : cars) { + if (car.getPosition() > max) { + max = car.getPosition(); + } + } + return max; + } + + private static List collectWinners(List cars, int max) { + List winners = new ArrayList<>(); + for (Car car : cars) { + if (car.getPosition() == max) { + winners.add(car.getName()); + } + } + return winners; + } + + + private static String joinByCommaAndSpace(List names) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < names.size(); i = i + 1) { + sb.append(names.get(i)); + if (i < names.size() - 1) { + sb.append(", "); + } + } + return sb.toString(); } } diff --git a/src/main/java/racingcar/Car.java b/src/main/java/racingcar/Car.java new file mode 100644 index 0000000000..929b7cd7cd --- /dev/null +++ b/src/main/java/racingcar/Car.java @@ -0,0 +1,29 @@ +package racingcar; + +final class Car { + private final String name; + private int position = 0; + + Car(String name) { + if (name == null) { + throw new IllegalArgumentException("자동차 이름이 null입니다."); + } + String trimmed = name.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("자동차 이름이 비어 있습니다."); + } + this.name = trimmed; + } + + String getName() { + return name; + } + + int getPosition() { + return position; + } + + void move() { + position = position + 1; + } +} diff --git a/src/main/java/racingcar/InputParser.java b/src/main/java/racingcar/InputParser.java new file mode 100644 index 0000000000..c3b55dca84 --- /dev/null +++ b/src/main/java/racingcar/InputParser.java @@ -0,0 +1,75 @@ +package racingcar; + +import java.util.ArrayList; +import java.util.List; + +final class InputParser { + private static final int MAX_NAME_LENGTH = 5; + + private InputParser() { + // utility class + } + + /** 이름 입력 처리 */ + static List parseNames(String line) { + if (line == null) { + throw new IllegalArgumentException("이름 입력이 null입니다."); + } + + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("이름을 한 개 이상 입력해야 합니다."); + } + + String[] parts = trimmed.split(","); + List names = new ArrayList<>(); + + for (String raw : parts) { + String name = ""; + if (raw != null) { + name = raw.trim(); + } + + if (name.isEmpty()) { + throw new IllegalArgumentException("빈 이름은 허용되지 않습니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("이름은 5자 이하여야 합니다: " + name); + } + names.add(name); + } + return names; + } + + /** 라운드 입력 처리 */ + static int parseRounds(String line) { + if (line == null) { + throw new IllegalArgumentException("라운드 입력이 null입니다."); + } + + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("라운드는 1 이상의 정수여야 합니다."); + } + + // 숫자만 허용 + for (int i = 0; i < trimmed.length(); i = i + 1) { + char ch = trimmed.charAt(i); + if (!Character.isDigit(ch)) { + throw new IllegalArgumentException("라운드는 숫자만 입력해야 합니다: " + trimmed); + } + } + + int rounds; + try { + rounds = Integer.parseInt(trimmed); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("라운드 파싱에 실패했습니다: " + trimmed); + } + + if (rounds <= 0) { + throw new IllegalArgumentException("라운드는 1 이상의 정수여야 합니다."); + } + return rounds; + } +} diff --git a/src/main/java/racingcar/MoveRules.java b/src/main/java/racingcar/MoveRules.java new file mode 100644 index 0000000000..6515f47a87 --- /dev/null +++ b/src/main/java/racingcar/MoveRules.java @@ -0,0 +1,6 @@ +package racingcar; + +@FunctionalInterface +interface MoveRules { + boolean shouldMove(); +} diff --git a/src/main/java/racingcar/RacingGame.java b/src/main/java/racingcar/RacingGame.java new file mode 100644 index 0000000000..4f0c673193 --- /dev/null +++ b/src/main/java/racingcar/RacingGame.java @@ -0,0 +1,33 @@ +package racingcar; + +import java.util.ArrayList; +import java.util.List; + +final class RacingGame { + private final List cars = new ArrayList<>(); + private final int rounds; + + RacingGame(List names, int rounds) { + if (names == null || names.isEmpty()) { + throw new IllegalArgumentException("자동차 이름 목록이 비어 있습니다."); + } + if (rounds <= 0) { + throw new IllegalArgumentException("라운드 수는 1 이상의 정수여야 합니다."); + } + + for (String n : names) { + cars.add(new Car(n)); + } + this.rounds = rounds; //테스트용 보관 + } + + /** 자동차 조회 get메소드 */ + List getCars() { + return new ArrayList<>(cars); + } + + /** round 조회 */ + int getRounds() { + return rounds; + } +} diff --git a/src/main/java/racingcar/RandomMoveRule.java b/src/main/java/racingcar/RandomMoveRule.java new file mode 100644 index 0000000000..da71d7c6eb --- /dev/null +++ b/src/main/java/racingcar/RandomMoveRule.java @@ -0,0 +1,15 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Randoms; + +final class RandomMoveRule implements MoveRules { + + @Override + public boolean shouldMove() { + int value = Randoms.pickNumberInRange(0, 9); + if (value >= 4) { + return true; + } + return false; + } +}