Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e9fdf33
docs: 자동차 경주 게임 README 초기 작성
litrod1733 Oct 23, 2025
3f8e47c
feat(car): Car 클래스 초안 작성(name, position 필드 및 생성자 추가)
litrod1733 Oct 24, 2025
3a66c0a
feat(cars): Cars 클래스 초안 작성(fromCsv, fromNames 기본 구조 추가)
litrod1733 Oct 24, 2025
8721a06
fix(cars): 생성자 매개변수 타입 및 CSV 파싱 문법 수정
litrod1733 Oct 24, 2025
b0fb041
feat: RacingGame, MoveStrategy, RandomMoveStrategy 클래스 뼈대 작성
litrod1733 Oct 24, 2025
8e4571b
feat(racing): 자동차 이동 로직 및 이동 전략 구현
litrod1733 Oct 24, 2025
6fa09d3
feat(car): 이름 유효성 검증 로직 추가(빈 문자열 및 5자 초과 예외 처리)
litrod1733 Oct 24, 2025
f808a2a
feat(cars): CSV 입력 및 이름 목록 유효성 검증 로직 추가
litrod1733 Oct 24, 2025
66360bb
refactor(car): move 메서드 제거 및 moveIf로 이동 로직 일원화
litrod1733 Oct 24, 2025
2681b44
refactor(racinggame): MoveStrategy 의존성 주입 방식으로 구조 개선
litrod1733 Oct 24, 2025
a7c21c0
feat(strategy): 테스트용 변수 수정
litrod1733 Oct 24, 2025
8e623e8
test(car): MoveStrategy 스텁을 이용한 Car 이동 로직 단위 테스트 추가
litrod1733 Oct 26, 2025
a98e8a9
test(cars): fromCsv와 fromNames의 생성 및 중복 이름 검증 테스트 추가
litrod1733 Oct 26, 2025
a08c6b5
test(strategy): RandomMoveStrategy의 동작 검증 및 예외 발생 방지 테스트 추가
litrod1733 Oct 26, 2025
2bc1f2e
test(racinggame): MoveStrategy 스텁을 이용한 play() 동작 테스트 추가
litrod1733 Oct 26, 2025
725a0b6
feat(validator): 이름/시도 횟수 검증 유틸 추가
litrod1733 Oct 27, 2025
9185aa5
test(validator): 이름 제약 및 시도 횟수 검증 테스트 추가
litrod1733 Oct 27, 2025
f0346fd
feat(view): 콘솔 입력 전담 InputView 추가 (이름/시도 횟수)
litrod1733 Oct 27, 2025
52725b6
feat(view): 라운드 결과 및 최종 우승자 출력용 OutputView 추가
litrod1733 Oct 27, 2025
c99f91d
feat(domain): WinnerCalculator 추가 (최대 위치 기반 공동 우승자 계산)
litrod1733 Oct 27, 2025
d2171e7
test(winnercalculator): woni 단독 우승 시나리오 및 공동 우승 시나리오 테스트 추가
litrod1733 Oct 27, 2025
502b110
feat(app): Application에서 라운드 루프 수행 및 결과 출력
litrod1733 Oct 27, 2025
26248d7
fix(app): RandomMoveStrategy 누락 수정 및 라운드 이동 로직 정상화
litrod1733 Oct 27, 2025
2b04228
fix(view): 라운드 출력 포맷 수정으로 ApplicationTest 통과
litrod1733 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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# java-racingcar-precourse
## 과제 목표
- 사용자로부터 자동차 이름과 시도 횟수를 입력받는다.
- 각 자동차가 전진 또는 정지하며 경주를 진행한다.
- 최종 우승자를 출력한다.
---
## 기능 요구 사항
- 자동차 이름은 쉼표(,)로 구분하며, 각 이름은 **5자 이하**여야 한다.
- 시도 횟수는 숫자로 입력받는다.
- 전진 조건은 `0~9` 사이 무작위 값 중 **4 이상일 경우 전진**한다.
- 시도 횟수만큼 게임을 반복하고, 매 라운드마다 결과를 출력한다.
- 최종 우승자는 여러 명일 수도 있다.
---
## 기능 목록
- [ ] 자동차 이름 입력받기
- [ ] 시도 횟수 입력받기
- [ ] 입력값 검증 (이름 길이, 공백, 음수 등)
- [ ] 랜덤 전진 조건 생성
- [ ] 전진 결과 출력
- [ ] 우승자 판별 및 출력
---
## 테스트 요구 사항
- `ApplicationTest` 또는 각 기능 단위 테스트 작성
- 입력/출력에 대한 검증 포함
26 changes: 26 additions & 0 deletions src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
package racingcar;

