diff --git a/README.md b/README.md new file mode 100644 index 00000000..a9823886 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +## 움직이는 자동차 [O] +### 기능 요구사항 +- 자동차는 이름을 가지고 있다. +- 자동차는 움직일 수 있다. +- 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다. +### 새로운 프로그래밍 요구사항 +- 자동차가 움직이는 기능이 의도대로 동작하는지 테스트한다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. +- 기본적으로 Java Style Guide을 원칙으로 한다. +- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. + - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. +- 3항 연산자를 쓰지 않는다. +- else 예약어를 쓰지 않는다. + - else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. + - 힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다. +- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. + - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. +### 기존 프로그래밍 요구사항 +- 메인 메서드는 만들지 않는다. + +## 우승 자동차 구하기 [O] +### 기능 요구사항 +- n대의 자동차가 참여할 수 있다. +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. + - 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 구할 수 있다. 우승자는 한 명 이상일 수 있다. +### 새로운 프로그래밍 요구사항 +- 우승자를 구하는 기능이 의도대로 동작하는지 테스트한다. + +## 게임 실행 [O] +### 기능 요구사항 +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. +
**실행 결과** +- 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다. +``` +경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분). +neo,brie,brown +시도할 회수는 몇회인가요? +5 + +실행 결과 +neo : - +brie : - +brown : - + +neo : -- +brie : - +brown : -- + +neo : --- +brie : -- +brown : --- + +neo : ---- +brie : --- +brown : ---- + +neo : ----- +brie : ---- +brown : ----- + +neo : ----- +brie : ---- +brown : ----- + +neo, brown가 최종 우승했습니다. +``` + +### 새로운 프로그래밍 요구사항 +- 메인 메서드를 추가하여 실행 가능한 애플리케이션으로 만든다. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 239f9e78..8d5f4636 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,9 @@ dependencies { testImplementation platform('org.assertj:assertj-bom:3.25.1') testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('org.assertj:assertj-core') + testImplementation("org.mockito:mockito-core:5.14.1") + compileOnly("org.projectlombok:lombok:1.18.34") + annotationProcessor('org.projectlombok:lombok:1.18.34') } test { 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/RacingCarGame.java b/src/main/java/RacingCarGame.java new file mode 100644 index 00000000..453a0b2d --- /dev/null +++ b/src/main/java/RacingCarGame.java @@ -0,0 +1,19 @@ +import controller.RacingCarController; +import dao.RacingCarDao; +import service.RacingCarService; +import service.RacingCarServiceImpl; +import util.NumberGenerateUtil; +import view.RacingCarView; + +public class RacingCarGame { + public static void main(String[] args) { + RacingCarView racingCarView = new RacingCarView(); + RacingCarDao racingCarDao = new RacingCarDao(); + NumberGenerateUtil numberGenerateUtil = new NumberGenerateUtil(); + RacingCarService racingCarService = new RacingCarServiceImpl(racingCarDao, numberGenerateUtil); + RacingCarController game = new RacingCarController(racingCarView, racingCarService); + + game.set(); + game.run(); + } +} diff --git a/src/main/java/controller/RacingCarController.java b/src/main/java/controller/RacingCarController.java new file mode 100644 index 00000000..4b03f50b --- /dev/null +++ b/src/main/java/controller/RacingCarController.java @@ -0,0 +1,39 @@ +package controller; + +import converter.RacingCarConverter; +import lombok.RequiredArgsConstructor; +import service.RacingCarService; +import view.RacingCarView; + +@RequiredArgsConstructor +public class RacingCarController { + + final RacingCarView racingCarView; + final RacingCarService racingCarService; + + int round; + boolean isGameInit = false; + + public void set() { + racingCarService.createRacingCar(racingCarView.inputRacingCarName().stream().map( + RacingCarConverter::toRacingCarCreateDto).toList()); + round = racingCarView.inputRound(); + + isGameInit = true; + } + + public void run() { + if (!isGameInit) + throw new IllegalStateException("게임이 초기화되지 않았습니다."); + + racingCarView.printResultTitle(); + for(int currentRound = 0; currentRound < round; ++currentRound){ + racingCarService.move(); + racingCarView.printResult(racingCarService.getResults()); + } + + racingCarView.printWinner(racingCarService.getResults()); + + isGameInit = false; + } +} diff --git a/src/main/java/converter/RacingCarConverter.java b/src/main/java/converter/RacingCarConverter.java new file mode 100644 index 00000000..8486fbb5 --- /dev/null +++ b/src/main/java/converter/RacingCarConverter.java @@ -0,0 +1,27 @@ +package converter; + +import domain.RacingCar; +import dto.RacingCarCreateDto; +import dto.RacingCarResultDto; +import java.util.stream.Collectors; + +public class RacingCarConverter { + public static RacingCarCreateDto toRacingCarCreateDto(String name) { + return RacingCarCreateDto.builder().name(name).build(); + } + + public static RacingCarResultDto toRacingCarResultDto(RacingCar racingCar) { + return RacingCarResultDto.builder(). + name(racingCar.getName()) + .resultString(racingCar.getResults().stream().map(RacingCarConverter::resultToString).collect( + Collectors.joining())) + .distance((int) racingCar.getResults().stream().filter(v -> v).count()).build(); + } + + public static String resultToString(Boolean result) { + if (result) + return "-"; + + return ""; + } +} diff --git a/src/main/java/dao/RacingCarDao.java b/src/main/java/dao/RacingCarDao.java new file mode 100644 index 00000000..a6075410 --- /dev/null +++ b/src/main/java/dao/RacingCarDao.java @@ -0,0 +1,20 @@ +package dao; + +import domain.RacingCar; +import dto.RacingCarCreateDto; +import java.util.ArrayList; +import java.util.List; + +public class RacingCarDao { + private final List racingCars = new ArrayList<>(); + + public RacingCar insert(RacingCarCreateDto racingCarCreateDto) { + RacingCar racingCar = new RacingCar(racingCarCreateDto.getName()); + racingCars.add(racingCar); + return racingCar; + } + + public List select() { + return racingCars; + } +} diff --git a/src/main/java/domain/RacingCar.java b/src/main/java/domain/RacingCar.java new file mode 100644 index 00000000..813e634f --- /dev/null +++ b/src/main/java/domain/RacingCar.java @@ -0,0 +1,15 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@RequiredArgsConstructor +public class RacingCar { + private final String name; + private final List results = new ArrayList<>(); +} diff --git a/src/main/java/dto/RacingCarCreateDto.java b/src/main/java/dto/RacingCarCreateDto.java new file mode 100644 index 00000000..536c6fcb --- /dev/null +++ b/src/main/java/dto/RacingCarCreateDto.java @@ -0,0 +1,14 @@ +package dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RacingCarCreateDto { + private String name; +} diff --git a/src/main/java/dto/RacingCarResultDto.java b/src/main/java/dto/RacingCarResultDto.java new file mode 100644 index 00000000..29d0d9eb --- /dev/null +++ b/src/main/java/dto/RacingCarResultDto.java @@ -0,0 +1,16 @@ +package dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RacingCarResultDto { + private String name; + private String resultString; + private int distance; +} diff --git a/src/main/java/service/RacingCarService.java b/src/main/java/service/RacingCarService.java new file mode 100644 index 00000000..31329d38 --- /dev/null +++ b/src/main/java/service/RacingCarService.java @@ -0,0 +1,13 @@ +package service; + +import dto.RacingCarCreateDto; +import dto.RacingCarResultDto; +import java.util.List; + +public interface RacingCarService { + void createRacingCar(List racingCarCreateDtoList); + + void move(); + + List getResults(); +} diff --git a/src/main/java/service/RacingCarServiceImpl.java b/src/main/java/service/RacingCarServiceImpl.java new file mode 100644 index 00000000..7cfb31e0 --- /dev/null +++ b/src/main/java/service/RacingCarServiceImpl.java @@ -0,0 +1,38 @@ +package service; + +import converter.RacingCarConverter; +import dao.RacingCarDao; +import domain.RacingCar; +import dto.RacingCarCreateDto; +import dto.RacingCarResultDto; +import java.util.List; +import lombok.RequiredArgsConstructor; +import util.NumberGenerateUtil; + +@RequiredArgsConstructor +public class RacingCarServiceImpl implements RacingCarService { + final RacingCarDao racingCarDao; + final NumberGenerateUtil numberGenerateUtil; + + @Override + public void createRacingCar(final List racingCarCreateDtoList) { + List racingCars = racingCarCreateDtoList.stream().map(racingCarDao::insert).toList(); + } + + @Override + public void move() { + for (RacingCar racingCar : racingCarDao.select()){ + int power = numberGenerateUtil.generateRandomNumber(); + + if (0 <= power && power <= 9) + racingCar.getResults().add(power >= 4); + else + throw new NumberFormatException("0~9 사이의 Power 값을 필요로 합니다."); + } + } + + @Override + public List getResults() { + return racingCarDao.select().stream().map(RacingCarConverter::toRacingCarResultDto).toList(); + } +} diff --git a/src/main/java/util/NumberGenerateUtil.java b/src/main/java/util/NumberGenerateUtil.java new file mode 100644 index 00000000..2acba90b --- /dev/null +++ b/src/main/java/util/NumberGenerateUtil.java @@ -0,0 +1,11 @@ +package util; + +import java.security.SecureRandom; + +public class NumberGenerateUtil { + + public int generateRandomNumber() { + SecureRandom secureRandom = new SecureRandom(); + return secureRandom.nextInt(10); + } +} diff --git a/src/main/java/view/RacingCarView.java b/src/main/java/view/RacingCarView.java new file mode 100644 index 00000000..24875ea7 --- /dev/null +++ b/src/main/java/view/RacingCarView.java @@ -0,0 +1,78 @@ +package view; + +import dto.RacingCarResultDto; +import java.lang.module.ResolutionException; +import java.util.InputMismatchException; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + + +public class RacingCarView { + public static class RacingCarInputException extends RuntimeException { + + public RacingCarInputException() { + super("유효하지 않은 입력입니다."); + } + + public RacingCarInputException(final String s) { + super(s); + } + } + + public List inputRacingCarName() { + Scanner scanner = new Scanner(System.in); + + System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + String input = scanner.nextLine().replaceAll(" ", ""); + + if (Pattern.compile("[!@#$%^&*().?\"{}|<>]").matcher(input).find()) + throw new RacingCarInputException("이름에 특수기호를 입력할 수 없습니다."); + + List names = List.of(input.split(",")); + + if (names.size() <= 1) + throw new RacingCarInputException("최소한 두 명 이상의 플레이어가 필요합니다."); + + + return names; + } + + public int inputRound() { + Scanner scanner = new Scanner(System.in); + + System.out.println("시도할 회수는 몇회인가요?"); + + try { + int round = scanner.nextInt(); + + if (round <= 0) + throw new NumberFormatException("횟수는 1회 이상이여야 합니다."); + + return round; + } catch (InputMismatchException e) { + throw new RacingCarInputException(); + } catch (NumberFormatException e) { + throw new RacingCarInputException(e.getMessage()); + } + } + + public void printResultTitle() { + System.out.println("\n실행 결과"); + } + + public void printResult(List racingCarResultDtoList) { + for (RacingCarResultDto racingCarResultDto : racingCarResultDtoList) { + System.out.printf("%s : %s%n", racingCarResultDto.getName(), racingCarResultDto.getResultString()); + } + System.out.println(); + } + + public void printWinner(List racingCarResultDtoList) { + int maxDistance = racingCarResultDtoList.stream().mapToInt(RacingCarResultDto::getDistance).max().orElseThrow(); + + List winners = racingCarResultDtoList.stream().filter(v -> v.getDistance() == maxDistance).map(RacingCarResultDto::getName).toList(); + + System.out.println(String.join(", ", winners) + "가 최종 우승했습니다."); + } +} 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/RacingCarConverterTest.java b/src/test/java/RacingCarConverterTest.java new file mode 100644 index 00000000..16537fd4 --- /dev/null +++ b/src/test/java/RacingCarConverterTest.java @@ -0,0 +1,38 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import converter.RacingCarConverter; +import domain.RacingCar; +import dto.RacingCarCreateDto; +import dto.RacingCarResultDto; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class RacingCarConverterTest { + + @Test + public void testToRacingCarCreateDto() { + RacingCarCreateDto expected = new RacingCarCreateDto("tester"); + RacingCarCreateDto actual = RacingCarConverter.toRacingCarCreateDto("tester"); + assertThat(actual).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + public void testToRacingCarResultDto() { + RacingCarResultDto expected = new RacingCarResultDto("tester", "---", 3); + RacingCar racingCar = new RacingCar("tester"); + racingCar.getResults().addAll(List.of(false, false, true, true, true)); + RacingCarResultDto actual = RacingCarConverter.toRacingCarResultDto(racingCar); + assertThat(actual).usingRecursiveComparison().isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testResultToString(boolean value) { + String expected = value ? "-" : ""; + String actual = RacingCarConverter.resultToString(value); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/RacingCarTest.java b/src/test/java/RacingCarTest.java new file mode 100644 index 00000000..7b373836 --- /dev/null +++ b/src/test/java/RacingCarTest.java @@ -0,0 +1,57 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import converter.RacingCarConverter; +import dao.RacingCarDao; +import dto.RacingCarCreateDto; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import service.RacingCarService; +import service.RacingCarServiceImpl; +import util.NumberGenerateUtil; + +public class RacingCarTest { + + @Nested + @DisplayName("자동차 이동 테스트") + class RacingCarMoveTest { + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) + @DisplayName("자동차 이동 조건 테스트") + void testCondition(int value) { + NumberGenerateUtil numberGenerateUtil = mock(NumberGenerateUtil.class); + when(numberGenerateUtil.generateRandomNumber()).thenReturn(value); + RacingCarDao racingCarDao = new RacingCarDao(); + RacingCarService racingCarService = new RacingCarServiceImpl(racingCarDao, numberGenerateUtil); + List racingCarCreateDtoList = Stream.of("tester1", "tester2").map(v -> RacingCarConverter.toRacingCarCreateDto("")).toList(); + racingCarService.createRacingCar(racingCarCreateDtoList); + + racingCarService.move(); + + int expected = value >= 4 ? 1 : 0; + int actual = racingCarService.getResults().get(0).getDistance(); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 10}) + @DisplayName("자동차 이동 조건 예외처리 테스트") + void moveConditionException(int value) { + NumberGenerateUtil numberGenerateUtil = mock(NumberGenerateUtil.class); + when(numberGenerateUtil.generateRandomNumber()).thenReturn(value); + RacingCarDao racingCarDao = new RacingCarDao(); + RacingCarService racingCarService = new RacingCarServiceImpl(racingCarDao, numberGenerateUtil); + List racingCarCreateDtoList = Stream.of("tester1", "tester2").map(v -> RacingCarConverter.toRacingCarCreateDto("")).toList(); + racingCarService.createRacingCar(racingCarCreateDtoList); + + assertThatThrownBy(racingCarService::move).isInstanceOf(NumberFormatException.class).hasMessage("0~9 사이의 Power 값을 필요로 합니다."); + } + } +} diff --git a/src/test/java/RacingCarViewTest.java b/src/test/java/RacingCarViewTest.java new file mode 100644 index 00000000..e04f2eae --- /dev/null +++ b/src/test/java/RacingCarViewTest.java @@ -0,0 +1,64 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import view.RacingCarView; +import view.RacingCarView.RacingCarInputException; + +public class RacingCarViewTest { + + @Nested + class InputRacingCarNameTest { + @Test + @DisplayName("자동차 이름 입력이 하나일 경우 테스트") + void inputWithLessThanTwoName() { + RacingCarView racingCarView = new RacingCarView(); + System.setIn(new ByteArrayInputStream("test1\n".getBytes())); + + assertThatThrownBy(racingCarView::inputRacingCarName).isInstanceOf(RacingCarInputException.class).hasMessage("최소한 두 명 이상의 플레이어가 필요합니다."); + } + + @Test + @DisplayName("자동차 이름에 띄어쓰기가 있을 경우 테스트") + void inputWithSpace() { + RacingCarView racingCarView = new RacingCarView(); + System.setIn(new ByteArrayInputStream("test1, test2\n".getBytes())); + + assertThat(racingCarView.inputRacingCarName().stream().noneMatch(s -> s.contains(" "))).isTrue(); + } + + @Test + @DisplayName("자동차 이름에 특수기호 있을 경우 테스트") + void inputWithSpecialSymbol() { + RacingCarView racingCarView = new RacingCarView(); + System.setIn(new ByteArrayInputStream("test1#,test2&\n".getBytes())); + + assertThatThrownBy(racingCarView::inputRacingCarName).isInstanceOf(RacingCarInputException.class).hasMessage("이름에 특수기호를 입력할 수 없습니다."); + } + } + + @Nested + class InputRoundTest { + @Test + @DisplayName("시도할 횟수 입력이 음수일 경우 테스트") + void inputWithNegativeNumber() { + RacingCarView racingCarView = new RacingCarView(); + System.setIn(new ByteArrayInputStream("-1\n".getBytes())); + + assertThatThrownBy(racingCarView::inputRound).isInstanceOf(RacingCarInputException.class).hasMessage("횟수는 1회 이상이여야 합니다."); + } + + @Test + @DisplayName("시도할 횟수 입력이 숫자가 아닌 경우 테스트") + void inputWithInvalidCharacter() { + RacingCarView racingCarView = new RacingCarView(); + System.setIn(new ByteArrayInputStream("abc\n".getBytes())); + + assertThatThrownBy(racingCarView::inputRound).isInstanceOf(RacingCarInputException.class).hasMessage("유효하지 않은 입력입니다."); + } + } + +}