diff --git a/README.md b/README.md index 1969313..ebec1a3 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,52 @@ 자동차 경주 미션 저장소 +## 도메인 다이어그램 + +```mermaid +graph TD + RacingCarController --> InputView + RacingCarController --> OutputView + RacingCarController --> RacingGame + + RacingGame --> Players + RacingGame --> TryCount + + Players --> Player + + Player --> Name + Player --> Distance + + Distance --> Random + +``` + +## 기능 구현 목록 + +### RacingGame + +- [x] Players와 Trycount 관리한다. + +### Players + +- [x] 여러 명일 수 있다. + +### Player + +- [x] 이름을 가진다. + - [x] 최소 1자, 최대 5자까지 가능하다. + - [x] 중간 공백은 허용하고 쉼표로 구분한다. +- [x] 거리를 가진다. + +### 입력 + +- [x] 플레이어의 이름을 입력한다. + - [x] 앞, 뒤 공백은 제거한다. +- [x] 이동 횟수를 입력한다. + +### 출력 + +- [x] 최종 결과를 출력한다. +- [x] 최종 우승자를 출력한다. + - [x] 여러명일 수 있다. + diff --git a/build.gradle b/build.gradle index 3697236..05690aa 100644 --- a/build.gradle +++ b/build.gradle @@ -6,13 +6,18 @@ version '1.0-SNAPSHOT' repositories { mavenCentral() + maven { url 'https://jitpack.io' } + } dependencies { - testImplementation platform('org.junit:junit-bom:5.9.1') - testImplementation platform('org.assertj:assertj-bom:3.25.1') - testImplementation('org.junit.jupiter:junit-jupiter') - testImplementation('org.assertj:assertj-core') + implementation 'com.github.kokodak:mission-utils:1.0.0' + + implementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + implementation 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + implementation 'org.mockito:mockito-inline:3.12.4' + implementation 'org.assertj:assertj-core:3.21.0' + implementation 'org.junit.jupiter:junit-jupiter:5.8.1' } java { diff --git a/src/main/java/RacingMain.java b/src/main/java/RacingMain.java deleted file mode 100644 index 4394287..0000000 --- a/src/main/java/RacingMain.java +++ /dev/null @@ -1,7 +0,0 @@ -public class RacingMain { - - public static void main(String[] args) { - // TODO: MVC 패턴을 기반으로 자동차 경주 미션 구현해보기 - System.out.println("Hello, World!"); - } -} diff --git a/src/main/java/racingcar/RacingMain.java b/src/main/java/racingcar/RacingMain.java new file mode 100644 index 0000000..c9842d0 --- /dev/null +++ b/src/main/java/racingcar/RacingMain.java @@ -0,0 +1,15 @@ +package racingcar; + +import racingcar.controller.RacingCarController; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class RacingMain { + public static void main(String[] args) { + final InputView inputView = new InputView(); + final OutputView outputView = new OutputView(); + + final RacingCarController racingCarController = new RacingCarController(inputView, outputView); + racingCarController.run(); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/controller/RacingCarController.java b/src/main/java/racingcar/controller/RacingCarController.java new file mode 100644 index 0000000..7915317 --- /dev/null +++ b/src/main/java/racingcar/controller/RacingCarController.java @@ -0,0 +1,94 @@ +package racingcar.controller; + +import racingcar.domain.RacingGame; +import racingcar.domain.players.Player; +import racingcar.domain.players.Players; +import racingcar.domain.trycount.TryCount; +import racingcar.view.InputView; +import racingcar.view.OutputView; +import racingcar.view.dto.PlayerResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class RacingCarController { + private final InputView inputView; + private final OutputView outputView; + + public RacingCarController(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + public void run() { + final Players players = getPlayers(); + final TryCount tryCount = getTryCount(); + final RacingGame racingGame = initRacingGame(players, tryCount); + + PlayGame(racingGame); + + printResult(racingGame); + } + + private void PlayGame(final RacingGame racingGame) { + printResultMessage(); + final int tryCount = racingGame.getTryCount(); + for(int i=0; i playerResponses = getPlayerResponses(racingGame.findWinners()); + + outputView.printResult(playerResponses); + } + + private List getPlayerResponses(final List players) { + return players.stream() + .map(PlayerResponse::from) + .collect(Collectors.toList()); + } + + private RacingGame initRacingGame(Players players, TryCount tryCount) { + return new RacingGame(players, tryCount); + } + + private TryCount getTryCount() { + return new TryCount(inputView.readTryCount()); + } + + private Players getPlayers() { + final List players = new ArrayList<>(); + players.addAll(createPlayers()); + + return new Players(players); + } + private List createPlayers() { + final List playerNames = inputView.readNames(); + + return playerNames.stream() + .map(Player::new) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java new file mode 100644 index 0000000..06abfb8 --- /dev/null +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -0,0 +1,44 @@ +package racingcar.domain; + +import racingcar.domain.players.Player; +import racingcar.domain.players.Players; +import racingcar.domain.trycount.TryCount; + +import java.util.List; +import java.util.stream.Collectors; + +public class RacingGame { + private final Players players; + private final TryCount tryCount; + + public RacingGame(Players players, TryCount tryCount) { + this.players = players; + this.tryCount = tryCount; + } + + public void startRace() { + players.race(); + } + + public int getTryCount() { + return tryCount.getTryCount(); + } + + public List getPlayers() { + return players.getPlayers(); + } + + public List findWinners() { + int maxDistance = getMaxDistance(); + return players.getPlayers().stream() + .filter(p -> p.getDistance() == maxDistance) + .collect(Collectors.toList()); + } + private int getMaxDistance() { + return players.getPlayers().stream() + .mapToInt(Player::getDistance) + .max() + .orElse(0); + } +} + diff --git a/src/main/java/racingcar/domain/players/Movement.java b/src/main/java/racingcar/domain/players/Movement.java new file mode 100644 index 0000000..637b0c7 --- /dev/null +++ b/src/main/java/racingcar/domain/players/Movement.java @@ -0,0 +1,26 @@ +package racingcar.domain.players; + +import racingcar.domain.random.Random; + +public class Movement { + private String movement =""; + + public void move(){ + final Random random = new Random(); + if(random.getRandom()>=4) + moveForward(); + } + + private void moveForward(){ + this.movement +="-"; + } + + public String getMovement() { + return movement; + } + + public int getDistance() { + return movement.length(); + } + +} diff --git a/src/main/java/racingcar/domain/players/Name.java b/src/main/java/racingcar/domain/players/Name.java new file mode 100644 index 0000000..1bb9026 --- /dev/null +++ b/src/main/java/racingcar/domain/players/Name.java @@ -0,0 +1,53 @@ +package racingcar.domain.players; + +import java.util.Objects; + +public class Name { + + private static final int UPPER_BOUND = 5; + private final String name; + + public Name(final String input) { + validate(input); + this.name = input; + } + + private void validate(final String input) { + validateNullAndBlank(input); + validateLength(input); + } + + private void validateNullAndBlank(final String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("[ERROR] 이름이 존재하지 않습니다."); + } + } + + private void validateLength(final String input) { + if (input.length() > UPPER_BOUND) { + throw new IllegalArgumentException("[ERROR] 이름은 " + UPPER_BOUND + "글자 이하여야 합니다. 현재 이름: " + input); + } + } + + public String getName() { + return name; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Name n = (Name) o; + return Objects.equals(name, n.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + +} diff --git a/src/main/java/racingcar/domain/players/Player.java b/src/main/java/racingcar/domain/players/Player.java new file mode 100644 index 0000000..068df27 --- /dev/null +++ b/src/main/java/racingcar/domain/players/Player.java @@ -0,0 +1,42 @@ +package racingcar.domain.players; + +import java.util.Objects; + +public class Player { + private final Name name; + private final Movement movement; + + public Player(String name) { + this.name = new Name(name); + this.movement = new Movement(); + } + + public String getName() { + return name.getName(); + } + + public String getMovement() { + return movement.getMovement(); + } + + public int getDistance() { + return movement.getDistance(); + } + + public void race() { + movement.move(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Player player = (Player) o; + return Objects.equals(name, player.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/java/racingcar/domain/players/Players.java b/src/main/java/racingcar/domain/players/Players.java new file mode 100644 index 0000000..acd69a2 --- /dev/null +++ b/src/main/java/racingcar/domain/players/Players.java @@ -0,0 +1,29 @@ +package racingcar.domain.players; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Players { + private final List players; + + public Players(final List players) { + validateDuplicate(players); + this.players = List.copyOf(players); + } + private void validateDuplicate(final List players) { + final Set uniquePlayers = new HashSet<>(players); + + if (uniquePlayers.size() != players.size()) { + throw new IllegalArgumentException("[ERROR] 참가자 이름은 중복될 수 없습니다."); + } + } + public void race() { + players.forEach(Player::race); + } + + public List getPlayers() { + return players; + } + +} diff --git a/src/main/java/racingcar/domain/random/Random.java b/src/main/java/racingcar/domain/random/Random.java new file mode 100644 index 0000000..79bf552 --- /dev/null +++ b/src/main/java/racingcar/domain/random/Random.java @@ -0,0 +1,15 @@ +package racingcar.domain.random; + +import org.kokodak.Randoms; + +public class Random { + private final int random; + + public Random() { + this.random = Randoms.pickNumberInRange(0, 9); + } + + public int getRandom() { + return random; + } +} diff --git a/src/main/java/racingcar/domain/trycount/TryCount.java b/src/main/java/racingcar/domain/trycount/TryCount.java new file mode 100644 index 0000000..720946d --- /dev/null +++ b/src/main/java/racingcar/domain/trycount/TryCount.java @@ -0,0 +1,32 @@ +package racingcar.domain.trycount; + +public class TryCount { + + private static final String NOT_AN_INTEGER = "[ERROR] 정수가 아닙니다."; + private static final String LESS_THAN_ZERO = "[ERROR] 정수가 0보다 작습니다."; + private static final String INTEGER_REGEX = "[+-]?\\d*(\\.\\d+)?"; + private final int tryCount; + + public TryCount(String input) { + validate(input); + this.tryCount = Integer.parseInt(input); + } + private void validate(final String input) { + validateInteger(input); + validateBiggerThanZero(Integer.parseInt(input)); + } + private void validateInteger(final String input) { + if (!(input.matches(INTEGER_REGEX))) { + throw new IllegalArgumentException(NOT_AN_INTEGER); + } + } + private void validateBiggerThanZero(final int input) { + if (input<=0) { + throw new IllegalArgumentException(LESS_THAN_ZERO); + } + } + + public int getTryCount() { + return tryCount; + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000..b1b0578 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,21 @@ +package racingcar.view; + +import org.kokodak.Console; + +import java.util.List; + +public class InputView { + + public List readNames() { + System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + final String input = Console.readLine(); + final List names = Parser.parseByDelimiter(input, ","); + + return Parser.trim(names); + } + public String readTryCount() { + System.out.println("시도할 회수는 몇회인가요?"); + + return Console.readLine(); + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000..50baf1f --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,42 @@ +package racingcar.view; + +import racingcar.view.dto.PlayerResponse; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.text.MessageFormat.format; + +public class OutputView { + + private static final String PRINT_PATTERN = "{0} : {1}"; + + public void printPlayerData(final PlayerResponse player) { + System.out.println(getPlayerData(player)); + } + + private String getPlayerData(final PlayerResponse player) { + final String name = player.getName(); + final String movement = player.getMovement(); + + return format(PRINT_PATTERN, name, movement); + } + + public void printResultMessage() { + System.out.println(); + System.out.println("실행 결과"); + } + + public void printResult(final List players) { + System.out.println(format("{0}가 최종 우승했습니다.", getNamesFormat(players))); + } + private String getNamesFormat(final List players) { + return players.stream() + .map(PlayerResponse::getName) + .collect(Collectors.joining(", ")); + } + + public void println() { + System.out.println(); + } +} diff --git a/src/main/java/racingcar/view/Parser.java b/src/main/java/racingcar/view/Parser.java new file mode 100644 index 0000000..32206ce --- /dev/null +++ b/src/main/java/racingcar/view/Parser.java @@ -0,0 +1,17 @@ +package racingcar.view; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Parser { + + public static List parseByDelimiter(final String value, final String delimiter) { + return Arrays.asList(value.split(delimiter, -1)); + } + public static List trim(final List values) { + return values.stream() + .map(String::trim) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/racingcar/view/dto/PlayerResponse.java b/src/main/java/racingcar/view/dto/PlayerResponse.java new file mode 100644 index 0000000..f98df59 --- /dev/null +++ b/src/main/java/racingcar/view/dto/PlayerResponse.java @@ -0,0 +1,27 @@ +package racingcar.view.dto; + +import racingcar.domain.players.Player; + +public class PlayerResponse { + private final String name; + private final String movement; + + private PlayerResponse(final String name, final String movement) { + this.name = name; + this.movement = movement; + } + + public static PlayerResponse from(final Player player) { + final String name = player.getName(); + final String movement = player.getMovement(); + return new PlayerResponse(name, movement); + } + + public String getName() { + return name; + } + public String getMovement() { + return movement; + } + +} diff --git a/src/test/java/racingcar/domain/players/NameTest.java b/src/test/java/racingcar/domain/players/NameTest.java new file mode 100644 index 0000000..d84ed7b --- /dev/null +++ b/src/test/java/racingcar/domain/players/NameTest.java @@ -0,0 +1,28 @@ +package racingcar.domain.players; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NameTest { + + @DisplayName("이름이 존재하지 않으면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void ValueIsNotExist(String value) { + assertThatThrownBy(() -> new Name(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } + + @DisplayName("이름이 글자수를 초과하면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"123456", "aaaaaaaaaaaa", "-100770"}) + void ValueExceedsLength(String value) { + assertThatThrownBy(() -> new Name(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } +} diff --git a/src/test/java/racingcar/domain/players/PlayersTest.java b/src/test/java/racingcar/domain/players/PlayersTest.java new file mode 100644 index 0000000..599a217 --- /dev/null +++ b/src/test/java/racingcar/domain/players/PlayersTest.java @@ -0,0 +1,21 @@ +package racingcar.domain.players; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PlayersTest { + @Test + void 중복되는_이름이_존재하면_예외를_던진다() { + final List players = List.of( + new Player("dazzl"), + new Player("dazzl"), + new Player("koko")); + + assertThatThrownBy(() -> new Players(players)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } +} diff --git a/src/test/java/racingcar/domain/trycount/TryCountTest.java b/src/test/java/racingcar/domain/trycount/TryCountTest.java new file mode 100644 index 0000000..03cd79a --- /dev/null +++ b/src/test/java/racingcar/domain/trycount/TryCountTest.java @@ -0,0 +1,30 @@ +package racingcar.domain.trycount; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TryCountTest { + + @DisplayName("횟수가 정수가 아니면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"100j", "aaaa10","bbbb"}) + void ValueIsNotAnInteger(String value) { + assertThatThrownBy(() -> new TryCount(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } + + @DisplayName("횟수가 0 이하면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"0", "-1", "-1000", "-123123"}) + void ValueIsLessThanZero(String value) { + assertThatThrownBy(() -> new TryCount(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } + + +} diff --git a/src/test/java/racingcar/view/ParserTest.java b/src/test/java/racingcar/view/ParserTest.java new file mode 100644 index 0000000..e166ac8 --- /dev/null +++ b/src/test/java/racingcar/view/ParserTest.java @@ -0,0 +1,36 @@ +package racingcar.view; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ParserTest { + @Test + void 구분자를_기준으로_문자열을_파싱한다() { + final String value = "pobi,jason"; + + final List result = Parser.parseByDelimiter(value, ","); + + assertThat(result).containsExactly("pobi", "jason"); + } + + @Test + void 구분자를_기준으로_빈문자열을_파싱한다() { + final String value = ",,"; + + final List result = Parser.parseByDelimiter(value, ","); + + assertThat(result).containsExactly("", "", ""); + } + + @Test + void 문자열의_앞뒤_공백은_제거한다() { + final List value = List.of(" pobi", "jason ", " crong "); + + final List result = Parser.trim(value); + + assertThat(result).containsExactly("pobi", "jason", "crong"); + } +}