diff --git a/.gitignore b/.gitignore index 63d2f1a0c..5c247a149 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle build/ +build !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -34,3 +35,11 @@ out/ ### VS Code ### .vscode/ +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen diff --git a/src/main/java/cleancode/minesweeper/tobe/AnotherGame.java b/src/main/java/cleancode/minesweeper/tobe/AnotherGame.java new file mode 100644 index 000000000..64290246d --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/AnotherGame.java @@ -0,0 +1,15 @@ +package cleancode.minesweeper.tobe; + +import cleancode.minesweeper.tobe.game.GameRunnable; + +public class AnotherGame implements GameRunnable { +// @Override +// public void initialize() { +// // ...필요없는데... +// } + + @Override + public void run() { + // 게임 진행 + } +} 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..7be464352 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java @@ -0,0 +1,61 @@ +package cleancode.minesweeper.tobe; + +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 { + + 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(); + } + + + /** + * DIP (Dependency Inversion Principle) + * 스프링의 3대 요소 + * IOC, DI + * PSA, AOP + * + * DI = Depedency Injection + * + * IoC = Inversion of Control + * + * DIP 는 고수준 모듈과 저수준 모듈이 직접적으로 의존하는 것이 아닌 추상화에 서로 의존해야 한다. + * DI 는 의존성을 주입한다. 필요한 의존성을 내가 직접 생성하는 것이 아니라 외부에서 주입받겠다. DI 를 생각하면 떠올라야하는 숫자가 있음. 3 + * 객체(A)가 있고, 또 다른 객체(B) 하나가 있음. A 객체가 B 객체를 필요로 함. 이 둘이 의존성을 갖고 싶은데, A가 B를 생성해서 사용하는게 아니라 의존성을 주입받고 싶음. + * 생성자나 다른 메소드를 통해 주입받고자 할때 A와 B 는 주입하는 걸 할 수 있음. 그러니 제 3자가 A와 B의 의존성을 맺어줄 수 밖에 없음. + * Spring 에서는 이걸 "Spring Context"="IOC Container" 가 해줌. + * 이 것들(객체의 결정과 주입)이 Runtime 에 실행됨 + * + * DI 와 붙어다니는 IoC + * IoC 는 제어의 역전. Spring 에서만 사용되지 않음. 더 큰 개념 + * 프로그램의 흐름을 개발자가 아닌 프레임워크가 담당하도록 하는 것. + * 제어의 순방향 = 프로그램은 개발자가 만드는 것 = 내가 만든 프로그램 개발자가 제어 -> 근데 이 제어 흐름이 역전됐다. + * 내가 만든 프로그램이 미리 만들어진 공장같은 프레임워크가 있고 프레임워크 안에 요소로 내 코드가 들어가서 일부분 톱니바퀴의 하나처럼 동작하는 것. + * 프레임워크는 톱니바퀴만 빠져있는 것. 내가 톱니바퀴만 하나 만들어서 내 애플리케이션이야! 하면서 뾱하고 끼우면 됨. + * 이땐 프레임워크가 메인이 되는 것임. 내 코드는 프레임 워크의 일부가 되면서. 제어가 프레임워크 쪽으로 넘어가는 것 + * Spring Framework = 코드를 입력했을때 Spring 이 제공하는 여러가지 기능을 사용하면서 규격에 맞춰서 코딩함 -> 이런 것들이 제어의 역전 + * + * 객체의 레벨에서 보면 IoC 컨테이너라는 친구가 객체를 직접적으로 생성해주고 생명주기를 관리해 줌. + * MineSweeper 가 아까는 ConsoleInputHandler 를 class 내에서 생성하고 사용함. + * IoC 컨테이너가 하는 일은. "생성과 소멸은 내가 알아서 해줄게" "객체 자체의 생명주기도 내가 알아서 다 해줄게, 의존성 주입도 DI 로 해줄게" + * "너는 쓰기만 해!!!" + * + * 객체레벨에서도 프로그램 제어권이 IoC 컨테이너라고 하는 친구에게 주도권이 있기 때문에 객체의 생성들 즉, + * Spring 에서는 Spring 이 관리하는 객체들을 Bean 이라고 하는데 Spring 에서는 이 Bean 들을 생성하고 Bean 들끼리 의존성을 주입해주고 + * 생명주기를 관리하는 일을 IoC 컨테이너가 하게 됨. + * + * @Component, @Service -> 내가 직접 생성 안함. IoC 컨테이너가 생성해줌. + */ +} \ No newline at end of file diff --git a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java new file mode 100644 index 000000000..77670ea00 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -0,0 +1,122 @@ +package cleancode.minesweeper.tobe; + +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.BoardIndexConverter; +import cleancode.minesweeper.tobe.minesweeper.io.InputHandler; +import cleancode.minesweeper.tobe.minesweeper.io.OutputHandler; +import cleancode.minesweeper.tobe.minesweeper.board.position.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.user.UserAction; + +// 모든 지뢰찾기 게임 로직을 여기에 둘 것임 +public class Minesweeper implements GameInitializable, GameRunnable { + // 중요한 문자열, 숫자야. 유지보수할때 잘 봐야해! 할 수 있는 것 = 매직넘버, 매직스트링 +// private static final int BOARD_ROW_SIZE = 8; +// private static final int BOARD_COL_SIZE = 10; + // 상수 컨벤션 = 대문자와 언더스코어로 이루어져 있게 해야함 + + + private final GameBoard gameBoard; + private final BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); + // 입출력에 대한건 여기서! + 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.showGameStartCommand(); + + while (gameBoard.isInProgress()) { + try { + outputHandler.showBoard(gameBoard); + +// String cellInput = getCellInputFromUser(); + CellPosition cellInput = getCellInputFromUser(); + UserAction userAction = getUserActionInputFromUser(); + + actOnCell(cellInput, userAction); + } catch (GameException e) { // 의도적인 Exception + outputHandler.showExceptionMessage(e); + + } catch (Exception e) { // 예상하지 못한 Exception + outputHandler.showSimpleMessage("프로그램에 문제가 생겼습니다."); +// e.printStackTrace(); // 실무에서는 Antipattern 실무에서는 log 시스템에서 log 를 남기고 별도의 조치를 취함 + } + } + + outputHandler.showBoard(gameBoard); // 마지막 결과값 보여줌 + + if (gameBoard.isWinStatus()) { + outputHandler.showGameWinningComment(); + } + if (gameBoard.isLoseStatus()) { + outputHandler.showGameLosingComment(); + } + } + + private CellPosition getCellInputFromUser() { + outputHandler.showCommentForSelectingCell(); + CellPosition cellPosition = inputHandler.getCellPositionFromUser(); // 이때, index 로서의 기능은 할 수 있게 함. + // 보드 길이에 따른 Position 검증은 GameBoard 에! + // 한군데서 하면 되는데 너무 잘게 쪼개는거 아닌가요? -> 그렇게 느낄 수 있겠지만 책임을 조금 분리! + // 보드가 있는 곳에서 자연스럽게 검증을 해보자! + if (gameBoard.isInvalidCellPosition(cellPosition)) { + throw new GameException("잘못된 좌표를 선택하셨습니다."); + } + return cellPosition; + } + + private UserAction getUserActionInputFromUser() { + outputHandler.showCommentFOrUserAction(); + return inputHandler.getUserActionFromUser() ; + } + + private void actOnCell(CellPosition cellPosition, UserAction userAction) { + + if (doesUserChooseToPlantFlag(userAction)) { + gameBoard.flagAt(cellPosition); + + return; + } + + if (doesUserChooseToOpenCell(userAction)) { + gameBoard.openAt(cellPosition); + } + + System.out.println("잘못된 번호를 선택하셨습니다."); + } + + private boolean doesUserChooseToPlantFlag(UserAction userAction) { + return userAction == UserAction.FLAG; + } + + private boolean doesUserChooseToOpenCell(UserAction userAction) { + return userAction == UserAction.OPEN; + } + +// private boolean isAllCellOpened() { +// boolean isAllOpened = true; +// for (int row = 0; row < BOARD_ROW_SIZE; row++) { +// for (int col = 0; col < BOARD_COL_SIZE; col++) { +// if (BOARD[row][col].equals(CLOSED_CELL_SIGN)) { // 네모 박스가 하나라도 있는지 확인 +// isAllOpened = false; +// } +// } +// } +// return isAllOpened; +// } +} 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..e64ee1f11 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/game/GameInitializable.java @@ -0,0 +1,6 @@ +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..ab1ef2c93 --- /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/board/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameBoard.java new file mode 100644 index 000000000..1b51a47fb --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameBoard.java @@ -0,0 +1,262 @@ +package cleancode.minesweeper.tobe.minesweeper.board; + +import cleancode.minesweeper.tobe.minesweeper.gamelevel.GameLevel; +import cleancode.minesweeper.tobe.minesweeper.board.position.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.board.position.CellPositions; +import cleancode.minesweeper.tobe.minesweeper.board.position.RelativePosition; +import cleancode.minesweeper.tobe.minesweeper.board.cell.*; + +import java.util.List; + +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); // 너가 가진 거에서 파라미터가 주어진 것을 뺀 나머지 position 을 줘 + initializeNumberCells(numberPositionCandidates); + } + + public void openAt(CellPosition cellPosition) { + if (isLandMineCellAt(cellPosition)) { + openOneCell(cellPosition); + changeGameStatusToLose(); + return; + } + + openSurroundedCells(cellPosition); + checkIfGameIsOver(); + return; + } + + public void flagAt(CellPosition cellPosition) { // Cell 의 상태 변경 + 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) // 이거 지뢰Cell 이야? + .count(); + + return (int) count; + +// int count = 0; +// if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCellAt(row - 1, col - 1)) { +// count++; +// } +// if (row - 1 >= 0 && isLandMineCellAt(row - 1, col)) { +// count++; +// } +// if (row - 1 >= 0 && col + 1 < colSize && isLandMineCellAt(row - 1, col + 1)) { +// count++; +// } +// if (col - 1 >= 0 && isLandMineCellAt(row, col - 1)) { +// count++; +// } +// if (col + 1 < colSize && isLandMineCellAt(row, col + 1)) { +// count++; +// } +// if (row + 1 < rowSize && col - 1 >= 0 && isLandMineCellAt(row + 1, col - 1)) { +// count++; +// } +// if (row + 1 < rowSize && isLandMineCellAt(row + 1, col)) { +// count++; +// } +// if (row + 1 < rowSize && col + 1 < colSize && isLandMineCellAt(row + 1, col + 1)) { +// count++; +// } +// return count; + } + private List calculateSurroundedPositions(CellPosition cellPosition, int rowSize, int colSize) { + return RelativePosition.SURROUNDED_POSITION.stream() + .filter(relativePosition -> cellPosition.canCalculatePositionBy(relativePosition)) // relativePosition 이 계산 가능한 Position 이야? 0 이상이야? + .map(relativePosition -> cellPosition.calculatePositionBy(relativePosition)) // 그러면 relativePosition 으로 새로운 좌표 계산해! + .filter(position -> position.isRowIndexLessThan(rowSize)) // 근데 이거 boardSize 이상이야? + .filter(position -> position.isColIndexLessThan(colSize)) + .toList(); + } + + private void updateCellAt(CellPosition position, Cell cell) { + board[position.getRowIndex()][position.getColIndex()] = cell; + } + + private void openOneCell(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + cell.open(); + } + + private void openSurroundedCells(CellPosition cellPosition) { + if (cellPosition.isRowIndexMoreThanOrEqual(getRowSize()) + || cellPosition.isColIndexMoreThanOrEqual(getColSize())) { // 얘도 바깥에서 BoardSize 보다 큰지는 확인했지만 재귀로 연산을 하기때문에 두어야 함. + return; + } + + if (isOpenedCell(cellPosition)) { + return; + } + + if (isLandMineCellAt(cellPosition)) { + return; + } + + // 여기까지 안열렸으면 아직 안열린 cell 이니까 열어! + openOneCell(cellPosition); // 오픈 + + if (doesCellHaveLandMineCount(cellPosition)) { // 숫자가 있으면! + // 열고 숫자를 초기화 한 것임 +// BOARD[row][col] = Cell.ofNearbyLandMineCount(NEAR_BY_LAND_MINE_COUNTS[row][col]); + return; + } + + calculateSurroundedPositions(cellPosition, getRowSize(), getColSize()) + .forEach(this::openSurroundedCells); +// == +// RelativePosition.SURROUNDED_POSITION.stream() +// .filter(relativePosition -> cellPosition.canCalculatePositionBy(relativePosition)) // if 는 filter // cellPosition 이 canCalculate 한지 +// .map(relativePosition -> cellPosition.calculatePositionBy(relativePosition)) // relativePosition 이 주어졌을 때 calculatePositionBy 를 하면 새로운 cellPosition 들이 우루루 나옴 +// .filter(position -> position.isRowIndexLessThan(getRowSize())) // board RowSize 와 생성된 Cell 의 row position 체크 +// .filter(position -> position.isColIndexLessThan(getColSize())) // board ColSize 와 생성된 Cell 의 col psoition 체크 +// .forEach(this::openSurroundedCells); // 이것에 대해 openSurroundedCells 호출 +// == +// for (RelativePosition relativePosition : RelativePosition.SURROUNDED_POSITION) { +// if (cellPosition.canCalculatePositionBy(relativePosition)) { +// // 둘다 0 보다 컸을때 새로운 cellposition 을 만들 수 있음! +// CellPosition nextCellPosition = cellPosition.calculatePositionBy(relativePosition); +// openSurroundedCells(nextCellPosition); +// } +// } + } + + 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() { +// return Arrays.stream(board)// BOARD 라는 이중 string 배열에 stream 을 걸면 String[] 형태의 Stream 이 나옴 Stream +// // 그냥 Map 을 하면 Stream> 이 나오는데 flatMap 을 하면서 평탄화를 통해 이중배열을 배열로, 즉, Stream 으로 만들어주는 것 +// .flatMap(stringArr -> Arrays.stream(stringArr)) // flatMap 을 하면 Stream 이 생기는데 이 stringArray 를 하나씩 돌면서 다시 Stream 만들거다 +// // 여기까지가 Stream +// .allMatch(Cell::isChecked); + + // cell 을 가공하던 책임이 cells 안으로 들어가면서 목록을 구성하는 책임이 Cells 안으로 들어감 + Cells cells = Cells.from(board); + return cells.isAllChecked(); + + } + + private void changeGameStatusToLose() { + gameStatus = GameStatus.LOSE; + } + + private void changeGameStatusToWin() { + gameStatus = GameStatus.WIN; + } + + 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..28afa3328 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/GameStatus.java @@ -0,0 +1,16 @@ +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..6c161f7b5 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cell.java @@ -0,0 +1,19 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +public interface Cell { + + boolean isLandMine(); + + boolean hasLandMineCount(); + + // flag, open, isChecked 는 다 공통일 것 같음 + void flag(); + + void open(); + + boolean isChecked(); + + boolean isOpened(); + + CellSnapshot getSnapshot(); +} 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..1fd6843fc --- /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; // 들어온 status 랑 내가 갖고있는 status 가 같은지 확인 + } + + public CellSnapshotStatus getStatus() { + return status; + } + + public int getNearbyLandMineCount() { + return nearbyLandMineCount; + } + // value object 니까! + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + CellSnapshot snapShot = (CellSnapshot) o; + return nearbyLandMineCount == snapShot.nearbyLandMineCount && status == snapShot.status; + } + + @Override + public int hashCode() { + return Objects.hash(status, nearbyLandMineCount); + } +} 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..a634b978f --- /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..17b21af2c --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/CellState.java @@ -0,0 +1,37 @@ +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); + } + + // flag, open, isChecked 는 다 공통일 것 같음 + public void flag() { + this.isFlagged = true; + } + + public void open() { + this.isOpened = true; + } + + public boolean isChecked() { + return isFlagged || isOpened; // 닫혀있는데 깃발이 꽂혀있거나, 열었거나 + } + + 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..01b98dd86 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/Cells.java @@ -0,0 +1,33 @@ +package cleancode.minesweeper.tobe.minesweeper.board.cell; + +import java.util.Arrays; +import java.util.List; + +// 일급 컬렉션 조건은 필드가 하나다! 컬렉션은 하나! +// 컬렉션에는 리스트, Set, Map 모두 올 수 있음 +public class Cells { + + private final List cells; + + private Cells(List cells) { + this.cells = cells; + } + + public static Cells of(List cells) { + return new Cells(cells); + } + + // Cells 가 cellsList 에 대한 가공의 책임을 가져가게 되고 + public static Cells from(Cell[][] cells) { + List cellsList = Arrays.stream(cells) + .flatMap(c -> Arrays.stream(c)) + .toList(); + + return of(cellsList); + } + + 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..b3d9a86f5 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/EmptyCell.java @@ -0,0 +1,46 @@ +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.isChecked(); + } + + @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..9f88406d7 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/LandMineCell.java @@ -0,0 +1,46 @@ +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.isChecked(); + } + + @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..cd181e6b2 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/cell/NumberCell.java @@ -0,0 +1,51 @@ +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.isChecked(); + } + + @Override + public boolean isOpened() { + return cellState.isOpened(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/CellPosition.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/CellPosition.java new file mode 100644 index 000000000..236e935de --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/CellPosition.java @@ -0,0 +1,80 @@ +package cleancode.minesweeper.tobe.minesweeper.board.position; + +import java.util.Objects; + +public class CellPosition { + + // final 을 붙여 불변성 + private final int rowIndex; + private final int colIndex; + + + private CellPosition(int rowIndex, int colIndex) { + if (rowIndex < 0 || colIndex < 0) { + // 게임 중에 발생되지 않기를 바라는 예외라서 GameException 이 아니라 개발자가 직접 확인해 주세요! 하는 마음으로 IllegalArgumentException 을 던짐 + throw new IllegalArgumentException("올바르지 않은 좌표입니다."); + } + this.rowIndex = rowIndex; + this.colIndex = colIndex; + } + + public static CellPosition of(int rowIndex, int colIndex) { + return new CellPosition(rowIndex, colIndex); + } + + /** + * equals 와 hashCode 재정의를 통해서 동등성 보장! + * rowIndex 와 colIndex 값이 같으면 같은 값! + */ + @Override + public boolean equals(Object o) { + 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("움직일 수 있는 좌표가 아닙니다. 좌표입니다."); + // calculatePositionBy 만 단독으로 호출될때를 대비해서 if 문 적용. + } + + public boolean isRowIndexLessThan(int rowIndex) { + return this.rowIndex < rowIndex; + } + + public boolean isColIndexLessThan(int colIndex) { + return this.colIndex < rowIndex; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/CellPositions.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/CellPositions.java new file mode 100644 index 000000000..af3405869 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/CellPositions.java @@ -0,0 +1,58 @@ +package cleancode.minesweeper.tobe.minesweeper.board.position; + +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) { // 얘는 landMineCount 인지 모름 + // shuffle 할때 그냥 컬렉션(positions) 가져다 쓰면 순서가 뒤죽박죽 될 수 있음. + // 순서가 주ㅇ요할때는 새롭게 컬렉션을 만들어라 + List cellPositions = new ArrayList<>(positions); + Collections.shuffle(cellPositions); + return cellPositions.subList(0, count); // 앞쪽에서 count 갯수만큼 자름 + } + + public List subtract(List positionsListToSubtract) { + List cellPositions = new ArrayList<>(positions); // 전체 positions 리스트로 새로 생성 + CellPositions positionsToSubtract = CellPositions.of(positionsListToSubtract); // 뺄 positions 객체로 새로 생성2 + + return cellPositions.stream() + .filter(position -> positionsToSubtract.doesNotContain(position)) + .toList(); + } + + private boolean doesNotContain(CellPosition position) { + return !positions.contains(position); + } + + public List getPositions() { + return new ArrayList<>(positions); // 외부에서 참조할 수 없도록 positions 을 새롭게 만들어서 리턴해줌 + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/RelativePosition.java b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/RelativePosition.java new file mode 100644 index 000000000..070ae9264 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/board/position/RelativePosition.java @@ -0,0 +1,50 @@ +package cleancode.minesweeper.tobe.minesweeper.board.position; + +import java.util.List; +import java.util.Objects; + +// 얘는 음수 값도 가질 수 있기 때문에 딱히 유효성 검증은 하지 않아도될듯! +public class RelativePosition { + public static final List SURROUNDED_POSITION = 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 (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..0fdbb8ed8 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/config/GameConfig.java @@ -0,0 +1,30 @@ +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..e63c9cd8b --- /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..ba858300a --- /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 { // GameLevel(추상화된 클래스) 의 Spec 을 만족시키는 구현체 + @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..c695191fd --- /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 { // GameLevel(추상화된 클래스) 의 Spec 을 만족시키는 구현체 + @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..c04c014f3 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/GameLevel.java @@ -0,0 +1,9 @@ +package cleancode.minesweeper.tobe.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..cdd265286 --- /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 { // GameLevel(추상화된 클래스) 의 Spec 을 만족시키는 구현체 + @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..f4aa50443 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/gamelevel/VeryBeginner.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.minesweeper.gamelevel; + +public class VeryBeginner implements GameLevel { // GameLevel(추상화된 클래스) 의 Spec 을 만족시키는 구현체 + @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..0b5766da9 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/BoardIndexConverter.java @@ -0,0 +1,34 @@ +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) { // 'a' // 알파벳이 j 까지만 대응되어 있음 + int colIndex = cellInputCol - BASE_CHAR_FOR_COL; // a가 들어가면 0, b가 들어가면 1 + if (colIndex < 0) { // a = 97 인데 cellInput 이 96 이면 알파벳이 아니기 때문 // 현재 대문자는 생각하지 않음. + 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..77a7b22bd --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleInputHandler.java @@ -0,0 +1,38 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.position.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..b5ed2bfac --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/ConsoleOutputHandler.java @@ -0,0 +1,82 @@ +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.board.position.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.io.sign.CellSignFinder; +import cleancode.minesweeper.tobe.minesweeper.io.sign.CellSignProvider; + +import java.util.List; +import java.util.stream.IntStream; + +public class ConsoleOutputHandler implements OutputHandler { + private final CellSignFinder cellSignFinder = new CellSignFinder(); + + @Override + public void showGameStartCommand() { + 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); // 2자리 수 이상일때 col 정렬 맞출 수 있도록 함 + 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.print(board.getSign(cellPosition) + " "); // 여기는 getter 를 안쓰는게 이상해 // 내가 여기에 보드를 그릴테니 cell 내용을 줘! + } + System.out.println(); + } + System.out.println(); + } + + private static String generateColAlphabets(GameBoard board) { + List alphabets = IntStream.range(0, board.getColSize()) // 0 부터 colSize 까지 range 만듦 + .mapToObj(index -> (char)('a' + index)) // 'a' + 0 = 'a' + .map(c -> c.toString()) + .toList(); // 알파벳 묶음이 될 것임 + String joiningAlphabets = String.join(" ", alphabets); // 알파벳들을 연결해줘 + return joiningAlphabets; + } + + @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..de67183e8 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/InputHandler.java @@ -0,0 +1,11 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.position.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..95703d630 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/OutputHandler.java @@ -0,0 +1,23 @@ +package cleancode.minesweeper.tobe.minesweeper.io; + +import cleancode.minesweeper.tobe.minesweeper.board.GameBoard; +import cleancode.minesweeper.tobe.minesweeper.exception.GameException; + +public interface OutputHandler { + + void showGameStartCommand(); + + void showBoard(GameBoard board); + + void showGameWinningComment(); + + void showGameLosingComment(); + + void showCommentForSelectingCell(); + + void showCommentFOrUserAction(); + + void showExceptionMessage(GameException e); + + void showSimpleMessage(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..6a3bac655 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignFinder.java @@ -0,0 +1,24 @@ +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)) // provider 의 supports 가 현재 snapShot 과 같은가? true 인거 하나 나옴 + .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..c2000aa20 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvidable.java @@ -0,0 +1,13 @@ +package cleancode.minesweeper.tobe.minesweeper.io.sign; + +import cleancode.minesweeper.tobe.minesweeper.board.cell.CellSnapshot; + +// cellsnapshot 을 넘겨줬을때 sign 을 return 하는 함수 +public interface CellSignProvidable { + + boolean supports(CellSnapshot cellSnapshot); + + String provide(CellSnapshot cellSnapshot); // cellSnapshot 을 받는 이유는 numberCell 값을 받기 위해 + + +} 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..4d440b278 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/CellSignProvider.java @@ -0,0 +1,68 @@ +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; + } + }, + LANDMINE(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()); + } + }, + UNCHECKED(CellSnapshotStatus.UNCHECKED){ + public String provide(CellSnapshot cellSnapshot) { + return UNCHECKED_SIGN; + } + }; + + private final CellSnapshotStatus status; + + 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 = "□"; + + CellSignProvider(CellSnapshotStatus status) { + this.status = status; + } + + + @Override + public boolean supports(CellSnapshot cellSnapshot) { + return cellSnapshot.isSameStatus(status); // 각 Enum 이 각자 상태에 맞는 cellstatus 를 갖고 있기 때문에 비교 가능 + } + + public static String findCellSignFrom(CellSnapshot cellSnapshot) { + CellSignProvider cellSignProvider = findBy(cellSnapshot); // snapshot 에 맞는 provider 를 하나 추출한다음 + return cellSignProvider.provide(cellSnapshot); // provide 해봤다. + } + + private static CellSignProvider findBy(CellSnapshot cellSnapshot) { + return Arrays.stream(values()) + .filter(provider -> provider.supports(cellSnapshot)) + .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..6d04d475c --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/EmptyCellSignProvider.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 EmptyCellSignProvider implements CellSignProvidable { + protected 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..7fc8e4641 --- /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 { + 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..19e20cf28 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/LandMineCellSignProvider.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 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..251ee8aff --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/NumberCellSignProvider.java @@ -0,0 +1,17 @@ +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..dd2cd143b --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/io/sign/UncheckedCellSignProvider.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 UncheckedCellSignProvider implements CellSignProvidable { + 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..964a34d3c --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/minesweeper/user/UserAction.java @@ -0,0 +1,14 @@ +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/main/java/cleancode/studycafe/asis/StudyCafePassMachine.java b/src/main/java/cleancode/studycafe/asis/StudyCafePassMachine.java index 1e910a5f2..d274fbb89 100644 --- a/src/main/java/cleancode/studycafe/asis/StudyCafePassMachine.java +++ b/src/main/java/cleancode/studycafe/asis/StudyCafePassMachine.java @@ -11,6 +11,7 @@ import java.util.List; public class StudyCafePassMachine { + private final StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); private final InputHandler inputHandler = new InputHandler(); private final OutputHandler outputHandler = new OutputHandler(); @@ -23,53 +24,27 @@ public void run() { outputHandler.askPassTypeSelection(); StudyCafePassType studyCafePassType = inputHandler.getPassTypeSelectingUserAction(); - if (studyCafePassType == StudyCafePassType.HOURLY) { - StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); - List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); - List hourlyPasses = studyCafePasses.stream() - .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.HOURLY) - .toList(); - outputHandler.showPassListForSelection(hourlyPasses); - StudyCafePass selectedPass = inputHandler.getSelectPass(hourlyPasses); + + + if (isPassTypeHourly(studyCafePassType)) { + StudyCafePass selectedPass = getPassTypeInputFromUser(StudyCafePassType.HOURLY); outputHandler.showPassOrderSummary(selectedPass, null); - } else if (studyCafePassType == StudyCafePassType.WEEKLY) { - StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); - List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); - List weeklyPasses = studyCafePasses.stream() - .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.WEEKLY) - .toList(); - outputHandler.showPassListForSelection(weeklyPasses); - StudyCafePass selectedPass = inputHandler.getSelectPass(weeklyPasses); + return; + } + + if (isPassTypeWeekly(studyCafePassType)) { + StudyCafePass selectedPass = getPassTypeInputFromUser(StudyCafePassType.WEEKLY); outputHandler.showPassOrderSummary(selectedPass, null); - } else if (studyCafePassType == StudyCafePassType.FIXED) { - StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); - List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); - List fixedPasses = studyCafePasses.stream() - .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.FIXED) - .toList(); - outputHandler.showPassListForSelection(fixedPasses); - StudyCafePass selectedPass = inputHandler.getSelectPass(fixedPasses); - - List lockerPasses = studyCafeFileHandler.readLockerPasses(); - StudyCafeLockerPass lockerPass = lockerPasses.stream() - .filter(option -> - option.getPassType() == selectedPass.getPassType() - && option.getDuration() == selectedPass.getDuration() - ) - .findFirst() - .orElse(null); - - boolean lockerSelection = false; - if (lockerPass != null) { - outputHandler.askLockerPass(lockerPass); - lockerSelection = inputHandler.getLockerSelection(); - } - - if (lockerSelection) { - outputHandler.showPassOrderSummary(selectedPass, lockerPass); - } else { - outputHandler.showPassOrderSummary(selectedPass, null); - } + return; + } + + if (isPassTypeFixed(studyCafePassType)) { + StudyCafePass selectedPass = getPassTypeInputFromUser(StudyCafePassType.FIXED); + StudyCafeLockerPass lockerPass = getSelectedLockerPass(selectedPass); // 고정석 가져오기 + + boolean lockerSelection = doesUserChooseToUseLocker(lockerPass); + + showAllInfo(selectedPass, lockerPass, lockerSelection); } } catch (AppException e) { outputHandler.showSimpleMessage(e.getMessage()); @@ -78,4 +53,67 @@ public void run() { } } + private boolean doesUserChooseToUseLocker(StudyCafeLockerPass lockerPass) { + boolean lockerSelection = false; + if (lockerPass != null) { + lockerSelection = getUserUseTheLocker(lockerPass); + } + return lockerSelection; + } + + private void showAllInfo(StudyCafePass selectedPass, StudyCafeLockerPass lockerPass, boolean lockerSelection) { + if (lockerSelection) { + outputHandler.showPassOrderSummary(selectedPass, lockerPass); + } else { + outputHandler.showPassOrderSummary(selectedPass, null); + } + } + + private boolean getUserUseTheLocker(StudyCafeLockerPass lockerPass) { + boolean lockerSelection; + outputHandler.askLockerPass(lockerPass); + lockerSelection = inputHandler.getLockerSelection(); + return lockerSelection; + } + + private StudyCafeLockerPass getSelectedLockerPass(StudyCafePass selectedPass) { + List lockerPasses = studyCafeFileHandler. readLockerPasses(); // FIXED,4,10000 + StudyCafeLockerPass lockerPass = lockerPasses.stream() + .filter(option -> option.isSelectedLockerPassType(selectedPass)) + .findFirst() + .orElse(null); + return lockerPass; + } + + private static boolean isPassTypeFixed(StudyCafePassType studyCafePassType) { + return studyCafePassType == StudyCafePassType.FIXED; + } + + private static boolean isPassTypeWeekly(StudyCafePassType studyCafePassType) { + return studyCafePassType == StudyCafePassType.WEEKLY; + } + + private static boolean isPassTypeHourly(StudyCafePassType studyCafePassType) { + return studyCafePassType == StudyCafePassType.HOURLY; + } + + private void showPassListForSelection(List hourlyPasses) { + outputHandler.showPassListForSelection(hourlyPasses); + } + + private StudyCafePass getPassTypeInputFromUser(StudyCafePassType passType) { + List passTypeList = getPassTypeList(passType); + showPassListForSelection(passTypeList); + StudyCafePass inputPassType = inputHandler.getSelectPass(passTypeList); + return inputPassType; + } + + private static List getPassTypeList(StudyCafePassType passType) { + StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); + List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); + return studyCafePasses.stream() + .filter(studyCafePass -> studyCafePass.getPassType() == passType) + .toList(); + } + } diff --git a/src/main/java/cleancode/studycafe/asis/io/StudyCafeFileHandler.java b/src/main/java/cleancode/studycafe/asis/io/StudyCafeFileHandler.java index 1dc0328f6..76ed84e98 100644 --- a/src/main/java/cleancode/studycafe/asis/io/StudyCafeFileHandler.java +++ b/src/main/java/cleancode/studycafe/asis/io/StudyCafeFileHandler.java @@ -11,7 +11,7 @@ import cleancode.studycafe.asis.model.StudyCafePassType; public class StudyCafeFileHandler { - + // 이용권 목록을 파일로 읽는다. public List readStudyCafePasses() { try { List lines = Files.readAllLines(Paths.get("src/main/resources/cleancode/studycafe/pass-list.csv")); diff --git a/src/main/java/cleancode/studycafe/asis/model/StudyCafeLockerPass.java b/src/main/java/cleancode/studycafe/asis/model/StudyCafeLockerPass.java index d6cf932ae..507672eec 100644 --- a/src/main/java/cleancode/studycafe/asis/model/StudyCafeLockerPass.java +++ b/src/main/java/cleancode/studycafe/asis/model/StudyCafeLockerPass.java @@ -16,13 +16,13 @@ public static StudyCafeLockerPass of(StudyCafePassType passType, int duration, i return new StudyCafeLockerPass(passType, duration, price); } - public StudyCafePassType getPassType() { - return passType; - } - - public int getDuration() { - return duration; - } +// public StudyCafePassType getPassType() { +// return passType; +// } +// +// public int getDuration() { +// return duration; +// } public int getPrice() { return price; @@ -41,4 +41,8 @@ public String display() { return ""; } + public boolean isSelectedLockerPassType(StudyCafePass selectedPass) { + return this.passType == selectedPass.getPassType() && + this.duration == selectedPass.getDuration(); + } } diff --git a/src/main/java/cleancode/studycafe/mine/StudyCafeApplication.java b/src/main/java/cleancode/studycafe/mine/StudyCafeApplication.java new file mode 100644 index 000000000..6ac7fb1a2 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/StudyCafeApplication.java @@ -0,0 +1,10 @@ +package cleancode.studycafe.mine; + +public class StudyCafeApplication { + + public static void main(String[] args) { + StudyCafePassMachine studyCafePassMachine = new StudyCafePassMachine(); + studyCafePassMachine.run(); + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/StudyCafePassMachine.java b/src/main/java/cleancode/studycafe/mine/StudyCafePassMachine.java new file mode 100644 index 000000000..36710ccb7 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/StudyCafePassMachine.java @@ -0,0 +1,81 @@ +package cleancode.studycafe.mine; + +import cleancode.studycafe.mine.exception.AppException; +import cleancode.studycafe.mine.io.InputHandler; +import cleancode.studycafe.mine.io.OutputHandler; +import cleancode.studycafe.mine.io.StudyCafeFileHandler; +import cleancode.studycafe.mine.model.StudyCafeLockerPass; +import cleancode.studycafe.mine.model.StudyCafePass; +import cleancode.studycafe.mine.model.StudyCafePassType; + +import java.util.List; + +public class StudyCafePassMachine { + + private final InputHandler inputHandler = new InputHandler(); + private final OutputHandler outputHandler = new OutputHandler(); + + public void run() { + try { + outputHandler.showWelcomeMessage(); + outputHandler.showAnnouncement(); + + outputHandler.askPassTypeSelection(); + StudyCafePassType studyCafePassType = inputHandler.getPassTypeSelectingUserAction(); + + if (studyCafePassType == StudyCafePassType.HOURLY) { + StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); + List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); + List hourlyPasses = studyCafePasses.stream() + .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.HOURLY) + .toList(); + outputHandler.showPassListForSelection(hourlyPasses); + StudyCafePass selectedPass = inputHandler.getSelectPass(hourlyPasses); + outputHandler.showPassOrderSummary(selectedPass, null); + } else if (studyCafePassType == StudyCafePassType.WEEKLY) { + StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); + List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); + List weeklyPasses = studyCafePasses.stream() + .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.WEEKLY) + .toList(); + outputHandler.showPassListForSelection(weeklyPasses); + StudyCafePass selectedPass = inputHandler.getSelectPass(weeklyPasses); + outputHandler.showPassOrderSummary(selectedPass, null); + } else if (studyCafePassType == StudyCafePassType.FIXED) { + StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler(); + List studyCafePasses = studyCafeFileHandler.readStudyCafePasses(); + List fixedPasses = studyCafePasses.stream() + .filter(studyCafePass -> studyCafePass.getPassType() == StudyCafePassType.FIXED) + .toList(); + outputHandler.showPassListForSelection(fixedPasses); + StudyCafePass selectedPass = inputHandler.getSelectPass(fixedPasses); + + List lockerPasses = studyCafeFileHandler.readLockerPasses(); + StudyCafeLockerPass lockerPass = lockerPasses.stream() + .filter(option -> + option.getPassType() == selectedPass.getPassType() + && option.getDuration() == selectedPass.getDuration() + ) + .findFirst() + .orElse(null); + + boolean lockerSelection = false; + if (lockerPass != null) { + outputHandler.askLockerPass(lockerPass); + lockerSelection = inputHandler.getLockerSelection(); + } + + if (lockerSelection) { + outputHandler.showPassOrderSummary(selectedPass, lockerPass); + } else { + outputHandler.showPassOrderSummary(selectedPass, null); + } + } + } catch (AppException e) { + outputHandler.showSimpleMessage(e.getMessage()); + } catch (Exception e) { + outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다."); + } + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/exception/AppException.java b/src/main/java/cleancode/studycafe/mine/exception/AppException.java new file mode 100644 index 000000000..a70bfca3d --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/exception/AppException.java @@ -0,0 +1,9 @@ +package cleancode.studycafe.mine.exception; + +public class AppException extends RuntimeException { + + public AppException(String message) { + super(message); + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/io/InputHandler.java b/src/main/java/cleancode/studycafe/mine/io/InputHandler.java new file mode 100644 index 000000000..9345990d6 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/io/InputHandler.java @@ -0,0 +1,40 @@ +package cleancode.studycafe.mine.io; + +import cleancode.studycafe.mine.exception.AppException; +import cleancode.studycafe.mine.model.StudyCafePass; +import cleancode.studycafe.mine.model.StudyCafePassType; + +import java.util.List; +import java.util.Scanner; + +public class InputHandler { + + private static final Scanner SCANNER = new Scanner(System.in); + + public StudyCafePassType getPassTypeSelectingUserAction() { + String userInput = SCANNER.nextLine(); + + if ("1".equals(userInput)) { + return StudyCafePassType.HOURLY; + } + if ("2".equals(userInput)) { + return StudyCafePassType.WEEKLY; + } + if ("3".equals(userInput)) { + return StudyCafePassType.FIXED; + } + throw new AppException("잘못된 입력입니다."); + } + + public StudyCafePass getSelectPass(List passes) { + String userInput = SCANNER.nextLine(); + int selectedIndex = Integer.parseInt(userInput) - 1; + return passes.get(selectedIndex); + } + + public boolean getLockerSelection() { + String userInput = SCANNER.nextLine(); + return "1".equals(userInput); + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/io/OutputHandler.java b/src/main/java/cleancode/studycafe/mine/io/OutputHandler.java new file mode 100644 index 000000000..e36dcdaa8 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/io/OutputHandler.java @@ -0,0 +1,68 @@ +package cleancode.studycafe.mine.io; + +import cleancode.studycafe.mine.model.StudyCafeLockerPass; +import cleancode.studycafe.mine.model.StudyCafePass; + +import java.util.List; + +public class OutputHandler { + + public void showWelcomeMessage() { + System.out.println("*** 프리미엄 스터디카페 ***"); + } + + public void showAnnouncement() { + System.out.println("* 사물함은 고정석 선택 시 이용 가능합니다. (추가 결제)"); + System.out.println("* !오픈 이벤트! 2주권 이상 결제 시 10% 할인, 12주권 결제 시 15% 할인! (결제 시 적용)"); + System.out.println(); + } + + public void askPassTypeSelection() { + System.out.println("사용하실 이용권을 선택해 주세요."); + System.out.println("1. 시간 이용권(자유석) | 2. 주단위 이용권(자유석) | 3. 1인 고정석"); + } + + public void showPassListForSelection(List passes) { + System.out.println(); + System.out.println("이용권 목록"); + for (int index = 0; index < passes.size(); index++) { + StudyCafePass pass = passes.get(index); + System.out.println(String.format("%s. ", index + 1) + pass.display()); + } + } + + public void askLockerPass(StudyCafeLockerPass lockerPass) { + System.out.println(); + String askMessage = String.format( + "사물함을 이용하시겠습니까? (%s)", + lockerPass.display() + ); + + System.out.println(askMessage); + System.out.println("1. 예 | 2. 아니오"); + } + + public void showPassOrderSummary(StudyCafePass selectedPass, StudyCafeLockerPass lockerPass) { + System.out.println(); + System.out.println("이용 내역"); + System.out.println("이용권: " + selectedPass.display()); + if (lockerPass != null) { + System.out.println("사물함: " + lockerPass.display()); + } + + double discountRate = selectedPass.getDiscountRate(); + int discountPrice = (int) (selectedPass.getPrice() * discountRate); + if (discountPrice > 0) { + System.out.println("이벤트 할인 금액: " + discountPrice + "원"); + } + + int totalPrice = selectedPass.getPrice() - discountPrice + (lockerPass != null ? lockerPass.getPrice() : 0); + System.out.println("총 결제 금액: " + totalPrice + "원"); + System.out.println(); + } + + public void showSimpleMessage(String message) { + System.out.println(message); + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/io/StudyCafeFileHandler.java b/src/main/java/cleancode/studycafe/mine/io/StudyCafeFileHandler.java new file mode 100644 index 000000000..ea8524142 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/io/StudyCafeFileHandler.java @@ -0,0 +1,56 @@ +package cleancode.studycafe.mine.io; + +import cleancode.studycafe.mine.model.StudyCafeLockerPass; +import cleancode.studycafe.mine.model.StudyCafePass; +import cleancode.studycafe.mine.model.StudyCafePassType; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class StudyCafeFileHandler { + + public List readStudyCafePasses() { + try { + List lines = Files.readAllLines(Paths.get("src/main/resources/cleancode/studycafe/pass-list.csv")); + List studyCafePasses = new ArrayList<>(); + for (String line : lines) { + String[] values = line.split(","); + StudyCafePassType studyCafePassType = StudyCafePassType.valueOf(values[0]); + int duration = Integer.parseInt(values[1]); + int price = Integer.parseInt(values[2]); + double discountRate = Double.parseDouble(values[3]); + + StudyCafePass studyCafePass = StudyCafePass.of(studyCafePassType, duration, price, discountRate); + studyCafePasses.add(studyCafePass); + } + + return studyCafePasses; + } catch (IOException e) { + throw new RuntimeException("파일을 읽는데 실패했습니다.", e); + } + } + + public List readLockerPasses() { + try { + List lines = Files.readAllLines(Paths.get("src/main/resources/cleancode/studycafe/locker.csv")); + List lockerPasses = new ArrayList<>(); + for (String line : lines) { + String[] values = line.split(","); + StudyCafePassType studyCafePassType = StudyCafePassType.valueOf(values[0]); + int duration = Integer.parseInt(values[1]); + int price = Integer.parseInt(values[2]); + + StudyCafeLockerPass lockerPass = StudyCafeLockerPass.of(studyCafePassType, duration, price); + lockerPasses.add(lockerPass); + } + + return lockerPasses; + } catch (IOException e) { + throw new RuntimeException("파일을 읽는데 실패했습니다.", e); + } + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/model/StudyCafeLockerPass.java b/src/main/java/cleancode/studycafe/mine/model/StudyCafeLockerPass.java new file mode 100644 index 000000000..8663deb48 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/model/StudyCafeLockerPass.java @@ -0,0 +1,44 @@ +package cleancode.studycafe.mine.model; + +public class StudyCafeLockerPass { + + private final StudyCafePassType passType; + private final int duration; + private final int price; + + private StudyCafeLockerPass(StudyCafePassType passType, int duration, int price) { + this.passType = passType; + this.duration = duration; + this.price = price; + } + + public static StudyCafeLockerPass of(StudyCafePassType passType, int duration, int price) { + return new StudyCafeLockerPass(passType, duration, price); + } + + public StudyCafePassType getPassType() { + return passType; + } + + public int getDuration() { + return duration; + } + + public int getPrice() { + return price; + } + + public String display() { + if (passType == StudyCafePassType.HOURLY) { + return String.format("%s시간권 - %d원", duration, price); + } + if (passType == StudyCafePassType.WEEKLY) { + return String.format("%s주권 - %d원", duration, price); + } + if (passType == StudyCafePassType.FIXED) { + return String.format("%s주권 - %d원", duration, price); + } + return ""; + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/model/StudyCafePass.java b/src/main/java/cleancode/studycafe/mine/model/StudyCafePass.java new file mode 100644 index 000000000..baa785544 --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/model/StudyCafePass.java @@ -0,0 +1,50 @@ +package cleancode.studycafe.mine.model; + +public class StudyCafePass { + + private final StudyCafePassType passType; + private final int duration; + private final int price; + private final double discountRate; + + private StudyCafePass(StudyCafePassType passType, int duration, int price, double discountRate) { + this.passType = passType; + this.duration = duration; + this.price = price; + this.discountRate = discountRate; + } + + public static StudyCafePass of(StudyCafePassType passType, int duration, int price, double discountRate) { + return new StudyCafePass(passType, duration, price, discountRate); + } + + public StudyCafePassType getPassType() { + return passType; + } + + public int getDuration() { + return duration; + } + + public int getPrice() { + return price; + } + + public double getDiscountRate() { + return discountRate; + } + + public String display() { + if (passType == StudyCafePassType.HOURLY) { + return String.format("%s시간권 - %d원", duration, price); + } + if (passType == StudyCafePassType.WEEKLY) { + return String.format("%s주권 - %d원", duration, price); + } + if (passType == StudyCafePassType.FIXED) { + return String.format("%s주권 - %d원", duration, price); + } + return ""; + } + +} diff --git a/src/main/java/cleancode/studycafe/mine/model/StudyCafePassType.java b/src/main/java/cleancode/studycafe/mine/model/StudyCafePassType.java new file mode 100644 index 000000000..0bb48006e --- /dev/null +++ b/src/main/java/cleancode/studycafe/mine/model/StudyCafePassType.java @@ -0,0 +1,15 @@ +package cleancode.studycafe.mine.model; + +public enum StudyCafePassType { + + HOURLY("시간 단위 이용권"), + WEEKLY("주 단위 이용권"), + FIXED("1인 고정석"); + + private final String description; + + StudyCafePassType(String description) { + this.description = description; + } + +} diff --git a/src/test/java/cleancode/SampleTest.java b/src/test/java/cleancode/SampleTest.java index fc43aa46e..0300f00e1 100644 --- a/src/test/java/cleancode/SampleTest.java +++ b/src/test/java/cleancode/SampleTest.java @@ -1,22 +1,191 @@ package cleancode; +import cleancode.minesweeper.tobe.Minesweeper; +import cleancode.minesweeper.tobe.minesweeper.board.GameBoard; +import cleancode.minesweeper.tobe.minesweeper.board.GameStatus; +import cleancode.minesweeper.tobe.minesweeper.board.cell.*; +import cleancode.minesweeper.tobe.minesweeper.board.position.CellPosition; +import cleancode.minesweeper.tobe.minesweeper.config.GameConfig; +import cleancode.minesweeper.tobe.minesweeper.exception.GameException; +import cleancode.minesweeper.tobe.minesweeper.gamelevel.Advanced; +import cleancode.minesweeper.tobe.minesweeper.gamelevel.Beginner; +import cleancode.minesweeper.tobe.minesweeper.gamelevel.Middle; +import cleancode.minesweeper.tobe.minesweeper.gamelevel.VeryBeginner; +import cleancode.minesweeper.tobe.minesweeper.io.BoardIndexConverter; +import cleancode.minesweeper.tobe.minesweeper.io.ConsoleInputHandler; +import cleancode.minesweeper.tobe.minesweeper.io.ConsoleOutputHandler; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThat; class SampleTest { + // ConsoleInputHandler.java + @DisplayName("Cell의 종류에 따라 적절한 상태로 설정되어있는지 확인한다.") + @Test + void checkSnapshot() { + // given + Cell cell = new EmptyCell(); + + CellSnapshot testCellSnapshot = null; + if (cell instanceof EmptyCell) { + testCellSnapshot = CellSnapshot.ofEmpty(); + } + if (cell instanceof NumberCell) { + testCellSnapshot = CellSnapshot.ofNumber(3); + } + if (cell instanceof LandMineCell) { + testCellSnapshot = CellSnapshot.ofLandMine(); + } + + // when + CellSnapshot cellSnapshot = cell.getSnapshot(); + + // then + assertThat(testCellSnapshot.getStatus()).isEqualTo(testCellSnapshot.getStatus()); + } + + // GameLevel + @DisplayName("레벨에 맞는 보드의 가로 크기를 가졌는지 확인한다.") @Test - void sample() { + void getRowSize() { // given - int a = 1; - int b = 2; + GameConfig gameConfig = new GameConfig( + new Advanced(), + new ConsoleInputHandler(), + new ConsoleOutputHandler() + ); + + int testRowSize = 0; + if (gameConfig.getGameLevel() instanceof VeryBeginner) { + testRowSize = 4; + } + if (gameConfig.getGameLevel() instanceof Beginner) { + testRowSize = 8; + } + if (gameConfig.getGameLevel() instanceof Middle) { + testRowSize = 14; + } + if (gameConfig.getGameLevel() instanceof Advanced) { + testRowSize = 20; + } + // when - int sum = a + b; + int rowSize = gameConfig.getGameLevel().getRowSize(); // then - assertThat(sum).isEqualTo(3); + assertThat(testRowSize).isEqualTo(rowSize); } + // Advanced + @DisplayName("레벨에 맞는 보드의 세로 크기를 가졌는지 확인한다.") + @Test + void getColSize() { + // given + GameConfig gameConfig = new GameConfig( + new Advanced(), + new ConsoleInputHandler(), + new ConsoleOutputHandler() + ); + + + int testColSize = 0; + if (gameConfig.getGameLevel() instanceof VeryBeginner) { + testColSize = 5; + } + if (gameConfig.getGameLevel() instanceof Beginner) { + testColSize = 10; + } + if (gameConfig.getGameLevel() instanceof Middle) { + testColSize = 18; + } + if (gameConfig.getGameLevel() instanceof Advanced) { + testColSize = 24; + } + + // when + int colSize = gameConfig.getGameLevel().getColSize(); + + // then + assertThat(testColSize).isEqualTo(colSize); + } + + // Advanced + @DisplayName("레벨에 맞는 보드의 지뢰 갯수를 확인한다.") + @Test + void getLandMineCount() { + // given + GameConfig gameConfig = new GameConfig( + new Advanced(), + new ConsoleInputHandler(), + new ConsoleOutputHandler() + ); + + int testLandMineCount = 0; + if (gameConfig.getGameLevel() instanceof VeryBeginner) { + testLandMineCount = 2; + } + if (gameConfig.getGameLevel() instanceof Beginner) { + testLandMineCount = 10; + } + if (gameConfig.getGameLevel() instanceof Middle) { + testLandMineCount = 40; + } + if (gameConfig.getGameLevel() instanceof Advanced) { + testLandMineCount = 99; + } + + // when + int landMineCount = gameConfig.getGameLevel().getLandMineCount(); + + // then + assertThat(testLandMineCount).isEqualTo(landMineCount); + } + + @DisplayName("입력에 따른 행 변환이 의도에 맞게 되었는지 확인한다.") + @Test + void convertRowForm() { + // given + String userInput = "a5"; + int testRow = 4; + BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); + + // when + int rowIndex = boardIndexConverter.getSelectedRowIndex(userInput); + + // then + assertThat(testRow).isEqualTo(rowIndex); + } + + @DisplayName("입력에 따른 열 변환이 의도에 맞게 되었는지 확인한다.") + @Test + void convertColForm() { + // given + String userInput = "a5"; + int testCol = 0; + BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); + + // when + int colIndex = boardIndexConverter.getSelectedColIndex(userInput); + + // then + assertThat(testCol).isEqualTo(colIndex); + } + + @DisplayName("GameException 이 의도한 메시지를 보여주는지 확인한다.") + @Test + void checkGameException() { + // given + String testMessage = "test success"; + GameException gameException = new GameException(testMessage); + + // when + String message = gameException.getMessage(); + + // then + assertThat(testMessage).isEqualTo(message); + } }