diff --git a/README.md b/README.md index ba24573e89a..b3a3f436430 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,123 @@ # 자동차 경주 게임 + ## 진행 방법 -* 자동차 경주 게임 요구사항을 파악한다. -* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. -* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. -* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. + +- 자동차 경주 게임 요구사항을 파악한다. +- 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. +- 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. +- 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) + +- [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) ## 🚀 3단계 - 자동차 경주 ### 기능 요구사항 -* 초간단 자동차 경주 게임을 구현한다. -* 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. -* 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. -* 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다. -* 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다. - -- [x] 자동차의 처음 위치는 0이다. -- [x] 사용자가 입력한 개수만큼의 자동차가 존재한다. -- [x] 사용자가 0 또는 음수의 자동차 개수를 입력하면 에러를 반환한다. -- [x] random값이 4 이상일 경우에 자동차의 위치는 1 추가된다. -- [x] m 라운드가 끝나면 자동차 경주는 종료된다. + +- 초간단 자동차 경주 게임을 구현한다. +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다. +- 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다. + +* [x] 자동차의 처음 위치는 0이다. +* [x] 사용자가 입력한 개수만큼의 자동차가 존재한다. +* [x] 사용자가 0 또는 음수의 자동차 개수를 입력하면 에러를 반환한다. +* [x] random값이 4 이상일 경우에 자동차의 위치는 1 추가된다. +* [x] m 라운드가 끝나면 자동차 경주는 종료된다. ### 프로그래밍 요구사항 -* 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 - * 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. - * UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. -* 자바 코드 컨벤션을 지키면서 프로그래밍한다. - * 이 과정의 Code Style은 intellij idea Code Style. Java을 따른다. - * intellij idea Code Style. Java을 따르려면 code formatting 단축키(Windows : Ctrl + Alt + L. Mac : ⌥ (Option) + ⌘ (Command) + L.)를 사용한다. -* else 예약어를 쓰지 않는다. - * 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. - * else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. + +- 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 + - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + - UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. + - 이 과정의 Code Style은 intellij idea Code Style. Java을 따른다. + - intellij idea Code Style. Java을 따르려면 code formatting 단축키(Windows : Ctrl + Alt + L. Mac : ⌥ (Option) + ⌘ (Command) + L.)를 사용한다. +- else 예약어를 쓰지 않는다. + - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. + - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. ### 개발 구조 + Game : 게임의 시작과 종료를 담당 Race : 전체 라운드의 진행을 담당 +GameSettings : 자동차 경주 게임의 설정값 +Car : 자동차 객체 InputView : 사용자의 입력을 받음 -ResultView : 결과를 출력 \ No newline at end of file +ResultView : 결과를 출력 + +## 🚀 4단계 - 자동차 경주(우승자) + +### 기능 요구사항 + +- 각 자동차에 이름을 부여할 수 있다. 자동차 이름은 5자를 초과할 수 없다. +- 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분한다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다. + +### 구현 TODO 목록 + +1. **자동차 이름 기능** + + - [x] `Car` 클래스에 이름 필드 추가 + - [x] 이름 유효성 검사 (5자 초과 불가) + - [x] `Car` 생성자에 이름 파라미터 추가 + - [x] `CarTest`에 이름 관련 테스트 추가 + - [x] 이름이 5자를 초과하면 예외 발생 + - [x] 유효한 이름으로 생성 가능 + +2. **자동차 이름 출력** + + - [x] `ResultView` 수정 + - [x] 자동차 이름과 위치를 함께 출력하는 형식 변경 + - [x] `presentCars` 메소드 파라미터 수정 (이름 정보 추가) + - [x] `Race` 클래스 수정 + - [x] `getCarPositions` 메소드를 `getCarStatus`로 변경 + - [x] 자동차 이름과 위치를 함께 반환하도록 수정 + +3. **자동차 이름 입력** + + - [x] `InputView` 수정 + - [x] 자동차 이름 입력 메소드 추가 + - [x] 쉼표로 구분된 이름 문자열 파싱 로직 추가 + - [x] `GameSettings` 수정 + - [x] 자동차 이름 목록 필드 추가 + - [x] 생성자 수정 + +4. **우승자 판정** + + - [x] `Race` 클래스에 우승자 판정 로직 추가 + - [x] `getWinners` 메소드 추가 + - [x] 가장 멀리 간 자동차들 찾기 + - [x] `ResultView`에 우승자 출력 메소드 추가 + - [x] `Game` 클래스 수정 + - [x] 경주 종료 후 우승자 출력 로직 추가 + +### 개발 구조 + +- **Game**: 게임의 전체 생명주기 관리 및 사용자 인터랙션 조정 +- **Race**: 경주 진행 상태 관리, 자동차 이동 처리, 우승자 판정 +- **GameSettings**: 게임 설정값(자동차 이름, 라운드 수) 검증 및 관리 +- **Car**: 자동차의 기본 속성(이름, 위치) 관리 +- **InputView**: 사용자 입력 처리 및 검증 +- **ResultView**: 게임 진행 상태 및 결과 출력 +- **CarStatus**: 자동차의 현재 상태를 불변 객체로 표현 +- **MoveStrategy**: 자동차 이동 전략 정의 (RandomMoveStrategy, FixedMoveStrategy) + +### 프로그래밍 요구사항 + +- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. + - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. +- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. + - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. +- 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 + - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + - UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. + - 참고문서: https://google.github.io/styleguide/javaguide.html 또는 https://myeonguni.tistory.com/1596 +- else 예약어를 쓰지 않는다. + - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. + - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/main/java/Car.java b/src/main/java/Car.java index 5175e2535fe..d989069567d 100644 --- a/src/main/java/Car.java +++ b/src/main/java/Car.java @@ -1,19 +1,37 @@ public class Car { - private static final int MOVEMENT_THRESHOLD = 4; + private final String name; private int position = 0; - public void move(int seed) { - if (seed < 0 || seed > 9) { - throw new IllegalArgumentException("Invalid seed: " + seed); + public Car(String name) { + validateName(name); + this.name = name.trim(); + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Name cannot be blank"); } + if (name.length() > 5) { + throw new IllegalArgumentException("Name cannot be longer than 5 characters"); + } + } - if (seed >= MOVEMENT_THRESHOLD) { + public void move(boolean shouldMove) { + if (shouldMove) { position++; } } + public String getName() { + return name; + } + public int getPosition() { return position; } + + public boolean isWinner(int maxPosition) { + return position == maxPosition; + } } diff --git a/src/main/java/CarStatus.java b/src/main/java/CarStatus.java new file mode 100644 index 00000000000..d0302ba48e4 --- /dev/null +++ b/src/main/java/CarStatus.java @@ -0,0 +1,43 @@ +import java.util.Objects; + +public class CarStatus { + + private final String name; + private final int position; + + public CarStatus(Car car) { + this.name = car.getName(); + this.position = car.getPosition(); + } + + // 테스트용 생성자 + public CarStatus(String name, int position) { + this.name = name; + this.position = position; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CarStatus carStatus = (CarStatus) o; + return position == carStatus.position && Objects.equals(name, carStatus.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, position); + } +} diff --git a/src/main/java/Cars.java b/src/main/java/Cars.java new file mode 100644 index 00000000000..6a67f9fd04b --- /dev/null +++ b/src/main/java/Cars.java @@ -0,0 +1,59 @@ +import java.util.ArrayList; +import java.util.List; + +public class Cars { + + private final List cars; + + private Cars(List cars) { + validateCars(cars); + this.cars = new ArrayList<>(cars); + } + + public static Cars fromNames(String[] carNames) { + List carList = new ArrayList<>(); + for (String carName : carNames) { + carList.add(new Car(carName)); + } + return new Cars(carList); + } + + private void validateCars(List cars) { + if (cars == null || cars.isEmpty()) { + throw new IllegalArgumentException("자동차 목록이 비어있습니다."); + } + } + + public List getCarStatuses() { + List statuses = new ArrayList<>(); + for (Car car : cars) { + statuses.add(new CarStatus(car)); + } + return statuses; + } + + public void moveAll(MoveStrategy moveStrategy) { + for (Car car : cars) { + car.move(moveStrategy.shouldMove()); + } + } + + public int findMaxPosition() { + int maxPosition = 0; + for (Car car : cars) { + maxPosition = Math.max(maxPosition, car.getPosition()); + } + return maxPosition; + } + + public List findWinners() { + int maxPosition = findMaxPosition(); + List winners = new ArrayList<>(); + for (Car car : cars) { + if (car.isWinner(maxPosition)) { + winners.add(new CarStatus(car)); + } + } + return winners; + } +} diff --git a/src/main/java/Game.java b/src/main/java/Game.java index a12021dad10..a5b0c4793ef 100644 --- a/src/main/java/Game.java +++ b/src/main/java/Game.java @@ -1,25 +1,20 @@ public class Game { - private final InputView inputView; - private final ResultView resultView; - public Game(InputView inputView, ResultView resultView) { - this.inputView = inputView; - this.resultView = resultView; + public static void main(String[] args) { + Game game = new Game(); + game.start(); } public void start() { - GameSettings settings = inputView.getGameSettings(); - resultView.presentStartMessage(); + GameSettings settings = InputView.getGameSettings(); + ResultView.presentStartMessage(); Race race = new Race(settings); while (race.isRaceInProgress()) { + ResultView.presentCars(race.getCarStatuses()); race.runRound(); - resultView.presentCars(race.getCarPositions()); } - } - - public static void main(String[] args) { - Game game = new Game(new InputView(), new ResultView()); - game.start(); + ResultView.presentCars(race.getCarStatuses()); + ResultView.presentWinners(race.getWinners()); } } diff --git a/src/main/java/GameSettings.java b/src/main/java/GameSettings.java index 6c9d813371a..1a457f068e9 100644 --- a/src/main/java/GameSettings.java +++ b/src/main/java/GameSettings.java @@ -1,17 +1,42 @@ public class GameSettings { - private final int carCount; + + private final String[] carNames; private final int roundCount; - public GameSettings(int carCount, int roundCount) { - this.carCount = carCount; + public GameSettings(String[] carNames, int roundCount) { + validateCarNames(carNames); + validateRoundCount(roundCount); + + this.carNames = carNames; this.roundCount = roundCount; } + private void validateCarNames(String[] carNames) { + if (carNames == null || carNames.length == 0) { + throw new IllegalArgumentException("자동차 이름 목록이 비어있습니다."); + } + for (String name : carNames) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("자동차 이름은 비어있을 수 없습니다."); + } + } + } + + private void validateRoundCount(int roundCount) { + if (roundCount < 1) { + throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); + } + } + + public String[] getCarNames() { + return carNames; + } + public int getCarCount() { - return carCount; + return carNames.length; } public int getRoundCount() { return roundCount; } -} \ No newline at end of file +} diff --git a/src/main/java/InputView.java b/src/main/java/InputView.java index 3858ddbb538..81afbe954db 100644 --- a/src/main/java/InputView.java +++ b/src/main/java/InputView.java @@ -1,19 +1,27 @@ import java.util.Scanner; public class InputView { - private final Scanner scanner; - public InputView() { - this.scanner = new Scanner(System.in); + private static final String CAR_NAME_DELIMITER = ","; + private static final Scanner scanner = new Scanner(System.in); + + private InputView() { + // private 생성자로 인스턴스화 방지 } - public GameSettings getGameSettings() { - int carCount = promptInt("자동차 대수는 몇 대 인가요?"); + public static GameSettings getGameSettings() { + String inputCarNames = prompt("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + String[] carNames = inputCarNames.split(CAR_NAME_DELIMITER); int roundCount = promptInt("시도할 회수는 몇 회 인가요?"); - return new GameSettings(carCount, roundCount); + return new GameSettings(carNames, roundCount); + } + + private static String prompt(String message) { + System.out.println(message); + return scanner.nextLine(); } - private int promptInt(String message) { + private static int promptInt(String message) { System.out.println(message); while (!scanner.hasNextInt()) { System.out.println("That's not a valid number!"); diff --git a/src/main/java/MoveStrategy.java b/src/main/java/MoveStrategy.java new file mode 100644 index 00000000000..82efa93af78 --- /dev/null +++ b/src/main/java/MoveStrategy.java @@ -0,0 +1,5 @@ +@FunctionalInterface +public interface MoveStrategy { + + boolean shouldMove(); +} diff --git a/src/main/java/Race.java b/src/main/java/Race.java index 652af54f1ff..27eb70b6fb4 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -1,46 +1,50 @@ -import java.util.ArrayList; import java.util.List; -import java.util.Random; public class Race { - private static final Random random = new Random(); + private final MoveStrategy moveStrategy; private final int totalRounds; - private final List cars; + private final Cars cars; private int currentRound = 0; public Race(GameSettings settings) { - if (settings.getCarCount() < 1 || settings.getRoundCount() < 1) { - throw new IllegalArgumentException("Invalid game settings: " + settings); - } + this(settings, new RandomMoveStrategy()); + } + public Race(GameSettings settings, MoveStrategy moveStrategy) { this.totalRounds = settings.getRoundCount(); - this.cars = new ArrayList<>(); - for (int i = 0; i < settings.getCarCount(); i++) { - this.cars.add(new Car()); - } + this.cars = Cars.fromNames(settings.getCarNames()); + this.moveStrategy = moveStrategy; } - public List getCarPositions() { - List positions = new ArrayList<>(); - for (Car car : cars) { - positions.add(car.getPosition()); - } - return positions; + public List getCarStatuses() { + return cars.getCarStatuses(); } public void runRound() { + validateRaceInProgress(); + cars.moveAll(moveStrategy); + currentRound++; + } + + public boolean isRaceInProgress() { + return currentRound < totalRounds; + } + + private void validateRaceInProgress() { if (!isRaceInProgress()) { throw new IllegalStateException("Race has already finished"); } + } - for (Car car : cars) { - car.move(random.nextInt(10)); + private void validateRaceFinished() { + if (isRaceInProgress()) { + throw new IllegalStateException("Race is still in progress"); } - currentRound++; } - public boolean isRaceInProgress() { - return currentRound < totalRounds; + public List getWinners() { + validateRaceFinished(); + return cars.findWinners(); } } diff --git a/src/main/java/RandomMoveStrategy.java b/src/main/java/RandomMoveStrategy.java new file mode 100644 index 00000000000..87368c73a60 --- /dev/null +++ b/src/main/java/RandomMoveStrategy.java @@ -0,0 +1,13 @@ +import java.util.Random; + +public class RandomMoveStrategy implements MoveStrategy { + + private static final int RANDOM_NUMBER_RANGE = 10; + private static final int MOVE_THRESHOLD = 4; + private final Random random = new Random(); + + @Override + public boolean shouldMove() { + return random.nextInt(RANDOM_NUMBER_RANGE) >= MOVE_THRESHOLD; + } +} diff --git a/src/main/java/ResultView.java b/src/main/java/ResultView.java index 9c175aedb95..82f0332b796 100644 --- a/src/main/java/ResultView.java +++ b/src/main/java/ResultView.java @@ -1,15 +1,30 @@ import java.util.List; +import java.util.StringJoiner; public class ResultView { - public void presentStartMessage() { + private ResultView() { + // private 생성자로 인스턴스화 방지 + } + + public static void presentStartMessage() { System.out.println("실행 결과"); } - public void presentCars(List carPositions) { - for (int carPosition : carPositions) { - System.out.println("-".repeat(carPosition + 1)); + public static void presentCars(List cars) { + for (CarStatus car : cars) { + String positionIndicator = "-".repeat(car.getPosition() + 1); + String output = String.format("%s : %s", car.getName(), positionIndicator); + System.out.println(output); } System.out.println(); } + + public static void presentWinners(List winners) { + StringJoiner joiner = new StringJoiner(", "); + for (CarStatus winner : winners) { + joiner.add(winner.getName()); + } + System.out.println(joiner + "가 최종 우승했습니다."); + } } diff --git a/src/test/java/CarTest.java b/src/test/java/CarTest.java index 17f6e41b84c..91a5417a416 100644 --- a/src/test/java/CarTest.java +++ b/src/test/java/CarTest.java @@ -1,31 +1,45 @@ import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class CarTest { @Test - @DisplayName("랜덤 변수가 4이상이면 차가 한 칸 움직인다.") - void carMovesOneStepIfRandomNumberIsGreaterThanEqual4() { - for (int i = 0; i < 4; i++) { - Car car = new Car(); - car.move(i); - assertThat(car.getPosition()).isEqualTo(0); - } - for (int i = 4; i < 10; i++) { - Car car = new Car(); - car.move(i); - assertThat(car.getPosition()).isEqualTo(1); - } + @DisplayName("자동차는 이름을 가질 수 있다.") + void carHasName() { + Car car = new Car("MyCar"); + assertThat(car.getName()).isEqualTo("MyCar"); } @Test - @DisplayName("자동차를 움직이는 변수는 0에서 9사이의 값이다.") - void carMoveVariableIsBetween0And9() { - Car car = new Car(); - assertThrows(IllegalArgumentException.class, () -> car.move(-1)); - assertThrows(IllegalArgumentException.class, () -> car.move(10)); + @DisplayName("자동차 이름은 빈 문자열이 될 수 없다") + void carNameCannotBeEmpty() { + assertThatThrownBy(() -> new Car("")).isInstanceOf(IllegalArgumentException.class).hasMessage("Name cannot be blank"); + } + + @Test + @DisplayName("자동차 이름은 공백만으로 구성될 수 없다") + void carNameCannotBeBlank() { + assertThatThrownBy(() -> new Car(" ")).isInstanceOf(IllegalArgumentException.class).hasMessage("Name cannot be blank"); + } + + @Test + @DisplayName("자동차 이름은 5자를 초과할 수 없다") + void carNameCannotExceed5Characters() { + assertThatThrownBy(() -> new Car("123456")).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Name cannot be longer than 5 characters"); + } + + @ParameterizedTest + @DisplayName("shouldMove가 true일 경우 자동차의 위치는 1 추가되고, false일 경우 위치가 변하지 않는다") + @CsvSource({"false, 0", "true, 1"}) + void carMovesAccordingToShouldMove(boolean shouldMove, int expectedPosition) { + Car car = new Car("MyCar"); + car.move(shouldMove); + assertThat(car.getPosition()).isEqualTo(expectedPosition); } } diff --git a/src/test/java/CarsTest.java b/src/test/java/CarsTest.java new file mode 100644 index 00000000000..772b0b3c87d --- /dev/null +++ b/src/test/java/CarsTest.java @@ -0,0 +1,57 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CarsTest { + + @Test + @DisplayName("단독 우승자가 있는 경우 해당 자동차만 반환한다") + void getSingleWinner() { + // given + Cars cars = createCars("car1", "car2", "car3"); + cars.moveAll(new FixedMoveStrategy(new boolean[]{true, false, false})); + + // then + assertThat(cars.findWinners()).hasSize(1); + assertThat(cars.findWinners()).contains(createCarStatus("car1", 1)); + } + + @Test + @DisplayName("공동 우승자가 있는 경우 모든 우승자를 반환한다") + void getMultipleWinners() { + // given + Cars cars = createCars("car1", "car2", "car3"); + cars.moveAll(new FixedMoveStrategy(new boolean[]{true, true, false})); + + // then + assertThat(cars.findWinners()).hasSize(2); + assertThat(cars.findWinners()).contains(createCarStatus("car1", 1), createCarStatus("car2", 1)); + } + + private Cars createCars(String... names) { + return Cars.fromNames(names); + } + + private CarStatus createCarStatus(String name, int position) { + return new CarStatus(name, position); + } + + private static class FixedMoveStrategy implements MoveStrategy { + + private final boolean[] moves; + private int index = 0; + + FixedMoveStrategy(boolean[] moves) { + this.moves = moves; + } + + @Override + public boolean shouldMove() { + if (index >= moves.length) { + index = 0; + } + return moves[index++]; + } + } +} diff --git a/src/test/java/GameSettingsTest.java b/src/test/java/GameSettingsTest.java new file mode 100644 index 00000000000..535482d9fc2 --- /dev/null +++ b/src/test/java/GameSettingsTest.java @@ -0,0 +1,48 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class GameSettingsTest { + + @Test + @DisplayName("자동차 이름 목록이 비어있으면 에러가 발생한다") + void emptyCarNamesListThrowsError() { + assertThatThrownBy(() -> new GameSettings(new String[0], 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름 목록이 비어있습니다."); + + assertThatThrownBy(() -> new GameSettings(null, 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름 목록이 비어있습니다."); + } + + @Test + @DisplayName("자동차 이름이 비어있으면 에러가 발생한다") + void emptyCarNameThrowsError() { + String[] carNamesWithBlank = {"car1", "", "car3"}; + assertThatThrownBy(() -> new GameSettings(carNamesWithBlank, 3)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + + String[] carNamesWithNull = {"car1", null, "car3"}; + assertThatThrownBy(() -> new GameSettings(carNamesWithNull, 3)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("시도 횟수가 1 미만이면 에러가 발생한다") + void roundCountLessThanOneThrowsError() { + String[] carNames = {"car1", "car2", "car3"}; + assertThatThrownBy(() -> new GameSettings(carNames, 0)).isInstanceOf(IllegalArgumentException.class).hasMessage("시도 횟수는 1 이상이어야 합니다."); + + assertThatThrownBy(() -> new GameSettings(carNames, -1)).isInstanceOf(IllegalArgumentException.class).hasMessage("시도 횟수는 1 이상이어야 합니다."); + } + + @Test + @DisplayName("유효한 입력으로 GameSettings가 생성된다") + void validInputCreatesGameSettings() { + String[] carNames = {"car1", "car2", "car3"}; + GameSettings settings = new GameSettings(carNames, 3); + + assertThat(settings.getCarNames()).isEqualTo(carNames); + assertThat(settings.getCarCount()).isEqualTo(3); + assertThat(settings.getRoundCount()).isEqualTo(3); + } +} diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index 7f7bdfd8251..e2ca442fd6c 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -1,6 +1,5 @@ import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -9,65 +8,48 @@ class RaceTest { @Test - @DisplayName("자동차 개수는 양수여야 한다.") - void negativeOrZeroCarCountInputReturnsError() { - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(-1, 3)); - }); - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(0, 3)); - }); - - Race race = new Race(new GameSettings(1, 3)); - assertThat(race.getCarPositions()).hasSize(1); - } - - @Test - @DisplayName("경주 횟수는 양수여야 한다.") - void negativeOrZeroRaceCountInputReturnsError() { - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(5, -1)); - }); - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(5, 0)); - }); - - assertDoesNotThrow(() -> { - new Race(new GameSettings(5, 1)); - }); - } - - @Test - @DisplayName("경기 시작시 자동차의 위치는 0이다.") + @DisplayName("경기 시작시 자동차의 위치는 0이다") void carPositionsAtStartAreZero() { - Race race = new Race(new GameSettings(5, 3)); - for (Integer carPositions : race.getCarPositions()) { - assertThat(carPositions).isZero(); + String[] carNames = {"car1", "car2", "car3", "car4", "car5"}; + Race race = new Race(new GameSettings(carNames, 3)); + for (CarStatus carStatus : race.getCarStatuses()) { + assertThat(carStatus.getPosition()).isZero(); } } @Test - @DisplayName("한 라운드가 진행되면 자동차의 위치는 기존 위치이거나, 기존 위치 + 1이다.") + @DisplayName("한 라운드가 진행되면 자동차의 위치는 기존 위치이거나, 기존 위치 + 1이다") void carPositionsAfterOneRoundAreEitherSameOrIncremented() { - Race race = new Race(new GameSettings(5, 3)); - race.runRound(); - List initialPositions = race.getCarPositions(); + String[] carNames = {"car1", "car2", "car3", "car4", "car5"}; + Race race = new Race(new GameSettings(carNames, 3)); + List initialStatuses = race.getCarStatuses(); race.runRound(); - List finalPositions = race.getCarPositions(); + List finalStatuses = race.getCarStatuses(); - for (int i = 0; i < initialPositions.size(); i++) { - assertThat(finalPositions.get(i)).isIn(initialPositions.get(i), initialPositions.get(i) + 1); + for (int i = 0; i < initialStatuses.size(); i++) { + assertThat(finalStatuses.get(i).getPosition()).isIn(initialStatuses.get(i).getPosition(), initialStatuses.get(i).getPosition() + 1); } } @Test - @DisplayName("전체 라운드를 넘어가면 에러가 발생한다.") + @DisplayName("전체 라운드를 넘어가면 에러가 발생한다") void exceedingTotalRoundsThrowsError() { - Race race = new Race(new GameSettings(5, 3)); + String[] carNames = {"car1", "car2", "car3", "car4", "car5"}; + Race race = new Race(new GameSettings(carNames, 3)); for (int i = 0; i < 3; i++) { race.runRound(); } - assertThrows(IllegalStateException.class, race::runRound); + assertThatThrownBy(race::runRound).isInstanceOf(IllegalStateException.class).hasMessage("Race has already finished"); + } + + @Test + @DisplayName("경주가 진행 중일 때 우승자를 조회하면 예외가 발생한다") + void getWinnersBeforeRaceFinishThrowsError() { + String[] carNames = {"car1", "car2"}; + Race race = new Race(new GameSettings(carNames, 3)); + race.runRound(); // 1라운드만 진행 + + assertThatThrownBy(race::getWinners).isInstanceOf(IllegalStateException.class).hasMessage("Race is still in progress"); } }