Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
# java-racingcar-precourse

## 기능 목록

### 1. 자동차
- [x] 자동차 이름은 1~5자 조건을 지키며 생성된다.
- [x] 자동차는 이름과 위치를 보관하고, 이동 가능할 때 위치가 1 증가한다.
- [x] 여러 대의 자동차를 묶어 이동시키고 최대 위치를 확인한다.

### 2. 이동 규칙
- [x] 난수 생성값이 4 이상일 때 이동을 허용한다.

### 3. 경주 게임
- [x] 입력된 시도 횟수만큼 라운드를 진행한다.
- [x] 각 라운드에서 모든 자동차가 한 번씩 이동을 시도한다.
- [x] 가장 멀리 전진한 자동차가 우승자가 되며, 동일 최대 위치면 공동 우승자를 반환한다.

### 4. 입력
- [ ] 안내 문구를 명세와 동일하게 출력한 뒤 자동차 이름을 입력받는다.
- [ ] 안내 문구를 출력한 뒤 시도 횟수를 입력받는다.
- [ ] 입력된 문자열은 1차 검증을 수행한다.
- [ ] 검증된 문자열을 도메인 객체로 변환한다.

### 5. 검증
- [ ] 자동차 수는 2대 이상이어야 한다.
- [ ] 시도 횟수는 숫자만으로 이루어진 1 이상의 정수여야 한다.
- [ ] 잘못된 입력이 들어오면 예외를 발생시킨 후 애플리케이션을 종료한다.

### 6. 출력
- [ ] 횟수가 입력된 이후 '실행 결과' 문구를 출력한다.
- [ ] 각 라운드마다 모든 자동차의 현재 위치를 출력 명세의 '차수별 실행 결과' 형식에 맞게 출력한다.
- [ ] 라운드와 라운드 사이에 빈 줄을 한 줄 출력한다.
- [ ] 경주 종료 후 최종 우승자를 출력한다.
- [x] 출력 시 우승자 이름은 입력 순서를 유지한다.
18 changes: 16 additions & 2 deletions src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
package racingcar;

public class Application {
import racingcar.controller.RacingGameController;
import racingcar.domain.strategy.MoveStrategy;
import racingcar.domain.strategy.RandomMoveStrategy;
import racingcar.validator.InputValidator;
import racingcar.view.InputView;
import racingcar.view.OutputView;
import racingcar.view.RacingConsole;

public final class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
InputValidator validator = new InputValidator();
InputView inputView = new InputView(validator);
OutputView outputView = new OutputView();
RacingConsole console = new RacingConsole(inputView, outputView);
MoveStrategy moveStrategy = new RandomMoveStrategy();
RacingGameController controller = new RacingGameController(console, moveStrategy);
controller.run();
}
}
37 changes: 37 additions & 0 deletions src/main/java/racingcar/controller/RacingGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package racingcar.controller;

import java.util.List;
import racingcar.domain.car.CarName;
import racingcar.domain.car.Cars;
import racingcar.domain.game.RacingGame;
import racingcar.domain.round.RaceRound;
import racingcar.domain.strategy.MoveStrategy;
import racingcar.view.RacingConsole;

