Skip to content

[강민혁] 자동차 경주 미션 제출 #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 13, 2025
Merged
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 1단계 - 움직이는 자동차
## 기능 요구사항
### 자동차 기본 속성
- 자동차는 고유한 이름을 가진다.
- 이름을 가지는 Car 객체를 통해 관리
Comment on lines +3 to +5
Copy link

Choose a reason for hiding this comment

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

"자동차는 고유한 이름을 가진다."라는 조건이 있는데요.
현재 어플리케이션에서 자동차 이름의 input으로 "a, a, b, c, d"를 입력한다면 어떤 결과가 나올까요?

위와 같이 이름에 공백이 있는 경우 trim 후 이름으로 사용하는것도 고려해보시면 좋을것같아요. (사용성 측면에서요😁)

Copy link
Author

Choose a reason for hiding this comment

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

중복과 공백을 고려하여 예외 처리가 필요할 것 같습니다.


### 자동차 이동 로직
자동차는 다음 규칙에 따라 이동한다
- 0에서 9 사이의 랜덤 값을 생성한다.
- 랜덤 값이 4 이상일 경우 자동차는 1칸 전진한다.
- 랜덤 값이 3 이하일 경우 자동차는 현재 위치에 멈춘다.

# 2단계 - 우승 자동차 구하기
## 기능 요구사항
### 자동차 참여 기능
- n대의 자동차가 게임에 참여할 수 있어야 함
- 자동차는 고유한 이름을 가진다.

### 게임 진행 과정
- 주어진 횟수 동안 게임이 진행됨
- 매 라운드마다 모든 자동차는 전진하거나 정지함
Comment on lines +19 to +21
Copy link

Choose a reason for hiding this comment

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

위와 같은 맥락으로 횟수로 0, -1이 들어가면 어떤 결과가 나올까요?
미션 요구사항에 명시된 조건 외에도 제공하는 기능에 대한 예외 케이스들을 추가로 고려해보시면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

명시된 조건 이외에 것은 일부러 생략을 했었는데요, 신경 써야할 부분인 것 같습니다.


### 우승자 선별 과정
- 게임 완료 후, 가장 높은 점수를 가진 자동차가 우승
- 공동 우승자 선별 가능
- 가장 position의 값이 큰 자동차가 우승

# 3단계 - 게임 실행
## 기능 요구사항
### 자동차 이름
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 게임 내에서 같은 차량의 이름을 갖는 경우 Exception 발생
- 앞 뒤 빈 공백은 제거해야 한다.

### 반복 횟수
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 사용자의 입력을 통해 상호작용한다. 그렇기 때문에 예외 처리가 필요하다 양수인 정수만 가능.

### 우승자 판별
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자는 findWinners() 라는 메서드로 판별한다.

### 전진 기준
- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
- 1단계 요구사항과 동일함.
15 changes: 15 additions & 0 deletions src/main/java/racingcar/RacingCarMain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package racingcar;

import racingcar.controller.RacingController;
import racingcar.ui.InputView;
import racingcar.util.DefaultRandomNumberGenerator;

import java.util.List;

public class RacingCarMain {
public static void main(String[] args) {
RacingController racingController = new RacingController(new DefaultRandomNumberGenerator());

racingController.playGame();
}
}
59 changes: 59 additions & 0 deletions src/main/java/racingcar/controller/RacingController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package racingcar.controller;

import racingcar.domain.Car;
import racingcar.domain.Cars;
import racingcar.domain.RacingGame;
import racingcar.domain.vo.Name;
import racingcar.domain.vo.Repetition;
import racingcar.ui.InputView;
import racingcar.ui.OutputView;
import racingcar.util.RandomNumberGenerator;

import java.util.List;

