diff --git a/README.md b/README.md index d0286c859f..959efc922a 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ # java-racingcar-precourse +## 기능 요구 사항 체크리스트 +- [ ] 자동차 이름 목록 입력받기 +- [ ] 시도할 횟수 입력받기 +- [ ] 자동차(Car)가 이동 조건을 만족하면 전진(위치 +1) +- [ ] 이동 조건: 0~9 난수 중 4 이상이면 전진 (Randoms.pickNumberInRange 사용) +- [ ] 입력된 횟수만큼 라운드를 반복하며 모든 자동차 이동 +- [ ] 매 라운드마다 `이름 : -----` 형태로 현황 출력 +- [ ] 최종 최대 위치를 가진 자동차(들) 이름을 쉼표로 연결해 출력 +- [ ] 잘못된 입력 시 IllegalArgumentException 발생 후 애플리케이션 종료 \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..c082bc3a55 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,28 @@ package racingcar; +import camp.nextstep.edu.missionutils.Console; +import racingcar.domain.Race; +import racingcar.view.IOView; + +import java.util.List; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + try { + List names = IOView.readCarNames(); + int attempts = IOView.readAttemptCount(); + + Race race = new Race(names); + IOView.printStart(); + + for (int i = 0; i < attempts; i++) { + race.playOneRound(); + IOView.printRound(race.getCars()); + } + + IOView.printWinners(race.findWinners()); + } finally { + Console.close(); + } } } diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..bf8883692d --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,25 @@ +package racingcar.domain; + +import racingcar.util.Validators; + +public class Car { + private final String name; + private int position = 0; + + public Car(String name) { + Validators.validateName(name); + this.name = name.trim(); + } + + public void forward() { + position += 1; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/racingcar/domain/Race.java b/src/main/java/racingcar/domain/Race.java new file mode 100644 index 0000000000..2dfea6ba69 --- /dev/null +++ b/src/main/java/racingcar/domain/Race.java @@ -0,0 +1,50 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import racingcar.util.Validators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Race { + private final List cars; + + public Race(List names) { + Validators.validateNamesList(names); + List list = new ArrayList<>(); + for (String n : names) { + list.add(new Car(n)); + } + this.cars = Collections.unmodifiableList(list); + } + + public void playOneRound() { + for (Car c : cars) { + int n = Randoms.pickNumberInRange(0, 9); + if (n >= 4) { + c.forward(); + } + } + } + + public List getCars() { + return cars; + } + + public List findWinners() { + int max = 0; + for (Car c : cars) { + if (c.getPosition() > max) { + max = c.getPosition(); + } + } + List winners = new ArrayList<>(); + for (Car c : cars) { + if (c.getPosition() == max) { + winners.add(c.getName()); + } + } + return winners; + } +} diff --git a/src/main/java/racingcar/util/Validators.java b/src/main/java/racingcar/util/Validators.java new file mode 100644 index 0000000000..ae4e046046 --- /dev/null +++ b/src/main/java/racingcar/util/Validators.java @@ -0,0 +1,48 @@ +package racingcar.util; + +import java.util.List; + +public class Validators { + + public static void validateName(String name) { + if (name == null) { + throw new IllegalArgumentException("자동차 이름은 null일 수 없습니다."); + } + String t = name.trim(); + if (t.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 비어 있을 수 없습니다."); + } + if (t.length() > 5) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다: " + t); + } + } + + public static void validateNamesList(List names) { + if (names == null || names.isEmpty()) { + throw new IllegalArgumentException("자동차 이름 목록이 비어 있습니다."); + } + for (String n : names) { + validateName(n); + } + } + + public static int parsePositiveInt(String s) { + if (s == null) { + throw new IllegalArgumentException("시도 횟수 입력이 비어 있습니다."); + } + String t = s.trim(); + if (t.isEmpty()) { + throw new IllegalArgumentException("시도 횟수 입력이 비어 있습니다."); + } + int v; + try { + v = Integer.parseInt(t); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("시도 횟수는 정수여야 합니다."); + } + if (v < 1) { + throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); + } + return v; + } +} diff --git a/src/main/java/racingcar/view/IOView.java b/src/main/java/racingcar/view/IOView.java new file mode 100644 index 0000000000..9c756264e9 --- /dev/null +++ b/src/main/java/racingcar/view/IOView.java @@ -0,0 +1,53 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; +import racingcar.domain.Car; +import racingcar.util.Validators; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public class IOView { + + public static List readCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String line = Console.readLine(); + if (line == null) { + throw new IllegalArgumentException("입력이 비어 있습니다."); + } + String[] parts = line.split(","); + List names = new ArrayList<>(); + for (String p : parts) { + names.add(p.trim()); + } + Validators.validateNamesList(names); + return names; + } + + public static int readAttemptCount() { + System.out.println("시도할 횟수는 몇 회인가요?"); + String line = Console.readLine(); + return Validators.parsePositiveInt(line); + } + + public static void printStart() { + System.out.println(); + System.out.println("실행 결과"); + } + + public static void printRound(List cars) { + for (Car c : cars) { + System.out.println(c.getName() + " : " + "-".repeat(c.getPosition())); + } + System.out.println(); + } + + public static void printWinners(List winners) { + StringJoiner sj = new StringJoiner(", "); + for (String w : winners) { + sj.add(w); + } + System.out.println("최종 우승자 : " + sj); + } +} diff --git a/src/test/java/racingcar/CarTest.java b/src/test/java/racingcar/CarTest.java new file mode 100644 index 0000000000..36038149ea --- /dev/null +++ b/src/test/java/racingcar/CarTest.java @@ -0,0 +1,37 @@ +package racingcar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.util.Validators; +import static org.assertj.core.api.Assertions.*; + +class CarTest { + + @Test + @DisplayName("자동차 이름 규칙과 forward 동작을 검증한다") + void car_name_and_forward() { + Car car = new Car("pobi"); + assertThat(car.getPosition()).isZero(); + + car.forward(); + assertThat(car.getPosition()).isEqualTo(1); + + assertThatThrownBy(() -> new Car("abcdef")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new Car(" ")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("시도 횟수 입력은 1 이상 정수만 허용된다") + void validate_attempt_input() { + assertThat(Validators.parsePositiveInt("3")).isEqualTo(3); + assertThatThrownBy(() -> Validators.parsePositiveInt("0")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Validators.parsePositiveInt("-1")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Validators.parsePositiveInt("abc")) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/racingcar/RaceTest.java b/src/test/java/racingcar/RaceTest.java new file mode 100644 index 0000000000..71f9dfe0c0 --- /dev/null +++ b/src/test/java/racingcar/RaceTest.java @@ -0,0 +1,33 @@ +package racingcar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.domain.Race; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class RaceTest { + + @Test + @DisplayName("Race 생성 시 이름 검증 및 초기 상태 확인") + void race_initial_state() { + List names = List.of("pobi", "woni", "jun"); + Race race = new Race(names); + + for (Car c : race.getCars()) { + assertThat(c.getPosition()).isZero(); + } + + assertThat(race.findWinners()).containsExactlyInAnyOrderElementsOf(names); + } + + @Test + @DisplayName("잘못된 이름이 포함되면 Race 생성 시 예외 발생") + void invalid_name_throws_exception() { + assertThatThrownBy(() -> new Race(List.of("abcdef", "pobi"))) + .isInstanceOf(IllegalArgumentException.class); + } +}