diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..0a768b9b7 Binary files /dev/null and b/.DS_Store differ diff --git a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java new file mode 100644 index 000000000..0d8fdb595 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java @@ -0,0 +1,21 @@ +package cleancode.minesweeper.tobe; + +import cleancode.minesweeper.tobe.minesweeper.Minesweeper; +import cleancode.minesweeper.tobe.minesweeper.config.GameConfig; +import cleancode.minesweeper.tobe.minesweeper.gamelevel.Beginner; +import cleancode.minesweeper.tobe.minesweeper.io.ConsoleInputHandler; +import cleancode.minesweeper.tobe.minesweeper.io.ConsoleOutputHandler; + +public class GameApplication { + + // 이 클래스는 딱 프로그램 실행에 진입점만 가지게 된다. + // 이름도 MinesweeperGame 에서 GameApplication 으로 변경한다. -> 이렇게 변경하면 지뢰찾기게임(Minesweeper 뿐만이 아닌 다른 게임도 실행할 수 있게 된다.) + // 게임 실행에 대한 책임과 지뢰찾기 도메인 자체, 지뢰찾기 게임을 담당하는 역할을 분리했다. + public static void main(String[] args) { + GameConfig gameConfig = new GameConfig(new Beginner(), new ConsoleInputHandler(), new ConsoleOutputHandler()); + + Minesweeper minesweeper = new Minesweeper(gameConfig); + minesweeper.initialize(); + minesweeper.run(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java deleted file mode 100644 index dd85c3ce0..000000000 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ /dev/null @@ -1,187 +0,0 @@ -package cleancode.minesweeper.tobe; - -import java.util.Random; -import java.util.Scanner; - -public class MinesweeperGame { - - private static String[][] board = new String[8][10]; - private static Integer[][] landMineCounts = new Integer[8][10]; - private static boolean[][] landMines = new boolean[8][10]; - private static int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 - - public static void main(String[] args) { - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - System.out.println("지뢰찾기 게임 시작!"); - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - Scanner scanner = new Scanner(System.in); - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 10; j++) { - board[i][j] = "□"; - } - } - for (int i = 0; i < 10; i++) { - int col = new Random().nextInt(10); - int row = new Random().nextInt(8); - landMines[row][col] = true; - } - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 10; j++) { - int count = 0; - if (!landMines[i][j]) { - if (i - 1 >= 0 && j - 1 >= 0 && landMines[i - 1][j - 1]) { - count++; - } - if (i - 1 >= 0 && landMines[i - 1][j]) { - count++; - } - if (i - 1 >= 0 && j + 1 < 10 && landMines[i - 1][j + 1]) { - count++; - } - if (j - 1 >= 0 && landMines[i][j - 1]) { - count++; - } - if (j + 1 < 10 && landMines[i][j + 1]) { - count++; - } - if (i + 1 < 8 && j - 1 >= 0 && landMines[i + 1][j - 1]) { - count++; - } - if (i + 1 < 8 && landMines[i + 1][j]) { - count++; - } - if (i + 1 < 8 && j + 1 < 10 && landMines[i + 1][j + 1]) { - count++; - } - landMineCounts[i][j] = count; - continue; - } - landMineCounts[i][j] = 0; - } - } - while (true) { - System.out.println(" a b c d e f g h i j"); - for (int i = 0; i < 8; i++) { - System.out.printf("%d ", i + 1); - for (int j = 0; j < 10; j++) { - System.out.print(board[i][j] + " "); - } - System.out.println(); - } - if (gameStatus == 1) { - System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); - break; - } - if (gameStatus == -1) { - System.out.println("지뢰를 밟았습니다. GAME OVER!"); - break; - } - System.out.println(); - System.out.println("선택할 좌표를 입력하세요. (예: a1)"); - String input = scanner.nextLine(); - System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); - String input2 = scanner.nextLine(); - char c = input.charAt(0); - char r = input.charAt(1); - int col; - switch (c) { - case 'a': - col = 0; - break; - case 'b': - col = 1; - break; - case 'c': - col = 2; - break; - case 'd': - col = 3; - break; - case 'e': - col = 4; - break; - case 'f': - col = 5; - break; - case 'g': - col = 6; - break; - case 'h': - col = 7; - break; - case 'i': - col = 8; - break; - case 'j': - col = 9; - break; - default: - col = -1; - break; - } - int row = Character.getNumericValue(r) - 1; - if (input2.equals("2")) { - board[row][col] = "⚑"; - boolean open = true; - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 10; j++) { - if (board[i][j].equals("□")) { - open = false; - } - } - } - if (open) { - gameStatus = 1; - } - } else if (input2.equals("1")) { - if (landMines[row][col]) { - board[row][col] = "☼"; - gameStatus = -1; - continue; - } else { - open(row, col); - } - boolean open = true; - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 10; j++) { - if (board[i][j].equals("□")) { - open = false; - } - } - } - if (open) { - gameStatus = 1; - } - } else { - System.out.println("잘못된 번호를 선택하셨습니다."); - } - } - } - - private static void open(int row, int col) { - if (row < 0 || row >= 8 || col < 0 || col >= 10) { - return; - } - if (!board[row][col].equals("□")) { - return; - } - if (landMines[row][col]) { - return; - } - if (landMineCounts[row][col] != 0) { - board[row][col] = String.valueOf(landMineCounts[row][col]); - return; - } else { - board[row][col] = "■"; - } - open(row - 1, col - 1); - open(row - 1, col); - open(row - 1, col + 1); - open(row, col - 1); - open(row, col + 1); - open(row + 1, col - 1); - open(row + 1, col); - open(row + 1, col + 1); - } - -} diff --git a/src/main/java/cleancode/minesweeper/tobe/game/GameInitializable.java b/src/main/java/cleancode/minesweeper/tobe/game/GameInitializable.java new file mode 100644 index 000000000..4acf16e46 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/game/GameInitializable.java @@ -0,0 +1,5 @@ +package cleancode.minesweeper.tobe.game; + +public interface GameInitializable { + void initialize(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/game/GameRunnable.java b/src/main/java/cleancode/minesweeper/tobe/game/GameRunnable.java new file mode 100644 index 000000000..1d6c157bc --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/game/GameRunnable.java @@ -0,0 +1,5 @@ +package cleancode.minesweeper.tobe.game; + +public interface GameRunnable { + void run(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/Minesweeper.java new file mode 100644 index 000000000..7a85dc033 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/Minesweeper.java @@ -0,0 +1,99 @@ +package cleancode.minesweeper.tobe.minesweeper; + +import cleancode.minesweeper.tobe.minesweeper.board.GameBoard; +import cleancode.minesweeper.tobe.minesweeper.config.GameConfig; +import cleancode.minesweeper.tobe.minesweeper.exception.GameException; +import cleancode.minesweeper.tobe.game.GameInitializable; +import cleancode.minesweeper.tobe.game.GameRunnable; +import cleancode.minesweeper.tobe.minesweeper.io.InputHandler; +import cleancode.minesweeper.tobe.minesweeper.io.OutputHandler; +import cleancode.minesweeper.tobe.minesweeper.board.positoion.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.user.UserAction; + +public class Minesweeper implements GameInitializable, GameRunnable { + + // BOARD 도 하는 일이 너무 많고 중요하기 때문에 Minesweeper 클래스 내부에 상수로 두기에는 너무 책임이 과도하다. + // 이렇게 GameBoard 클래스를 두면 Minesweeper 입장에서는 Cell[][] 이중배열에 대해서는 모른다. + // 객체로 추상화가 되었고, 데이터 구조에 대한 것은 캐슐화가 되었기 때문이다. + private final GameBoard gameBoard; + // SRP: cellInput 이라는 사용자의 입력을 받아서 rowIndex, colIndex 로 변환하는 역할을 하는 또 하나의 클래스로 볼 수 있지 않을까? + + // 게임이 진행되는 핵심 로직들과 사용자 입출력에 대한 로직 책임을 분리한다. + // DIP: InputHandler, OutputHandler 는 이제 Console 에 관한 것은 모른다. 인터페이스만 의존하고 있다. + // 구현체가 변경되어도 Minesweeper 클래스는 영향을 받지 않는다. + private final InputHandler inputHandler; + private final OutputHandler outputHandler; + + public Minesweeper(GameConfig gameConfig) { + gameBoard = new GameBoard(gameConfig.getGameLevel()); + this.inputHandler = gameConfig.getInputHandler(); + this.outputHandler = gameConfig.getOutputHandler(); + } + + @Override + public void initialize() { + gameBoard.initializeGame(); + } + + @Override + public void run() { + outputHandler.showGameStartComments(); + + while (gameBoard.isInProgress()) { + try { + outputHandler.showBoard(gameBoard); + + CellPosition cellPosition = getCellInputFromUser(); + UserAction userActionInput = getUserActionInputFromUser(); + actOnCell(cellPosition, userActionInput); + } catch (GameException e) { + outputHandler.showExceptionMessage(e); + } catch (Exception e) { + outputHandler.showSimpleMessage("프로그램에 문제가 생겼습니다."); + } + } + outputHandler.showBoard(gameBoard); + + if (gameBoard.isWinStatus()) { + outputHandler.showGameWinningComment(); + } + if (gameBoard.isLoseStatus()) { + outputHandler.showGameLosingComment(); + } + } + + private CellPosition getCellInputFromUser() { + outputHandler.showCommentForSelectingCell(); + CellPosition cellPosition = inputHandler.getCellPositionFromUser(); + if (gameBoard.isInvalidCellPosition(cellPosition)) { + throw new GameException("잘못된 좌표를 선택하셨습니다."); + } + + return cellPosition; + } + + private UserAction getUserActionInputFromUser() { + outputHandler.showCommentForUserAction(); + return inputHandler.getUserActionFromUser(); + } + + private void actOnCell(CellPosition cellPosition, UserAction userActionInput) { + if (doesUserChooseToPlantFlag(userActionInput)) { + gameBoard.flagAt(cellPosition); + return; + } + if (doesUserChooseToOpenCell(userActionInput)) { + gameBoard.openAt(cellPosition); + return; + } + throw new GameException("잘못된 번호를 선택하셨습니다."); + } + + private boolean doesUserChooseToPlantFlag(UserAction userAction) { + return userAction == UserAction.FLAG; + } + + private boolean doesUserChooseToOpenCell(UserAction userAction) { + return userAction == UserAction.OPEN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameBoard.java new file mode 100644 index 000000000..aba30a265 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameBoard.java @@ -0,0 +1,240 @@ +package cleancode.minesweeper.tobe.minesweeper.board; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.Cell; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.Cells; +import cleancode.minesweeper.tobe.minesweeper.board.cell.EmptyCell; +import cleancode.minesweeper.tobe.minesweeper.board.cell.LandMineCell; +import cleancode.minesweeper.tobe.minesweeper.board.cell.NumberCell; +import cleancode.minesweeper.tobe.minesweeper.gamelevel.GameLevel; +import cleancode.minesweeper.tobe.minesweeper.board.positoion.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.board.positoion.CellPositions; +import cleancode.minesweeper.tobe.minesweeper.board.positoion.RelativePosition; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Stack; + +public class GameBoard { + + private final Cell[][] board; + private final int landMineCount; + private GameStatus gameStatus; + + public GameBoard(GameLevel gameLevel) { + int rowSize = gameLevel.getRowSize(); + int colSize = gameLevel.getColSize(); + board = new Cell[rowSize][colSize]; + + landMineCount = gameLevel.getLandMineCount(); + initializeGameStatus(); + } + + public void initializeGame() { + initializeGameStatus(); + CellPositions cellPositions = CellPositions.from(board); + + initializeEmptyCells(cellPositions); + + List landMinePositions = cellPositions.extractRandomPositions(landMineCount); + initializeLandMineCells(landMinePositions); + + List numberPositionCandidates = cellPositions.subtract(landMinePositions); + initializeNumberCells(numberPositionCandidates); + } + + public void openAt(CellPosition cellPosition) { + if (isLandMineCellAt(cellPosition)) { + openOneCellAt(cellPosition); + changeGameStatusToLose(); + return; + } + +// openSurroundedCells(cellPosition); + openSurroundedCells2(cellPosition); + checkIfGameIsOver(); + } + + public void flagAt(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + cell.flag(); + + checkIfGameIsOver(); + } + + public boolean isInvalidCellPosition(CellPosition cellPosition) { + int rowSize = getRowSize(); + int colSize = getColSize(); + + return cellPosition.isRowIndexMoreThanOrEqual(rowSize) + || cellPosition.isColIndexMoreThanOrEqual(colSize); + } + + public boolean isInProgress() { + return gameStatus == GameStatus.IN_PROGRESS; + } + + public boolean isWinStatus() { + return gameStatus == GameStatus.WIN; + } + + public boolean isLoseStatus() { + return gameStatus == GameStatus.LOSE; + } + + public CellSnapshot getSnapshot(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + return cell.getSnapshot(); + } + + public int getRowSize() { + return board.length; + } + + public int getColSize() { + return board[0].length; + } + + private void initializeGameStatus() { + gameStatus = GameStatus.IN_PROGRESS; + } + + private void initializeEmptyCells(CellPositions cellPositions) { + List allPositions = cellPositions.getPositions(); + for (CellPosition position : allPositions) { + updateCellAt(position, new EmptyCell()); + } + } + + private void initializeLandMineCells(List landMinePositions) { + for (CellPosition position : landMinePositions) { + updateCellAt(position, new LandMineCell()); + } + } + + private void initializeNumberCells(List numberPositionCandidates) { + for (CellPosition candidatePosition : numberPositionCandidates) { + int count = countNearbyLandMines(candidatePosition); + if (count != 0) { + updateCellAt(candidatePosition, new NumberCell(count)); + } + } + } + + private int countNearbyLandMines(CellPosition cellPosition) { + int rowSize = getRowSize(); + int colSize = getColSize(); + + long count = calculateSurroundedPositions(cellPosition, rowSize, colSize).stream() + .filter(this::isLandMineCellAt) + .count(); + + return (int) count; + } + + private List calculateSurroundedPositions(CellPosition cellPosition, int rowSize, int colSize) { + return RelativePosition.SURROUNDED_POSITIONS.stream() + .filter(cellPosition::canCalculatePositionBy) + .map(cellPosition::calculatePositionBy) + .filter(position -> position.isRowIndexLessThan(rowSize)) + .filter(position -> position.isColIndexLessThan(colSize)) + .toList(); + } + + private void updateCellAt(CellPosition position, Cell cell) { + board[position.getRowIndex()][position.getColIndex()] = cell; + } + + private void openSurroundedCells(CellPosition cellPosition) { + if (isOpenedCell(cellPosition)) { + return; + } + if (isLandMineCellAt(cellPosition)) { + return; + } + + openOneCellAt(cellPosition); + + if (doesCellHaveLandMineCount(cellPosition)) { + return; + } + + List surroundedPositions = calculateSurroundedPositions(cellPosition, getRowSize(), getColSize()); + surroundedPositions.forEach(this::openSurroundedCells); + } + + private void openSurroundedCells2(CellPosition cellPosition) { + Deque deque = new ArrayDeque<>(); + deque.push(cellPosition); + + while (!deque.isEmpty()) { + openAndPushCellAt(deque); + } + } + + private void openAndPushCellAt(Deque deque) { + CellPosition currentCellPosition = deque.pop(); + if (isOpenedCell(currentCellPosition)) { + return; + } + if (isLandMineCellAt(currentCellPosition)) { + return; + } + + openOneCellAt(currentCellPosition); + + if (doesCellHaveLandMineCount(currentCellPosition)) { + return; + } + + List surroundedPositions = calculateSurroundedPositions(currentCellPosition, getRowSize(), getColSize()); + for (CellPosition surroundedPosition : surroundedPositions) { + deque.push(surroundedPosition); + } + } + + private void openOneCellAt(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + cell.open(); + } + + private boolean isOpenedCell(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + return cell.isOpened(); + } + + private boolean isLandMineCellAt(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + return cell.isLandMine(); + } + + private boolean doesCellHaveLandMineCount(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + return cell.hasLandMineCount(); + } + + private void checkIfGameIsOver() { + if (isAllCellChecked()) { + changeGameStatusToWin(); + } + } + + private boolean isAllCellChecked() { + Cells cells = Cells.from(board); + return cells.isAllChecked(); + } + + private void changeGameStatusToWin() { + gameStatus = GameStatus.WIN; + } + + private void changeGameStatusToLose() { + gameStatus = GameStatus.LOSE; + } + + private Cell findCell(CellPosition cellPosition) { + return board[cellPosition.getRowIndex()][cellPosition.getColIndex()]; + } + +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameStatus.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameStatus.java new file mode 100644 index 000000000..1b4a1acd2 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameStatus.java @@ -0,0 +1,14 @@ +package cleancode.minesweeper.tobe.minesweeper.board; + +public enum GameStatus { + IN_PROGRESS("진행 중"), + WIN("승리"), + LOSE("패배") + ; + + private final String description; + + GameStatus(String description) { + this.description = description; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cell.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cell.java new file mode 100644 index 000000000..7b34556bc --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cell.java @@ -0,0 +1,20 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public interface Cell { + + + + boolean isLandMine(); + boolean hasLandMineCount(); + + CellSnapshot getSnapshot(); + + // isOpened, isFlagged 는 Cell 의 공통 기능이므로 그대로 둔다. + void flag(); + + void open(); + + boolean isChecked(); + + boolean isOpened(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellSnapshot.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellSnapshot.java new file mode 100644 index 000000000..44ae78747 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellSnapshot.java @@ -0,0 +1,62 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import java.util.Objects; + +public class CellSnapshot { + private final CellSnapshotStatus status; + private final int nearbyLandMineCount; + + private CellSnapshot(CellSnapshotStatus status, int nearbyLandMineCount) { + this.status = status; + this.nearbyLandMineCount = nearbyLandMineCount; + } + + public static CellSnapshot of(CellSnapshotStatus status, int nearbyLandMineCount) { + return new CellSnapshot(status, nearbyLandMineCount); + } + + public static CellSnapshot ofEmpty() { + return new CellSnapshot(CellSnapshotStatus.EMPTY, 0); + } + + public static CellSnapshot ofFlag() { + return new CellSnapshot(CellSnapshotStatus.FLAG, 0); + } + + public static CellSnapshot ofLandMine() { + return new CellSnapshot(CellSnapshotStatus.LAND_MINE, 0); + } + + public static CellSnapshot ofNumber(int nearbyLandMineCount) { + return new CellSnapshot(CellSnapshotStatus.NUMBER, nearbyLandMineCount); + } + + public static CellSnapshot ofUnchecked() { + return new CellSnapshot(CellSnapshotStatus.UNCHECKED, 0); + } + + public boolean isSameStatus(CellSnapshotStatus cellSnapshotStatus) { + return this.status == cellSnapshotStatus; + } + + public CellSnapshotStatus getStatus() { + return status; + } + + public int getNearbyLandMineCount() { + return nearbyLandMineCount; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + CellSnapshot that = (CellSnapshot) object; + return getNearbyLandMineCount() == that.getNearbyLandMineCount() && getStatus() == that.getStatus(); + } + + @Override + public int hashCode() { + return Objects.hash(getStatus(), getNearbyLandMineCount()); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellSnapshotStatus.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellSnapshotStatus.java new file mode 100644 index 000000000..b40bc74fc --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellSnapshotStatus.java @@ -0,0 +1,16 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public enum CellSnapshotStatus { + EMPTY("빈 셀"), + FLAG("깃발"), + LAND_MINE("지뢰"), + NUMBER("숫자"), + UNCHECKED("확인 전") + ; + + private final String description; + + CellSnapshotStatus(String description) { + this.description = description; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellState.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellState.java new file mode 100644 index 000000000..b0c571269 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellState.java @@ -0,0 +1,32 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public class CellState { + private boolean isFlagged; + private boolean isOpened; + + private CellState(boolean isFlagged, boolean isOpened) { + this.isFlagged = isFlagged; + this.isOpened = isOpened; + } + + public static CellState initialize() { + return new CellState(false, false); + } + + // isOpened, isFlagged 는 Cell 의 공통 기능이므로 그대로 둔다. + public void flag() { + this.isFlagged = true; + } + + public void open() { + this.isOpened = true; + } + + public boolean isOpened() { + return isOpened; + } + + public boolean isFlagged() { + return isFlagged; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cells.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cells.java new file mode 100644 index 000000000..a380f04e3 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cells.java @@ -0,0 +1,28 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import java.util.Arrays; +import java.util.List; + +public class Cells { + private final List cells; + + private Cells(List cells) { + this.cells = cells; + } + + public static Cells of(List cells) { + return new Cells(cells); + } + + public static Cells from(Cell[][] cells) { + List cellList = Arrays.stream(cells) + .flatMap(Arrays::stream) + .toList(); + return of(cellList); + } + + public boolean isAllChecked() { + return cells.stream() + .allMatch(Cell::isChecked); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCell.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCell.java new file mode 100644 index 000000000..e3e6e113e --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCell.java @@ -0,0 +1,48 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public class EmptyCell implements Cell { + + + private final CellState cellState = CellState.initialize(); + + @Override + public boolean isLandMine() { + return false; + } + + @Override + public boolean hasLandMineCount() { + return false; + } + + @Override + public CellSnapshot getSnapshot() { + if (cellState.isOpened()) { + return CellSnapshot.ofEmpty(); + } + if (cellState.isFlagged()) { + return CellSnapshot.ofFlag(); + } + return CellSnapshot.ofUnchecked(); + } + + @Override + public void flag() { + cellState.flag(); + } + + @Override + public void open() { + cellState.open(); + } + + @Override + public boolean isChecked() { + return cellState.isOpened(); + } + + @Override + public boolean isOpened() { + return cellState.isOpened(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCell.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCell.java new file mode 100644 index 000000000..1a2a0e8ba --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCell.java @@ -0,0 +1,47 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public class LandMineCell implements Cell { + + private final CellState cellState = CellState.initialize(); + + @Override + public boolean isLandMine() { + return true; + } + + @Override + public boolean hasLandMineCount() { + return false; + } + + @Override + public CellSnapshot getSnapshot() { + if (cellState.isOpened()) { + return CellSnapshot.ofLandMine(); + } + if (cellState.isFlagged()) { + return CellSnapshot.ofFlag(); + } + return CellSnapshot.ofUnchecked(); + } + + @Override + public void flag() { + cellState.flag(); + } + + @Override + public void open() { + cellState.open(); + } + + @Override + public boolean isChecked() { + return cellState.isFlagged(); + } + + @Override + public boolean isOpened() { + return cellState.isOpened(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCell.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCell.java new file mode 100644 index 000000000..7f2d461df --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCell.java @@ -0,0 +1,53 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public class NumberCell implements Cell { + + private final int nearbyLandMineCount; + private final CellState cellState = CellState.initialize(); + + + public NumberCell(int nearbyLandMineCount) { + this.nearbyLandMineCount = nearbyLandMineCount; + } + + @Override + public boolean isLandMine() { + return false; + } + + @Override + public boolean hasLandMineCount() { + return true; + } + + @Override + public CellSnapshot getSnapshot() { + if (cellState.isOpened()) { + return CellSnapshot.ofNumber(nearbyLandMineCount); + } + if (cellState.isFlagged()) { + return CellSnapshot.ofFlag(); + } + return CellSnapshot.ofUnchecked(); + } + + @Override + public void flag() { + cellState.flag(); + } + + @Override + public void open() { + cellState.open(); + } + + @Override + public boolean isChecked() { + return cellState.isOpened(); + } + + @Override + public boolean isOpened() { + return cellState.isOpened(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/CellPosition.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/CellPosition.java new file mode 100644 index 000000000..91457deaf --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/CellPosition.java @@ -0,0 +1,73 @@ +package cleancode.minesweeper.tobe.minesweeper.board.positoion; + +import java.util.Objects; + +public class CellPosition { + private final int rowIndex; + private final int colIndex; + + private CellPosition(int rowIndex, int colIndex) { + if (rowIndex < 0 || colIndex < 0) { + throw new IllegalArgumentException("올바르지 않은 좌표입니다."); + } + this.rowIndex = rowIndex; + this.colIndex = colIndex; + } + + public static CellPosition of(int rowIndex, int colIndex) { + return new CellPosition(rowIndex, colIndex); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CellPosition that = (CellPosition) o; + return rowIndex == that.rowIndex && colIndex == that.colIndex; + } + + @Override + public int hashCode() { + return Objects.hash(rowIndex, colIndex); + } + + public boolean isRowIndexMoreThanOrEqual(int rowIndex) { + return this.rowIndex >= rowIndex; + } + + public boolean isColIndexMoreThanOrEqual(int colIndex) { + return this.colIndex >= colIndex; + } + + public int getRowIndex() { + return rowIndex; + } + + public int getColIndex() { + return colIndex; + } + + public boolean canCalculatePositionBy(RelativePosition relativePosition) { + return this.rowIndex + relativePosition.getDeltaRow() >= 0 + && this.colIndex + relativePosition.getDeltaCol() >= 0; + } + + public CellPosition calculatePositionBy(RelativePosition relativePosition) { + if (this.canCalculatePositionBy(relativePosition)) { + return CellPosition.of( + this.rowIndex + relativePosition.getDeltaRow(), + this.colIndex + relativePosition.getDeltaCol() + ); + } + throw new IllegalArgumentException("움직일 수 있는 좌표가 아닙니다."); + } + + public boolean isRowIndexLessThan(int rowIndex) { + return this.rowIndex < rowIndex; + } + + public boolean isColIndexLessThan(int colIndex) { + return this.colIndex < colIndex; + } + +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/CellPositions.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/CellPositions.java new file mode 100644 index 000000000..6e157b78f --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/CellPositions.java @@ -0,0 +1,57 @@ +package cleancode.minesweeper.tobe.minesweeper.board.positoion; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.Cell; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CellPositions { + private final List positions; + + private CellPositions(List positions) { + this.positions = positions; + } + + public static CellPositions of(List positions) { + return new CellPositions(positions); + } + + + public static CellPositions from(Cell[][] board) { + List cellPositions = new ArrayList<>(); + + for (int row = 0; row < board.length; row++) { + for (int col = 0; col < board[0].length; col++) { + CellPosition cellPosition = CellPosition.of(row, col); + cellPositions.add(cellPosition); + } + } + + return of(cellPositions); + } + + public List extractRandomPositions(int count) { + List cellPositions = new ArrayList<>(positions); + + Collections.shuffle(cellPositions); + return cellPositions.subList(0, count); + } + + public List subtract(List positionListToSubtract) { + List cellPositions = new ArrayList<>(positions); + CellPositions positionsToSubtract = CellPositions.of(positionListToSubtract); + + return cellPositions.stream() + .filter(positionsToSubtract::doesNotContain) + .toList(); + } + + private boolean doesNotContain(CellPosition position) { + return !positions.contains(position); + } + + public List getPositions() { + return new ArrayList<>(positions); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/RelativePosition.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/RelativePosition.java new file mode 100644 index 000000000..089962019 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/positoion/RelativePosition.java @@ -0,0 +1,52 @@ +package cleancode.minesweeper.tobe.minesweeper.board.positoion; + +import java.util.List; +import java.util.Objects; + +public class RelativePosition { + + public static final List SURROUNDED_POSITIONS = List.of( + RelativePosition.of(-1, -1), + RelativePosition.of(-1, 0), + RelativePosition.of(-1, 1), + RelativePosition.of(0, -1), + RelativePosition.of(0, 1), + RelativePosition.of(1, -1), + RelativePosition.of(1, 0), + RelativePosition.of(1, 1) + ); + + private final int deltaRow; + private final int deltaCol; + + private RelativePosition(int deltaRow, int deltaCol) { + this.deltaRow = deltaRow; + this.deltaCol = deltaCol; + } + + public static RelativePosition of(int deltaRow, int deltaCol) { + return new RelativePosition(deltaRow, deltaCol); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RelativePosition that = (RelativePosition) o; + return deltaRow == that.deltaRow && deltaCol == that.deltaCol; + } + + @Override + public int hashCode() { + return Objects.hash(deltaRow, deltaCol); + } + + public int getDeltaRow() { + return deltaRow; + } + + public int getDeltaCol() { + return deltaCol; + } + +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/config/GameConfig.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/config/GameConfig.java new file mode 100644 index 000000000..b22e1e711 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/config/GameConfig.java @@ -0,0 +1,29 @@ +package cleancode.minesweeper.tobe.minesweeper.config; + +import cleancode.minesweeper.tobe.minesweeper.gamelevel.GameLevel; +import cleancode.minesweeper.tobe.minesweeper.io.InputHandler; +import cleancode.minesweeper.tobe.minesweeper.io.OutputHandler; + +public class GameConfig { + private final GameLevel gameLevel; + private final InputHandler inputHandler; + private final OutputHandler outputHandler; + + public GameConfig(GameLevel gameLevel, InputHandler inputHandler, OutputHandler outputHandler) { + this.gameLevel = gameLevel; + this.inputHandler = inputHandler; + this.outputHandler = outputHandler; + } + + public GameLevel getGameLevel() { + return gameLevel; + } + + public InputHandler getInputHandler() { + return inputHandler; + } + + public OutputHandler getOutputHandler() { + return outputHandler; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/exception/GameException.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/exception/GameException.java new file mode 100644 index 000000000..aba08dcb0 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/exception/GameException.java @@ -0,0 +1,7 @@ +package cleancode.minesweeper.tobe.minesweeper.exception; + +public class GameException extends RuntimeException { + public GameException(String message) { + super(message); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Advanced.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Advanced.java new file mode 100644 index 000000000..774c3ba39 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Advanced.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.minesweeper.gamelevel; + +public class Advanced implements GameLevel{ + @Override + public int getRowSize() { + return 20; + } + + @Override + public int getColSize() { + return 24; + } + + @Override + public int getLandMineCount() { + return 99; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Beginner.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Beginner.java new file mode 100644 index 000000000..24cc69eac --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Beginner.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.minesweeper.gamelevel; + +public class Beginner implements GameLevel{ + @Override + public int getRowSize() { + return 8; + } + + @Override + public int getColSize() { + return 10; + } + + @Override + public int getLandMineCount() { + return 10; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/GameLevel.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/GameLevel.java new file mode 100644 index 000000000..59fd3527d --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/GameLevel.java @@ -0,0 +1,11 @@ +package cleancode.minesweeper.tobe.minesweeper.gamelevel; + +// 추상화를 정말 바로 보여주는 구조이다. 인터페이스가 갖고 있는 스펙들 즉, 선언된 메서드 선언부들이 이 객체가 어떠한 역할을 갖는지 설명을 해준다. +// 이 GameLevel 인터페이스를 MineSweeper 안에 넣어줄 것이다. +// Minesweeper 객체는 GameLevel 을 받을 것이지만, 인터페이스여서 런타임 시점에 어떤 GameLevel 구현체가 들어오는지는 모른다. 하지만 GameLevel 인터페이스의 스펙은 알고 있다. +// Minesweeper 는 GameLevel 의 스펙을 통해 구현하면 된다. +public interface GameLevel { + int getRowSize(); + int getColSize(); + int getLandMineCount(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Middle.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Middle.java new file mode 100644 index 000000000..0182d008b --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/Middle.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.minesweeper.gamelevel; + +public class Middle implements GameLevel{ + @Override + public int getRowSize() { + return 14; + } + + @Override + public int getColSize() { + return 18; + } + + @Override + public int getLandMineCount() { + return 40; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/VeryBeginner.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/VeryBeginner.java new file mode 100644 index 000000000..8e910853d --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/VeryBeginner.java @@ -0,0 +1,20 @@ +package cleancode.minesweeper.tobe.minesweeper.gamelevel; + +public class VeryBeginner implements GameLevel{ + + + @Override + public int getRowSize() { + return 4; + } + + @Override + public int getColSize() { + return 5; + } + + @Override + public int getLandMineCount() { + return 2; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/BoardIndexConverter.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/BoardIndexConverter.java new file mode 100644 index 000000000..3a606f94d --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/BoardIndexConverter.java @@ -0,0 +1,36 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.exception.GameException; + +public class BoardIndexConverter { + + private static final char BASE_CHAR_FOR_COL = 'a'; + + public int getSelectedRowIndex(String cellInput) { + String cellInputRow = cellInput.substring(1); + return convertRowFrom(cellInputRow); + } + + public int getSelectedColIndex(String cellInput) { + char cellInputCol = cellInput.charAt(0); + return convertColFrom(cellInputCol); + } + + private int convertRowFrom(String cellInputRow) { + int rowIndex = Integer.parseInt(cellInputRow) - 1; + if (rowIndex < 0) { + throw new GameException("잘못된 입력입니다."); + } + + return rowIndex; + } + + private int convertColFrom(char cellInputCol) { + int colIndex = cellInputCol - BASE_CHAR_FOR_COL; + if (colIndex < 0) { + throw new GameException("잘못된 입력입니다."); + } + + return colIndex; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleInputHandler.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleInputHandler.java new file mode 100644 index 000000000..0a8df890a --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleInputHandler.java @@ -0,0 +1,33 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.positoion.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.user.UserAction; + +import java.util.Scanner; + +public class ConsoleInputHandler implements InputHandler { + public static final Scanner SCANNER = new Scanner(System.in); + private final BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); + + @Override + public UserAction getUserActionFromUser() { + String userInput = SCANNER.nextLine(); + + if ("1".equals(userInput)) { + return UserAction.OPEN; + } + if ("2".equals(userInput)) { + return UserAction.FLAG; + } + return UserAction.UNKNOWN; + } + + @Override + public CellPosition getCellPositionFromUser() { + String userInput = SCANNER.nextLine(); + + int rowIndex = boardIndexConverter.getSelectedRowIndex(userInput); + int colIndex = boardIndexConverter.getSelectedColIndex(userInput); + return CellPosition.of(rowIndex, colIndex); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleOutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleOutputHandler.java new file mode 100644 index 000000000..9df42e405 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleOutputHandler.java @@ -0,0 +1,81 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.GameBoard; +import cleancode.minesweeper.tobe.minesweeper.exception.GameException; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.io.sign.CellSignFinder; +import cleancode.minesweeper.tobe.minesweeper.io.sign.CellSignProvider; +import cleancode.minesweeper.tobe.minesweeper.board.positoion.CellPosition; + +import java.util.List; +import java.util.stream.IntStream; + +public class ConsoleOutputHandler implements OutputHandler { + + private final CellSignFinder cellSignFinder = new CellSignFinder(); + + @Override + public void showGameStartComments() { + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + System.out.println("지뢰찾기 게임 시작!"); + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + } + + @Override + public void showBoard(GameBoard board) { + String alphabets = generateColAlphabets(board); + + System.out.println(" " + alphabets); + for (int row = 0; row < board.getRowSize(); row++) { + System.out.printf("%2d ", row + 1); + for (int col = 0; col < board.getColSize(); col++) { + CellPosition cellPosition = CellPosition.of(row, col); + + CellSnapshot snapshot = board.getSnapshot(cellPosition); +// String cellSign = cellSignFinder.findCellSignFrom(snapshot); + String cellSign = CellSignProvider.findCellSignFrom(snapshot); + + System.out.print(cellSign + " "); + } + System.out.println(); + } + } + + private String generateColAlphabets(GameBoard board) { + List alphabets = IntStream.range(0, board.getColSize()) + .mapToObj(index -> (char) (index + 'a')) + .map(String::valueOf) + .toList(); + return String.join(" ", alphabets); + } + + @Override + public void showGameWinningComment() { + System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); + } + + @Override + public void showGameLosingComment() { + System.out.println("지뢰를 밟았습니다. GAME OVER!"); + } + + @Override + public void showCommentForSelectingCell() { + System.out.println("선택할 좌표를 입력하세요. (예: a1)"); + } + + @Override + public void showCommentForUserAction() { + System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); + } + + @Override + public void showExceptionMessage(GameException e) { + System.out.println(e.getMessage()); + } + + @Override + public void showSimpleMessage(String message) { + System.out.println(message); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/InputHandler.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/InputHandler.java new file mode 100644 index 000000000..873514dac --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/InputHandler.java @@ -0,0 +1,10 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.positoion.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.user.UserAction; + +public interface InputHandler { + UserAction getUserActionFromUser(); + + CellPosition getCellPositionFromUser(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/OutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/OutputHandler.java new file mode 100644 index 000000000..ff8be444e --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/OutputHandler.java @@ -0,0 +1,41 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.GameBoard; +import cleancode.minesweeper.tobe.minesweeper.exception.GameException; + +public interface OutputHandler { + + // show, print 중에 show 는 추상적인 느낌이고, print 는 구체적인 느낌이 강하다. + // print 는 console 에 print 한다는 느낌이 강하다. + // 높은 추상화 레벨인 OutputHandler 입장에서는 print 보다는 show 가 더 낫겠다. + + void showGameStartComments(); + + void showBoard(GameBoard board); + + + void showGameWinningComment(); + + void showGameLosingComment(); + + void showCommentForSelectingCell(); + + void showCommentForUserAction(); + + void showExceptionMessage(GameException e); + + void showSimpleMessage(String message); + + +// void printGameWinningComment(); + +// void printGameLosingComment(); + +// void printCommentForSelectingCell(); + +// void printCommentForUserAction(); + +// void printExceptionMessage(GameException e); +// +// void printSimpleMessage(String message); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignFinder.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignFinder.java new file mode 100644 index 000000000..95ea9948f --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignFinder.java @@ -0,0 +1,23 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; + +import java.util.List; + +public class CellSignFinder { + public static final List CELL_SIGN_PROVIDERS = List.of( + new EmptyCellSignProvider(), + new FlagCellSignProvider(), + new LandMineCellSignProvider(), + new NumberCellSignProvider(), + new UncheckedCellSignProvider() + ); + + public String findCellSignFrom(CellSnapshot snapshot) { + return CELL_SIGN_PROVIDERS.stream() + .filter(provider -> provider.supports(snapshot)) + .findFirst() + .map(provider -> provider.provide(snapshot)) + .orElseThrow(() -> new IllegalArgumentException("확인할 수 없는 셀입니다.")); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvidable.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvidable.java new file mode 100644 index 000000000..d4071c612 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvidable.java @@ -0,0 +1,9 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; + +public interface CellSignProvidable { + + boolean supports(CellSnapshot cellSnapshot); + String provide(CellSnapshot cellSnapshot); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvider.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvider.java new file mode 100644 index 000000000..782407b8e --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvider.java @@ -0,0 +1,69 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshotStatus; + +import java.util.Arrays; + +public enum CellSignProvider implements CellSignProvidable{ + EMPTY(CellSnapshotStatus.EMPTY) { + @Override + public String provide(CellSnapshot cellSnapshot) { + return EMPTY_SIGN; + } + }, + FLAG(CellSnapshotStatus.FLAG) { + @Override + public String provide(CellSnapshot cellSnapshot) { + return FLAG_SIGN; + } + }, + LAND_MINE(CellSnapshotStatus.LAND_MINE) { + @Override + public String provide(CellSnapshot cellSnapshot) { + return LAND_MINE_SIGN; + } + }, + NUMBER(CellSnapshotStatus.NUMBER) { + @Override + public String provide(CellSnapshot cellSnapshot) { + return String.valueOf(cellSnapshot.getNearbyLandMineCount()); + } + }, + UNKNOWN(CellSnapshotStatus.UNCHECKED) { + @Override + public String provide(CellSnapshot cellSnapshot) { + return UNCHECKED_SIGN; + } + } + ; + + + private static final String EMPTY_SIGN = "■"; + private static final String FLAG_SIGN = "⚑"; + private static final String LAND_MINE_SIGN = "☼"; + private static final String UNCHECKED_SIGN = "□"; + + private final CellSnapshotStatus status; + + CellSignProvider(CellSnapshotStatus status) { + this.status = status; + } + + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(status); + } + + public static String findCellSignFrom(CellSnapshot snapshot) { + CellSignProvider cellSignProvider = findBy(snapshot); + return cellSignProvider.provide(snapshot); + } + + private static CellSignProvider findBy(CellSnapshot snapshot) { + return Arrays.stream(values()) + .filter(provider -> provider.supports(snapshot)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("확인할 수 없는 셀입니다.")); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/EmptyCellSignProvider.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/EmptyCellSignProvider.java new file mode 100644 index 000000000..f9cb72c17 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/EmptyCellSignProvider.java @@ -0,0 +1,19 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshotStatus; + +public class EmptyCellSignProvider implements CellSignProvidable{ + + private static final String EMPTY_SIGN = "■"; + + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(CellSnapshotStatus.EMPTY); + } + + @Override + public String provide(CellSnapshot cellSnapshot) { + return EMPTY_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/FlagCellSignProvider.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/FlagCellSignProvider.java new file mode 100644 index 000000000..acf57e7de --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/FlagCellSignProvider.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshotStatus; + +public class FlagCellSignProvider implements CellSignProvidable{ + private static final String FLAG_SIGN = "⚑"; + + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(CellSnapshotStatus.FLAG); + } + + @Override + public String provide(CellSnapshot cellSnapshot) { + return FLAG_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/LandMineCellSignProvider.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/LandMineCellSignProvider.java new file mode 100644 index 000000000..6db991ab9 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/LandMineCellSignProvider.java @@ -0,0 +1,19 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshotStatus; + +public class LandMineCellSignProvider implements CellSignProvidable{ + private static final String LAND_MINE_SIGN = "☼"; + + + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(CellSnapshotStatus.LAND_MINE); + } + + @Override + public String provide(CellSnapshot cellSnapshot) { + return LAND_MINE_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/NumberCellSignProvider.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/NumberCellSignProvider.java new file mode 100644 index 000000000..649e16ffe --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/NumberCellSignProvider.java @@ -0,0 +1,16 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshotStatus; + +public class NumberCellSignProvider implements CellSignProvidable { + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(CellSnapshotStatus.NUMBER); + } + + @Override + public String provide(CellSnapshot cellSnapshot) { + return String.valueOf(cellSnapshot.getNearbyLandMineCount()); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/UncheckedCellSignProvider.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/UncheckedCellSignProvider.java new file mode 100644 index 000000000..6edabc12e --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/UncheckedCellSignProvider.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshotStatus; + +public class UncheckedCellSignProvider implements CellSignProvidable { + private static final String UNCHECKED_SIGN = "□"; + + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(CellSnapshotStatus.UNCHECKED); + } + + @Override + public String provide(CellSnapshot cellSnapshot) { + return UNCHECKED_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/user/UserAction.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/user/UserAction.java new file mode 100644 index 000000000..1fb6c2c17 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/user/UserAction.java @@ -0,0 +1,11 @@ +package cleancode.minesweeper.tobe.minesweeper.user; + +public enum UserAction { + OPEN("셀 열기"), FLAG("깃발 꽂기"), UNKNOWN("알 수 없음"); + + private final String description; + + UserAction(String description) { + this.description = description; + } +} diff --git a/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellStateTest.java b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellStateTest.java new file mode 100644 index 000000000..f1ce51a72 --- /dev/null +++ b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellStateTest.java @@ -0,0 +1,48 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CellStateTest { + + @Test + @DisplayName("셀 상태를 초기화하면 오픈되지 않고, 깃발이 꽂히지 않은 상태로 초기화된다.") + void initialize() { + // given + CellState cellState = CellState.initialize(); + + // when + + // then + assertFalse(cellState.isOpened()); + assertFalse(cellState.isFlagged()); + } + + @Test + @DisplayName("셀 상태를 깃발 꽂은 상태로 바꿀 수 있다.") + void modifyCellStateFlagged() { + // given + CellState cellState = CellState.initialize(); + + // when + cellState.flag(); + + // then + assertTrue(cellState.isFlagged()); + } + + @Test + @DisplayName("셀 상태를 연 상태로 바꿀 수 있다.") + void modifyCellStateOpened() { + // given + CellState cellState = CellState.initialize(); + + // when + cellState.open(); + + // then + assertTrue(cellState.isOpened()); + } +} diff --git a/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCellTest.java b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCellTest.java new file mode 100644 index 000000000..ecf19420b --- /dev/null +++ b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCellTest.java @@ -0,0 +1,47 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class EmptyCellTest { + + @Test + @DisplayName("빈 셀을 열면 셀의 상태가 열린 상태로 변경된다.") + void cellSnapshotIsEmptyWhenCellStateIsOpened() { + // given + EmptyCell emptyCell = new EmptyCell(); + + // when + emptyCell.open(); + + // then + assertThat(emptyCell.getSnapshot()).isEqualTo(CellSnapshot.ofEmpty()); + } + + @Test + @DisplayName("빈 셀에 깃발을 꽂으면 셀의 상태가 깃발이 꽂힌 상태로 변경된다.") + void cellSnapshotIsFlagWhenCellStateIsFlagged() { + // given + EmptyCell emptyCell = new EmptyCell(); + + // when + emptyCell.flag(); + + // then + assertThat(emptyCell.getSnapshot()).isEqualTo(CellSnapshot.ofFlag()); + } + + @Test + @DisplayName("빈 셀을 초기화하면 셀의 상태가 아무것도 표시 안한 상태로 초기화된다.") + void cellSnapshotIsUncheckedWhenJustInitialize() { + // given + EmptyCell emptyCell = new EmptyCell(); + + // when + + // then + assertThat(emptyCell.getSnapshot()).isEqualTo(CellSnapshot.ofUnchecked()); + } +} diff --git a/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCellTest.java b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCellTest.java new file mode 100644 index 000000000..e246403ef --- /dev/null +++ b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCellTest.java @@ -0,0 +1,47 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class LandMineCellTest { + + @Test + @DisplayName("지뢰 셀을 열면 셀의 상태가 주변에 있는 지뢰 셀 상태로 변경된다.") + void cellSnapshotIsNumberWhenCellStateIsOpened() { + // given + LandMineCell landMineCell = new LandMineCell(); + + // when + landMineCell.open(); + + // then + assertThat(landMineCell.getSnapshot()).isEqualTo(CellSnapshot.ofLandMine()); + } + + @Test + @DisplayName("지뢰 셀에 깃발을 꽂으면 셀의 상태가 깃발이 꽂힌 상태로 변경된다.") + void cellSnapshotIsFlagWhenCellStateIsFlagged() { + // given + LandMineCell landMineCell = new LandMineCell(); + + // when + landMineCell.flag(); + + // then + assertThat(landMineCell.getSnapshot()).isEqualTo(CellSnapshot.ofFlag()); + } + + @Test + @DisplayName("지뢰 셀을 초기화하면 셀의 상태가 아무것도 표시 안한 상태로 초기화된다.") + void cellSnapshotIsUncheckedWhenJustInitialize() { + // given + LandMineCell landMineCell = new LandMineCell(); + + // when + + // then + assertThat(landMineCell.getSnapshot()).isEqualTo(CellSnapshot.ofUnchecked()); + } +} diff --git a/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCellTest.java b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCellTest.java new file mode 100644 index 000000000..b024c3e21 --- /dev/null +++ b/src/test/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCellTest.java @@ -0,0 +1,51 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + + +class NumberCellTest { + + @Test + @DisplayName("숫자 셀을 열면 셀의 상태가 주변에 있는 지뢰의 숫자를 표기한 NUMBER 상태로 변경된다.") + void cellSnapshotIsNumberWhenCellStateIsOpened() { + // given + int nearByLandMineCount = 1; + NumberCell numberCell = new NumberCell(nearByLandMineCount); + + // when + numberCell.open(); + + // then + assertThat(numberCell.getSnapshot()).isEqualTo(CellSnapshot.ofNumber(nearByLandMineCount)); + } + + @Test + @DisplayName("숫자 셀에 깃발을 꽂으면 셀의 상태가 깃발이 꽂힌 상태로 변경된다.") + void cellSnapshotIsFlagWhenCellStateIsFlagged() { + // given + int nearByLandMineCount = 1; + NumberCell numberCell = new NumberCell(nearByLandMineCount); + + // when + numberCell.flag(); + + // then + assertThat(numberCell.getSnapshot()).isEqualTo(CellSnapshot.ofFlag()); + } + + @Test + @DisplayName("숫자 셀을 초기화하면 셀의 상태가 아무것도 표시 안한 상태로 초기화된다.") + void cellSnapshotIsUncheckedWhenJustInitialize() { + // given + int nearByLandMineCount = 1; + NumberCell numberCell = new NumberCell(nearByLandMineCount); + + // when + + // then + assertThat(numberCell.getSnapshot()).isEqualTo(CellSnapshot.ofUnchecked()); + } +}