public class RacingController {

private final RandomNumberGenerator randomNumberGenerator;

public RacingController(final RandomNumberGenerator randomNumberGenerator) {
this.randomNumberGenerator = randomNumberGenerator;
}

public void playGame() {
final Repetition repetition = initializeRoundCount();
final Cars cars = initializeCars();

final RacingGame game = new RacingGame(cars, randomNumberGenerator);

executeGame(game, repetition);

displayWinners(game);
}

private Repetition initializeRoundCount() {
return new Repetition(InputView.inputRacingCount());
}

private Cars initializeCars() {
return createCars(InputView.inputCarNames());
}

private void executeGame(final RacingGame game, final Repetition repetition) {
OutputView.printGameStart();

for (int i = 0; i < repetition.value(); i++) {
game.moveCars();
OutputView.printRoundResult(game.getCars());
}
}

private static void displayWinners(RacingGame game) {
OutputView.printWinners(game.getCars());
}

private Cars createCars(List<String> carNames) {
return new Cars(carNames.stream()
.map(name -> new Car(new Name(name)))
.toList());
}
}
31 changes: 31 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racingcar.domain;

import racingcar.domain.vo.Name;
import racingcar.domain.vo.Position;
import racingcar.util.RandomNumberGenerator;

public class Car {
private static final int MOVING_STANDARD = 4;

private final Name name;
private Position position;

public Car(final Name name) {
this.name = name;
this.position = new Position();
}

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

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

public void move(final RandomNumberGenerator randomNumberGenerator) {
if (randomNumberGenerator.generate() >= MOVING_STANDARD) {
position = position.forward();
}
}
}
44 changes: 44 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package racingcar.domain;

import racingcar.domain.vo.Winners;
import racingcar.exception.ApplicationError;
import racingcar.exception.ApplicationException;
import racingcar.util.RandomNumberGenerator;

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

public record Cars(List<Car> value) {

public Cars {
validateDuplicateNames(value);
}

public void moveCars(RandomNumberGenerator randomNumberGenerator) {
for (Car car : value) {
car.move(randomNumberGenerator);
}
}

public Winners findWinners() {
int maxPosition = value.stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);

return new Winners(value.stream()
.filter(car -> car.getPosition() == maxPosition)
.toList());
}

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

if (uniqueNames.size() < value.size()) {
throw new ApplicationException(ApplicationError.DUPLICATE_NAME);
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/racingcar/domain/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package racingcar.domain;

import racingcar.domain.vo.Winners;
import racingcar.util.RandomNumberGenerator;

import java.util.List;

public class RacingGame {
private final Cars cars;
private final RandomNumberGenerator randomNumberGenerator;

public RacingGame(final Cars cars, final RandomNumberGenerator randomNumberGenerator) {
this.cars = cars;
this.randomNumberGenerator = randomNumberGenerator;
}

public void moveCars() {
cars.moveCars(randomNumberGenerator);
}

public Winners getWinners() {
return cars.findWinners();
}

public List<Car> getCars() {
return cars.value();
}
}
29 changes: 29 additions & 0 deletions src/main/java/racingcar/domain/vo/Name.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package racingcar.domain.vo;

import racingcar.exception.ApplicationError;
import racingcar.exception.ApplicationException;

public record Name(String value) {

public Name {
validateName(value);
}

private void validateName(final String name) {
if (isNullOrEmpty(name)) {
throw new ApplicationException(ApplicationError.INVALID_NAME_EMPTY);
}

if (isTooLong(name)) {
throw new ApplicationException(ApplicationError.INVALID_NAME_TOO_LONG);
}
}

private boolean isNullOrEmpty(String name) {
return name == null || name.trim().isEmpty();
}

private boolean isTooLong(String name) {
return name.trim().length() > 5;
}
}
30 changes: 30 additions & 0 deletions src/main/java/racingcar/domain/vo/Position.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package racingcar.domain.vo;

import racingcar.exception.ApplicationError;
import racingcar.exception.ApplicationException;

public record Position(int value) {
private static final int MIN_VALUE = 0;

public Position() {
this(MIN_VALUE);
}

public Position {
validatePosition(value);
}

public Position forward() {
return new Position(value + 1);
}
Comment on lines +13 to +19
Copy link

Choose a reason for hiding this comment

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

값객체로 만들어 스스로 검증을 진행하고 포지션을 앞으로 이동시키는 책임을 수행하게 변경된점 좋네요!
이전보다 객체들이 더 살아있는 느낌이 드네요:)


private void validatePosition(int value) {
if (isNegative(value)) {
throw new ApplicationException(ApplicationError.INVALID_POSITION_NEGATIVE);
}
}

private boolean isNegative(int value) {
return value < MIN_VALUE;
}
}
23 changes: 23 additions & 0 deletions src/main/java/racingcar/domain/vo/Repetition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.domain.vo;