import racingcar.controller.RacingGame;
import racingcar.domain.*;
import racingcar.domain.strategy.RandomMoveStrategy;
import racingcar.util.Validator;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.List;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
String rawNames = InputView.readNames();
Validator.validateNames(List.of(rawNames.split(",")));
Cars cars = Cars.fromCsv(rawNames);

String rawAttempts = InputView.readAttempts();
int attempts = Validator.validateAttempts(rawAttempts);

OutputView.printStart();

RandomMoveStrategy strategy = new RandomMoveStrategy();
for (int i = 0; i < attempts; i++) {
cars.moveAll(new RandomMoveStrategy());
OutputView.printRound(cars);
}

List<String> winners = WinnerCalculator.calculate(cars);
OutputView.printWinners(winners);
}
}
25 changes: 25 additions & 0 deletions src/main/java/racingcar/controller/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package racingcar.controller;

import racingcar.domain.Cars;
import racingcar.domain.strategy.MoveStrategy;

public class RacingGame {
private final Cars cars;
private final int attempts;
private final MoveStrategy strategy;

public RacingGame(Cars cars, int attempts, MoveStrategy strategy) {
this.cars = cars;
this.attempts = attempts;
this.strategy = strategy;
}
public void play() {
// 시도 횟수만큼 반복
for (int i = 0; i < attempts; i++) {
cars.moveAll(strategy);
}
}
public Cars getCars() {
return cars;
}
}
38 changes: 38 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racingcar.domain;

import racingcar.domain.strategy.MoveStrategy;

public class Car {
private static final int MAX_NAME_LENGTH = 5;

private final String name;
private int position = 0;

public Car(String name) {
validateName(name);
this.name = name;
}
private void validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("자동차 이름은 비어 있을 수 없습니다.");
}
if (name.trim().length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다.");
}
}
public void moveIf(MoveStrategy strategy) {
if (strategy.movable()) {
this.position++;
}
}
public String getName() {
return this.name;
}
public int getPosition() {
return this.position;
}
@Override
public String toString() {
return String.format("Car{name = '%s', position = %d}", name, position);
}
}
59 changes: 59 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package racingcar.domain;

import racingcar.domain.strategy.MoveStrategy;

import java.util.*;
import java.util.stream.Stream;

public class Cars implements Iterable<Car> {
private final List<Car> cars;

private Cars(List<Car> cars) {
this.cars = List.copyOf(cars);
}
public static Cars fromCsv(String csv) {
List<String> names = Arrays.stream(csv.split(",")).map(String::trim).filter(name -> !name.isEmpty()).toList();

validateDuplicate(names);
return fromNames(names);
}
public static Cars fromNames(List<String> names) {
if (names.isEmpty()) {
throw new IllegalArgumentException("자동차 이름 목록이 비어 있습니다.");
}

List<Car> cars = names.stream().map(Car::new).toList();

return new Cars(cars);
}
private static void validateCsv(String csv) {
if (csv == null || csv.trim().isEmpty()) {
throw new IllegalArgumentException("자동차 이름을 입력해야 합니다.");
}
}
private static void validateDuplicate(List<String> names) {
Set<String> distinctNames = new HashSet<>(names);
if (distinctNames.size() != names.size()) {
throw new IllegalArgumentException("자동차 이름은 중복될 수 없습니다.");
}
}
public void moveAll(MoveStrategy strategy) {
for (Car car : cars) {
car.moveIf(strategy);
}
}

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

public Stream<Car> stream() {
return cars.stream();
}

@Override
public Iterator<Car> iterator() {
return cars.iterator();
}

}
32 changes: 32 additions & 0 deletions src/main/java/racingcar/domain/WinnerCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package racingcar.domain;

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

