Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d804401
docs(README): 요구사항 및 기능 구현 목록 정리
525tea Oct 27, 2025
633ee25
feat(model): Car 도메인 클래스, 단위 테스트 구현
525tea Oct 27, 2025
1d41b19
feat(model): ValueGenerator 인터페이스 정의
525tea Oct 27, 2025
1d20523
feat(model) : Cars 일급 컬렉션 구현, 단위 테스트 추가
525tea Oct 27, 2025
b1bc9b0
feat(model): RandomValueGenerator 구현
525tea Oct 27, 2025
a5f9248
feat(model): RacingGame 도메인 클래스, 단위 테스트 구현
525tea Oct 27, 2025
ef6167c
refactor(model): Cars 도메인 getStatus 메서드 추가, 우승자 산출 메서드명 변경, 테스트 코드 수정
525tea Oct 27, 2025
16597ef
feat(view): InputView 사용자 입력 기능 구현
525tea Oct 27, 2025
5e2fb42
feat(model): 입력값 검증을 위한 Validator 클래스 구현, 단위 테스트 추가
525tea Oct 27, 2025
9614b3e
refactor(constant): Validator 예외 처리 규칙에 맞게 예외 메시지 상수명 변경
525tea Oct 27, 2025
75cdba5
feat(view): OutputView 라운드 결과 및 우승자 출력 기능 구현
525tea Oct 27, 2025
3f74170
refactor(view, model): InputView, Validator의 static 삭제, 인스턴스 기반 구조로 변경
525tea Oct 27, 2025
1aed89c
test(validator): Validator 인스턴스 기반 구조에 맞게 단위 테스트 수정
525tea Oct 27, 2025
3037a2b
feat(controller): GameController 및 Application 구성, 게임 실행 흐름 완성
525tea Oct 27, 2025
37032d1
test: RacingGame 테스트 추가, 깊은 복사 로직으로 보완
525tea Oct 27, 2025
ffd8d60
test: Car 이동/정지 조건, 초기 상태 테스트 추가
525tea Oct 27, 2025
aa5c9e4
test: Cars 전체 이동, 우승자 선별 로직 테스트 추가
525tea Oct 27, 2025
6e5af97
refactor(view): CarStatus DTO 도입 및 출력 책임 분리
525tea Oct 27, 2025
0a609b4
test: RacingGameTest를 DTO 구조에 맞게 수정
525tea Oct 27, 2025
45325f4
refactor(naming): 메서드 네이밍 및 시그니처 일관성 정리
525tea Oct 27, 2025
ac8de40
refactor : Validator의 검증 로직을 도메인으로 이동
525tea Oct 27, 2025
90e7c65
refactor : 예외 메시지 상수화
525tea Oct 27, 2025
4575bfb
style: 코드 컨벤션 준수 및 스타일 정리
525tea Oct 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,92 @@
# java-racingcar-precourse

## 자동차 경주 게임

초간단 자동차 경주 게임을 구현한다.

주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.

각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.

자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.

사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.

전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.

경주가 끝나면 가장 멀리 이동한 자동차(들)가 우승하며, 우승자가 여러 명이면 쉼표(,)로 구분하여 출력한다.

사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, 애플리케이션은 종료되어야 한다.


---


# 기능 구현 목록

## View

### 1. 입력 (InputVew)
- [x] 경주할 자동차 이름을 입력받는다
- [x] 입력 요청 문구를 출력한다
- [x] 입력된 문자열을 `,` 기준으로 분리한다
- [x] 입력된 원시 문자열(List)을 반환한다

- [x] 시도할 횟수를 입력받는다
- [x] 입력 요청 문구를 출력한다
- [x] 입력 문자열을 반환한다

### 2. 출력 (OutputView)
- [x] 각 턴마다 자동차 이름과 이동 거리를 출력한다
- [x] 최종 우승자 목록을 전달 받아 출력한다
- [x] 우승자가 다수일 경우 `,`로 구분하여 출력한다


## Model

### 1. 검증 기능 (Validator)
- 입력 형식 및 비즈니스 규칙에 따른 유효성 검증을 담당한다
- [x] 문자열 입력값 검증
- [x] null, 빈 문자열, 공백 문자열 예외 발생
- [x] 자동차 이름이 없는 경우 예외 발생
- [x] 이름이 1자 미만 5자 초과일 경우 예외 발생
- [x] `,`로 구분된 이름 목록에 공백 요소 존재 . 예외 발생
- [x] 자동차 이름 중복일 경우 예외 발생
- [x] 시도 횟수 입력값 검증
- [x] 숫자가 아닐 경우 예외 발생
- [x] 0 이하의 숫자일 경우 예외 발생

### 2. 도메인 핵심 로직 (Model)
- [x] Car 클래스
- [x] 자동차 이름과 현재 위치를 가진다
- [x] 이동 조건 충족 시 위치를 1 증가시킨다
- [x] 상태를 조회할 수 있다
- [x] 객체 자신의 유효성을 검증한다