import racingcar.exception.ApplicationError;
import racingcar.exception.ApplicationException;

public record Repetition(int value) {

public static final int MIN_REPETITION = 0;

public Repetition {
validateRepetition(value);
}

private void validateRepetition(int repetition) {
if (isUnderMinimumRepetition(repetition)) {
throw new ApplicationException(ApplicationError.INVALID_REPETITION_NEGATIVE);
}
}

private boolean isUnderMinimumRepetition(int repetition) {
return repetition < MIN_REPETITION;
}
}
11 changes: 11 additions & 0 deletions src/main/java/racingcar/domain/vo/Winners.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racingcar.domain.vo;

import racingcar.domain.Car;

import java.util.List;

public record Winners(List<Car> value) {
public int countWinners() {
return value.size();
}
}
27 changes: 27 additions & 0 deletions src/main/java/racingcar/exception/ApplicationError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racingcar.exception;

public enum ApplicationError {

/* name exception */
INVALID_NAME_EMPTY("이름은 빈 값이 작성될 수 없습니다."),
INVALID_NAME_TOO_LONG("이름은 5자 이하만 가능합니다."),
Comment on lines +3 to +7
Copy link

Choose a reason for hiding this comment

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

에러를 enum으로 관리하는 것은 민혁님이 말씀주신것과 같이 여러가지 장점을 가져요!
추후 web으로 넘어갔을때는 에러별 http status code를 지정하는 등 다양한 방법으로 확장해서 사용할 수도 있습니다. 시스템에서 에러를 체계적으로 관리하는것은 매우 중요한 부분이라 앞으로도 어떻게 관리하면 좋을지에 대해 계속 고민해보시고 시도해보시면 좋을 것 같아요!


/* position exception */
INVALID_POSITION_NEGATIVE("위치는 음수가 될 수 없습니다."),

/* repetition exception */
INVALID_REPETITION_NEGATIVE("반복 횟수는 최소 0 이상의 양수를 입력해주세요."),

/* cars exception */
DUPLICATE_NAME("중복된 이름이 존재합니다.");

private final String description;

ApplicationError(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}
7 changes: 7 additions & 0 deletions src/main/java/racingcar/exception/ApplicationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package racingcar.exception;

public class ApplicationException extends RuntimeException {
public ApplicationException(ApplicationError error) {
super(error.getDescription());
}
}
29 changes: 29 additions & 0 deletions src/main/java/racingcar/ui/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package racingcar.ui;
Copy link

Choose a reason for hiding this comment

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

패키지 이름을 ui로 지어주신 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

유저와 상호작용은 controller를 주었고 실제로 보여지고 작성되는 부분은 ui로 작성했습니다. 혹시 어떤 패키징 방식이 일반적일까요?

Copy link

Choose a reason for hiding this comment

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

이번 미션 요구사항 중 MVC패턴을 고려하는게 있어서 말씀드린거였어요!


import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class InputView {

private static final Scanner scanner = new Scanner(System.in);

private InputView() {

}
Comment on lines +7 to +13
Copy link

Choose a reason for hiding this comment

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

크!! 꼼꼼히 챙겨주셨네요👍
객체를 설계할때는 다른 사용자가 내 의도와 다르게 객체를 사용할 수 있는 여지를 최대한 줄이는겢 좋아요!


public static List<String> inputCarNames() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
String input = scanner.nextLine();
return parseCarNames(input);
}

public static int inputRacingCount() {
System.out.println("시도할 회수는 몇회인가요?");
return scanner.nextInt();
}

private static List<String> parseCarNames(final String input) {
return Arrays.stream(input.split(",")).toList();
}
}
Loading