Skip to content
Open
183 changes: 38 additions & 145 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,163 +1,56 @@
# 미션 - 자동차 경주

## 🔍 진행 방식
## 프로젝트 구조

- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.
### 도메인 (Domain)
레이싱 카 게임의 핵심 비즈니스 로직입니다.

## 📮 미션 제출 방법
- **Car**: 이름과 위치를 가진 단일 자동차를 표현
- **RaceTrack**: 자동차 컬렉션과 이동을 관리
- **RacingGame**: 게임 흐름, 라운드, 최종 결과를 제어

- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다.
- GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고해 제출한다.
- GitHub에 미션을 제출한 후 [우아한테크코스 지원](https://apply.techcourse.co.kr) 사이트에 접속하여 프리코스 과제를 제출한다.
- 자세한 방법은 [제출 가이드](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 참고
- **Pull Request만 보내고 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.**
### 애플리케이션 (Application)
게임 흐름을 조율하고 도메인과 프레젠테이션을 연결합니다.

## 🚨 과제 제출 전 체크 리스트 - 0점 방지
- **GameSetUp**: 초기 게임 설정을 처리
- **GameExecutor**: 게임 실행과 결과 발표를 관리
- **RacingGameFacade**: 게임 운영을 위한 단순화된 메서드 제공

- 기능 구현을 모두 정상적으로 했더라도 **요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리**한다.
- 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는지 확인한다.
- **테스트가 실패할 경우 0점으로 처리**되므로, 반드시 확인 후 제출한다.
### 컨트롤러 (Controller)
애플리케이션 계층과 뷰 간의 상호작용을 조정합니다.

### 테스트 실행 가이드
- **RacingGameController**: 레이싱 게임 실행의 진입점

- 터미널에서 `java -version`을 실행하여 Java 버전이 17인지 확인한다.
Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 17로 실행되는지 확인한다.
- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고,
Windows 사용자의 경우 `gradlew.bat clean test` 또는 `./gradlew.bat clean test` 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다.
### 뷰 (View)
사용자 인터페이스 관련 기능을 처리합니다.

```
BUILD SUCCESSFUL in 0s
```
- **InputView**: 자동차 이름과 레이스 횟수에 대한 사용자 입력 관리
- **OutputView**: 레이스 결과와 우승자 표시

---
### DTO (Data Transfer Objects)
계층 간 안전하게 데이터를 전달하기 위한 객체입니다.

## 🚀 기능 요구 사항
- **CarStatus**: 자동차의 이름과 위치에 관한 정보
- **RoundResult**: 단일 라운드 레이싱 결과
- **GameResult**: 우승자 이름을 포함한 최종 게임 결과

초간단 자동차 경주 게임을 구현한다.
### 공통 (Common)
유틸리티 클래스와 상수입니다.

- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.
- **AppConfig**: 애플리케이션 설정 및 의존성 주입
- **RacingGameConfig**: 게임 관련 설정
- **InputParser**: 사용자 입력의 파싱 및 유효성 검사
- **Constants**: 오류 메시지, 게임 규칙, UI 텍스트를 위한 다양한 상수

### 입출력 요구 사항
## 설계 결정

#### 입력
### 싱글톤 패턴
- AppConfig는 애플리케이션 전체 의존성 관리를 위해 싱글톤 패턴 사용

- 경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분)
### 퍼사드 패턴
- 컨트롤러가 내부 복잡도를 몰라도 되도록 단순화된 API를 제공한다는 점에서 Facade 패턴

```
pobi,woni,jun
```
### 전략 패턴
- 이동 로직은 RaceTrack에 캡슐화되어 다양한 이동 전략을 허용함

- 시도할 회수

```
5
```

#### 출력

- 각 차수별 실행 결과

```
pobi : --
woni : ----
jun : ---
```

- 단독 우승자 안내 문구

```
최종 우승자 : pobi
```

- 공동 우승자 안내 문구

```
최종 우승자 : pobi, jun
```

#### 실행 결과 예시

```
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 회수는 몇회인가요?
5

실행 결과
pobi : -
woni :
jun : -

pobi : --
woni : -
jun : --

pobi : ---
woni : --
jun : ---

pobi : ----
woni : ---
jun : ----

pobi : -----
woni : ----
jun : -----

최종 우승자 : pobi, jun
```

---

## 🎯 프로그래밍 요구 사항

- JDK 17 버전에서 실행 가능해야 한다. **JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.**
- 프로그램 실행의 시작점은 `Application`의 `main()`이다.
- `build.gradle` 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
- [Java 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/master/styleguide/java) 가이드를 준수하며 프로그래밍한다.
- 프로그램 종료 시 `System.exit()`를 호출하지 않는다.
- 프로그램 구현이 완료되면 `ApplicationTest`의 모든 테스트가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.**
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.

### 추가된 요구 사항

- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
- 테스트 도구 사용법이 익숙하지 않다면 `test/java/study`를 참고하여 학습한 후 테스트를 구현한다.

### 라이브러리

- JDK에서 제공하는 Random 및 Scanner API 대신 `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다.
- Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다.
- 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.

#### 사용 예시

- 0에서 9까지의 정수 중 한 개의 정수 반환

```java
Randoms.pickNumberInRange(0,9);
```

---

## ✏️ 과제 진행 요구 사항

- 미션은 [java-racingcar-6](https://github.com/woowacourse-precourse/java-racingcar-6) 저장소를 Fork & Clone해 시작한다.
- **기능을 구현하기 전 `docs/README.md`에 구현할 기능 목록을 정리**해 추가한다.
- **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다.
- [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다.
- 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고한다.
### 불변성
- DTO는 스레드 안전성과 예측 가능성을 위해 불변으로 설계됨
7 changes: 6 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package racingcar;

import racingcar.common.config.AppConfig;
import racingcar.controller.RacingGameController;

public class Application {

public static void main(String[] args) {
// TODO: 프로그램 구현
RacingGameController controller = AppConfig.getInstance().getRacingGameController();
controller.run();
}
}
29 changes: 29 additions & 0 deletions src/main/java/racingcar/application/GameExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package racingcar.application;

import racingcar.domain.RacingGame;
import racingcar.dto.GameResult;
import racingcar.dto.RoundResult;
import racingcar.view.OutputView;

public class GameExecutor {

private final OutputView outputView;

public GameExecutor(OutputView outputView) {
this.outputView = outputView;
}

public void executeGame(RacingGame racingGame) {
outputView.printExecuteResult();

for(int i = 0; i < racingGame.getTotalRound(); i++) {
RoundResult result = racingGame.executeRound();
outputView.printCurrentRoundResult(result);
}
}

public void announceWinner(RacingGame racingGame) {
GameResult result = racingGame.determineWinner();
outputView.printWinner(result);
}
}
34 changes: 34 additions & 0 deletions src/main/java/racingcar/application/GameSetUp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package racingcar.application;

import racingcar.common.parser.InputParser;
import racingcar.common.config.RacingGameConfig;
import racingcar.view.InputView;

import java.util.List;

public class GameSetUp {

private final InputView inputView;
private final InputParser inputParser;

public GameSetUp(InputView inputView, InputParser inputParser) {
this.inputView = inputView;
this.inputParser = inputParser;
}

public RacingGameConfig createRacingGameConfig() {
List<String> carNames = setUpCarNames();
int totalRound = setUpTotalRound();
return new RacingGameConfig(carNames, totalRound);
}

private List<String> setUpCarNames() {
String carNamesInput = inputView.printCarNames();
return inputParser.parseCarNames(carNamesInput);
}

private int setUpTotalRound() {
String totalRoundInput = inputView.printRaceCount();
return inputParser.parseTotalRound(totalRoundInput);
}
}
25 changes: 25 additions & 0 deletions src/main/java/racingcar/application/RacingGameFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package racingcar.application;

import racingcar.common.config.RacingGameConfig;
import racingcar.domain.RacingGame;

public class RacingGameFacade {

private final GameSetUp gameSetUp;
private final GameExecutor gameExecutor;

public RacingGameFacade(
GameSetUp gameSetUp,
GameExecutor gameExecutor
) {
this.gameSetUp = gameSetUp;
this.gameExecutor = gameExecutor;
}

public void playGame() {
RacingGameConfig config = gameSetUp.createRacingGameConfig();
RacingGame racingGame = new RacingGame(config);
gameExecutor.executeGame(racingGame);
gameExecutor.announceWinner(racingGame);
}
}
40 changes: 40 additions & 0 deletions src/main/java/racingcar/common/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package racingcar.common.config;

import racingcar.common.parser.InputParser;
import racingcar.controller.RacingGameController;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public final class AppConfig {

private static final AppConfig INSTANCE = new AppConfig();
private final RacingGameController racingGameController;

private AppConfig() {
this.racingGameController = new RacingGameController(
inputView(),
outputView(),
parser()
);
}

public static AppConfig getInstance() {
return INSTANCE;
}

public RacingGameController getRacingGameController() {
return racingGameController;
}

private InputView inputView() {
return new InputView();
}

private OutputView outputView() {
return new OutputView();
}

private InputParser parser() {
return new InputParser();
}
}
39 changes: 39 additions & 0 deletions src/main/java/racingcar/common/config/RacingGameConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package racingcar.common.config;

import racingcar.common.constant.ErrorMessages;

import java.util.ArrayList;
import java.util.List;

public class RacingGameConfig {

private final List<String> carNames = new ArrayList<>();
private final int totalRound;

public RacingGameConfig(List<String> carNames, int totalRound) {
validateCarNames(carNames);
validateTotalRound(totalRound);
this.carNames.addAll(carNames);
this.totalRound = totalRound;
}

public List<String> getCarNames() {
return carNames;
}

public int getTotalRound() {
return totalRound;
}

private void validateCarNames(List<String> carNames) {
if(carNames == null || carNames.isEmpty()) {
throw new IllegalArgumentException(ErrorMessages.EMPTY_CAR_NAMES);
}
}

private void validateTotalRound(int totalRound) {
if(totalRound <= 0) {
throw new IllegalArgumentException(ErrorMessages.INVALID_TOTAL_ROUND);
}
}
}
Loading