diff --git a/docs/README.md b/docs/README.md index e69de29..dd979c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,110 @@ +## Feature + +> 입력 예외가 발생한 경우 `[Error] {message}` 형태로 출력 후 다시 입력 진행 + +- 다리의 길이를 `입력`받고 입력받은 Length(N)만큼 다리를 설치한다 + - 다리의 길이가 `3..20`이 아닌 경우 예외 처리 + - 각 길이 별 숫자가 0이면 위 칸(U), 1이면 아래 칸(D)을 건널 수 있다 + +- 플레이어는 이동할 칸을 입력한다 + - U or D가 아닌 경우 예외 처리 + - 이동을 하고 난 후 해당 칸을 건널 수 있으면 O, 없으면 X를 다리에 표시한다 + +- 플레이어는 다리를 완전히 건너면 게임을 클리어한다 + +- 플레이어는 다리 건너기에 실패하면 `재시작(R) / 종료(Q)`중에 하나를 선택할 수 있다 + - 재시작(R) -> 처음 생성한 다리 그대로 다시 건너기 + - 종료(Q) -> 게임 종료 + +- 플레이어는 게임을 클리어 했거나 건너기 실패 후 종료하면 게임 결과를 받을 수 있다 + 1. 사용자가 진행한 게임 결과 + 2. 게임 성공 여부 (성공 / 실패) + 3. 총 시도 횟수 (다리 건넌 횟수) + +
+
+ +## Model + +### `bridge/BridgeDirection` + +- 다리 이동 방향과 관련된 Enum 컴포넌트 + +### `bridge/Bridge` + +- Input Size 길이만큼 랜덤 생성된 Bridge 컴포넌트 + - 사용자가 건너갈 Bridge의 정답 기준 + - `BridgeMaker -> List`을 기준으로 Bridge 생성 + +### `bridge/BridgeLine` + +- 위 아래 각각의 Bridge를 나타내는 컴포넌트 + +### `bridge/BridgeMap` + +- 사용자가 건너는 전체 다리 정보와 관련된 Map 컴포넌트 + - `BridgeLine upLine, BridgeLine downLine`을 포함 + +### `game/GameStatus` + +- 게임 상태와 관련된 Enum 컴포넌트 + - `IN_PROGRESS` -> 게임 진행 + - `GAME_CLEAR` -> 게임 성공 + - `GAME_FAIL` -> 게임 실패 + +### `game/GameRoundStatus` + +- 각 라운드별 다리 건넌 상태를 표현하기 위한 컴포넌트 + - `ROUND_SUCCESS` -> 건너기 성공 + - `ROUND_FAIL` -> 건너기 실패 + - `ROUND_NONE` -> 건너지 않음 + +### `game/GameResultStatus` + +- 게임 종료 후 출력 관련된 Enum 컴포넌트 + - `CLEAR` -> 성공 + - `FAIL` -> 실패 + +### `game/GameTracker` + +- 게임 진행과 관련된 컴포넌트를 묶은 컴포넌트 + - 다리 전체 맵 + - 게임 현재 상태 + - 총 시도 횟수 + +
+
+ +## Utils + +### `ExceptionConstants` + +- 전역 예외 메시지 통합 컴포넌트 + +### `validator/Validator` + +- 사용자 Input에 대한 기본 검증 컴포넌트 + +
+
+ +## View + +### `InputView` + +- 사용자 Input을 받기 위한 컴포넌트 + +### `OutputView` + +- 다리 건너기 게임 진행과 관련된 출력 컴포넌트 + +
+
+ +## Controller + +### `BridgeGame` + +- 다리 건너기 게임을 관리하는 컴포넌트 + +
diff --git a/src/main/java/bridge/Application.java b/src/main/java/bridge/Application.java index 5cb72df..1fca992 100644 --- a/src/main/java/bridge/Application.java +++ b/src/main/java/bridge/Application.java @@ -1,8 +1,9 @@ package bridge; -public class Application { +import bridge.controller.BridgeGame; +public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + new BridgeGame().run(); } } diff --git a/src/main/java/bridge/BridgeGame.java b/src/main/java/bridge/BridgeGame.java deleted file mode 100644 index 834c1c8..0000000 --- a/src/main/java/bridge/BridgeGame.java +++ /dev/null @@ -1,23 +0,0 @@ -package bridge; - -/** - * 다리 건너기 게임을 관리하는 클래스 - */ -public class BridgeGame { - - /** - * 사용자가 칸을 이동할 때 사용하는 메서드 - *

- * 이동을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. - */ - public void move() { - } - - /** - * 사용자가 게임을 다시 시도할 때 사용하는 메서드 - *

- * 재시작을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. - */ - public void retry() { - } -} diff --git a/src/main/java/bridge/BridgeMaker.java b/src/main/java/bridge/BridgeMaker.java index 27e9f2c..89cb097 100644 --- a/src/main/java/bridge/BridgeMaker.java +++ b/src/main/java/bridge/BridgeMaker.java @@ -1,15 +1,22 @@ package bridge; +import bridge.model.bridge.BridgeDirection; + import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static bridge.utils.BridgeConstants.MAX_VALUE; +import static bridge.utils.BridgeConstants.MIN_VALUE; +import static bridge.utils.ExceptionConstants.BridgeMakerException.BRIDGE_SIZE_IS_OUT_OF_RANGE; /** * 다리의 길이를 입력 받아서 다리를 생성해주는 역할을 한다. */ public class BridgeMaker { - private final BridgeNumberGenerator bridgeNumberGenerator; - public BridgeMaker(BridgeNumberGenerator bridgeNumberGenerator) { + public BridgeMaker(final BridgeNumberGenerator bridgeNumberGenerator) { this.bridgeNumberGenerator = bridgeNumberGenerator; } @@ -17,7 +24,23 @@ public BridgeMaker(BridgeNumberGenerator bridgeNumberGenerator) { * @param size 다리의 길이 * @return 입력받은 길이에 해당하는 다리 모양. 위 칸이면 "U", 아래 칸이면 "D"로 표현해야 한다. */ - public List makeBridge(int size) { - return null; + public List makeBridge(final int size) { + validateBridgeSize(size); + + return IntStream.generate(bridgeNumberGenerator::generate) + .limit(size) + .mapToObj(BridgeDirection::fromNumber) + .map(BridgeDirection::getCommand) + .collect(Collectors.toList()); + } + + private void validateBridgeSize(final int size) { + if (isOutOfRange(size)) { + throw new IllegalArgumentException(BRIDGE_SIZE_IS_OUT_OF_RANGE.message); + } + } + + private boolean isOutOfRange(final int size) { + return size < MIN_VALUE || size > MAX_VALUE; } } diff --git a/src/main/java/bridge/BridgeNumberGenerator.java b/src/main/java/bridge/BridgeNumberGenerator.java index 56187b7..0787377 100644 --- a/src/main/java/bridge/BridgeNumberGenerator.java +++ b/src/main/java/bridge/BridgeNumberGenerator.java @@ -2,6 +2,5 @@ @FunctionalInterface public interface BridgeNumberGenerator { - int generate(); } diff --git a/src/main/java/bridge/InputView.java b/src/main/java/bridge/InputView.java deleted file mode 100644 index c3911c8..0000000 --- a/src/main/java/bridge/InputView.java +++ /dev/null @@ -1,28 +0,0 @@ -package bridge; - -/** - * 사용자로부터 입력을 받는 역할을 한다. - */ -public class InputView { - - /** - * 다리의 길이를 입력받는다. - */ - public int readBridgeSize() { - return 0; - } - - /** - * 사용자가 이동할 칸을 입력받는다. - */ - public String readMoving() { - return null; - } - - /** - * 사용자가 게임을 다시 시도할지 종료할지 여부를 입력받는다. - */ - public String readGameCommand() { - return null; - } -} diff --git a/src/main/java/bridge/OutputView.java b/src/main/java/bridge/OutputView.java deleted file mode 100644 index 69a433a..0000000 --- a/src/main/java/bridge/OutputView.java +++ /dev/null @@ -1,23 +0,0 @@ -package bridge; - -/** - * 사용자에게 게임 진행 상황과 결과를 출력하는 역할을 한다. - */ -public class OutputView { - - /** - * 현재까지 이동한 다리의 상태를 정해진 형식에 맞춰 출력한다. - *

- * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. - */ - public void printMap() { - } - - /** - * 게임의 최종 결과를 정해진 형식에 맞춰 출력한다. - *

- * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. - */ - public void printResult() { - } -} diff --git a/src/main/java/bridge/controller/BridgeGame.java b/src/main/java/bridge/controller/BridgeGame.java new file mode 100644 index 0000000..150a611 --- /dev/null +++ b/src/main/java/bridge/controller/BridgeGame.java @@ -0,0 +1,129 @@ +package bridge.controller; + +import bridge.BridgeMaker; +import bridge.BridgeRandomNumberGenerator; +import bridge.model.bridge.Bridge; +import bridge.model.bridge.BridgeDirection; +import bridge.model.game.GameProcessDecisionCommand; +import bridge.model.game.GameRoundStatus; +import bridge.model.game.GameTracker; +import bridge.view.InputView; +import bridge.view.OutputView; + +import java.util.List; + +import static bridge.model.game.GameRoundStatus.ROUND_FAIL; +import static bridge.model.game.GameRoundStatus.ROUND_SUCCESS; +import static bridge.model.game.GameStatus.GAME_CLEAR; +import static bridge.model.game.GameStatus.GAME_FAIL; + +/** + * 다리 건너기 게임을 관리하는 클래스 + */ +public class BridgeGame { + private Bridge bridge; + private GameTracker gameTracker; + + public void run() { + try { + initializeGame(); + startGame(); + printGameResult(); + } catch (final IllegalArgumentException e) { + OutputView.printErrorMessage(e.getMessage()); + } + } + + private void initializeGame() { + OutputView.printStartGame(); + initializeBridge(); + initalizeGameTracker(); + } + + private void initializeBridge() { + final BridgeMaker bridgeMaker = new BridgeMaker(new BridgeRandomNumberGenerator()); + final List bridgeDirections = bridgeMaker.makeBridge(InputView.readBridgeSize()); + bridge = new Bridge(bridgeDirections); + } + + private void initalizeGameTracker() { + gameTracker = new GameTracker(); + } + + private void startGame() { + while (gameTracker.isGameInProgress()) { + processEachRound(); + handleGameProcess(); + } + } + + private void processEachRound() { + final int currentOrder = gameTracker.getCurrentOrder(); + final GameRoundStatus roundStatus = moveEachRound(currentOrder); + + if (roundStatus.isRoundFail()) { + gameTracker.updateGameStatus(GAME_FAIL); + return; + } + + if (roundStatus.isRoundSuccess() && bridge.isEndOfBridge(currentOrder)) { + gameTracker.updateGameStatus(GAME_CLEAR); + } + } + + private GameRoundStatus moveEachRound(final int currentOrder) { + final BridgeDirection bridgeDirection = bridge.getBridgeDirectionByIndex(currentOrder); + final BridgeDirection playerMoveCommand = BridgeDirection.fromCommand(InputView.readMoving()); + return move(bridgeDirection, playerMoveCommand); + } + + /** + * 사용자가 칸을 이동할 때 사용하는 메서드 + *

+ * 이동을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. + */ + private GameRoundStatus move( + final BridgeDirection bridgeDirection, + final BridgeDirection playerDirection + ) { + final GameRoundStatus roundStatus = judgeRoundByDirection(bridgeDirection, playerDirection); + gameTracker.updateMap(playerDirection, roundStatus); + return roundStatus; + } + + private GameRoundStatus judgeRoundByDirection( + final BridgeDirection bridgeDirection, + final BridgeDirection playerDirection + ) { + if (bridgeDirection == playerDirection) { + return ROUND_SUCCESS; + } + return ROUND_FAIL; + } + + private void handleGameProcess() { + if (gameTracker.isGameFail()) { + handleRetryProcess(); + } + } + + private void handleRetryProcess() { + final GameProcessDecisionCommand decisionCommand = GameProcessDecisionCommand.from(InputView.readGameCommand()); + if (decisionCommand.isRetryDecision()) { + retry(); + } + } + + /** + * 사용자가 게임을 다시 시도할 때 사용하는 메서드 + *

+ * 재시작을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. + */ + private void retry() { + gameTracker.retryGame(); + } + + private void printGameResult() { + OutputView.printResult(gameTracker); + } +} diff --git a/src/main/java/bridge/model/bridge/Bridge.java b/src/main/java/bridge/model/bridge/Bridge.java new file mode 100644 index 0000000..be0c931 --- /dev/null +++ b/src/main/java/bridge/model/bridge/Bridge.java @@ -0,0 +1,31 @@ +package bridge.model.bridge; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class Bridge { + private final List bridge; + + public Bridge(final List bridge) { + this.bridge = translateBridgeDirection(bridge); + } + + private List translateBridgeDirection(final List bridge) { + return bridge.stream() + .map(BridgeDirection::fromCommand) + .collect(Collectors.toList()); + } + + public List getBridge() { + return Collections.unmodifiableList(bridge); + } + + public BridgeDirection getBridgeDirectionByIndex(final int index) { + return bridge.get(index); + } + + public boolean isEndOfBridge(final int currentOrder) { + return currentOrder + 1 == bridge.size(); + } +} diff --git a/src/main/java/bridge/model/bridge/BridgeDirection.java b/src/main/java/bridge/model/bridge/BridgeDirection.java new file mode 100644 index 0000000..58b5283 --- /dev/null +++ b/src/main/java/bridge/model/bridge/BridgeDirection.java @@ -0,0 +1,41 @@ +package bridge.model.bridge; + +import java.util.Arrays; + +import static bridge.utils.ExceptionConstants.BridgeDirectionException.INVALID_DIRECTION_COMMAND; +import static bridge.utils.ExceptionConstants.BridgeDirectionException.INVALID_DIRECTION_NUMBER; + +public enum BridgeDirection { + UP(1, "U"), + DOWN(0, "D"), + ; + + private final int number; + private final String command; + + BridgeDirection( + final int number, + final String command + ) { + this.number = number; + this.command = command; + } + + public static BridgeDirection fromNumber(final int number) { + return Arrays.stream(values()) + .filter(bridgeDirection -> bridgeDirection.number == number) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_DIRECTION_NUMBER.message)); + } + + public static BridgeDirection fromCommand(final String command) { + return Arrays.stream(values()) + .filter(bridgeDirection -> bridgeDirection.command.equals(command)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_DIRECTION_COMMAND.message)); + } + + public String getCommand() { + return command; + } +} diff --git a/src/main/java/bridge/model/bridge/BridgeLine.java b/src/main/java/bridge/model/bridge/BridgeLine.java new file mode 100644 index 0000000..6a89ddc --- /dev/null +++ b/src/main/java/bridge/model/bridge/BridgeLine.java @@ -0,0 +1,36 @@ +package bridge.model.bridge; + +import bridge.model.game.GameRoundStatus; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public class BridgeLine { + private final List line; + + private BridgeLine() { + this.line = new ArrayList<>(); + } + + public static BridgeLine create() { + return new BridgeLine(); + } + + public void addLine(final GameRoundStatus status) { + line.add(status.getValue()); + } + + public int getLength() { + return line.size(); + } + + @Override + public String toString() { + final StringJoiner joiner = new StringJoiner(" | ", "[ ", " ]"); + for (String value : line) { + joiner.add(value); + } + return joiner.toString(); + } +} diff --git a/src/main/java/bridge/model/bridge/BridgeMap.java b/src/main/java/bridge/model/bridge/BridgeMap.java new file mode 100644 index 0000000..c9f5854 --- /dev/null +++ b/src/main/java/bridge/model/bridge/BridgeMap.java @@ -0,0 +1,48 @@ +package bridge.model.bridge; + +import bridge.model.game.GameRoundStatus; + +import static bridge.model.bridge.BridgeDirection.DOWN; +import static bridge.model.bridge.BridgeDirection.UP; +import static bridge.model.game.GameRoundStatus.ROUND_NONE; + +public class BridgeMap { + private final BridgeLine upLine; + private final BridgeLine downLine; + + private BridgeMap( + final BridgeLine upLine, + final BridgeLine downLine + ) { + this.upLine = upLine; + this.downLine = downLine; + } + + public static BridgeMap init() { + return new BridgeMap(BridgeLine.create(), BridgeLine.create()); + } + + public void updateMap( + final BridgeDirection direction, + final GameRoundStatus status + ) { + if (direction == UP) { + upLine.addLine(status); + downLine.addLine(ROUND_NONE); + } + + if (direction == DOWN) { + upLine.addLine(ROUND_NONE); + downLine.addLine(status); + } + } + + public int getLineLength() { + return upLine.getLength(); + } + + @Override + public String toString() { + return upLine.toString() + "\n" + downLine.toString(); + } +} diff --git a/src/main/java/bridge/model/game/GameProcessDecisionCommand.java b/src/main/java/bridge/model/game/GameProcessDecisionCommand.java new file mode 100644 index 0000000..ebdf996 --- /dev/null +++ b/src/main/java/bridge/model/game/GameProcessDecisionCommand.java @@ -0,0 +1,32 @@ +package bridge.model.game; + +import java.util.Arrays; + +import static bridge.utils.ExceptionConstants.GameProcessDecisionCommandException.INVALID_PROCESS_DECISION_COMMAND; + +public enum GameProcessDecisionCommand { + RETRY("R"), + QUIT("Q"), + ; + + private final String value; + + GameProcessDecisionCommand(final String value) { + this.value = value; + } + + public static GameProcessDecisionCommand from(final String value) { + return Arrays.stream(values()) + .filter(gameProcessDecisionCommand -> gameProcessDecisionCommand.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_PROCESS_DECISION_COMMAND.message)); + } + + public String getValue() { + return value; + } + + public boolean isRetryDecision() { + return this == RETRY; + } +} diff --git a/src/main/java/bridge/model/game/GameResultStatus.java b/src/main/java/bridge/model/game/GameResultStatus.java new file mode 100644 index 0000000..6568a91 --- /dev/null +++ b/src/main/java/bridge/model/game/GameResultStatus.java @@ -0,0 +1,17 @@ +package bridge.model.game; + +public enum GameResultStatus { + CLEAR("성공"), + FAIL("실패"), + ; + + private final String value; + + GameResultStatus(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/bridge/model/game/GameRoundStatus.java b/src/main/java/bridge/model/game/GameRoundStatus.java new file mode 100644 index 0000000..93079f5 --- /dev/null +++ b/src/main/java/bridge/model/game/GameRoundStatus.java @@ -0,0 +1,31 @@ +package bridge.model.game; + +public enum GameRoundStatus { + ROUND_SUCCESS("O", true), + ROUND_FAIL("X", false), + ROUND_NONE(" ", false), + ; + + private final String value; + private final boolean success; + + GameRoundStatus( + final String value, + final boolean success + ) { + this.value = value; + this.success = success; + } + + public String getValue() { + return value; + } + + public boolean isRoundSuccess() { + return this == ROUND_SUCCESS; + } + + public boolean isRoundFail() { + return this == ROUND_FAIL; + } +} diff --git a/src/main/java/bridge/model/game/GameStatus.java b/src/main/java/bridge/model/game/GameStatus.java new file mode 100644 index 0000000..ddd8afa --- /dev/null +++ b/src/main/java/bridge/model/game/GameStatus.java @@ -0,0 +1,20 @@ +package bridge.model.game; + +public enum GameStatus { + IN_PROGRESS, + GAME_CLEAR, + GAME_FAIL, + ; + + public boolean isGameInProgress() { + return this == IN_PROGRESS; + } + + public boolean isGameClear() { + return this == GAME_CLEAR; + } + + public boolean isGameFail() { + return this == GAME_FAIL; + } +} diff --git a/src/main/java/bridge/model/game/GameTracker.java b/src/main/java/bridge/model/game/GameTracker.java new file mode 100644 index 0000000..ac43f4f --- /dev/null +++ b/src/main/java/bridge/model/game/GameTracker.java @@ -0,0 +1,72 @@ +package bridge.model.game; + +import bridge.model.bridge.BridgeDirection; +import bridge.model.bridge.BridgeMap; + +import static bridge.model.game.GameResultStatus.CLEAR; +import static bridge.model.game.GameResultStatus.FAIL; +import static bridge.model.game.GameStatus.IN_PROGRESS; + +public class GameTracker { + private BridgeMap bridgeMap; + private GameStatus gameStatus; + private int attemptCount; + + public GameTracker() { + this.bridgeMap = BridgeMap.init(); + this.gameStatus = IN_PROGRESS; + this.attemptCount = 1; + } + + public void updateMap( + final BridgeDirection direction, + final GameRoundStatus status + ) { + bridgeMap.updateMap(direction, status); + } + + public void retryGame() { + bridgeMap = BridgeMap.init(); + gameStatus = IN_PROGRESS; + attemptCount++; + } + + public void updateGameStatus(final GameStatus gameStatus) { + this.gameStatus = gameStatus; + } + + public String displayResultStatus() { + if (isGameClear()) { + return CLEAR.getValue(); + } + return FAIL.getValue(); + } + + public int getCurrentOrder() { + return bridgeMap.getLineLength(); + } + + public boolean isGameInProgress() { + return gameStatus.isGameInProgress(); + } + + public boolean isGameClear() { + return gameStatus.isGameClear(); + } + + public boolean isGameFail() { + return gameStatus.isGameFail(); + } + + public BridgeMap getBridgeMap() { + return bridgeMap; + } + + public GameStatus getGameStatus() { + return gameStatus; + } + + public int getAttemptCount() { + return attemptCount; + } +} diff --git a/src/main/java/bridge/utils/BridgeConstants.java b/src/main/java/bridge/utils/BridgeConstants.java new file mode 100644 index 0000000..773f429 --- /dev/null +++ b/src/main/java/bridge/utils/BridgeConstants.java @@ -0,0 +1,6 @@ +package bridge.utils; + +public interface BridgeConstants { + int MIN_VALUE = 3; + int MAX_VALUE = 20; +} diff --git a/src/main/java/bridge/utils/ExceptionConstants.java b/src/main/java/bridge/utils/ExceptionConstants.java new file mode 100644 index 0000000..91f3902 --- /dev/null +++ b/src/main/java/bridge/utils/ExceptionConstants.java @@ -0,0 +1,51 @@ +package bridge.utils; + +public interface ExceptionConstants { + enum BridgeDirectionException { + INVALID_DIRECTION_NUMBER("위아래 두 칸으로 이루어진 다리를 생성할 때 [0, 1]중 하나의 값으로만 위/아래를 정해야 합니다."), + INVALID_DIRECTION_COMMAND("사용자가 이동할 수 있는 방향은 [U, D]중 하나여야 합니다."), + ; + + public final String message; + + BridgeDirectionException(final String message) { + this.message = message; + } + } + + enum GameProcessDecisionCommandException { + INVALID_PROCESS_DECISION_COMMAND("게임 재시작, 게임 종료에 대해서 [R, Q] 커맨드만 입력할 수 있습니다."), + ; + + public final String message; + + GameProcessDecisionCommandException(final String message) { + this.message = message; + } + } + + enum BridgeMakerException { + BRIDGE_SIZE_IS_OUT_OF_RANGE("다리 길이는 3..20 범위여야 합니다."), + ; + + public final String message; + + BridgeMakerException(final String message) { + this.message = message; + } + } + + enum InputException { + INPUT_MUST_NOT_CONTAINS_SPACE("공백없이 입력해주세요."), + INPUT_MUST_BE_NUMERIC("숫자를 입력해주세요."), + INVALID_MOVE_COMMAND("U, D중 하나를 입력해주세요"), + INVALID_GAME_PROCESS_COMMAND("R, Q중 하나를 입력해주세요"), + ; + + public final String message; + + InputException(final String message) { + this.message = message; + } + } +} diff --git a/src/main/java/bridge/utils/validator/BridgeLengthValidator.java b/src/main/java/bridge/utils/validator/BridgeLengthValidator.java new file mode 100644 index 0000000..e741799 --- /dev/null +++ b/src/main/java/bridge/utils/validator/BridgeLengthValidator.java @@ -0,0 +1,23 @@ +package bridge.utils.validator; + +import static bridge.utils.BridgeConstants.MAX_VALUE; +import static bridge.utils.BridgeConstants.MIN_VALUE; + +public class BridgeLengthValidator extends Validator { + @Override + public void validate(final String userInput) { + validateInputHasSpace(userInput); + validateInputIsNumeric(userInput); + validateBridgeLengthIsInRange(userInput); + } + + private void validateBridgeLengthIsInRange(final String userInput) { + if (isOutOfRange(Integer.parseInt(userInput))) { + throw new IllegalArgumentException(); + } + } + + private boolean isOutOfRange(final int bridgeLength) { + return bridgeLength < MIN_VALUE || bridgeLength > MAX_VALUE; + } +} diff --git a/src/main/java/bridge/utils/validator/GameProcessCommandValidator.java b/src/main/java/bridge/utils/validator/GameProcessCommandValidator.java new file mode 100644 index 0000000..1e01ee1 --- /dev/null +++ b/src/main/java/bridge/utils/validator/GameProcessCommandValidator.java @@ -0,0 +1,23 @@ +package bridge.utils.validator; + +import static bridge.model.game.GameProcessDecisionCommand.QUIT; +import static bridge.model.game.GameProcessDecisionCommand.RETRY; +import static bridge.utils.ExceptionConstants.InputException.INVALID_GAME_PROCESS_COMMAND; + +public class GameProcessCommandValidator extends Validator { + @Override + public void validate(final String userInput) { + validateInputHasSpace(userInput); + validateGameProcessCommandIsValid(userInput); + } + + private void validateGameProcessCommandIsValid(final String userInput) { + if (isInvalidGameProcessCommand(userInput)) { + throw new IllegalArgumentException(INVALID_GAME_PROCESS_COMMAND.message); + } + } + + private boolean isInvalidGameProcessCommand(final String userInput) { + return !RETRY.getValue().equals(userInput) && !QUIT.getValue().equals(userInput); + } +} diff --git a/src/main/java/bridge/utils/validator/MoveCommandValidator.java b/src/main/java/bridge/utils/validator/MoveCommandValidator.java new file mode 100644 index 0000000..ad62388 --- /dev/null +++ b/src/main/java/bridge/utils/validator/MoveCommandValidator.java @@ -0,0 +1,23 @@ +package bridge.utils.validator; + +import static bridge.model.bridge.BridgeDirection.DOWN; +import static bridge.model.bridge.BridgeDirection.UP; +import static bridge.utils.ExceptionConstants.InputException.INVALID_MOVE_COMMAND; + +public class MoveCommandValidator extends Validator { + @Override + public void validate(final String userInput) { + validateInputHasSpace(userInput); + validateMoveCommandIsValid(userInput); + } + + private void validateMoveCommandIsValid(final String userInput) { + if (isInvalidMoveCommand(userInput)) { + throw new IllegalArgumentException(INVALID_MOVE_COMMAND.message); + } + } + + private boolean isInvalidMoveCommand(final String userInput) { + return !UP.getCommand().equals(userInput) && !DOWN.getCommand().equals(userInput); + } +} diff --git a/src/main/java/bridge/utils/validator/Validator.java b/src/main/java/bridge/utils/validator/Validator.java new file mode 100644 index 0000000..7eae0a1 --- /dev/null +++ b/src/main/java/bridge/utils/validator/Validator.java @@ -0,0 +1,27 @@ +package bridge.utils.validator; + +import static bridge.utils.ExceptionConstants.InputException.INPUT_MUST_BE_NUMERIC; +import static bridge.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE; + +public abstract class Validator { + abstract void validate(final String userInput); + + protected void validateInputHasSpace(final String userInput) { + if (hasSpace(userInput)) { + throw new IllegalArgumentException(INPUT_MUST_NOT_CONTAINS_SPACE.message); + } + } + + private boolean hasSpace(final String userInput) { + return userInput.chars() + .anyMatch(Character::isWhitespace); + } + + protected void validateInputIsNumeric(final String userInput) { + try { + Integer.parseInt(userInput); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException(INPUT_MUST_BE_NUMERIC.message); + } + } +} diff --git a/src/main/java/bridge/view/InputView.java b/src/main/java/bridge/view/InputView.java new file mode 100644 index 0000000..3e40490 --- /dev/null +++ b/src/main/java/bridge/view/InputView.java @@ -0,0 +1,70 @@ +package bridge.view; + +import bridge.utils.validator.BridgeLengthValidator; +import bridge.utils.validator.GameProcessCommandValidator; +import bridge.utils.validator.MoveCommandValidator; +import camp.nextstep.edu.missionutils.Console; + +/** + * 사용자로부터 입력을 받는 역할을 한다. + */ +public class InputView { + private static final String INPUT_BRIDGE_LENGTH = "다리의 길이를 입력해주세요."; + private static final String INPUT_MOVE_COMMAND = "이동할 칸을 선택해주세요. (위: U, 아래: D)"; + private static final String INPUT_GAME_PROCESS_COMMAND = "게임을 다시 시도할지 여부를 입력해주세요. (재시도: R, 종료: Q)"; + + private static final BridgeLengthValidator BRIDGE_LENGTH_VALIDATOR = new BridgeLengthValidator(); + private static final MoveCommandValidator MOVE_COMMAND_VALIDATOR = new MoveCommandValidator(); + private static final GameProcessCommandValidator GAME_PROCESS_COMMAND_VALIDATOR = new GameProcessCommandValidator(); + + /** + * 다리의 길이를 입력받는다. + */ + public static int readBridgeSize() { + try { + System.out.println(INPUT_BRIDGE_LENGTH); + + final String input = Console.readLine(); + BRIDGE_LENGTH_VALIDATOR.validate(input); + + return Integer.parseInt(input); + } catch (final IllegalArgumentException e) { + OutputView.printErrorMessage(e.getMessage()); + return readBridgeSize(); + } + } + + /** + * 사용자가 이동할 칸을 입력받는다. + */ + public static String readMoving() { + try { + System.out.println(INPUT_MOVE_COMMAND); + + final String input = Console.readLine(); + MOVE_COMMAND_VALIDATOR.validate(input); + + return input; + } catch (final IllegalArgumentException e) { + OutputView.printErrorMessage(e.getMessage()); + return readMoving(); + } + } + + /** + * 사용자가 게임을 다시 시도할지 종료할지 여부를 입력받는다. + */ + public static String readGameCommand() { + try { + System.out.println(INPUT_GAME_PROCESS_COMMAND); + + final String input = Console.readLine(); + GAME_PROCESS_COMMAND_VALIDATOR.validate(input); + + return input; + } catch (final IllegalArgumentException e) { + OutputView.printErrorMessage(e.getMessage()); + return readGameCommand(); + } + } +} diff --git a/src/main/java/bridge/view/OutputView.java b/src/main/java/bridge/view/OutputView.java new file mode 100644 index 0000000..91c2b64 --- /dev/null +++ b/src/main/java/bridge/view/OutputView.java @@ -0,0 +1,44 @@ +package bridge.view; + +import bridge.model.bridge.BridgeMap; +import bridge.model.game.GameTracker; + +/** + * 사용자에게 게임 진행 상황과 결과를 출력하는 역할을 한다. + */ +public class OutputView { + private static final String START_GAME = "다리 건너기 게임을 시작합니다."; + private static final String FINAL_RESULT = "최종 게임 결과"; + private static final String GAME_IS_SUCCESSFUL = "게임 성공 여부: %s"; + private static final String NUMBER_OF_ATTEMPTS = "총 시도한 횟수: %d"; + private static final String ERROR_MESSAGE_FORMAT = "[ERROR] %s"; + + public static void printStartGame() { + System.out.println(START_GAME); + } + + /** + * 현재까지 이동한 다리의 상태를 정해진 형식에 맞춰 출력한다. + *

+ * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. + */ + public static void printMap(final BridgeMap bridgeMap) { + System.out.println(bridgeMap); + } + + /** + * 게임의 최종 결과를 정해진 형식에 맞춰 출력한다. + *

+ * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다. + */ + public static void printResult(final GameTracker gameTracker) { + System.out.println(FINAL_RESULT); + System.out.println(gameTracker.getBridgeMap()); + System.out.println(String.format(GAME_IS_SUCCESSFUL, gameTracker.displayResultStatus())); + System.out.println(String.format(NUMBER_OF_ATTEMPTS, gameTracker.getAttemptCount())); + } + + public static void printErrorMessage(final String message) { + System.out.printf(ERROR_MESSAGE_FORMAT, message); + } +} diff --git a/src/test/java/bridge/ApplicationTest.java b/src/test/java/bridge/ApplicationTest.java index 1a163ec..0d3970f 100644 --- a/src/test/java/bridge/ApplicationTest.java +++ b/src/test/java/bridge/ApplicationTest.java @@ -1,14 +1,15 @@ package bridge; +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.util.Lists.newArrayList; -import camp.nextstep.edu.missionutils.test.NsTest; -import java.util.List; -import org.junit.jupiter.api.Test; - class ApplicationTest extends NsTest { private static final String ERROR_MESSAGE = "[ERROR]"; @@ -26,11 +27,11 @@ class ApplicationTest extends NsTest { assertRandomNumberInRangeTest(() -> { run("3", "U", "D", "U"); assertThat(output()).contains( - "최종 게임 결과", - "[ O | | O ]", - "[ | O | ]", - "게임 성공 여부: 성공", - "총 시도한 횟수: 1" + "최종 게임 결과", + "[ O | | O ]", + "[ | O | ]", + "게임 성공 여부: 성공", + "총 시도한 횟수: 1" ); int upSideIndex = output().indexOf("[ O | | O ]"); @@ -39,6 +40,34 @@ class ApplicationTest extends NsTest { }, 1, 0, 1); } + @Test + void 기능_테스트2() { + assertRandomNumberInRangeTest(() -> { + run("3", "U", "U", "R", "U", "D", "D"); + assertThat(output()).contains( + "최종 게임 결과", + "[ O | | ]", + "[ | O | O ]", + "게임 성공 여부: 성공", + "총 시도한 횟수: 2" + ); + }, 1, 0, 0); + } + + @Test + void 기능_테스트3() { + assertRandomNumberInRangeTest(() -> { + run("3", "U", "U", "Q"); + assertThat(output()).contains( + "최종 게임 결과", + "[ O | X ]", + "[ | ]", + "게임 성공 여부: 실패", + "총 시도한 횟수: 1" + ); + }, 1, 0); + } + @Test void 예외_테스트() { assertSimpleTest(() -> { diff --git a/src/test/java/bridge/BridgeMakerTest.java b/src/test/java/bridge/BridgeMakerTest.java new file mode 100644 index 0000000..688c89b --- /dev/null +++ b/src/test/java/bridge/BridgeMakerTest.java @@ -0,0 +1,39 @@ +package bridge; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static bridge.utils.ExceptionConstants.BridgeMakerException.BRIDGE_SIZE_IS_OUT_OF_RANGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class BridgeMakerTest { + private static final BridgeMaker BRIDGE_MAKER = new BridgeMaker(new BridgeRandomNumberGenerator()); + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 21}) + @DisplayName("길이가 3..20 범위가 아니면 다리를 생성할 수 없다") + void throwExceptionByOutOfRange(final int size) { + assertThatThrownBy(() -> BRIDGE_MAKER.makeBridge(size)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(BRIDGE_SIZE_IS_OUT_OF_RANGE.message); + } + + @ParameterizedTest + @ValueSource(ints = {3, 10, 20}) + @DisplayName("Size 길이 만큼의 다리를 생성한다") + void construct(final int size) { + // when + final List bridge = BRIDGE_MAKER.makeBridge(size); + + // then + assertAll( + () -> assertThat(bridge).hasSize(size), + () -> assertThat(bridge).allMatch(value -> value.equals("U") || value.equals("D")) + ); + } +} diff --git a/src/test/java/bridge/common/BridgeFixture.java b/src/test/java/bridge/common/BridgeFixture.java new file mode 100644 index 0000000..e51c337 --- /dev/null +++ b/src/test/java/bridge/common/BridgeFixture.java @@ -0,0 +1,40 @@ +package bridge.common; + +import bridge.model.game.GameRoundStatus; + +import java.util.List; + +public class BridgeFixture { + public static final String INIT_BRIDGE_LINE = String.format("[ %s ]", ""); + public static final String INIT_BRIDGE_MAP = + new StringBuilder() + .append(String.format("[ %s ]", "")) + .append("\n") + .append(String.format("[ %s ]", "")) + .toString(); + + public static String createBridgeMap( + final String upLineFormat, + final List upLineDirections, + final String downLineFormat, + final List downLineDirections + ) { + return new StringBuilder() + .append(createBridgeLine(upLineFormat, upLineDirections)) + .append("\n") + .append(createBridgeLine(downLineFormat, downLineDirections)) + .toString(); + } + + public static String createBridgeLine( + final String format, + final List directions + ) { + return String.format( + format, + directions.stream() + .map(GameRoundStatus::getValue) + .toArray() + ); + } +} diff --git a/src/test/java/bridge/model/bridge/BridgeDirectionTest.java b/src/test/java/bridge/model/bridge/BridgeDirectionTest.java new file mode 100644 index 0000000..b035903 --- /dev/null +++ b/src/test/java/bridge/model/bridge/BridgeDirectionTest.java @@ -0,0 +1,70 @@ +package bridge.model.bridge; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static bridge.model.bridge.BridgeDirection.DOWN; +import static bridge.model.bridge.BridgeDirection.UP; +import static bridge.utils.ExceptionConstants.BridgeDirectionException.INVALID_DIRECTION_COMMAND; +import static bridge.utils.ExceptionConstants.BridgeDirectionException.INVALID_DIRECTION_NUMBER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class BridgeDirectionTest { + @Nested + @DisplayName("숫자로 BridgeDirection 조회") + class FromNumber { + @Test + @DisplayName("[0, 1]이 아닌 다른 숫자로 BridgeDirection을 조회하면 예외가 발생한다") + void throwExceptionByInvalidDirectionNumber() { + assertAll( + () -> assertThatThrownBy(() -> BridgeDirection.fromNumber(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_DIRECTION_NUMBER.message), + () -> assertThatThrownBy(() -> BridgeDirection.fromNumber(-2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_DIRECTION_NUMBER.message) + ); + } + + @Test + @DisplayName("정상적인 숫자로 BridgeDirection을 조회한다") + void success() { + assertAll( + () -> assertThat(BridgeDirection.fromNumber(0)).isEqualTo(DOWN), + () -> assertThat(BridgeDirection.fromNumber(1)).isEqualTo(UP) + ); + } + } + + @Nested + @DisplayName("Command로 BridgeDirection 조회") + class FromCommand { + @Test + @DisplayName("[U, D]가 아닌 다른 Command로 BridgeDirection을 조회하면 예외가 발생한다") + void throwExceptionByInvalidDirectionCommand() { + assertAll( + () -> assertThatThrownBy(() -> BridgeDirection.fromCommand("u")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_DIRECTION_COMMAND.message), + () -> assertThatThrownBy(() -> BridgeDirection.fromCommand("d")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_DIRECTION_COMMAND.message), + () -> assertThatThrownBy(() -> BridgeDirection.fromCommand("h")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_DIRECTION_COMMAND.message) + ); + } + + @Test + @DisplayName("정상적인 Command로 BridgeDirection을 조회한다") + void success() { + assertAll( + () -> assertThat(BridgeDirection.fromCommand("U")).isEqualTo(UP), + () -> assertThat(BridgeDirection.fromCommand("D")).isEqualTo(DOWN) + ); + } + } +} diff --git a/src/test/java/bridge/model/bridge/BridgeLineTest.java b/src/test/java/bridge/model/bridge/BridgeLineTest.java new file mode 100644 index 0000000..16f011c --- /dev/null +++ b/src/test/java/bridge/model/bridge/BridgeLineTest.java @@ -0,0 +1,45 @@ +package bridge.model.bridge; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static bridge.common.BridgeFixture.INIT_BRIDGE_LINE; +import static bridge.common.BridgeFixture.createBridgeLine; +import static bridge.model.game.GameRoundStatus.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class BridgeLineTest { + @Test + @DisplayName("BridgeLine을 생성하고 GameRoundStatus 결과에 따라 각 칸을 추가하면서 채운다") + void construct() { + final BridgeLine bridgeLine = BridgeLine.create(); + assertThat(bridgeLine.toString()).isEqualTo(INIT_BRIDGE_LINE); + + // add success + bridgeLine.addLine(ROUND_SUCCESS); + assertAll( + () -> assertThat(bridgeLine.getLength()).isEqualTo(1), + () -> assertThat(bridgeLine.toString()) + .isEqualTo(createBridgeLine("[ %s ]", List.of(ROUND_SUCCESS))) + ); + + // add fail + bridgeLine.addLine(ROUND_FAIL); + assertAll( + () -> assertThat(bridgeLine.getLength()).isEqualTo(2), + () -> assertThat(bridgeLine.toString()) + .isEqualTo(createBridgeLine("[ %s | %s ]", List.of(ROUND_SUCCESS, ROUND_FAIL))) + ); + + // add none + bridgeLine.addLine(ROUND_NONE); + assertAll( + () -> assertThat(bridgeLine.getLength()).isEqualTo(3), + () -> assertThat(bridgeLine.toString()) + .isEqualTo(createBridgeLine("[ %s | %s | %s ]", List.of(ROUND_SUCCESS, ROUND_FAIL, ROUND_NONE))) + ); + } +} diff --git a/src/test/java/bridge/model/bridge/BridgeMapTest.java b/src/test/java/bridge/model/bridge/BridgeMapTest.java new file mode 100644 index 0000000..4c65d15 --- /dev/null +++ b/src/test/java/bridge/model/bridge/BridgeMapTest.java @@ -0,0 +1,62 @@ +package bridge.model.bridge; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static bridge.common.BridgeFixture.INIT_BRIDGE_MAP; +import static bridge.common.BridgeFixture.createBridgeMap; +import static bridge.model.bridge.BridgeDirection.DOWN; +import static bridge.model.bridge.BridgeDirection.UP; +import static bridge.model.game.GameRoundStatus.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class BridgeMapTest { + @Test + @DisplayName("BridgeMap을 생성하고 게임 진행에 따른 Map 현황을 확인한다") + void construct() { + final BridgeMap bridgeMap = BridgeMap.init(); + assertThat(bridgeMap.toString()).isEqualTo(INIT_BRIDGE_MAP); + + // Direction[U] -> Success + bridgeMap.updateMap(UP, ROUND_SUCCESS); + assertThat(bridgeMap.toString()) + .isEqualTo( + createBridgeMap( + "[ %s ]", List.of(ROUND_SUCCESS), + "[ %s ]", List.of(ROUND_NONE) + ) + ); + + // Direction[D] -> Success + bridgeMap.updateMap(DOWN, ROUND_SUCCESS); + assertThat(bridgeMap.toString()) + .isEqualTo( + createBridgeMap( + "[ %s | %s ]", List.of(ROUND_SUCCESS, ROUND_NONE), + "[ %s | %s ]", List.of(ROUND_NONE, ROUND_SUCCESS) + ) + ); + + // Direction[D] -> Success + bridgeMap.updateMap(DOWN, ROUND_SUCCESS); + assertThat(bridgeMap.toString()) + .isEqualTo( + createBridgeMap( + "[ %s | %s | %s ]", List.of(ROUND_SUCCESS, ROUND_NONE, ROUND_NONE), + "[ %s | %s | %s ]", List.of(ROUND_NONE, ROUND_SUCCESS, ROUND_SUCCESS) + ) + ); + + // Direction[U] -> Fail + bridgeMap.updateMap(UP, ROUND_FAIL); + assertThat(bridgeMap.toString()) + .isEqualTo( + createBridgeMap( + "[ %s | %s | %s | %s ]", List.of(ROUND_SUCCESS, ROUND_NONE, ROUND_NONE, ROUND_FAIL), + "[ %s | %s | %s | %s ]", List.of(ROUND_NONE, ROUND_SUCCESS, ROUND_SUCCESS, ROUND_NONE) + ) + ); + } +} diff --git a/src/test/java/bridge/model/bridge/BridgeTest.java b/src/test/java/bridge/model/bridge/BridgeTest.java new file mode 100644 index 0000000..bd5f2ad --- /dev/null +++ b/src/test/java/bridge/model/bridge/BridgeTest.java @@ -0,0 +1,36 @@ +package bridge.model.bridge; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static bridge.model.bridge.BridgeDirection.DOWN; +import static bridge.model.bridge.BridgeDirection.UP; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class BridgeTest { + @Test + @DisplayName("Bridge를 생성한다") + void construct() { + // given + final List element = List.of("U", "D", "D", "U"); + + // when + final Bridge bridge = new Bridge(element); + + // then + assertAll( + () -> assertThat(bridge.getBridge()).hasSize(4), + () -> assertThat(bridge.getBridgeDirectionByIndex(0)).isEqualTo(UP), + () -> assertThat(bridge.getBridgeDirectionByIndex(1)).isEqualTo(DOWN), + () -> assertThat(bridge.getBridgeDirectionByIndex(2)).isEqualTo(DOWN), + () -> assertThat(bridge.getBridgeDirectionByIndex(3)).isEqualTo(UP), + () -> assertThat(bridge.isEndOfBridge(0)).isFalse(), + () -> assertThat(bridge.isEndOfBridge(1)).isFalse(), + () -> assertThat(bridge.isEndOfBridge(2)).isFalse(), + () -> assertThat(bridge.isEndOfBridge(3)).isTrue() + ); + } +} diff --git a/src/test/java/bridge/model/game/GameProcessDecisionCommandTest.java b/src/test/java/bridge/model/game/GameProcessDecisionCommandTest.java new file mode 100644 index 0000000..cf78bc5 --- /dev/null +++ b/src/test/java/bridge/model/game/GameProcessDecisionCommandTest.java @@ -0,0 +1,38 @@ +package bridge.model.game; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static bridge.model.game.GameProcessDecisionCommand.QUIT; +import static bridge.model.game.GameProcessDecisionCommand.RETRY; +import static bridge.utils.ExceptionConstants.GameProcessDecisionCommandException.INVALID_PROCESS_DECISION_COMMAND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class GameProcessDecisionCommandTest { + @Test + @DisplayName("게임 재시작[R], 게임 종료[Q]가 아닌 Command로 GameProcessDecisionCommand를 조회할 수 없다") + void throwExceptionByInvalidProcessDecisionCommand() { + assertAll( + () -> assertThatThrownBy(() -> GameProcessDecisionCommand.from("r")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PROCESS_DECISION_COMMAND.message), + () -> assertThatThrownBy(() -> GameProcessDecisionCommand.from("q")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PROCESS_DECISION_COMMAND.message), + () -> assertThatThrownBy(() -> GameProcessDecisionCommand.from("h")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PROCESS_DECISION_COMMAND.message) + ); + } + + @Test + @DisplayName("GameProcessDecisionCommand를 조회한다") + void success() { + assertAll( + () -> assertThat(GameProcessDecisionCommand.from("R")).isEqualTo(RETRY), + () -> assertThat(GameProcessDecisionCommand.from("Q")).isEqualTo(QUIT) + ); + } +} diff --git a/src/test/java/bridge/model/game/GameTrackerTest.java b/src/test/java/bridge/model/game/GameTrackerTest.java new file mode 100644 index 0000000..9f6477d --- /dev/null +++ b/src/test/java/bridge/model/game/GameTrackerTest.java @@ -0,0 +1,118 @@ +package bridge.model.game; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static bridge.common.BridgeFixture.INIT_BRIDGE_MAP; +import static bridge.common.BridgeFixture.createBridgeMap; +import static bridge.model.bridge.BridgeDirection.DOWN; +import static bridge.model.bridge.BridgeDirection.UP; +import static bridge.model.game.GameResultStatus.CLEAR; +import static bridge.model.game.GameResultStatus.FAIL; +import static bridge.model.game.GameRoundStatus.*; +import static bridge.model.game.GameStatus.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class GameTrackerTest { + @Test + @DisplayName("GameTracker를 생성한다") + void construct() { + // when + final GameTracker gameTracker = new GameTracker(); + + // then + assertAll( + () -> assertThat(gameTracker.getBridgeMap().toString()).isEqualTo(INIT_BRIDGE_MAP), + () -> assertThat(gameTracker.getGameStatus()).isEqualTo(IN_PROGRESS), + () -> assertThat(gameTracker.getAttemptCount()).isEqualTo(1) + ); + } + + @Test + @DisplayName("다리 건너기를 진행한다 (updateMap)") + void updateMap() { + final GameTracker gameTracker = new GameTracker(); + assertThat(gameTracker.getBridgeMap().toString()).isEqualTo(INIT_BRIDGE_MAP); + + gameTracker.updateMap(UP, ROUND_SUCCESS); + assertThat(gameTracker.getBridgeMap().toString()) + .isEqualTo( + createBridgeMap( + "[ %s ]", List.of(ROUND_SUCCESS), + "[ %s ]", List.of(ROUND_NONE) + ) + ); + + gameTracker.updateMap(DOWN, ROUND_FAIL); + assertThat(gameTracker.getBridgeMap().toString()) + .isEqualTo( + createBridgeMap( + "[ %s | %s ]", List.of(ROUND_SUCCESS, ROUND_NONE), + "[ %s | %s ]", List.of(ROUND_NONE, ROUND_FAIL) + ) + ); + } + + @Test + @DisplayName("게임을 재시작한다") + void retryGame() { + final GameTracker gameTracker = new GameTracker(); + assertAll( + () -> assertThat(gameTracker.getBridgeMap().toString()).isEqualTo(INIT_BRIDGE_MAP), + () -> assertThat(gameTracker.getGameStatus()).isEqualTo(IN_PROGRESS), + () -> assertThat(gameTracker.getAttemptCount()).isEqualTo(1) + ); + + /* 다리 건너기 2회 진행 */ + gameTracker.updateMap(UP, ROUND_SUCCESS); + assertThat(gameTracker.getBridgeMap().toString()) + .isEqualTo( + createBridgeMap( + "[ %s ]", List.of(ROUND_SUCCESS), + "[ %s ]", List.of(ROUND_NONE) + ) + ); + + gameTracker.updateMap(DOWN, ROUND_FAIL); + assertThat(gameTracker.getBridgeMap().toString()) + .isEqualTo( + createBridgeMap( + "[ %s | %s ]", List.of(ROUND_SUCCESS, ROUND_NONE), + "[ %s | %s ]", List.of(ROUND_NONE, ROUND_FAIL) + ) + ); + + /* 게임 재시작 */ + gameTracker.retryGame(); + assertAll( + () -> assertThat(gameTracker.getBridgeMap().toString()).isEqualTo(INIT_BRIDGE_MAP), + () -> assertThat(gameTracker.getGameStatus()).isEqualTo(IN_PROGRESS), + () -> assertThat(gameTracker.getAttemptCount()).isEqualTo(2) + ); + } + + @Test + @DisplayName("게임 성공/실패 결과를 응답받는다") + void displayResultStatus() { + final GameTracker gameTracker = new GameTracker(); + + /* 게임 클리어 */ + gameTracker.updateGameStatus(GAME_CLEAR); + assertAll( + () -> assertThat(gameTracker.isGameClear()).isTrue(), + () -> assertThat(gameTracker.isGameFail()).isFalse(), + () -> assertThat(gameTracker.displayResultStatus()).isEqualTo(CLEAR.getValue()) + ); + + /* 게임 실패 */ + gameTracker.updateGameStatus(GAME_FAIL); + assertAll( + () -> assertThat(gameTracker.isGameClear()).isFalse(), + () -> assertThat(gameTracker.isGameFail()).isTrue(), + () -> assertThat(gameTracker.displayResultStatus()).isEqualTo(FAIL.getValue()) + ); + } +} diff --git a/src/test/java/bridge/utils/validator/BridgeLengthValidatorTest.java b/src/test/java/bridge/utils/validator/BridgeLengthValidatorTest.java new file mode 100644 index 0000000..b7e38a1 --- /dev/null +++ b/src/test/java/bridge/utils/validator/BridgeLengthValidatorTest.java @@ -0,0 +1,35 @@ +package bridge.utils.validator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static bridge.utils.ExceptionConstants.InputException.INPUT_MUST_BE_NUMERIC; +import static bridge.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class BridgeLengthValidatorTest { + private static final BridgeLengthValidator BRIDGE_LENGTH_VALIDATOR = new BridgeLengthValidator(); + + @Test + @DisplayName("다리 길이에 공백이 존재하면 예외가 발생한다") + void throwExceptionByInputHasSpace() { + assertThatThrownBy(() -> BRIDGE_LENGTH_VALIDATOR.validate("3 ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INPUT_MUST_NOT_CONTAINS_SPACE.message); + } + + @Test + @DisplayName("다리 길이가 숫자가 아니면 예외가 발생한다") + void throwExceptionByInputIsNotNumeric() { + assertThatThrownBy(() -> BRIDGE_LENGTH_VALIDATOR.validate("a")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INPUT_MUST_BE_NUMERIC.message); + } + + @Test + @DisplayName("다리 길이 검증에 성공한다") + void success() { + assertDoesNotThrow(() -> BRIDGE_LENGTH_VALIDATOR.validate("3")); + } +} diff --git a/src/test/java/bridge/utils/validator/GameProcessCommandValidatorTest.java b/src/test/java/bridge/utils/validator/GameProcessCommandValidatorTest.java new file mode 100644 index 0000000..8fead82 --- /dev/null +++ b/src/test/java/bridge/utils/validator/GameProcessCommandValidatorTest.java @@ -0,0 +1,39 @@ +package bridge.utils.validator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static bridge.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE; +import static bridge.utils.ExceptionConstants.InputException.INVALID_GAME_PROCESS_COMMAND; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class GameProcessCommandValidatorTest { + private static final GameProcessCommandValidator GAME_PROCESS_COMMAND_VALIDATOR = new GameProcessCommandValidator(); + + @Test + @DisplayName("입력한 게임 재시작/종료 커맨드에 공백이 존재하면 예외가 발생한다") + void throwExceptionByInputHasSpace() { + assertThatThrownBy(() -> GAME_PROCESS_COMMAND_VALIDATOR.validate("R ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INPUT_MUST_NOT_CONTAINS_SPACE.message); + } + + @Test + @DisplayName("입력한 게임 재시작/종료 커맨드가 [R, Q]중 하나가 아니면 예외가 발생한다") + void throwExceptionByGameProcessCommandIsInvalid() { + assertThatThrownBy(() -> GAME_PROCESS_COMMAND_VALIDATOR.validate("h")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_GAME_PROCESS_COMMAND.message); + } + + @Test + @DisplayName("입력한 게임 재시작/종료 커맨드 검증에 성공한다") + void success() { + assertAll( + () -> assertDoesNotThrow(() -> GAME_PROCESS_COMMAND_VALIDATOR.validate("R")), + () -> assertDoesNotThrow(() -> GAME_PROCESS_COMMAND_VALIDATOR.validate("Q")) + ); + } +} diff --git a/src/test/java/bridge/utils/validator/MoveCommandValidatorTest.java b/src/test/java/bridge/utils/validator/MoveCommandValidatorTest.java new file mode 100644 index 0000000..9b48fb6 --- /dev/null +++ b/src/test/java/bridge/utils/validator/MoveCommandValidatorTest.java @@ -0,0 +1,39 @@ +package bridge.utils.validator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static bridge.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE; +import static bridge.utils.ExceptionConstants.InputException.INVALID_MOVE_COMMAND; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class MoveCommandValidatorTest { + private static final MoveCommandValidator MOVE_COMMAND_VALIDATOR = new MoveCommandValidator(); + + @Test + @DisplayName("입력한 이동 칸에 공백이 존재하면 예외가 발생한다") + void throwExceptionByInputHasSpace() { + assertThatThrownBy(() -> MOVE_COMMAND_VALIDATOR.validate("U ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INPUT_MUST_NOT_CONTAINS_SPACE.message); + } + + @Test + @DisplayName("입력한 이동 칸이 [U, D]중 하나가 아니면 예외가 발생한다") + void throwExceptionByMoveCommandIsInvalid() { + assertThatThrownBy(() -> MOVE_COMMAND_VALIDATOR.validate("h")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_MOVE_COMMAND.message); + } + + @Test + @DisplayName("입력한 이동 칸 검증에 성공한다") + void success() { + assertAll( + () -> assertDoesNotThrow(() -> MOVE_COMMAND_VALIDATOR.validate("U")), + () -> assertDoesNotThrow(() -> MOVE_COMMAND_VALIDATOR.validate("D")) + ); + } +}