Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# java-racingcar-precourse

## 🎯 기능 목록

### 1. 입력
- [ ] "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" 문구를 출력하고, 사용자로부터 자동차 이름들을 입력받는다. (`Console.readLine()` 활용)
- [ ] "시도할 횟수는 몇 회인가요?" 문구를 출력하고, 사용자로부터 시도할 횟수를 입력받는다. (`Console.readLine()` 활용)
- [ ] [Test] `ApplicationTest`에 입력 프롬프트가 정상적으로 출력되는지 테스트 코드를 작성한다.

### 2. 예외 처리
- [ ] [예외] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션을 종료한다.
- [ ] 자동차 이름이 5자를 초과하는 경우
- [ ] 자동차 이름 입력을 비어있게 하거나, 쉼표만 입력하는 경우 (e.g., `,,`)
- [ ] 시도할 횟수가 숫자가 아닌 경우 (e.g., "a")
- [ ] 시도할 횟수가 1 미만의 정수인 경우 (e.g., "0" 또는 "-1")
- [ ] [Test] `ApplicationTest`의 `예외_테스트()`를 통해 각 예외 상황을 검증하는 테스트 코드를 작성한다.

### 3. 핵심 로직
- [ ] 입력받은 자동차 이름들을 쉼표(,) 기준으로 분리하여 각 자동차 객체를 생성한다. (1주 차 피드백: `List` 등 컬렉션 사용)
- [ ] 각 자동차는 이름과 현재 위치(position)를 상태로 가진다.
- [ ] 각 자동차별로 0에서 9 사이의 무작위 값을 구한다. (`Randoms.pickNumberInRange(0, 9)` 활용)
- [ ] 무작위 값이 4 이상일 경우, 해당 자동차의 위치를 1 증가시킨다. (전진)
- [ ] 무작위 값이 3 이하일 경우, 자동차의 위치는 변하지 않는다. (멈춤)
- [ ] 입력받은 시도할 횟수만큼 모든 자동차에 대해 전진 또는 멈춤 로직을 반복한다.

### 4. 출력
- [ ] "실행 결과" 문구를 출력한다.
- [ ] 각 차수(라운드)가 끝날 때마다 모든 자동차의 현재 상태(이름 : -)를 출력한다. (예: `pobi : --`)
- [ ] 모든 시도가 끝난 후, 최종 우승자를 결정한다. (가장 멀리 이동한 자동차)
- [ ] 최종 우승자 안내 문구를 출력한다. (예: "최종 우승자 : pobi" 또는 "최종 우승자 : pobi, jun")
- (1주 차 피드백: 공동 우승자가 여러 명일 경우 `String.join()`을 활용하여 쉼표(,)로 구분)
- [ ] [Test] `ApplicationTest`의 `기능_테스트()`를 통해 최종 실행 결과가 예상과 동일한지 검증하는 테스트 코드를 작성한다.
161 changes: 159 additions & 2 deletions src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,164 @@
package racingcar;

import camp.nextstep.edu.missionutils.Console;
import camp.nextstep.edu.missionutils.Randoms;

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

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
String carNamesInput = getCarNamesInput();
List<Car> cars = createCarsFromInput(carNamesInput);

String attemptCountInput = getAttemptCountInput();
int attemptCount = parseAndValidateAttemptCount(attemptCountInput);

System.out.println("\n실행 결과");
runRacingGame(cars, attemptCount);

printFinalWinners(cars); // --- 최종 우승자 출력 로직 추가 ---
}

private static String getCarNamesInput() {
System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
return Console.readLine();
}

private static String getAttemptCountInput() {
System.out.println("시도할 횟수는 몇 회인가요?");
return Console.readLine();
}

// --- 자동차 생성 및 검증 로직 ---

private static List<Car> createCarsFromInput(String carNamesInput) {
validateCarNamesNotEmpty(carNamesInput);
String[] names = carNamesInput.split(",");

return Arrays.stream(names)
.map(Application::validateAndCreateCar)
.collect(Collectors.toList());
}

private static void validateCarNamesNotEmpty(String carNamesInput) {
if (carNamesInput.isEmpty()) {
throw new IllegalArgumentException("자동차 이름이 입력되지 않았습니다.");
}
}

private static Car validateAndCreateCar(String name) {
validateNameLength(name);
return new Car(name);
}

private static void validateNameLength(String name) {
if (name.isEmpty()) {
throw new IllegalArgumentException("자동차 이름은 공백일 수 없습니다.");
}
if (name.length() > 5) {
throw new IllegalArgumentException("자동차 이름은 5자 이하만 가능합니다.");
}
}

// --- 시도 횟수 파싱 및 검증 로직 ---

private static int parseAndValidateAttemptCount(String attemptCountInput) {
validateAttemptCountNumeric(attemptCountInput);

int count = Integer.parseInt(attemptCountInput);
validateAttemptCountRange(count);

return count;
}

private static void validateAttemptCountNumeric(String attemptCountInput) {
if (attemptCountInput.isEmpty()) {
throw new IllegalArgumentException("시도 횟수는 1 이상의 정수여야 합니다.");
}
for (char c : attemptCountInput.toCharArray()) {
validateIsDigit(c);
}
}

private static void validateIsDigit(char c) {
if (!Character.isDigit(c)) {
throw new IllegalArgumentException("시도 횟수는 숫자만 가능합니다.");
}
}

private static void validateAttemptCountRange(int count) {
if (count < 1) {
throw new IllegalArgumentException("시도 횟수는 1 이상의 정수여야 합니다.");
}
}

// --- 레이싱 로직 ---

