diff --git a/README.md b/README.md index d0286c859f..5c33e3762d 100644 --- a/README.md +++ b/README.md @@ -1 +1,28 @@ -# java-racingcar-precourse +# 자동차 경주 게임 + +## 기능 구현 목록 +✅ (입력) 자동차 이름 목록 + - 유효성 검사: `,` 기준 구분, 5자 이하 (IllegalArgumentException) + + +✅ (입력) 이동 횟수 + - 유효성 검사: 음수 여부(IllegalArgumentException) + + +✅ 자동차의 전진 여부 + - 0~9 사이의 랜덤값 산정 + - 4 이상의 값일 경우 1칸 전진 + + +✅ 경주 게임 우승 여부 + - 가장 많이 이동한 자동차 선정 + + +✅ (출력) 회차별 실행 결과 + - 모든 자동차에 대해 `${자동차이름} : -` format 출력 + + +✅ (출력) 우승자 + - 1명의 수증자: `최종 우승자 : ${자동차이름}` format 출력 + - 2명 이상의 우승자: `,` 기준 구분 출력 + diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..731cda5a5c 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -2,6 +2,14 @@ public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + Validator validator = new Validator(); + RacingService racingService = new RacingService(); + OutputFormatter outputFormatter = new OutputFormatter(); + + // 실행시 필요한 클래스 주입 + RacingController controller = new RacingController(validator, racingService, outputFormatter); + + // 실행 + controller.run(); } } diff --git a/src/main/java/racingcar/CarList.java b/src/main/java/racingcar/CarList.java new file mode 100644 index 0000000000..f2da757abe --- /dev/null +++ b/src/main/java/racingcar/CarList.java @@ -0,0 +1,90 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.*; + +public class CarList { + private final Map carRecord; + private final int round; + + // 생성자(Key: 차 이름, value: 전진 횟수) + public CarList(String[] carNames, int round){ + this.carRecord = new LinkedHashMap<>(); + this.round = round; + + for(String car:carNames){ + carRecord.put(car.trim(),0); + } + } + + public Map getCarRecord(){ + return carRecord; + } + + public int getRound(){ + return round; + } + + // 자동차 이동값 저장 + public void saveMoves(){ + Set carNames = carRecord.keySet(); + Iterator carIter = carNames.iterator(); + + while(carIter.hasNext()){ + String car = carIter.next(); + int score = carRecord.get(car); + score += move(); + + carRecord.put(car, score); + } + } + + // 자동차 이동 + public int move(){ + int randomValue = Randoms.pickNumberInRange(0,9); + + if(randomValue >= 4){ + return 1; + }else { + return 0; + } + } + + // 우승자 선정 + public List selectWinners(Map carRecord){ + int highestScore = getHighestScore(); + List winners = new ArrayList<>(); + Set carNames = carRecord.keySet(); + Iterator carIter = carNames.iterator(); + + while(carIter.hasNext()){ + String car = carIter.next(); + int score = carRecord.get(car); + + if(score == highestScore){ + winners.add(car); + } + } + + return winners; + } + + // 최고 점수 산정 + public int getHighestScore(){ + int highestScore = 0; + Set carNames = carRecord.keySet(); + Iterator carIter = carNames.iterator(); + + while(carIter.hasNext()){ + String car = carIter.next(); + int score = carRecord.get(car); + + if(score > highestScore){ + highestScore = score; + } + } + + return highestScore; + } +} diff --git a/src/main/java/racingcar/OutputFormatter.java b/src/main/java/racingcar/OutputFormatter.java new file mode 100644 index 0000000000..8a8669fded --- /dev/null +++ b/src/main/java/racingcar/OutputFormatter.java @@ -0,0 +1,33 @@ +package racingcar; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class OutputFormatter { + private final StringBuilder sb = new StringBuilder("실행결과\n"); + + // 차수별 실행 결과 + public void getRoundRecord(Map carRecord){ + Set carNames = carRecord.keySet(); + Iterator carIter = carNames.iterator(); + + while(carIter.hasNext()){ + String car = carIter.next(); + String format = "-"; + sb.append(car+" : "+format.repeat(carRecord.get(car))+"\n"); + } + sb.append("\n"); + } + + // 우승자 안내 문구 + public void getWinners(List winners){ + sb.append("최종 우승자 : "+String.join(", ",winners)); + } + + // 문구 전달 + public StringBuilder getStringBuilder(){ + return sb; + } +} diff --git a/src/main/java/racingcar/RacingController.java b/src/main/java/racingcar/RacingController.java new file mode 100644 index 0000000000..33be7a9b61 --- /dev/null +++ b/src/main/java/racingcar/RacingController.java @@ -0,0 +1,48 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.Console; + +import java.io.IOException; +import java.util.Map; + +public class RacingController { + private final Validator validator; + private final RacingService racingService; + private final OutputFormatter outputFormatter; + + public RacingController(Validator validator, + RacingService racingService, OutputFormatter outputFormatter){ + this.validator = validator; + this.racingService = racingService; + this.outputFormatter = outputFormatter; + } + + public void run() { + + // 자동차 이름 입력 받기 + System.out.println("경주할 자동차 이름을 입력하세요." + + "(이름은 쉼표(,) 기준으로 구분)"); + String[] carNames = validator.validCarNames(Console.readLine()); + + // 경주 회차 입력 받기 + System.out.println("시도할 횟수는 몇 회인가요?"); + int round = validator.validRoundNumber(Console.readLine()); + + // 자동차 목록 생성 + racingService.startRace(carNames, round); + + // 차수별 실행 결과 작성 + for(int i=0; i record = racingService.runEachRace(); + outputFormatter.getRoundRecord(record); + } + + // 우승자 안내 문구 작성 + outputFormatter.getWinners(racingService.selectWinners()); + + // 최종 문구 출력 + System.out.println(outputFormatter.getStringBuilder()); + } + + +} diff --git a/src/main/java/racingcar/RacingService.java b/src/main/java/racingcar/RacingService.java new file mode 100644 index 0000000000..ae46792c24 --- /dev/null +++ b/src/main/java/racingcar/RacingService.java @@ -0,0 +1,24 @@ +package racingcar; + +import java.util.List; +import java.util.Map; + +public class RacingService { + private CarList carList; + + // 자동차 목록 객체 생성 + public void startRace(String[] carNames, int round){ + this.carList = new CarList(carNames, round); + } + + // 자동차 경주 시행 + public Map runEachRace(){ + carList.saveMoves(); + return carList.getCarRecord(); + } + + // 우승자 선정 + public List selectWinners(){ + return carList.selectWinners(carList.getCarRecord()); + } +} diff --git a/src/main/java/racingcar/Validator.java b/src/main/java/racingcar/Validator.java new file mode 100644 index 0000000000..409d7228ef --- /dev/null +++ b/src/main/java/racingcar/Validator.java @@ -0,0 +1,43 @@ +package racingcar; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class Validator { + // 자동차 문자열 검증 + public String[] validCarNames(String input){ + String[] carArr = input.split(","); + + // 차가 1대인 경우 + if(carArr.length == 1) throw new IllegalArgumentException(); + + for(String car:carArr){ + if(car.length() > 5 || car.isEmpty()){ // 차 이름이 5자 이상이거나 공백인 경우 + throw new IllegalArgumentException(); + } + } + + // 차 이름 중복 여부 확인 + Set uniqueCarNames = new HashSet<>(Arrays.asList(carArr)); + if(uniqueCarNames.size() != carArr.length){ + throw new IllegalArgumentException(); + } + + return carArr; + } + + // 경주 회차수 검증 + public int validRoundNumber(String input){ + try{ + int round = Integer.parseInt(input); + if(round <= 0) { + throw new Exception(); + } + return round; + + }catch(Exception e){ + throw new IllegalArgumentException(); + } + } +} diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java index 1d35fc33fe..f8cfb2aae5 100644 --- a/src/test/java/racingcar/ApplicationTest.java +++ b/src/test/java/racingcar/ApplicationTest.java @@ -1,8 +1,12 @@ package racingcar; import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Map; + import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; @@ -12,22 +16,261 @@ class ApplicationTest extends NsTest { private static final int MOVING_FORWARD = 4; private static final int STOP = 3; + // ===== saveMoves() 테스트 ===== + + @Test + @DisplayName("모든 자동차가 4 이상 값을 받으면 모두 1씩 전진") + void saveMoves_모두_전진() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong", "honux"}; + CarList carList = new CarList(carNames, 5); + + // 한 라운드 진행 + carList.saveMoves(); + + // 검증: 모든 차가 1씩 전진 (4, 4, 4) + Map result = carList.getCarRecord(); + assertThat(result.get("pobi")).isEqualTo(1); + assertThat(result.get("crong")).isEqualTo(1); + assertThat(result.get("honux")).isEqualTo(1); + }, + 4, 4, 4 // pobi, crong, honux 순서대로 4 + ); + } + + @Test + @DisplayName("모든 자동차가 3 이하 값을 받으면 모두 정지") + void saveMoves_모두_정지() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong"}; + CarList carList = new CarList(carNames, 5); + + // 한 라운드 진행 + carList.saveMoves(); + + // 검증: 모든 차가 정지 (0) + Map result = carList.getCarRecord(); + assertThat(result.get("pobi")).isEqualTo(0); + assertThat(result.get("crong")).isEqualTo(0); + }, + 2, 3 // pobi: 2, crong: 3 (모두 4 미만) + ); + } + + @Test + @DisplayName("일부는 전진, 일부는 정지") + void saveMoves_혼합() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong", "honux"}; + CarList carList = new CarList(carNames, 5); + + // 한 라운드 진행 + carList.saveMoves(); + + // 검증: pobi와 honux는 전진, crong은 정지 + Map result = carList.getCarRecord(); + assertThat(result.get("pobi")).isEqualTo(1); + assertThat(result.get("crong")).isEqualTo(0); + assertThat(result.get("honux")).isEqualTo(1); + }, + 5, 2, 7 // pobi: 5(전진), crong: 2(정지), honux: 7(전진) + ); + } + + @Test + @DisplayName("여러 라운드 진행 - 누적 점수") + void saveMoves_여러_라운드() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong"}; + CarList carList = new CarList(carNames, 3); + + // 3라운드 진행 + carList.saveMoves(); // 1라운드: pobi 4(전진), crong 2(정지) + carList.saveMoves(); // 2라운드: pobi 5(전진), crong 3(정지) + carList.saveMoves(); // 3라운드: pobi 4(전진), crong 6(전진) + + // 검증: pobi는 3칸, crong은 1칸 전진 + Map result = carList.getCarRecord(); + assertThat(result.get("pobi")).isEqualTo(3); + assertThat(result.get("crong")).isEqualTo(1); + }, + 4, 2, // 1라운드 + 5, 3, // 2라운드 + 4, 6 // 3라운드 + ); + } + + @Test + @DisplayName("경계값 테스트 - 4는 전진, 3은 정지") + void saveMoves_경계값() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong"}; + CarList carList = new CarList(carNames, 1); + + carList.saveMoves(); + + Map result = carList.getCarRecord(); + assertThat(result.get("pobi")).isEqualTo(1); // 4는 전진 + assertThat(result.get("crong")).isEqualTo(0); // 3은 정지 + }, + 4, 3 // pobi: 4(전진), crong: 3(정지) + ); + } + + + // ===== selectWinners() 테스트 ===== + + @Test + @DisplayName("한 명의 단독 우승자") + void selectWinners_단독_우승() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong", "honux"}; + CarList carList = new CarList(carNames, 3); + + // 3라운드 진행 + carList.saveMoves(); + carList.saveMoves(); + carList.saveMoves(); + + // 우승자 선정 + Map carRecord = carList.getCarRecord(); + List winners = carList.selectWinners(carRecord); + + // 검증: pobi만 3번 전진, 나머지는 정지 → pobi 단독 우승 + assertThat(winners).hasSize(1); + assertThat(winners).contains("pobi"); + assertThat(carRecord.get("pobi")).isEqualTo(3); + assertThat(carRecord.get("crong")).isEqualTo(0); + assertThat(carRecord.get("honux")).isEqualTo(0); + }, + 4, 2, 1, // 1라운드: pobi만 전진 + 5, 3, 2, // 2라운드: pobi만 전진 + 6, 1, 0 // 3라운드: pobi만 전진 + ); + } + + @Test + @DisplayName("여러 명의 공동 우승자") + void selectWinners_공동_우승() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong", "honux"}; + CarList carList = new CarList(carNames, 2); + + // 2라운드 진행 + carList.saveMoves(); + carList.saveMoves(); + + // 우승자 선정 + Map carRecord = carList.getCarRecord(); + List winners = carList.selectWinners(carRecord); + + // 검증: pobi와 crong이 2번, honux는 0번 → pobi, crong 공동 우승 + assertThat(winners).hasSize(2); + assertThat(winners).containsExactlyInAnyOrder("pobi", "crong"); + assertThat(carRecord.get("pobi")).isEqualTo(2); + assertThat(carRecord.get("crong")).isEqualTo(2); + assertThat(carRecord.get("honux")).isEqualTo(0); + }, + 5, 6, 2, // 1라운드: pobi, crong 전진 + 4, 7, 1 // 2라운드: pobi, crong 전진 + ); + } + + @Test + @DisplayName("모든 차가 같은 점수 - 전체 우승") + void selectWinners_전체_우승() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong", "honux"}; + CarList carList = new CarList(carNames, 2); + + // 2라운드 진행 + carList.saveMoves(); + carList.saveMoves(); + + // 우승자 선정 + Map carRecord = carList.getCarRecord(); + List winners = carList.selectWinners(carRecord); + + // 검증: 모두 2번 전진 → 전체 우승 + assertThat(winners).hasSize(3); + assertThat(winners).containsExactlyInAnyOrder("pobi", "crong", "honux"); + assertThat(carRecord.get("pobi")).isEqualTo(2); + assertThat(carRecord.get("crong")).isEqualTo(2); + assertThat(carRecord.get("honux")).isEqualTo(2); + }, + 4, 5, 6, // 1라운드: 모두 전진 + 7, 8, 9 // 2라운드: 모두 전진 + ); + } + @Test - void 기능_테스트() { + @DisplayName("아무도 움직이지 않음 - 모두 0점으로 공동 우승") + void selectWinners_모두_0점() { assertRandomNumberInRangeTest( - () -> { - run("pobi,woni", "1"); - assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi"); - }, - MOVING_FORWARD, STOP + () -> { + String[] carNames = {"pobi", "crong"}; + CarList carList = new CarList(carNames, 2); + + // 2라운드 진행 + carList.saveMoves(); + carList.saveMoves(); + + // 우승자 선정 + Map carRecord = carList.getCarRecord(); + List winners = carList.selectWinners(carRecord); + + // 검증: 모두 0점 → 전체 우승 + assertThat(winners).hasSize(2); + assertThat(winners).containsExactlyInAnyOrder("pobi", "crong"); + assertThat(carRecord.get("pobi")).isEqualTo(0); + assertThat(carRecord.get("crong")).isEqualTo(0); + }, + 2, 1, // 1라운드: 모두 정지 + 0, 3 // 2라운드: 모두 정지 ); } + + // ===== 통합 테스트 (saveMoves + selectWinners) ===== + @Test - void 예외_테스트() { - assertSimpleTest(() -> - assertThatThrownBy(() -> runException("pobi,javaji", "1")) - .isInstanceOf(IllegalArgumentException.class) + @DisplayName("실제 게임 시나리오 - 전체 흐름") + void 실제_게임_시나리오() { + assertRandomNumberInRangeTest( + () -> { + String[] carNames = {"pobi", "crong", "honux"}; + CarList carList = new CarList(carNames, 5); + + // 5라운드 진행 + for (int i = 0; i < 5; i++) { + carList.saveMoves(); + } + + // 점수 확인 + Map carRecord = carList.getCarRecord(); + assertThat(carRecord.get("pobi")).isEqualTo(5); // 4번 전진 + assertThat(carRecord.get("crong")).isEqualTo(2); // 2번 전진 + assertThat(carRecord.get("honux")).isEqualTo(3); // 3번 전진 + + // 우승자 확인 + List winners = carList.selectWinners(carRecord); + assertThat(winners).hasSize(1); + assertThat(winners).contains("pobi"); + }, + // 5라운드 * 3대 = 15개의 랜덤 값 + 4, 5, 6, // 1라운드: 모두 전진 + 7, 2, 8, // 2라운드: pobi, honux 전진 + 4, 1, 5, // 3라운드: pobi, honux 전진 + 5, 6, 2, // 4라운드: pobi, crong 전진 + 6, 0, 1 // 5라운드: pobi만 전진 ); }