diff --git a/README.md b/README.md index 491aece1..ef024dde 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# java-racingcar-precourse \ No newline at end of file +# java-racingcar-precourse + +- [x] 차량 클래스 + - [x] 차량 이동 + - [x] 유닛 테스팅 + - [x] 차량 이름이 5자 이상 일때 + - [x] 유닛 테스팅 +- [x] 레이싱 트랙 + - [x] 경기 참여 차량 저장 + - [x] 유닛 테스팅 + - [x] 경기 단계 진행 및 단계 결과 반환 + - [x] 유닛 테스팅 +- [x] 랜덤값 생성 Util + - [x] 특정 범위의 특정 개수의 Random값 생성 + - [x] 유닛 테스팅 + + +- [x] 예외 처리 + - [x] 게임 시행 횟수 타입 + - [x] 유닛 테스팅 + - [ ] 게임 시행 횟수가 0 이하일 때 + - [x] 유닛 테스팅 + - [ ] 게임 이름 입력이 공백일 때 + - [x] 유닛 테스팅 + - [ ] 게임 입력 시 이름에 공백이 있을 때 + - [x] 유닛 테스팅 diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..771a942d --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,11 @@ +import controller.RacingGameController; +import view.InputView; + +public class Application { + + public static void main(String[] args) { + RacingGameController racingGameController = new RacingGameController(); + racingGameController.carInit(); + racingGameController.runGame(); + } +} diff --git a/src/main/java/controller/RacingGameController.java b/src/main/java/controller/RacingGameController.java new file mode 100644 index 00000000..59ef2d0f --- /dev/null +++ b/src/main/java/controller/RacingGameController.java @@ -0,0 +1,51 @@ +package controller; + +import controller.res.CarInfoDto; +import domain.state.CarState; +import java.util.List; +import java.util.stream.Collectors; +import service.RacingGameService; +import view.InputView; +import view.OutputView; + +public class RacingGameController { + private final RacingGameService racingGameService; + + public RacingGameController() { + this.racingGameService = new RacingGameService(); + } + public void carInit(){ + while(true){ + try{ + var carName = InputView.readCarName(); + racingGameService.trackInit(carName); + break; + }catch (IllegalArgumentException e){ + OutputView.printError(e.getMessage()); + } + } + + } + + public int gameCountInit(){ + while(true){ + try{ + return InputView.readGameCount(); + }catch (IllegalArgumentException e){ + OutputView.printError(e.getMessage()); + } + } + } + public void runGame(){ + var gameCount = gameCountInit(); + + OutputView.printResultOutput(); + for(int i = 0; i < gameCount; i++){ + racingGameService.runStep(); + var cars = racingGameService.getCars(); + OutputView.printStep(cars.stream().map(CarInfoDto::toDTO).toList()); + } + + OutputView.printResult(racingGameService.getWinners().stream().map(CarInfoDto::toDTO).toList()); + } +} diff --git a/src/main/java/controller/res/CarInfoDto.java b/src/main/java/controller/res/CarInfoDto.java new file mode 100644 index 00000000..3cde9de6 --- /dev/null +++ b/src/main/java/controller/res/CarInfoDto.java @@ -0,0 +1,25 @@ +package controller.res; + +import domain.state.CarState; + +public class CarInfoDto { + private final String name; + private final int position; + + public CarInfoDto(String name, int position) { + this.name = name; + this.position = position; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public static CarInfoDto toDTO(CarState carState){ + return new CarInfoDto(carState.getName(), carState.getPosition()); + } +} diff --git a/src/main/java/domain/Car.java b/src/main/java/domain/Car.java new file mode 100644 index 00000000..3e5afb12 --- /dev/null +++ b/src/main/java/domain/Car.java @@ -0,0 +1,36 @@ +package domain; + +import domain.state.CarState; + +public class Car implements CarState { + private String name; + private int position; + + public Car(String name) { + setName(name); + this.position = 0; + } + + private void setName(String name){ + // ToDo: 다른 이름형식 제한 예정 + if(name.length() > 5){ + // TODO: ExcessiveParticipantsNameException + throw new IllegalArgumentException("[Error]: 자동차의 이름이 6자 이상입니다."); + } + this.name = name; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public void move(Integer seed){ + if(seed > 3){ + this.position ++; + } + } +} diff --git a/src/main/java/domain/Track.java b/src/main/java/domain/Track.java new file mode 100644 index 00000000..c987e632 --- /dev/null +++ b/src/main/java/domain/Track.java @@ -0,0 +1,49 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; + +public class Track { + private final List cars; + + public Track() { + this.cars = new ArrayList<>(); + } + + public void addCar(String carName){ + cars.add(new Car(carName)); + } + public void runStep(List seeds){ + validateSeedSize(seeds); + for(int i = 0; i < seeds.size(); i++){ + cars.get(i).move(seeds.get(i)); + } + } + + public List getCars(){ + return this.cars; + } + + public List getWinners(){ + int maxPosition = 0; + + List winner = new ArrayList<>(); + for(Car car : cars){ + if(car.getPosition() == maxPosition){ + winner.add(car); + continue; + } + if(car.getPosition() > maxPosition){ + winner.clear(); + maxPosition = car.getPosition(); + winner.add(car); + } + } + return winner; + } + private void validateSeedSize(List seeds){ + if(seeds.size() != cars.size()){ + throw new IllegalArgumentException("[ERROR]: Seed 개수가 차량 개수와 일치하지 않습니다"); + } + } +} diff --git a/src/main/java/domain/state/CarState.java b/src/main/java/domain/state/CarState.java new file mode 100644 index 00000000..b953aa3e --- /dev/null +++ b/src/main/java/domain/state/CarState.java @@ -0,0 +1,6 @@ +package domain.state; + +public interface CarState { + String getName(); + int getPosition(); +} diff --git a/src/main/java/service/RacingGameService.java b/src/main/java/service/RacingGameService.java new file mode 100644 index 00000000..70abd729 --- /dev/null +++ b/src/main/java/service/RacingGameService.java @@ -0,0 +1,33 @@ +package service; + +import domain.Track; +import domain.state.CarState; +import java.util.ArrayList; +import java.util.List; +import utils.Randoms; + +public class RacingGameService { + private Track track; + + public void trackInit(List carNames){ + track = new Track(); + for(String name : carNames){ + track.addCar(name); + } + } + + public void runStep(){ + track.runStep( + Randoms.pickNumbersInRange(track.getCars().size(), 0, 9) + ); + } + + public List getCars(){ + var cars = track.getCars(); + return new ArrayList<>(cars); + } + public List getWinners(){ + var winners = track.getWinners(); + return new ArrayList<>(winners); + } +} diff --git a/src/main/java/utils/Console.java b/src/main/java/utils/Console.java new file mode 100644 index 00000000..4488dd62 --- /dev/null +++ b/src/main/java/utils/Console.java @@ -0,0 +1,11 @@ +package utils; + +import java.util.Scanner; + +public class Console { + private static final Scanner sc = new Scanner(System.in); + + public static String readLine(){ + return sc.nextLine(); + } +} diff --git a/src/main/java/utils/Parser.java b/src/main/java/utils/Parser.java new file mode 100644 index 00000000..01b112cc --- /dev/null +++ b/src/main/java/utils/Parser.java @@ -0,0 +1,53 @@ +package utils; + +import java.util.Arrays; +import java.util.List; + +public class Parser { + private static final String INVALID_INPUT_ERROR = "[ERROR]: 이름을 확인할 수 없습니다. 다시 입력해주세요."; + private static final String INVALID_COUNT_ERROR = "[ERROR]: 유효한 숫자를 입력해주세요."; + public static List validateCarNameInput(String input){ + try { + validateNameContainsBlank(input); + + return Arrays.stream(input.split(", ")) + .map(String::trim) + .toList(); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + private static void validateNameContainsBlank(String input){ + if (input == null || + input.trim().isEmpty() || + Arrays.stream(input.split(",")).anyMatch(String::isBlank)) { + + throw new IllegalArgumentException(INVALID_INPUT_ERROR); + } + } + + public static int validateGameCountInput(String input) { + try { + validateGameCountIsPositive(input); + int count = Integer.parseInt(input.trim()); + validateGameCountIsPositive(count); + + return count; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INVALID_COUNT_ERROR); + } + } + + private static void validateGameCountIsPositive(String input){ + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException(INVALID_COUNT_ERROR); + } + } + private static void validateGameCountIsPositive(int count){ + if (count <= 0) { + throw new IllegalArgumentException(INVALID_COUNT_ERROR); + } + } + +} diff --git a/src/main/java/utils/Randoms.java b/src/main/java/utils/Randoms.java new file mode 100644 index 00000000..5fe7f425 --- /dev/null +++ b/src/main/java/utils/Randoms.java @@ -0,0 +1,28 @@ +package utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +public class Randoms { + private static final Random random = ThreadLocalRandom.current(); + private Randoms() { + } + + public static List pickNumbersInRange(final int count, final int start, final int end){ + validateRange(start, end); + List randomNumbers = new ArrayList<>(); + for(int i = 0; i < count; i++){ + randomNumbers.add(start + random.nextInt(end - start + 1)); + } + + return randomNumbers; + } + + private static void validateRange(final int start, final int end){ + if (start > end) { + throw new IllegalArgumentException(String.format("[Error]: 시작 번호[%d]가 끝 번호[%s] 보다 큽니다.", start, end)); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..cd9e7aab --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,22 @@ +package view; + +import java.util.List; +import utils.Console; +import utils.Parser; + +public class InputView { + private static final String GET_CAR_NAME_REQUEST = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String GET_GAME_COUNT_REQUEST = "시도할 회수는 몇회인가요?"; + + public static List readCarName(){ + System.out.println(GET_CAR_NAME_REQUEST); + return Parser.validateCarNameInput(Console.readLine()); + } + + + public static int readGameCount(){ + System.out.println(GET_GAME_COUNT_REQUEST); + return Parser.validateGameCountInput(Console.readLine()); + } + +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..051e26d4 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,32 @@ +package view; + +import controller.res.CarInfoDto; +import domain.state.CarState; +import java.util.List; +import java.util.stream.Collectors; + +public class OutputView { + private static final String RESULT_OUTPUT = "\n실행결과"; + private static final String WINNER_OUTPUT = "최종우승자 : %s\n"; + public static void printError(String error){ + System.out.println(error + "\n"); + } + + public static void printResultOutput(){ + System.out.println(RESULT_OUTPUT); + } + + public static void printStep(List cars){ + for(CarInfoDto car : cars){ + System.out.printf("%s : %s\n", car.getName(), "-".repeat(car.getPosition())); + } + System.out.println(); + } + + public static void printResult(List cars){ + System.out.printf(WINNER_OUTPUT, + cars.stream().map(CarInfoDto::getName) + .collect(Collectors.joining(", ") + )); + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/domain/CarTest.java b/src/test/java/domain/CarTest.java new file mode 100644 index 00000000..6d6d03e5 --- /dev/null +++ b/src/test/java/domain/CarTest.java @@ -0,0 +1,34 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CarTest { + + @Test + @DisplayName("차량 이동 테스트") + void moveCarTest (){ + //given + var movedCar = new Car("Move"); + var stoppedCar = new Car("Stop"); + //when + movedCar.move(4); + stoppedCar.move(2); + //then + assertEquals(movedCar.getPosition(), 1); + assertEquals(stoppedCar.getPosition(), 0); + + } + @Test + @DisplayName("잘못된 길이의 차량 이름 길이 테스트") + void wrongCarNameTest (){ + //given + final String carName = "666666"; + //when + + //then + assertThrows(IllegalArgumentException.class, () -> new Car(carName)); + } +} \ No newline at end of file diff --git a/src/test/java/domain/TrackTest.java b/src/test/java/domain/TrackTest.java new file mode 100644 index 00000000..7d052010 --- /dev/null +++ b/src/test/java/domain/TrackTest.java @@ -0,0 +1,76 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TrackTest { + + @Test + @DisplayName("generateCarTest") + void generateCarTest (){ + //given + var track = new Track(); + var carNames = List.of("Car1", "Car2", "Car3"); + //when + for (String carName : carNames) { + track.addCar(carName); + } + var savedCar = track.getCars(); + //then + assertEquals(carNames, savedCar.stream().map(Car::getName).toList()); + } + + @Test + @DisplayName("step 진행 테스트") + void stepTest (){ + //given + var track = new Track(); + var carNames = List.of("Car1", "Car2", "Car3"); + var seeds = List.of(2,3,4); + for (String carName : carNames) { + track.addCar(carName); + } + //when + track.runStep(seeds); + //then + var position = List.of(0,0,1); + assertEquals(position, track.getCars().stream().map(Car::getPosition).toList()); + } + + @Test + @DisplayName("우승자 반환 테스트") + void winnerTest (){ + //given + var track = new Track(); + var carNames = List.of("Car1", "Car2", "Car3"); + var seeds = List.of(2,3,4); + for (String carName : carNames) { + track.addCar(carName); + } + //when + track.runStep(seeds); + //then + var answer = List.of("Car3"); + assertEquals(answer, track.getWinners().stream().map(Car::getName).toList()); + } + + @Test + @DisplayName("다중우승자 반환 테스트") + void multipleWinnerTest (){ + //given + var track = new Track(); + var carNames = List.of("Car1", "Car2", "Car3"); + var seeds = List.of(2,5,4); + for (String carName : carNames) { + track.addCar(carName); + } + //when + track.runStep(seeds); + //then + var answer = List.of("Car2", "Car3"); + assertEquals(answer, track.getWinners().stream().map(Car::getName).toList()); + } +} \ No newline at end of file diff --git a/src/test/java/utils/ParserTest.java b/src/test/java/utils/ParserTest.java new file mode 100644 index 00000000..14656ce3 --- /dev/null +++ b/src/test/java/utils/ParserTest.java @@ -0,0 +1,55 @@ +package utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ParserTest { + @Test + @DisplayName("유효한 이름 입력 테스트") + void validNameInput (){ + //given + var input = "Car1, Car2, Car3"; + var expected = List.of("Car1", "Car2", "Car3"); + //when + var output = Parser.validateCarNameInput(input); + //then + assertEquals(expected, output); + } + + @Test + @DisplayName("공백을 포함한 이름 입력 테스트") + void invalidNameInputWithBlank (){ + //given + var input = "Car1, , Car3"; + //when + + //then + assertThrows(IllegalArgumentException.class, () -> Parser.validateCarNameInput(input)); + } + + @Test + @DisplayName("유효한 게임 입력 테스트") + void validGameCount (){ + //given + var input = "1"; + var expected = 1; + //when + var output = Parser.validateGameCountInput(input); + //then + assertEquals(output, expected); + } + + @Test + @DisplayName("숫자가 아닌 입력값 테스트") + void invalidGameCountWithType(){ + //given + var input = "a"; + //when + + //then + assertThrows(IllegalArgumentException.class, () -> Parser.validateGameCountInput(input)); + } +} \ No newline at end of file diff --git a/src/test/java/utils/RandomsTest.java b/src/test/java/utils/RandomsTest.java new file mode 100644 index 00000000..8ae46429 --- /dev/null +++ b/src/test/java/utils/RandomsTest.java @@ -0,0 +1,45 @@ +package utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +class RandomsTest { + + @RepeatedTest(100) + @DisplayName("랜덤값 생성 테스트") + void generateRandomValueTest(){ + //given + final int count = 3; + final int start = 0; + final int end = 9; + + //when + var randomValues = Randoms.pickNumbersInRange(count, start, end); + + //then + assertEquals(randomValues.size(), count); + + randomValues + .forEach(randomValue -> { + assertTrue(randomValue <= 9 && randomValue >= 0); + }); + } + + @Test + @DisplayName("랜덤값 : start, end Excpetion Test") + void wrongInputWithRangeTest() { + //given + final int count = 3; + final int wrongStart = 10; + final int wrongEnd = 1; + //when + + + //then + assertThrows(IllegalArgumentException.class, () -> Randoms.pickNumbersInRange(count, wrongStart, wrongEnd)); + + } +} \ No newline at end of file