public class WinnerCalculator {

private WinnerCalculator() {}

public static List<String> calculate(final Cars cars) {
List<Car> list = cars.toList();
if (list.isEmpty()) {
return Collections.emptyList();
}

int max = 0;
for (Car car : list) {
if (car.getPosition() > max) {
max = car.getPosition();
}
}

List<String> winners = new ArrayList<>();
for (Car car : list) {
if (car.getPosition() == max) {
winners.add(car.getName());
}
}
return Collections.unmodifiableList(winners);
}
}
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 movable();
}
14 changes: 14 additions & 0 deletions src/main/java/racingcar/domain/strategy/RandomMoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package racingcar.domain.strategy;

import camp.nextstep.edu.missionutils.Randoms;

public class RandomMoveStrategy implements MoveStrategy {
private static final int START_NUM = 0;
private static final int END_NUM = 9;
private static final int MOVE_THRESHOLD = 4;

@Override
public boolean movable() {
return Randoms.pickNumberInRange(START_NUM, END_NUM) >= MOVE_THRESHOLD;
}
}
58 changes: 58 additions & 0 deletions src/main/java/racingcar/util/Validator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package racingcar.util;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public final class Validator {

private Validator() {}

public static final String ERR_EMPTY_NAME = "자동차 이름은 비어 있을 수 없습니다.";
public static final String ERR_NAME_LENGTH = "자동차 이름은 5자 이하여야 합니다.";
public static final String ERR_DUPLICATE_NAME = "자동차 이름은 중복될 수 없습니다.";
public static final String ERR_ATTEMPTS_NOT_NUMBER = "시도 횟수는 숫자여야 합니다.";
public static final String ERR_ATTEMPTS_RANGE = "시도 횟수는 1 이상의 정수여야 합니다.";

public static void validateNames(final List<String> rawNames) {
if (rawNames == null || rawNames.isEmpty()) {
throw new IllegalArgumentException(ERR_EMPTY_NAME);
}

final Set<String> seen = new HashSet<>();
for (String raw : rawNames) {
String name = "";
if (raw != null) {
name = raw.trim();
}

if (name.isEmpty()) {
throw new IllegalArgumentException(ERR_EMPTY_NAME);
}
if (name.length() > 5) {
throw new IllegalArgumentException(ERR_NAME_LENGTH);
}
if (!seen.add(name)) {
throw new IllegalArgumentException(ERR_DUPLICATE_NAME);
}
}
}

public static int validateAttempts(final String raw) {
if (raw == null || raw.isBlank()) {
throw new IllegalArgumentException(ERR_ATTEMPTS_NOT_NUMBER);
}

for (char c : raw.toCharArray()) {
if (!Character.isDigit(c)) {
throw new IllegalArgumentException(ERR_ATTEMPTS_NOT_NUMBER);
}
}

int attempts = Integer.parseInt(raw);
if (attempts < 1) {
throw new IllegalArgumentException(ERR_ATTEMPTS_RANGE);
}
return attempts;
}
}
20 changes: 20 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package racingcar.view;

import camp.nextstep.edu.missionutils.Console;

public final class InputView {

private InputView() {}

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

public static String readAttempts() {
System.out.println("시도할 횟수는 몇 회인가요?");
String input = Console.readLine();
return input;
}
}
40 changes: 40 additions & 0 deletions src/main/java/racingcar/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package racingcar.view;

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

public final class OutputView {

private OutputView() {}

public static void printStart() {
System.out.println("실행 결과");
}

public static void printRound(final Cars cars) {
for (Car car : cars.toList()) {
printCarState(car.getName(), car.getPosition());
}
System.out.println();
}

private static void printCarState(final String name, final int position) {
StringBuilder bar = new StringBuilder();
for (int i = 0; i < position; i++) {
bar.append("-");
}
System.out.println(name + " : " + bar);
}

public static void printWinners(final List<String> winners) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < winners.size(); i++) {
sb.append(winners.get(i));
if (i < winners.size() - 1) {
sb.append(", ");
}
}
System.out.println("최종 우승자 : " + sb);
}
}
22 changes: 22 additions & 0 deletions src/test/java/racingcar/CarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package racingcar;

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

public class CarTest {

@Test
void 이동_전략이_true이면_한칸_이동한다() {
Car car = new Car("pobi");
car.moveIf(() -> true);
assertThat(car.getPosition()).isEqualTo(1);
}

@Test
void 이동_전략이_false이면_그대로_정지한다() {
Car car = new Car("pobi");
car.moveIf(() -> false);
assertThat(car.getPosition()).isEqualTo(0);
}
}
Loading