- [x] Cars 일급 컬렉션
- [x] private final List<Car>
- [x] 정적 팩토리 메서드로 생성한다
- [x] 모든 자동차를 한 번씩 전진시킨다
- [x] 최대 이동 거리를 반환한다
- [x] 우승자 목록을 반환한다
- [x] Car 목록의 이름 중복을 검증한다

- [x] ValueGenerator 인터페이스
- 랜덤값을 구한다
- [x] 구현체는 Randoms.pickNumberInRange(0,9)를 사용해 0~9의 정수를 반환한다

- [x] RacingGame 클래스
- 게임 전체 진행을 관리한다
- [x] 매 라운드마다 이동을 실행한다
- [x] 매 라운드마다 결과를 저장하고 반환한다
- [x] 게임이 종료된 후 최종 우승자를 계산한다


## Controller

- [x] 입력 → 검증 → 모델 실행 → 결과 출력의 전체 흐름을 제어
- InputView로 입력을 받는다
- Validator로 형식을 검증한다
- 유효한 입력값을 사용해 Cars, RacingGame을 초기화한다
- RacingGame의 결과를 OutputView에 전당해 출력한다
- 애플리케이션 진입점에서 실행된다
13 changes: 12 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package racingcar;

import racingcar.controller.GameController;
import racingcar.model.generator.RandomValueGenerator;
import racingcar.model.generator.ValueGenerator;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
InputView inputView = new InputView();
OutputView outputView = new OutputView();
ValueGenerator generator = new RandomValueGenerator();

GameController controller = new GameController(inputView, outputView, generator);
controller.run();
}
}
38 changes: 38 additions & 0 deletions src/main/java/racingcar/controller/GameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racingcar.controller;

import racingcar.model.car.Cars;
import racingcar.model.game.RacingGame;
import racingcar.model.generator.ValueGenerator;
import racingcar.model.vo.RoundLimit;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.List;

public class GameController {

private final InputView inputView;
private final OutputView outputView;
private final ValueGenerator generator;

public GameController(InputView inputView, OutputView outputView, ValueGenerator generator) {
this.inputView = inputView;
this.outputView = outputView;
this.generator = generator;
}

public void run() {
List<String> names = inputView.readCarNames();
String attemptInput = inputView.readAttemptCount();

Cars cars = Cars.of(names);
RoundLimit roundLimit = RoundLimit.of(attemptInput);
RacingGame game = RacingGame.of(cars, roundLimit);
outputView.printResultHeader();
for (int i = 0; i < roundLimit.value(); i++) {
game.playRound(generator);
outputView.printRoundResult(game.currentRoundSnapshots());
}
outputView.printWinners(game.findWinners());
}
}
15 changes: 15 additions & 0 deletions src/main/java/racingcar/model/Validator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package racingcar.model;

import racingcar.model.constant.ExceptionMessages;

