diff --git a/README.md b/README.md index 491aece1..8c434f96 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# java-racingcar-precourse \ No newline at end of file +# 카테캠 미니과제 2 - 자동차 경주 + +## 개요 +- 카카오 테크 캠퍼스 2차 미니과제 +- 간단한 자동차 경주 게임을 구현 +- 사용자가 입력한 자동차 이름과 경주 횟수에 따라 자동차들이 랜덤하게 전진하며, 가장 멀리 이동한 자동차가 우승 + +## 기능 목록 + +1. **자동차 경주 게임 초기 설정** + - [X] 자동차 이름 입력 및 검증 + - [X] 경주 횟수 입력 및 검증 + +2. **자동차 객체 생성** + - [X] 각 이름을 바탕으로 자동차 객체 생성 + - [X] 자동차 객체는 이름과 위치를 가짐 + +3. **경주 로직 구현** + - [X] 무작위 값을 통해 자동차의 전진 여부 결정 + - [X] 각 회차 결과 출력 + +4. **우승자 결정 및 출력** + - [X] 가장 멀리 이동한 자동차 결정 + - [X] 여러 우승자가 있을 경우 쉼표로 구분하여 출력 + +5. **입력 오류 처리** + - [X] 잘못된 입력에 대한 예외 처리 및 재입력 요청 + +6. **단위 테스트 작성** + - [X] 주요 로직에 대한 단위 테스트 구현 + +## 요구 사항 +- JDK 17 +- Google Java Style Guide 준수 +- indent depth 2 이내 +- 함수 길이 15라인 이하 +- else, switch/case 사용 금지 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..0da0a8db --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,8 @@ +import controller.CarRaceController; + +public class Application { + public static void main(String[] args) { + CarRaceController controller = new CarRaceController(); + controller.startRace(); + } +} diff --git a/src/main/java/controller/CarRaceController.java b/src/main/java/controller/CarRaceController.java new file mode 100644 index 00000000..32f64bfe --- /dev/null +++ b/src/main/java/controller/CarRaceController.java @@ -0,0 +1,32 @@ +package controller; + +import java.util.List; +import java.util.stream.Collectors; +import model.Car; +import model.Race; +import view.InputView; +import view.OutputView; + +public class CarRaceController { + public void startRace() { + try { + List carNames = InputView.getCarNames(); + int raceCount = InputView.getRaceCount(); + + Race race = new Race(carNames); + race.run(raceCount); + + List winners = race.getWinners(); + printWinners(winners); + } catch (IllegalArgumentException e) { + OutputView.printMessage(e.getMessage()); + } + } + + private void printWinners(List winners) { + String winnerNames = winners.stream() + .map(Car::getName) + .collect(Collectors.joining(", ")); + OutputView.printMessage("최종 우승자 : " + winnerNames); + } +} diff --git a/src/main/java/model/Car.java b/src/main/java/model/Car.java new file mode 100644 index 00000000..147cbe44 --- /dev/null +++ b/src/main/java/model/Car.java @@ -0,0 +1,26 @@ +package model; + +public class Car { + private final String name; + private int position; + + public Car(String name) { + if (name.length() > 5 || name.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 자동차 이름은 1자 이상 5자 이하만 가능합니다."); + } + this.name = name; + this.position = 0; // 생성자에서 위치를 초기화 + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public void move() { + this.position++; + } +} diff --git a/src/main/java/model/Race.java b/src/main/java/model/Race.java new file mode 100644 index 00000000..76a5e802 --- /dev/null +++ b/src/main/java/model/Race.java @@ -0,0 +1,53 @@ +package model; + +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import view.OutputView; // 추가 + +public class Race { + private final List cars; + private static final int FORWARD_THRESHOLD = 4; + private static final int RANDOM_BOUND = 10; + private static final Random RANDOM = new Random(); + + public Race(List carNames) { + this.cars = carNames.stream() + .map(Car::new) + .collect(Collectors.toList()); + } + + public List getCars() { + return cars; + } + + public void run(int raceCount) { + for (int i = 0; i < raceCount; i++) { + moveCars(); + OutputView.printRaceStatus(cars); // 현재 상태 출력 + } + } + + private void moveCars() { + for (Car car : cars) { + moveCar(car); + } + } + + private void moveCar(Car car) { + if (RANDOM.nextInt(RANDOM_BOUND) >= FORWARD_THRESHOLD) { + car.move(); + } + } + + public List getWinners() { + int maxPosition = cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..ce7193f3 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,72 @@ +package view; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class InputView { + private static final BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + + public static List getCarNames() { + while (true) { + try { + OutputView.printMessage("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String input = br.readLine(); + List carNames = parseCarNames(input); + validateCarNames(carNames); + return carNames; + } catch (IOException e) { + handleInputError("[ERROR] 입력을 읽는 중 오류가 발생했습니다. 다시 시도해주세요."); + } catch (IllegalArgumentException e) { + handleInputError(e.getMessage()); + } + } + } + + public static int getRaceCount() { + while (true) { + try { + OutputView.printMessage("시도할 횟수는 몇회인가요?"); + String input = br.readLine(); + return validateRaceCount(input); + } catch (IOException e) { + handleInputError("[ERROR] 입력을 읽는 중 오류가 발생했습니다. 다시 시도해주세요."); + } catch (IllegalArgumentException e) { + handleInputError(e.getMessage()); + } + } + } + + private static void handleInputError(String message) { + OutputView.printMessage(message); + } + + private static List parseCarNames(String input) { + return Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + } + + private static void validateCarNames(List carNames) { + for (String name : carNames) { + if (name.length() > 5 || name.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 자동차 이름은 1자 이상 5자 이하만 가능합니다."); + } + } + } + + private static int validateRaceCount(String input) { + try { + int raceCount = Integer.parseInt(input); + if (raceCount <= 0) { + throw new IllegalArgumentException("[ERROR] 경주 횟수는 1 이상이어야 합니다."); + } + return raceCount; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] 유효한 숫자를 입력하세요."); + } + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..5b42b341 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,19 @@ +package view; + +import model.Car; +import java.util.List; + +public class OutputView { + public static void printMessage(String message) { + System.out.println(message); + } + + public static void printRaceStatus(List cars) { + for (Car car : cars) { + String status = car.getName() + " : " + + "-".repeat(Math.max(0, car.getPosition())); + printMessage(status); + } + printMessage(""); // 빈 줄로 회차 구분 + } +} diff --git a/src/test/java/model/CarTest.java b/src/test/java/model/CarTest.java new file mode 100644 index 00000000..4f789951 --- /dev/null +++ b/src/test/java/model/CarTest.java @@ -0,0 +1,57 @@ +package model; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class CarTest { + @Test + public void carNameShouldBeFiveCharsOrLess() { + IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> new Car("abcdef") + ); + assertEquals("[ERROR] 자동차 이름은 1자 이상 5자 이하만 가능합니다.", thrown.getMessage()); + } + + @Test + public void carNameShouldNotBeEmpty() { + IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> new Car("") + ); + assertEquals("[ERROR] 자동차 이름은 1자 이상 5자 이하만 가능합니다.", thrown.getMessage()); + } + + @Test + public void carNameShouldBeSetCorrectly() { + Car car = new Car("pobi"); + assertEquals("pobi", car.getName()); + } + + @Test + public void carShouldStartAtPositionZero() { + Car car = new Car("test"); + assertEquals(0, car.getPosition()); + } + + @Test + public void carShouldMove() { + Car car = new Car("test"); + car.move(); + assertEquals(1, car.getPosition()); + } + + @Test + public void carShouldMoveMultipleTimes() { + Car car = new Car("test"); + car.move(); + car.move(); + assertEquals(2, car.getPosition()); + } + + @Test + public void carNameShouldAllowSpecialCharacters() { + Car car = new Car("p@bi"); + assertEquals("p@bi", car.getName()); + } +} diff --git a/src/test/java/model/RaceTest.java b/src/test/java/model/RaceTest.java new file mode 100644 index 00000000..b1fdda12 --- /dev/null +++ b/src/test/java/model/RaceTest.java @@ -0,0 +1,85 @@ +package model; + +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class RaceTest { + @Test + public void shouldCreateAndInitializeCars() { + List carNames = List.of("pobi", "woni", "jun"); + Race race = new Race(carNames); + + assertThat(race.getCars()).hasSize(3); + assertThat(race.getCars()).extracting("name", "position") + .containsExactly( + tuple("pobi", 0), + tuple("woni", 0), + tuple("jun", 0) + ); + } + + @Test + public void shouldDetermineWinners() { + List carNames = List.of("pobi", "woni", "jun"); + Race race = new Race(carNames); + + // 강제로 차를 움직여 우승자 결정 + race.getCars().get(0).move(); + race.getCars().get(0).move(); + race.getCars().get(1).move(); + race.getCars().get(2).move(); + race.getCars().get(2).move(); + + List winners = race.getWinners(); + assertThat(winners).hasSize(2); + assertThat(winners).extracting("name") + .contains("pobi", "jun"); + } + + @Test + public void shouldRunRaceAndMoveCars() { + List carNames = List.of("pobi", "woni", "jun"); + Race race = new Race(carNames); + + race.run(5); + + assertThat(race.getCars()).allSatisfy(car -> + assertThat(car.getPosition()).isGreaterThanOrEqualTo(0) + ); + } + + @Test + public void shouldHaveMultipleWinnersIfTied() { + List carNames = List.of("pobi", "woni", "jun"); + Race race = new Race(carNames); + + race.getCars().get(0).move(); + race.getCars().get(1).move(); + race.getCars().get(1).move(); + race.getCars().get(2).move(); + race.getCars().get(2).move(); + + List winners = race.getWinners(); + assertThat(winners).hasSize(2); + assertThat(winners).extracting("name") + .contains("woni", "jun"); + } + + @Test + public void shouldHaveSingleWinnerWhenOnlyOneCarHasMaxPosition() { + List carNames = List.of("pobi", "woni", "jun"); + Race race = new Race(carNames); + + race.getCars().get(0).move(); + race.getCars().get(0).move(); // pobi moves twice + race.getCars().get(1).move(); // woni moves once + race.getCars().get(2).move(); + + List winners = race.getWinners(); + assertThat(winners).hasSize(1); + assertThat(winners).extracting("name") + .contains("pobi"); + } +}