diff --git a/README.md b/README.md index 90a5236..2c79c1d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,22 @@ # 미션 - 자동차 경주 게임 +## 구현할 기능 목록 +- [x] 차의 이름과 위치를 관리하는 클래스를 구현한다. +- [x] 0부터 9사이의 정수값을 랜덤하게 생성 +- [x] 난수가 4이상이면 전진 +- [x] 난수가 3이하이면 이동하지 않는다 +- [x] 턴이 모두 지났을 때 가장 앞에 있는 차의 이름을 출력한다. +- [x] 턴이 모두 지났을 때 앞에 있는 차가 2개 이상일 경우 전부 출력한다. +- [x] 차의 이름에 대한 입력값을 쉼표로 구분할 수 있다. +- [x] 시도할 횟수를 입력 받을 수 있다. +- [x] 현재 자동차의 위치상황을 이름과 함께 출력할 수 있다. + +## 예외 처리 목록 +- [x] 빈 문자열 입력은 허용하지 않는다. +- [x] 중복되는 이름의 차를 입력했을 때 허용하지 않는다. +- [x] 시도할 횟수로 숫자가 아닌 값을 입력했을 때 허용하지 않는다. +- [x] 5글자 이상의 차 이름을 입력했을 때 허용하지 않는다. + ## 🚀 기능 요구사항 - 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. diff --git a/src/main/java/domain/Car.java b/src/main/java/domain/Car.java deleted file mode 100644 index e5b4a0a..0000000 --- a/src/main/java/domain/Car.java +++ /dev/null @@ -1,12 +0,0 @@ -package domain; - -public class Car { - private final String name; - private int position = 0; - - public Car(String name) { - this.name = name; - } - - // 추가 기능 구현 -} diff --git a/src/main/java/racingcar/RacingGameApplication.java b/src/main/java/racingcar/RacingGameApplication.java new file mode 100644 index 0000000..221c64a --- /dev/null +++ b/src/main/java/racingcar/RacingGameApplication.java @@ -0,0 +1,46 @@ +package racingcar; + +import racingcar.domain.Round; +import racingcar.repository.validator.CarManagerValidator; +import racingcar.dto.CarDto; +import racingcar.repository.CarManager; +import racingcar.ui.Printer; + +import java.util.List; + +public class RacingGameApplication { + + private final Printer printer = new Printer(); + private final CarManager carManager = new CarManager(); + private final CarManagerValidator carManagerValidator = new CarManagerValidator(); + public void run() { + printer.requestCarName(); + carManagerValidator.ValidateAddCars(carManager); + + printer.requestNumberOfRounds(); + Round rounds = receiveRounds(); + proceedRound(rounds); + + printer.printWinner(carManager.createWinnerMessage()); + } + + private void proceedRound(Round rounds) { + printer.printResultHeader(); + for (int i = 0; i < rounds.getRounds(); i++) { + List CarDtos = carManager.generateCarDtos(); + carManager.moveAllCars(CarDtos); + carManager.printCarState(printer); + } + } + + private Round receiveRounds() { + Round roundObject = new Round(); + return roundObject; + } + + public static void main(String[] args) { + RacingGameApplication app = new RacingGameApplication(); + app.run(); + } +} + diff --git a/src/main/java/racingcar/domain/Round.java b/src/main/java/racingcar/domain/Round.java new file mode 100644 index 0000000..1cf11f5 --- /dev/null +++ b/src/main/java/racingcar/domain/Round.java @@ -0,0 +1,17 @@ +package racingcar.domain; + +import racingcar.ui.validator.ReceiverValidator; + +public class Round { + private final int rounds; + private final ReceiverValidator receiverValidator = new ReceiverValidator(); + + public Round() { + int round = receiverValidator.getValidateReceiveNumberOfRounds(); + this.rounds = round; + } + + public int getRounds() { + return rounds; + } +} diff --git a/src/main/java/racingcar/domain/Winner.java b/src/main/java/racingcar/domain/Winner.java new file mode 100644 index 0000000..2076b9d --- /dev/null +++ b/src/main/java/racingcar/domain/Winner.java @@ -0,0 +1,26 @@ +package racingcar.domain; + +import racingcar.domain.car.Car; + +import java.util.List; + +public class Winner { + private static final String DELIMITER = ", "; + private final List winnerNames; + + public Winner(List winnerNames) { + this.winnerNames = winnerNames; + } + + public String getWinnerGroups() { + StringBuilder winnerGroups = new StringBuilder(); + winnerGroups.append(winnerNames.get(0).getName()); + + for (int i = 1; i < winnerNames.size(); i++) { + Car winner = winnerNames.get(i); + winnerGroups.append(DELIMITER); + winnerGroups.append(winner.getName()); + } + return winnerGroups.toString(); + } +} diff --git a/src/main/java/racingcar/domain/car/Car.java b/src/main/java/racingcar/domain/car/Car.java new file mode 100644 index 0000000..3381a47 --- /dev/null +++ b/src/main/java/racingcar/domain/car/Car.java @@ -0,0 +1,48 @@ +package racingcar.domain.car; + +import java.util.Objects; + +public class Car { + private final String name; + private int position = 0; + private static final int MOVE_INTERVAL = 1; + + public Car(String name) { + CarName carName = new CarName(name); + this.name = name; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public void move() { + position += MOVE_INTERVAL; + } + + @Override + public String toString() { + return name + " : " + "-".repeat(Math.max(0, position)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Car car = (Car) o; + return name.equals(car.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + public boolean isWinnerPosition(int winnerPosition) { + return winnerPosition == position; + } +} diff --git a/src/main/java/racingcar/domain/car/CarName.java b/src/main/java/racingcar/domain/car/CarName.java new file mode 100644 index 0000000..11df1b4 --- /dev/null +++ b/src/main/java/racingcar/domain/car/CarName.java @@ -0,0 +1,27 @@ +package racingcar.domain.car; + +import racingcar.domain.exception.NotBlankException; +import racingcar.domain.exception.NotValidNameLengthException; + +public class CarName { + private static final int CAR_NAME_MAX_LENGTH = 4; + private final String name; + + public CarName(String name) { + checkBlankName(name); + checkNameLength(name); + this.name = name; + } + + private void checkBlankName(String name) { + if (name.isBlank()) { + throw new NotBlankException(); + } + } + + private void checkNameLength(String name) { + if (name.length() > CAR_NAME_MAX_LENGTH) { + throw new NotValidNameLengthException(); + } + } +} diff --git a/src/main/java/racingcar/domain/exception/NotBlankException.java b/src/main/java/racingcar/domain/exception/NotBlankException.java new file mode 100644 index 0000000..01f670a --- /dev/null +++ b/src/main/java/racingcar/domain/exception/NotBlankException.java @@ -0,0 +1,9 @@ +package racingcar.domain.exception; + +public class NotBlankException extends RuntimeException { + private static final String MESSAGE = "빈 문자열 이름의 차를 생성할 수는 없습니다."; + + public NotBlankException() { + super(MESSAGE); + } +} diff --git a/src/main/java/racingcar/domain/exception/NotValidNameLengthException.java b/src/main/java/racingcar/domain/exception/NotValidNameLengthException.java new file mode 100644 index 0000000..4820a8c --- /dev/null +++ b/src/main/java/racingcar/domain/exception/NotValidNameLengthException.java @@ -0,0 +1,9 @@ +package racingcar.domain.exception; + +public class NotValidNameLengthException extends RuntimeException { + private static final String Message = "5자 이상의 이름을 가진 차는 생설할 수 없습니다."; + + public NotValidNameLengthException() { + super(Message); + } +} diff --git a/src/main/java/racingcar/dto/CarDto.java b/src/main/java/racingcar/dto/CarDto.java new file mode 100644 index 0000000..2c91cac --- /dev/null +++ b/src/main/java/racingcar/dto/CarDto.java @@ -0,0 +1,17 @@ +package racingcar.dto; + +import racingcar.domain.car.Car; + +public class CarDto { + private final Car car; + + public CarDto(Car car) { + this.car = car; + + } + + public Car getCar() { + return car; + } + +} diff --git a/src/main/java/racingcar/exception/NotZeroRoundException.java b/src/main/java/racingcar/exception/NotZeroRoundException.java new file mode 100644 index 0000000..8d39f3f --- /dev/null +++ b/src/main/java/racingcar/exception/NotZeroRoundException.java @@ -0,0 +1,9 @@ +package racingcar.exception; + +public class NotZeroRoundException extends IllegalArgumentException { + private static final String NOT_ZERO_ROUND_EXCEPTION_MESSAGE = "라운드 회수로 0은 입력하실 수 없습니다."; + + public NotZeroRoundException() { + super(NOT_ZERO_ROUND_EXCEPTION_MESSAGE); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/repository/CarManager.java b/src/main/java/racingcar/repository/CarManager.java new file mode 100644 index 0000000..3cac672 --- /dev/null +++ b/src/main/java/racingcar/repository/CarManager.java @@ -0,0 +1,88 @@ +package racingcar.repository; + +import racingcar.domain.Winner; +import racingcar.domain.car.Car; +import racingcar.dto.CarDto; +import racingcar.repository.Strategy.ForwardMoveStrategy; +import racingcar.repository.exception.NotDuplicateNameException; +import racingcar.ui.Printer; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CarManager { + private List cars; + private final ForwardMoveStrategy forwardMoveStrategy = new ForwardMoveStrategy(); + + public CarManager() { + cars = new ArrayList<>(); + } + + private void checkDuplicateName(Car car) { + if (cars.contains(car)) { + throw new NotDuplicateNameException("중복되는 이름의 차를 입력하실 수 없습니다."); + } + } + + public int size() { + return cars.size(); + } + + public void addAllCars(List carGroups) { + cars = new ArrayList<>(); + for (Car car : carGroups) { + checkDuplicateName(car); + this.cars.add(car); + } + } + + public List createAllCars(List carNames) { + List carGroups = new ArrayList<>(); + + for (String name : carNames) { + carGroups.add(new Car(name)); + } + return carGroups; + } + + public List generateCarDtos() { + List dtos = new ArrayList<>(); + for (Car car: cars) { + dtos.add(new CarDto(car)); + } + + return dtos; + } + + public void moveAllCars(List carNumberList) { + for (CarDto dto : carNumberList) { + forwardMoveStrategy.forwardDeciding(dto.getCar()); + } + } + + public void printCarState(Printer printer) { + for (Car car : cars) { + printer.printCarState(car); + } + printer.printNewLine(); + } + + public String createWinnerMessage() { + List winners = winnerList(); + Winner winner = new Winner(winners); + + return winner.getWinnerGroups(); + } + + private List winnerList() { + int winnerPosition = cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElseThrow(IllegalArgumentException::new); + + return cars.stream() + .filter(car -> car.isWinnerPosition(winnerPosition)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/racingcar/repository/Strategy/ForwardMoveStrategy.java b/src/main/java/racingcar/repository/Strategy/ForwardMoveStrategy.java new file mode 100644 index 0000000..6b79e29 --- /dev/null +++ b/src/main/java/racingcar/repository/Strategy/ForwardMoveStrategy.java @@ -0,0 +1,11 @@ +package racingcar.repository.Strategy; + +import racingcar.domain.car.Car; + +public class ForwardMoveStrategy extends RandomMoveStrategy{ + public void forwardDeciding(Car car){ + if(decideMoving() >= getThreshold()){ + car.move(); + } + } +} diff --git a/src/main/java/racingcar/repository/Strategy/MoveStrategy.java b/src/main/java/racingcar/repository/Strategy/MoveStrategy.java new file mode 100644 index 0000000..4673291 --- /dev/null +++ b/src/main/java/racingcar/repository/Strategy/MoveStrategy.java @@ -0,0 +1,5 @@ +package racingcar.repository.Strategy; + +interface MoveStrategy { + int decideMoving(); +} diff --git a/src/main/java/racingcar/repository/Strategy/RandomMoveStrategy.java b/src/main/java/racingcar/repository/Strategy/RandomMoveStrategy.java new file mode 100644 index 0000000..fbaf374 --- /dev/null +++ b/src/main/java/racingcar/repository/Strategy/RandomMoveStrategy.java @@ -0,0 +1,17 @@ +package racingcar.repository.Strategy; + +import java.util.Random; + +public class RandomMoveStrategy implements MoveStrategy{ + private final int UPPER_BOUND = 10; + private static final int THRESHOLD = 4; + + @Override + public int decideMoving(){ + return new Random().nextInt(UPPER_BOUND); + } + + public int getThreshold(){ + return THRESHOLD; + } +} diff --git a/src/main/java/racingcar/repository/exception/NotDuplicateNameException.java b/src/main/java/racingcar/repository/exception/NotDuplicateNameException.java new file mode 100644 index 0000000..35b94d7 --- /dev/null +++ b/src/main/java/racingcar/repository/exception/NotDuplicateNameException.java @@ -0,0 +1,7 @@ +package racingcar.repository.exception; + +public class NotDuplicateNameException extends RuntimeException { + public NotDuplicateNameException(String message) { + super(message); + } +} diff --git a/src/main/java/racingcar/repository/validator/CarManagerValidator.java b/src/main/java/racingcar/repository/validator/CarManagerValidator.java new file mode 100644 index 0000000..00e62ca --- /dev/null +++ b/src/main/java/racingcar/repository/validator/CarManagerValidator.java @@ -0,0 +1,31 @@ +package racingcar.repository.validator; + +import racingcar.domain.car.Car; +import racingcar.domain.exception.NotBlankException; +import racingcar.domain.exception.NotValidNameLengthException; +import racingcar.repository.CarManager; +import racingcar.repository.exception.NotDuplicateNameException; +import racingcar.ui.Printer; +import racingcar.ui.Receiver; + +import java.util.List; + +public class CarManagerValidator { + private final Receiver receiver = new Receiver(); + private final Printer printer = new Printer(); + + public void ValidateAddCars(CarManager carManager) { + List cars;; + boolean isValidate; + do{ + isValidate = false; + try{ + cars = carManager.createAllCars(receiver.receiveCarNames()); + carManager.addAllCars(cars); + }catch (NotBlankException | NotValidNameLengthException | NotDuplicateNameException e ) { + isValidate = true; + printer.printExceptionMessage(e); + } + }while(isValidate); + } +} diff --git a/src/main/java/racingcar/ui/Printer.java b/src/main/java/racingcar/ui/Printer.java new file mode 100644 index 0000000..8aceada --- /dev/null +++ b/src/main/java/racingcar/ui/Printer.java @@ -0,0 +1,45 @@ +package racingcar.ui; + +import racingcar.domain.car.Car; + +public class Printer { + private static final String CAR_NAME_REQUEST_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String ROUND_REQUEST_MESSAGE = "시도할 회수는 몇회인가요?"; + private static final String RESULT_HEADER = "실행 결과"; + private static final String WINNER_MESSAGE = "가 최종 우승했습니다."; + private static final String INPUT_MISMATCH_EXCEPTION_MESSAGE = "라운드 횟수로 숫자만 입력하실 수 있습니다."; + + public void requestCarName() { + System.out.println(CAR_NAME_REQUEST_MESSAGE); + } + + public void requestNumberOfRounds() { + System.out.println(ROUND_REQUEST_MESSAGE); + } + + public void printResultHeader() { + System.out.println(RESULT_HEADER); + } + + public void printWinner(String winnerNames) { + System.out.println(winnerNames + WINNER_MESSAGE); + } + + public void printCarState(Car car) { + System.out.println(car); + } + + public void printNewLine() { + System.out.println(); + } + + public void printExceptionMessage(Exception e) { + System.out.println(e.getMessage()); + } + + public void printInputMismatchExceptionMessage() { + Receiver receiver = new Receiver(); + receiver.clearBuffer(); + System.out.println(INPUT_MISMATCH_EXCEPTION_MESSAGE); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/ui/Receiver.java b/src/main/java/racingcar/ui/Receiver.java new file mode 100644 index 0000000..6188997 --- /dev/null +++ b/src/main/java/racingcar/ui/Receiver.java @@ -0,0 +1,31 @@ +package racingcar.ui; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class Receiver { + private static final Scanner scanner = new Scanner(System.in); + private static final String DELIMITER = ","; + + public List receiveCarNames() { + String carNames = scanner.nextLine(); + return splitInputLine(carNames); + } + + public int receiveNumberOfRounds() { + int numberOfRounds = scanner.nextInt(); + return numberOfRounds; + } + + public void clearBuffer() { + if (scanner.hasNextLine()) { + scanner.nextLine(); + } + } + + private List splitInputLine(String line) { + return Arrays.asList(line.split(DELIMITER)); + } + +} diff --git a/src/main/java/racingcar/ui/validator/ReceiverValidator.java b/src/main/java/racingcar/ui/validator/ReceiverValidator.java new file mode 100644 index 0000000..9db075e --- /dev/null +++ b/src/main/java/racingcar/ui/validator/ReceiverValidator.java @@ -0,0 +1,39 @@ +package racingcar.ui.validator; + +import racingcar.exception.NotZeroRoundException; +import racingcar.ui.Printer; +import racingcar.ui.Receiver; + +import java.util.InputMismatchException; + +public class ReceiverValidator { + private static final int INPUT_ZERO = 0; + + private Receiver receiver = new Receiver(); + private Printer printer = new Printer(); + + public int getValidateReceiveNumberOfRounds() { + int validateReceiveNumberOfRounds = 0, isValidate; + do{ + isValidate = 1; + try{ + validateReceiveNumberOfRounds = receiver.receiveNumberOfRounds(); + checkInputZeroRound(validateReceiveNumberOfRounds); + }catch (InputMismatchException e) { + isValidate = 0; + printer.printInputMismatchExceptionMessage(); + }catch (IllegalArgumentException e) { + isValidate = 0; + printer.printExceptionMessage(e); + } + }while(isValidate == 0); + return validateReceiveNumberOfRounds; + } + + private void checkInputZeroRound(int rounds) { + if (rounds == INPUT_ZERO) { + throw new NotZeroRoundException(); + } + } + +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000..25ddbd8 --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,54 @@ +package racingcar.domain; + +import racingcar.domain.car.Car; +import racingcar.domain.exception.NotBlankException; +import org.junit.jupiter.api.Test; +import racingcar.domain.exception.NotValidNameLengthException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class CarTest { + @Test + void move() { + //given + Car car = new Car("name"); + int prev = car.getPosition(); + + //when + car.move(); + int current = car.getPosition(); + + //then + assertThat(current).isEqualTo(prev + 1); + } + + @Test + void 차의_이름과_위치상황을_출력_포맷에_맞게_String으로_변환한다() { + //given + Car car = new Car("name"); + car.move(); + car.move(); + car.move(); + + //when + String expected = car.toString(); + + //then + assertThat(expected).isEqualTo("name : ---"); + } + + @Test + void 빈문자열의_이름이_주어지면_NotBlankException을_던진다() { + //then + assertThatExceptionOfType(NotBlankException.class) + .isThrownBy(() -> new Car("")); + } + + @Test + void _5글자_이상의_이름이_주어지면_NotValidNameLengthException을_던진다() { + //then + assertThatExceptionOfType(NotValidNameLengthException.class) + .isThrownBy(()-> new Car("oereo")); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/repository/CarManagerTest.java b/src/test/java/racingcar/repository/CarManagerTest.java new file mode 100644 index 0000000..da6d437 --- /dev/null +++ b/src/test/java/racingcar/repository/CarManagerTest.java @@ -0,0 +1,135 @@ +package racingcar.repository; + +import racingcar.domain.car.Car; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import racingcar.dto.CarDto; +import racingcar.repository.exception.NotDuplicateNameException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class CarManagerTest { + + private CarManager carManager; + + @BeforeEach + void setup() { + carManager = new CarManager(); + } + + @Test + void Car_객체를_추가한다() { + //given + Car car = new Car("oer"); + List carList = List.of(car); + + //when + carManager.addAllCars(carList); + int expected = carManager.size(); + + //then + assertThat(expected).isEqualTo(1); + } + + @Test + void 중복되는_이름의_Car_객체를_추가를_하면_NotDuplicateNameException을_던진다() { + //given + Car oereo = new Car("oer"); + Car pkalsh = new Car("pka"); + Car kouz95 = new Car("kouz"); + Car pkalsh2 = new Car("pka"); + + List carList = List.of(oereo, pkalsh, kouz95, pkalsh2); + + //when + //then + assertThatExceptionOfType(NotDuplicateNameException.class) + .isThrownBy(() -> carManager.addAllCars(carList)); + } + +// @Test +// void 랜덤_숫자_리스트를_넘기면_기준에_따라_플레이어가_이동한다() { +// //given +// Car oereo = new Car("oer"); +// Car pkalsh = new Car("pka"); +// Car kouz95 = new Car("kouz"); +// +// List carList = List.of(oereo, pkalsh, kouz95); +// carManager.addAllCars(carList); +// +// //when +// List randomNumberList = +// List.of(new CarDto(oereo), +// new CarDto(pkalsh), +// new CarDto(kouz95)); +// +// carManager.moveAllCars(randomNumberList); +// +// //then +// assertThat(oereo.getPosition()).isEqualTo(1); +// assertThat(pkalsh.getPosition()).isEqualTo(0); +// assertThat(kouz95.getPosition()).isEqualTo(1); +// } + + @Test + void dto에_존재하는_car_객체가_CarManager의_collection에_존재하는_car_객체와_같다() { + //given + Car oereo = new Car("oer"); + Car pkalsh = new Car("pka"); + Car kouz95 = new Car("kouz"); + + List carList = List.of(oereo, pkalsh, kouz95); + carManager.addAllCars(carList); + + //when + List randomNumberList = carManager.generateCarDtos(); + + //then + for (int i = 0; i < carList.size(); i++) { + assertThat(carList.get(i) == randomNumberList.get(i).getCar()) + .isTrue(); + } + } + + @Test + void 승자가_한명일_때_그_이름을_출력한다() { + //given + Car oereo = new Car("oer"); + Car pkalsh = new Car("pka"); + Car kouz95 = new Car("kouz"); + + List carList = List.of(oereo, pkalsh, kouz95); + carManager.addAllCars(carList); + pkalsh.move(); + + //when + String winnerMessage = carManager.createWinnerMessage(); + + //then + assertThat(winnerMessage).isEqualTo("pka"); + } + + @Test + void 승자가_두명이상일_때_그_이름들을_comma로_구분하여_출력한다() { + //given + Car oereo = new Car("oer"); + Car pkalsh = new Car("pka"); + Car kouz95 = new Car("kouz"); + + List carList = List.of(oereo, pkalsh, kouz95); + oereo.move(); + pkalsh.move(); + kouz95.move(); + carManager.addAllCars(carList); + + //when + String winnerMessage = carManager.createWinnerMessage(); + + //then + assertThat(winnerMessage).isEqualTo("oer, pka, kouz"); + } + +} \ No newline at end of file