public final class RacingGameController {
private final RacingConsole console;
private final MoveStrategy moveStrategy;

public RacingGameController(RacingConsole console, MoveStrategy moveStrategy) {
this.console = console;
this.moveStrategy = moveStrategy;
}

public void run() {
try {
List<CarName> carNames = console.readCarNames();
RaceRound raceRound = console.readRaceRound();

Cars cars = Cars.fromNames(carNames);
RacingGame racingGame = new RacingGame(cars, moveStrategy, raceRound);

console.printResultHeader();
List<List<RacingGame.CarSnapshot>> snapshots = racingGame.play();
snapshots.forEach(console::printRound);
console.printWinners(racingGame.winnerNames());
} catch (IllegalArgumentException exception) {
console.printError(exception.getMessage());
throw exception;
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/racingcar/domain/car/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racingcar.domain.car;

import racingcar.domain.strategy.MoveStrategy;

public final class Car {
private final CarName name;
private int position;

public Car(CarName name) {
this.name = name;
this.position = 0;
}

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

public int position() {
return position;
}

public void move(MoveStrategy strategy) {
if (strategy.canMove()) {
position++;
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/racingcar/domain/car/CarName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.domain.car;

import racingcar.view.UiText;

public class CarName {
private static final int MAX_LENGTH = 5;

private final String name;

public CarName(String rawName) {
String trimmedName = trim(rawName);
validateLength(trimmedName);
this.name = trimmedName;
}

public String value() {
return name;
}

private String trim(String rawName) {
if (rawName == null) {
throw new IllegalArgumentException(UiText.Error.NAME_EMPTY);
}
String trimmed = rawName.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException(UiText.Error.NAME_EMPTY);
}
return trimmed;
}

private void validateLength(String trimmedName) {
if (trimmedName.length() > MAX_LENGTH) {
throw new IllegalArgumentException(UiText.Error.NAME_UNVALID_LENGTH);
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/racingcar/domain/car/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.domain.car;

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

import racingcar.domain.strategy.MoveStrategy;

public final class Cars {
private final List<Car> cars;
public Cars(List<Car> cars) {
this.cars = List.copyOf(cars);
}

public static Cars fromNames(List<CarName> carNames) {
List<Car> cars = carNames.stream()
.map(Car::new)
.collect(Collectors.toUnmodifiableList());
return new Cars(cars);
}

public void moveAll(MoveStrategy strategy) {
cars.forEach(car -> car.move(strategy));
}

public int maxPosition() {
return cars.stream()
.mapToInt(Car::position)
.max()
.orElse(0);
}

public List<Car> cars() {
return Collections.unmodifiableList(cars);
}
}
40 changes: 40 additions & 0 deletions src/main/java/racingcar/domain/game/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package racingcar.domain.game;

import java.util.ArrayList;
import java.util.List;
import racingcar.domain.car.Cars;
import racingcar.domain.round.RaceRound;
import racingcar.domain.strategy.MoveStrategy;

public final class RacingGame {
private final Cars cars;
private final MoveStrategy moveStrategy;
private final RaceRound raceRound;

public RacingGame(Cars cars, MoveStrategy moveStrategy, RaceRound raceRound) {
this.cars = cars;
this.moveStrategy = moveStrategy;
this.raceRound = raceRound;
}

public List<List<CarSnapshot>> play() {
List<List<CarSnapshot>> snapshots = new ArrayList<>();
for (int i = 0; i < raceRound.value(); i++) {
cars.moveAll(moveStrategy);
snapshots.add(carsSnapshot());
}
return snapshots;
}

public List<String> winnerNames() {
return Winners.from(cars).names();
}

private List<CarSnapshot> carsSnapshot() {
return cars.cars().stream()
.map(car -> new CarSnapshot(car.name(), car.position()))
.toList();
}

public record CarSnapshot(String name, int position) {}
}
26 changes: 26 additions & 0 deletions src/main/java/racingcar/domain/game/Winners.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package racingcar.domain.game;

import java.util.List;
import racingcar.domain.car.Car;
import racingcar.domain.car.Cars;

public final class Winners {
private final List<String> names;

private Winners(List<String> names) {
this.names = List.copyOf(names);
}

public static Winners from(Cars cars) {
int maxPosition = cars.maxPosition();
List<String> names = cars.cars().stream()
.filter(car -> car.position() == maxPosition)
.map(Car::name)
.toList();
return new Winners(names);
}

public List<String> names() {
return names;
}
}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/domain/round/RaceRound.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.domain.round;

import racingcar.view.UiText;

public final class RaceRound {
private static final int MIN_ATTEMPT_COUNT = 1;

private final int value;

private RaceRound(int value) {
if (value < MIN_ATTEMPT_COUNT) {
throw new IllegalArgumentException(UiText.Error.ATTEMPT_TOO_SMALL);
}
this.value = value;
}

public static RaceRound of(int value) {
return new RaceRound(value);
}

public int value() {
return value;
}
}
6 changes: 6 additions & 0 deletions src/main/java/racingcar/domain/strategy/MoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package racingcar.domain.strategy;

@FunctionalInterface
public interface MoveStrategy {
boolean canMove();
}
15 changes: 15 additions & 0 deletions src/main/java/racingcar/domain/strategy/RandomMoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package racingcar.domain.strategy;

import camp.nextstep.edu.missionutils.Randoms;

public class RandomMoveStrategy implements MoveStrategy {
private static final int MIN = 0;
private static final int MAX = 9;
private static final int MIN_MOVE_VALUE = 4;

@Override
public boolean canMove() {
int randomNumber = Randoms.pickNumberInRange(MIN, MAX);
return randomNumber >= MIN_MOVE_VALUE;
}
}
57 changes: 57 additions & 0 deletions src/main/java/racingcar/validator/InputValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package racingcar.validator;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import racingcar.view.UiText;

public final class InputValidator {
private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");

public List<String> validateCarNames(String rawInput) {
String trimmedInput = trim(rawInput);
List<String> names = splitByComma(trimmedInput);
ensureNoEmptyName(names);
return names;
}

public int validateAttemptCount(String rawInput) {
String trimmedInput = trim(rawInput);
ensureNumeric(trimmedInput);
int attemptCount = Integer.parseInt(trimmedInput);
if (attemptCount < 1) {
throw new IllegalArgumentException(UiText.Error.ATTEMPT_TOO_SMALL);
}
return attemptCount;
}

private String trim(String input) {
if (input == null) {
throw new IllegalArgumentException(UiText.Error.EMPTY_INPUT);
}
String trimmed = input.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException(UiText.Error.EMPTY_INPUT);
}
return trimmed;
}

private List<String> splitByComma(String input) {
return Arrays.stream(input.split(","))
.map(String::trim)
.toList();
}

private void ensureNoEmptyName(List<String> names) {
if (names.isEmpty() || names.stream().anyMatch(String::isEmpty)) {
throw new IllegalArgumentException(UiText.Error.NAME_EMPTY);
}
}

private void ensureNumeric(String input) {
if (!NUMBER_PATTERN.matcher(input).matches()) {
throw new IllegalArgumentException(UiText.Error.NOT_NUMERIC);
}
}
}
Loading