diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..84f914de1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,38 @@ +# 구현할 기능 목록 + +## 0. 전체 예외 처리 +- [x] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. +- [x] 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다. + + +## 1. 경주할 자동차 +- [x] 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +### 입력 +- [x] 자동차 이름들 입력 +### 예외처리 +- [x] 자동차 이름은 쉼표(,)를 기준으로 구분 +- [x] 이름은 5자 이하만 가능 +### 단위테스트 +- [x] + +## 2. 경주 방법 +### 입력 +- [x] 시도할 횟수 입력 +### 예외처리 +- [x] 숫자여야 함 +### 단위테스트 +- [x] + +## 3. 실행 과정 +- [x] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있음 + - [x] 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우 +### 출력 +- [x] 각 차수별 실행 결과 + + +## 4. 실행 결과 +- [x] 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. +- [x] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. +### 출력 +- [x] 단독 우승자 안내 문구 +- [x] 공동 우승자 안내 문구 \ No newline at end of file diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index b9ed0456a..3b3c08e01 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,10 @@ package racingcar; +import racingcar.controller.GameController; + public class Application { public static void main(String[] args) { - // TODO 구현 진행 + GameController gameController = new GameController(); + gameController.start(); } } diff --git a/src/main/java/racingcar/Car.java b/src/main/java/racingcar/Car.java deleted file mode 100644 index ab3df9492..000000000 --- a/src/main/java/racingcar/Car.java +++ /dev/null @@ -1,12 +0,0 @@ -package racingcar; - -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/controller/GameController.java b/src/main/java/racingcar/controller/GameController.java new file mode 100644 index 000000000..f7bbf0750 --- /dev/null +++ b/src/main/java/racingcar/controller/GameController.java @@ -0,0 +1,51 @@ +package racingcar.controller; + +import java.util.List; +import java.util.function.Supplier; +import racingcar.domain.GameResult; +import racingcar.domain.Player; +import racingcar.domain.RacingGame; +import racingcar.domain.car.NumberGenerator; +import racingcar.domain.car.RandomNumberGenerator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class GameController { + + private RacingGame racingGame; + private GameResult gameResult; + + public GameController() { + setUp(); + } + + private void setUp() { + InputView inputView = new InputView(); + List playerNames = repeat(inputView::readPlayerName); + Player player = new Player(playerNames); + this.gameResult = new GameResult(player); + int gameRound = repeat(inputView::readGameRound); + this.racingGame = new RacingGame(player, gameRound); + } + + public void start() { + OutputView outputView = new OutputView(); + outputView.printResultMessage(); + do { + NumberGenerator numberGenerator = new RandomNumberGenerator(); + racingGame.play(numberGenerator); + outputView.printRoundResult(gameResult); + } while (racingGame.isGameContinue()); + outputView.printFinalWinner(gameResult); + } + + private T repeat(Supplier inputReader) { + try { + return inputReader.get(); + } catch (IllegalArgumentException e) { + OutputView outputView = new OutputView(); + outputView.printErrorMessage(e.getMessage()); + return repeat(inputReader); + } + } +} diff --git a/src/main/java/racingcar/domain/GameResult.java b/src/main/java/racingcar/domain/GameResult.java new file mode 100644 index 000000000..ce332e845 --- /dev/null +++ b/src/main/java/racingcar/domain/GameResult.java @@ -0,0 +1,41 @@ +package racingcar.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import racingcar.domain.car.Car; + +public class GameResult { + + private final Player player; + private Map playerPosition = new LinkedHashMap<>(); + + public GameResult(Player player) { + this.player = player; + } + + public Map getPlayerPosition() { + updatePosition(); + return playerPosition; + } + + private void updatePosition() { + List racingCars = player.getRacingCars(); + for (Car car : racingCars) { + playerPosition.put(car.getName(), car.getPosition()); + } + } + + public List getFinalWinner() { + List finalWinner = new ArrayList<>(); + int maxValue = Collections.max(playerPosition.values()); + for (Map.Entry m : playerPosition.entrySet()) { + if (m.getValue() == maxValue) { + finalWinner.add(m.getKey()); + } + } + return finalWinner; + } +} diff --git a/src/main/java/racingcar/domain/Player.java b/src/main/java/racingcar/domain/Player.java new file mode 100644 index 000000000..1842d1a6b --- /dev/null +++ b/src/main/java/racingcar/domain/Player.java @@ -0,0 +1,34 @@ +package racingcar.domain; + +import java.util.ArrayList; +import java.util.List; +import racingcar.domain.car.Car; +import racingcar.domain.car.NumberGenerator; + +public class Player { + + private final List racingCars; + + public Player(List playerNames) { + this.racingCars = setRacingCars(playerNames); + } + + private List setRacingCars(List playerNames) { + List racingCars = new ArrayList<>(); + for (String name : playerNames) { + Car car = new Car(name); + racingCars.add(car); + } + return racingCars; + } + + public void playOneRound(NumberGenerator numberGenerator) { + for (Car car : racingCars) { + car.moveOrStop(numberGenerator); + } + } + + public List getRacingCars() { + return racingCars; + } +} diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java new file mode 100644 index 000000000..b1aed8d5f --- /dev/null +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -0,0 +1,25 @@ +package racingcar.domain; + +import racingcar.domain.car.NumberGenerator; + +public class RacingGame { + + private static final int GAME_OVER_COUNT = 0; + + private final Player player; + private int gameRound; + + public RacingGame(Player player, int gameRound) { + this.player = player; + this.gameRound = gameRound; + } + + public void play(NumberGenerator numberGenerator) { + player.playOneRound(numberGenerator); + gameRound--; + } + + public boolean isGameContinue() { + return (gameRound != GAME_OVER_COUNT); + } +} 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 000000000..a401ecc05 --- /dev/null +++ b/src/main/java/racingcar/domain/car/Car.java @@ -0,0 +1,32 @@ +package racingcar.domain.car; + +/* +Car 기본 생성자를 추가할 수 없다. +name, position 변수의 접근 제어자인 private을 변경할 수 없다. +가능하면 setPosition(int position) 메소드를 추가하지 않고 구현한다. + */ + +public class Car { + + private static final int MOVING_CRITERIA = 4; + private final String name; + private int position = 0; + + public Car(String name) { + this.name = name; + } + + public void moveOrStop(NumberGenerator numberGenerator) { + if (numberGenerator.generate() >= MOVING_CRITERIA) { + position++; + } + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/racingcar/domain/car/NumberGenerator.java b/src/main/java/racingcar/domain/car/NumberGenerator.java new file mode 100644 index 000000000..37f8cb117 --- /dev/null +++ b/src/main/java/racingcar/domain/car/NumberGenerator.java @@ -0,0 +1,7 @@ +package racingcar.domain.car; + +@FunctionalInterface +public interface NumberGenerator { + + int generate(); +} diff --git a/src/main/java/racingcar/domain/car/RandomNumberGenerator.java b/src/main/java/racingcar/domain/car/RandomNumberGenerator.java new file mode 100644 index 000000000..fd6363363 --- /dev/null +++ b/src/main/java/racingcar/domain/car/RandomNumberGenerator.java @@ -0,0 +1,14 @@ +package racingcar.domain.car; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomNumberGenerator implements NumberGenerator { + + private static final int LOWER_BOUND = 0; + private static final int UPPER_BOUND = 9; + + @Override + public int generate() { + return Randoms.pickNumberInRange(LOWER_BOUND, UPPER_BOUND); + } +} diff --git a/src/main/java/racingcar/utils/InputValidator.java b/src/main/java/racingcar/utils/InputValidator.java new file mode 100644 index 000000000..2f0d6b4b1 --- /dev/null +++ b/src/main/java/racingcar/utils/InputValidator.java @@ -0,0 +1,38 @@ +package racingcar.utils; + +public final class InputValidator { + private static final int MAX_NAME_LENGTH = 5; + private static final String COMMA = ","; + + private static final String NOT_IN_COMMA = "자동차 이름을 쉼표(,)로 구분해 적어주세요."; + private static final String INVALID_NAME_LENGTH = "자동차 이름을 5글자 이내로 입력해주세요."; + private static final String NOT_NUMBER = "시도할 횟수를 숫자로 입력해주세요."; + + private InputValidator() { + + } + + public static void notContainsComma(String input) { + if (input.length() > MAX_NAME_LENGTH && !input.contains(COMMA)) { + throw new IllegalArgumentException(NOT_IN_COMMA); + } + } + + public static String[] moreThanLenFive(String input) { + String[] inputList = input.split(COMMA); + for (String str : inputList) { + if (str.trim().length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException(INVALID_NAME_LENGTH); + } + } + return inputList; + } + + public static int notNumber(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(NOT_NUMBER); + } + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 000000000..da0cca70c --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,27 @@ +package racingcar.view; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import racingcar.utils.InputValidator; +import camp.nextstep.edu.missionutils.Console; + + +public class InputView { + + public List readPlayerName() throws IllegalArgumentException { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String input = Console.readLine(); + InputValidator.notContainsComma(input); + String[] inputList = InputValidator.moreThanLenFive(input); + return Arrays.stream(inputList) + .map(String::trim) + .collect(Collectors.toList()); + } + + public int readGameRound() throws IllegalArgumentException { + System.out.println("시도할 회수는 몇회인가요?"); + String input = Console.readLine(); + return InputValidator.notNumber(input); + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 000000000..1ce992204 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,40 @@ +package racingcar.view; + +import java.util.List; +import java.util.Map; +import racingcar.domain.GameResult; + +public class OutputView { + + private static final String RESULT_OF_PLAY = "실행 결과"; + private static final String FINAL_WINNER = "최종 우승자"; + private static final String COLON = ":"; + private static final String BLANK = " "; + private static final String MOVING = "-"; + + public void printResultMessage() { + System.out.println(); + System.out.println(RESULT_OF_PLAY); + } + + public void printRoundResult(GameResult gameResult) { + Map playerPosition = gameResult.getPlayerPosition(); + for (String name : playerPosition.keySet()) { + System.out.print(name + BLANK + COLON + BLANK); + for (int i = 0; i < playerPosition.get(name); i++) { + System.out.print(MOVING); + } + System.out.println(); + } + System.out.println(); + } + + public void printFinalWinner(GameResult gameResult) { + List finalWinner = gameResult.getFinalWinner(); + System.out.println(FINAL_WINNER + BLANK + COLON + BLANK + String.join(", ", finalWinner)); + } + + public void printErrorMessage(String message) { + System.out.println("[ERROR]" + BLANK + message); + } +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 000000000..8317dbf1a --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,40 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import racingcar.domain.car.Car; +import racingcar.domain.car.NumberGenerator; + +class CarTest { + + // TODO parameterized test 적용! + @Test + void 기준보다_작은_수_이동하지_않음_확인() { + NumberGenerator numberGenerator = new TestNumberGenerator(3); + Car car = new Car("June"); + car.moveOrStop(numberGenerator); + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + void 기준보다_큰수_이동_확인() { + NumberGenerator numberGenerator = new TestNumberGenerator(7); + Car car = new Car("June"); + car.moveOrStop(numberGenerator); + assertThat(car.getPosition()).isEqualTo(1); + } + class TestNumberGenerator implements NumberGenerator { + + private final int number; + + public TestNumberGenerator(int number) { + this.number = number; + } + + @Override + public int generate() { + return number; + } + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/PlayerTest.java b/src/test/java/racingcar/domain/PlayerTest.java new file mode 100644 index 000000000..85effea1f --- /dev/null +++ b/src/test/java/racingcar/domain/PlayerTest.java @@ -0,0 +1,47 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import racingcar.domain.car.Car; +import racingcar.domain.car.NumberGenerator; + +class PlayerTest { + + static Player player; + + @BeforeAll + static void init() { + List playerNames = Arrays.asList("pobi", "conan", "crong"); + player = new Player(playerNames); + } + + @ParameterizedTest + @CsvSource(value = {"3,0", "4,1", "8,2", "2,2", "7,3"}) + void 올바르게_전진_확인(int number, int position) { + NumberGenerator numberGenerator = new TestNumberGenerator(number); + player.playOneRound(numberGenerator); + List racingCars = player.getRacingCars(); + for (Car car : racingCars) { + assertThat(car.getPosition()).isEqualTo(position); + } + } + + class TestNumberGenerator implements NumberGenerator { + + private final int number; + + public TestNumberGenerator(int number) { + this.number = number; + } + + @Override + public int generate() { + return number; + } + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/RacingGameTest.java b/src/test/java/racingcar/domain/RacingGameTest.java new file mode 100644 index 000000000..2ea3338fb --- /dev/null +++ b/src/test/java/racingcar/domain/RacingGameTest.java @@ -0,0 +1,37 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import racingcar.domain.car.NumberGenerator; +import racingcar.domain.car.RandomNumberGenerator; + +class RacingGameTest { + + static RacingGame racingGame; + + @BeforeAll + static void init() { + List playerNames = Arrays.asList("pobi", "conan", "crong"); + Player player = new Player(playerNames); + racingGame = new RacingGame(player, 4); + } + + @Test + void play_메소드에_따라_isGameContinue_변화_확인() { + // given + assertThat(racingGame.isGameContinue()).isEqualTo(true); + + // when + NumberGenerator numberGenerator = new RandomNumberGenerator(); + for (int i = 0; i < 4; i ++) { + racingGame.play(numberGenerator); + } + + // then + assertThat(racingGame.isGameContinue()).isEqualTo(false); + } +} \ No newline at end of file