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. 총 시도 횟수 (다리 건넌 횟수)
+
+
+
`을 기준으로 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"))
+ );
+ }
+}