public class Validator {

public void validateInputExists(String input) {
if (input == null) {
throw new IllegalArgumentException(ExceptionMessages.INPUT_NULL.get());
}
if (input.isBlank()) {
throw new IllegalArgumentException(ExceptionMessages.INPUT_BLANK.get());
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/racingcar/model/car/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.model.car;

import racingcar.model.generator.ValueGenerator;
import racingcar.model.vo.CarName;
import racingcar.model.vo.Position;

public class Car {

private final CarName name;
private final Position position;

public Car(String name) {
this.name = new CarName(name);
this.position = new Position();
}

public void move(ValueGenerator generator) {
position.move(generator.getValue());
}

public String name() {
return name.value();
}

public int position() {
return position.value();
}

public boolean isWinner(int maxPosition) {
return position.value() == maxPosition;
}

public CarStatus snapshot() {
return new CarStatus(name.value(), position.value());
}
}
3 changes: 3 additions & 0 deletions src/main/java/racingcar/model/car/CarStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package racingcar.model.car;

public record CarStatus(String name, int position) { }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 어떤 건가요?? 클래스인가요? 어떨 때 사용하나요?!

68 changes: 68 additions & 0 deletions src/main/java/racingcar/model/car/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package racingcar.model.car;

import racingcar.model.constant.ExceptionMessages;
import racingcar.model.generator.ValueGenerator;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class Cars {

private final List<Car> cars;

private Cars(List<Car> cars) {
validateDuplicateNames(cars);
this.cars = cars;
}

public static Cars of(List<String> names) {
if (names == null || names.isEmpty()) {
throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY.get());
}

List<Car> carList = names.stream()
.map(Car::new)
.collect(Collectors.toUnmodifiableList());

return new Cars(carList);
}

private void validateDuplicateNames(List<Car> cars) {
Set<String> uniqueNames = cars.stream()
.map(Car::name)
.collect(Collectors.toSet());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stream에 대해서 찾아보다가 .toList()가 .collect(Collectors.toList())를 간결한 형태로 사용했다는 걸 봤는데요! 이것도 .toSet()으로 간결하게 작성이 가능한가요?

if (uniqueNames.size() != cars.size()) {
throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME.get());
}
}

public void moveAll(ValueGenerator generator) {
cars.forEach(car -> car.move(generator));
}

public int maxPosition() {
return cars.stream()
.mapToInt(Car::position)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.map이랑 .mapToInt의 차이점이 뭔가요?

.max()
.orElse(0);
}

public List<String> findWinners() {
int maxPosition = maxPosition();
return cars.stream()
.filter(car -> car.isWinner(maxPosition))
.map(Car::name)
.collect(Collectors.toUnmodifiableList());
}

public List<CarStatus> snapshots() {
return cars.stream()
.map(Car::snapshot)
.collect(Collectors.toList());
}

public List<Car> cars() {
return List.copyOf(cars);
}
}
26 changes: 26 additions & 0 deletions src/main/java/racingcar/model/constant/ExceptionMessages.java

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 3주차 과제는 상수화해서 사용해보려고 합니다 많이 보고 배워요 감사합니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package racingcar.model.constant;

public enum ExceptionMessages {

INPUT_EMPTY("입력값이 비어있습니다."),
INPUT_NULL("입력값이 존재하지 않습니다."),
INPUT_BLANK("입력값에 공백만 포함될 수 없습니다."),

INVALID_NAME_EMPTY("자동차 이름은 비어있을 수 없습니다."),
INVALID_NAME_LENGTH("자동차 이름은 1자 이상 5자 이하만 가능합니다."),
DUPLICATE_NAME("자동차 이름은 중복될 수 없습니다."),

INVALID_NUMBER_FORMAT("시도 횟수는 숫자여야 합니다."),
INVALID_NUMBER_RANGE("시도 횟수는 1 이상의 정수여야 합니다."),
EXCEEDED_ROUND_LIMIT("라운드 한도를 초과했습니다.");

private final String message;

ExceptionMessages(String message) {
this.message = message;
}

public String get() {
return message;
}
}
57 changes: 57 additions & 0 deletions src/main/java/racingcar/model/game/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package racingcar.model.game;

import racingcar.model.car.CarStatus;
import racingcar.model.car.Cars;
import racingcar.model.constant.ExceptionMessages;
import racingcar.model.generator.ValueGenerator;
import racingcar.model.vo.RoundLimit;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class RacingGame {

private final Cars cars;
private final RoundLimit roundLimit;
private final List<List<CarStatus>> roundSnapshots = new ArrayList<>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 List 안에 List를 넣는 건가요?


private RacingGame(Cars cars, RoundLimit roundLimit) {
this.cars = cars;
this.roundLimit = roundLimit;
}

public static RacingGame of(Cars cars, RoundLimit roundLimit) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저번 주차 코드에서는 List.of에 대해서 물어보면서 불변 리스트 만들 때 사용한다고 알려주셨는데요 여기서 사용하는 of는 어떨 때 사용하나요? 무슨 의미인가요?

return new RacingGame(cars, roundLimit);
}

public void playRound(ValueGenerator generator) {
if (!roundLimit.hasRemaining(roundSnapshots.size())) {
throw new IllegalStateException(ExceptionMessages.EXCEEDED_ROUND_LIMIT.get());
}
cars.moveAll(generator);
roundSnapshots.add(cars.snapshots());
}

public List<CarStatus> currentRoundSnapshots() {
if (roundSnapshots.isEmpty()) {
return List.of();
}
List<CarStatus> lastRound = roundSnapshots.get(roundSnapshots.size() - 1);
return lastRound.stream()
.map(status -> new CarStatus(status.name(), status.position()))
.collect(Collectors.toList());
}

public List<List<CarStatus>> allRoundSnapshots() {
return roundSnapshots.stream()
.map(round -> round.stream()
.map(status -> new CarStatus(status.name(), status.position()))
.collect(Collectors.toList()))
.collect(Collectors.toList());
}

public List<String> findWinners() {
return cars.findWinners();
}
}
14 changes: 14 additions & 0 deletions src/main/java/racingcar/model/generator/RandomValueGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package racingcar.model.generator;

import camp.nextstep.edu.missionutils.Randoms;

public class RandomValueGenerator implements ValueGenerator {

private static final int MIN = 0;
private static final int MAX = 9;

@Override
public int getValue() {
return Randoms.pickNumberInRange(MIN, MAX);
}
}
6 changes: 6 additions & 0 deletions src/main/java/racingcar/model/generator/ValueGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package racingcar.model.generator;

@FunctionalInterface
public interface ValueGenerator {
int getValue();
}
Comment on lines +3 to +6

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터페이스는 어떨 때 사용하나요?

Loading