Skip to content
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# java-racingcar-precourse
# 자동차 경주 게임

## 기능 목록

### 1. 자동차 관리
- [x] 자동차 이름 입력 및 검증 (5자 이하, 쉼표로 구분)
- [x] 자동차 위치 관리 및 전진 로직

### 2. 경주 진행
- [x] 시도 횟수 입력 및 검증
- [x] 각 라운드별 자동차 전진 여부 결정 (무작위 값 4 이상)
- [x] 경주 결과 출력 (각 라운드별 자동차 위치)

### 3. 우승자 판정
- [x] 최종 우승자 판정 (가장 많이 전진한 자동차)
- [x] 공동 우승자 처리 (여러 명일 경우 쉼표로 구분)

### 4. 예외 처리
- [x] 잘못된 입력값에 대한 IllegalArgumentException 발생
- [x] 애플리케이션 종료 처리

## 실행 결과 예시

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

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

pobi : --
woni : -
jun : --

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

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

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

최종 우승자 : pobi, jun
```
28 changes: 27 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
package racingcar;

import racingcar.domain.RacingGame;
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();

RacingGame game = createRacingGame(inputView);
playRacingGame(game, outputView);
}

private static RacingGame createRacingGame(InputView inputView) {
var cars = inputView.readCarNames();
int rounds = inputView.readRounds();
return new RacingGame(cars, rounds);
}

private static void playRacingGame(RacingGame game, OutputView outputView) {
outputView.printResultHeader();

while (game.hasNextRound()) {
game.playNextRound();
outputView.printRoundResult(game.getCars());
}

var result = game.getResult();
outputView.printFinalResult(result);
}
}
71 changes: 71 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package racingcar.domain;

import camp.nextstep.edu.missionutils.Randoms;

public class Car {
private static final int MOVING_THRESHOLD = 4;
private static final int MIN_RANDOM_VALUE = 0;
private static final int MAX_RANDOM_VALUE = 9;

private final CarName name;
private int position;

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

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

public void move() {
int randomValue = Randoms.pickNumberInRange(MIN_RANDOM_VALUE, MAX_RANDOM_VALUE);
if (randomValue >= MOVING_THRESHOLD) {
position++;
}
}

public CarName getName() {
return name;
}

public int getPosition() {
return position;
}

public boolean isAtPosition(int targetPosition) {
return position == targetPosition;
}

public String getPositionString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < position; i++) {
sb.append("-");
}
return sb.toString();
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Car car = (Car) obj;
return position == car.position && name.equals(car.name);
}

@Override
public int hashCode() {
return name.hashCode() + position;
}

@Override
public String toString() {
return name.getName() + " : " + getPositionString();
}
}
49 changes: 49 additions & 0 deletions src/main/java/racingcar/domain/CarName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package racingcar.domain;

public class CarName {
private static final int MAX_NAME_LENGTH = 5;
private static final String EMPTY_NAME_ERROR = "자동차 이름은 비어있을 수 없습니다.";
private static final String INVALID_NAME_LENGTH_ERROR = "자동차 이름은 5자 이하여야 합니다.";

private final String name;

public CarName(String name) {
validateName(name);
this.name = name.trim();
}

private void validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException(EMPTY_NAME_ERROR);
}
if (name.trim().length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException(INVALID_NAME_LENGTH_ERROR);
}
}

public String getName() {
return name;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CarName carName = (CarName) obj;
return name.equals(carName.name);
}

@Override
public int hashCode() {
return name.hashCode();
}

@Override
public String toString() {
return name;
}
}
77 changes: 77 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package racingcar.domain;

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

public class Cars {
private static final String DUPLICATE_NAME_ERROR = "자동차 이름은 중복될 수 없습니다.";

private final List<Car> cars;

public Cars(List<Car> cars) {
validateCars(cars);
this.cars = new ArrayList<>(cars);
}

private void validateCars(List<Car> cars) {
if (cars == null || cars.isEmpty()) {
throw new IllegalArgumentException("자동차 목록은 비어있을 수 없습니다.");
}
validateDuplicateNames(cars);
}

private void validateDuplicateNames(List<Car> cars) {
List<String> names = cars.stream()
.map(car -> car.getName().getName())
.collect(Collectors.toList());

long uniqueNameCount = names.stream()
.distinct()
.count();

if (uniqueNameCount != names.size()) {
throw new IllegalArgumentException(DUPLICATE_NAME_ERROR);
}
}

public void moveAll() {
for (Car car : cars) {
car.move();
}
}

public List<Car> getCars() {
return Collections.unmodifiableList(cars);
}

public List<Car> getWinners() {
int maxPosition = getMaxPosition();
return cars.stream()
.filter(car -> car.isAtPosition(maxPosition))
.collect(Collectors.toList());
}

private int getMaxPosition() {
return cars.stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);
}

public int size() {
return cars.size();
}

public Car get(int index) {
return cars.get(index);
}

@Override
public String toString() {
return cars.stream()
.map(Car::toString)
.collect(Collectors.joining("\n"));
}
}
39 changes: 39 additions & 0 deletions src/main/java/racingcar/domain/RaceResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package racingcar.domain;

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

public class RaceResult {
private final List<Car> winners;

public RaceResult(List<Car> winners) {
this.winners = winners;
}

public List<Car> getWinners() {
return winners;
}

public String getWinnerNames() {
return winners.stream()
.map(car -> car.getName().getName())
.collect(Collectors.joining(", "));
}

public boolean hasSingleWinner() {
return winners.size() == 1;
}

public boolean hasMultipleWinners() {
return winners.size() > 1;
}

public int getWinnerCount() {
return winners.size();
}

@Override
public String toString() {
return "최종 우승자 : " + getWinnerNames();
}
}
63 changes: 63 additions & 0 deletions src/main/java/racingcar/domain/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package racingcar.domain;

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

public class RacingGame {
private final Cars cars;
private final int totalRounds;
private int currentRound;

public RacingGame(Cars cars, int totalRounds) {
validateTotalRounds(totalRounds);
this.cars = cars;
this.totalRounds = totalRounds;
this.currentRound = 0;
}

private void validateTotalRounds(int totalRounds) {
if (totalRounds <= 0) {
throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다.");
}
}

public boolean hasNextRound() {
return currentRound < totalRounds;
}

public void playNextRound() {
if (!hasNextRound()) {
throw new IllegalStateException("더 이상 진행할 라운드가 없습니다.");
}
cars.moveAll();
currentRound++;
}

public RaceResult getResult() {
if (hasNextRound()) {
throw new IllegalStateException("경주가 아직 완료되지 않았습니다.");
}
List<Car> winners = cars.getWinners();
return new RaceResult(winners);
}

public Cars getCars() {
return cars;
}

public int getCurrentRound() {
return currentRound;
}

public int getTotalRounds() {
return totalRounds;
}

public int getRemainingRounds() {
return totalRounds - currentRound;
}

public boolean isFinished() {
return !hasNextRound();
}
}
Loading