private static void runRacingGame(List<Car> cars, int attemptCount) {
for (int i = 0; i < attemptCount; i++) {
runSingleRound(cars);
printRoundResult(cars);
}
}

private static void runSingleRound(List<Car> cars) {
for (Car car : cars) {
int randomNumber = Randoms.pickNumberInRange(0, 9);
car.tryMove(randomNumber);
}
}

// --- 출력 로직 ---

private static void printRoundResult(List<Car> cars) {
for (Car car : cars) {
String positionBar = generatePositionBar(car.getPosition());
System.out.println(car.getName() + " : " + positionBar);
}
System.out.println();
}

private static String generatePositionBar(int position) {
StringBuilder bar = new StringBuilder();
for (int i = 0; i < position; i++) {
bar.append("-");
}
return bar.toString();
}

// --- 최종 우승자 결정 및 출력 ---

/**
* 4. 출력 - 최종 우승자 출력
*/
private static void printFinalWinners(List<Car> cars) {
List<String> winnerNames = findWinners(cars);
String winnerString = String.join(", ", winnerNames); // 공동 우승자 처리
System.out.println("최종 우승자 : " + winnerString);
}

/**
* 4. 출력 - 최종 우승자 결정 (가장 멀리 이동한 자동차)
*/
private static List<String> findWinners(List<Car> cars) {
int maxPosition = findMaxPosition(cars);

// Java API(stream)를 활용하여 maxPosition과 동일한 모든 우승자를 찾음
return cars.stream()
.filter(car -> car.getPosition() == maxPosition)
.map(Car::getName)
.collect(Collectors.toList());
}

private static int findMaxPosition(List<Car> cars) {
int maxPosition = 0;
for (Car car : cars) {
if (car.getPosition() > maxPosition) {
maxPosition = car.getPosition();
}
}
return maxPosition;
}
}
}
31 changes: 31 additions & 0 deletions src/main/java/racingcar/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racingcar;

public class Car {
private static final int MOVING_FORWARD_THRESHOLD = 4; // 전진 조건

private final String name;
private int position;

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

public String getName() {
return name;
}

public int getPosition() {
return position;
}

/**
* 무작위 값을 받아 4 이상이면 전진, 3 이하면 정지한다.
* @param randomNumber 0에서 9 사이의 무작위 값
*/
public void tryMove(int randomNumber) {
if (randomNumber >= MOVING_FORWARD_THRESHOLD) {
this.position++;
}
}
}
61 changes: 53 additions & 8 deletions src/test/java/racingcar/ApplicationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,69 @@ class ApplicationTest extends NsTest {
@Test
void 기능_테스트() {
assertRandomNumberInRangeTest(
() -> {
run("pobi,woni", "1");
assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
},
MOVING_FORWARD, STOP
() -> {
run("pobi,woni", "1");
assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
},
MOVING_FORWARD, STOP
);
}

@Test
void 예외_테스트() {
assertSimpleTest(() ->
assertThatThrownBy(() -> runException("pobi,javaji", "1"))
.isInstanceOf(IllegalArgumentException.class)
assertThatThrownBy(() -> runException("pobi,javaji", "1"))
.isInstanceOf(IllegalArgumentException.class)
);
}

@Test
void 입력_프롬프트_테스트() {
assertSimpleTest(() -> {
run("pobi", "1"); // 테스트를 위해 임시 입력값 제공
assertThat(output()).contains(
"경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)",
"시도할 횟수는 몇 회인가요?"
);
});
}

@Test
void 자동차_이름_공백_예외_테스트() {
assertSimpleTest(() -> {
// 이름이 쉼표(,) 사이에서 비어있는 경우 (e.g., ",,pobi")
assertThatThrownBy(() -> runException(",,pobi", "1"))
.isInstanceOf(IllegalArgumentException.class);

// 이름이 아예 비어있는 경우
assertThatThrownBy(() -> runException("", "1"))
.isInstanceOf(IllegalArgumentException.class);
});
}

@Test
void 시도_횟수_숫자_아님_예외_테스트() {
assertSimpleTest(() -> {
assertThatThrownBy(() -> runException("pobi,woni", "a"))
.isInstanceOf(IllegalArgumentException.class);
});
}

@Test
void 시도_횟수_1_미만_예외_테스트() {
assertSimpleTest(() -> {
// 시도 횟수가 비어있는 경우
assertThatThrownBy(() -> runException("pobi,woni", ""))
.isInstanceOf(IllegalArgumentException.class);

// 시도 횟수가 0인 경우
assertThatThrownBy(() -> runException("pobi,woni", "0"))
.isInstanceOf(IllegalArgumentException.class);
});
}

@Override
public void runMain() {
Application.main(new String[]{});
}
}
}
45 changes: 45 additions & 0 deletions src/test/java/racingcar/CarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package racingcar;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

class CarTest {
@Test
void 자동차_생성_테스트() {
// given
String carName = "pobi";

// when
Car car = new Car(carName);

// then
assertThat(car.getName()).isEqualTo(carName);
assertThat(car.getPosition()).isEqualTo(0);
}

@Test
void 자동차_전진_테스트() {
// given
Car car = new Car("pobi");
int forwardNumber = 4; // 4 이상은 전진

// when
car.tryMove(forwardNumber);

// then
assertThat(car.getPosition()).isEqualTo(1);
}

@Test
void 자동차_정지_테스트() {
// given
Car car = new Car("pobi");
int stopNumber = 3; // 3 이하는 정지

// when
car.tryMove(stopNumber);

// then
assertThat(car.getPosition()).isEqualTo(0);
}
}