From 37434cc9a116d0a2346918de9bc88151ef42a141 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Thu, 3 Oct 2024 22:37:28 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat(=EC=B6=94=EC=83=81):=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=20-=20=EC=9D=B4=EB=A6=84=20=EC=A7=93=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index dd85c3ce0..e3dad67d6 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -15,9 +15,9 @@ public static void main(String[] args) { 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 row = 0; row < 8; row++) { + for (int col = 0; col < 10; col++) { + board[row][col] = "□"; } } for (int i = 0; i < 10; i++) { @@ -25,38 +25,38 @@ public static void main(String[] args) { int row = new Random().nextInt(8); landMines[row][col] = true; } - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 10; j++) { + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 10; col++) { int count = 0; - if (!landMines[i][j]) { - if (i - 1 >= 0 && j - 1 >= 0 && landMines[i - 1][j - 1]) { + if (!landMines[row][col]) { + if (row - 1 >= 0 && col - 1 >= 0 && landMines[row - 1][col - 1]) { count++; } - if (i - 1 >= 0 && landMines[i - 1][j]) { + if (row - 1 >= 0 && landMines[row - 1][col]) { count++; } - if (i - 1 >= 0 && j + 1 < 10 && landMines[i - 1][j + 1]) { + if (row - 1 >= 0 && col + 1 < 10 && landMines[row - 1][col + 1]) { count++; } - if (j - 1 >= 0 && landMines[i][j - 1]) { + if (col - 1 >= 0 && landMines[row][col - 1]) { count++; } - if (j + 1 < 10 && landMines[i][j + 1]) { + if (col + 1 < 10 && landMines[row][col + 1]) { count++; } - if (i + 1 < 8 && j - 1 >= 0 && landMines[i + 1][j - 1]) { + if (row + 1 < 8 && col - 1 >= 0 && landMines[row + 1][col - 1]) { count++; } - if (i + 1 < 8 && landMines[i + 1][j]) { + if (row + 1 < 8 && landMines[row + 1][col]) { count++; } - if (i + 1 < 8 && j + 1 < 10 && landMines[i + 1][j + 1]) { + if (row + 1 < 8 && col + 1 < 10 && landMines[row + 1][col + 1]) { count++; } - landMineCounts[i][j] = count; + landMineCounts[row][col] = count; continue; } - landMineCounts[i][j] = 0; + landMineCounts[row][col] = 0; } } while (true) { @@ -78,78 +78,78 @@ public static void main(String[] args) { } System.out.println(); System.out.println("선택할 좌표를 입력하세요. (예: a1)"); - String input = scanner.nextLine(); + String cellInput = 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) { + String userActionInput = scanner.nextLine(); + char cellInputCol = cellInput.charAt(0); + char cellInputRow = cellInput.charAt(1); + int selectedColumnIndex; + switch (cellInputCol) { case 'a': - col = 0; + selectedColumnIndex = 0; break; case 'b': - col = 1; + selectedColumnIndex = 1; break; case 'c': - col = 2; + selectedColumnIndex = 2; break; case 'd': - col = 3; + selectedColumnIndex = 3; break; case 'e': - col = 4; + selectedColumnIndex = 4; break; case 'f': - col = 5; + selectedColumnIndex = 5; break; case 'g': - col = 6; + selectedColumnIndex = 6; break; case 'h': - col = 7; + selectedColumnIndex = 7; break; case 'i': - col = 8; + selectedColumnIndex = 8; break; case 'j': - col = 9; + selectedColumnIndex = 9; break; default: - col = -1; + selectedColumnIndex = -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; + int selectedRowIndex = Character.getNumericValue(cellInputRow) - 1; + if (userActionInput.equals("2")) { + board[selectedRowIndex][selectedColumnIndex] = "⚑"; + boolean isAllOpened = true; + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 10; col++) { + if (board[row][col].equals("□")) { + isAllOpened = false; } } } - if (open) { + if (isAllOpened) { gameStatus = 1; } - } else if (input2.equals("1")) { - if (landMines[row][col]) { - board[row][col] = "☼"; + } else if (userActionInput.equals("1")) { + if (landMines[selectedRowIndex][selectedColumnIndex]) { + board[selectedRowIndex][selectedColumnIndex] = "☼"; gameStatus = -1; continue; } else { - open(row, col); + open(selectedRowIndex, selectedColumnIndex); } - boolean open = true; - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 10; j++) { - if (board[i][j].equals("□")) { - open = false; + boolean isAllOpened = true; + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 10; col++) { + if (board[row][col].equals("□")) { + isAllOpened = false; } } } - if (open) { + if (isAllOpened) { gameStatus = 1; } } else { From abbc409edb2674ab7d600ca225e9e437ba26d35e Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Thu, 3 Oct 2024 22:52:18 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat(=EC=B6=94=EC=83=81):=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=20-=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 207 +++++++++--------- 1 file changed, 107 insertions(+), 100 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index e3dad67d6..38763c208 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -11,10 +11,108 @@ public class MinesweeperGame { private static int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 public static void main(String[] args) { - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - System.out.println("지뢰찾기 게임 시작!"); - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + showGameStartComments(); Scanner scanner = new Scanner(System.in); + initializeGame(); + while (true) { + showBoard(); + 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 cellInput = scanner.nextLine(); + System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); + String userActionInput = scanner.nextLine(); + char cellInputCol = cellInput.charAt(0); + char cellInputRow = cellInput.charAt(1); + int selectedColumnIndex = convertColFrom(cellInputCol); + int selectedRowIndex = convertRowFrom(cellInputRow); + if (userActionInput.equals("2")) { + board[selectedRowIndex][selectedColumnIndex] = "⚑"; + checkIfGameIsOver(); + } else if (userActionInput.equals("1")) { + if (landMines[selectedRowIndex][selectedColumnIndex]) { + board[selectedRowIndex][selectedColumnIndex] = "☼"; + gameStatus = -1; + continue; + } else { + open(selectedRowIndex, selectedColumnIndex); + } + checkIfGameIsOver(); + } else { + System.out.println("잘못된 번호를 선택하셨습니다."); + } + } + } + + private static void checkIfGameIsOver() { + boolean isAllOpened = isAllCellOpened(); + if (isAllOpened) { + gameStatus = 1; + } + } + + private static boolean isAllCellOpened() { + boolean isAllOpened = true; + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 10; col++) { + if (board[row][col].equals("□")) { + isAllOpened = false; + } + } + } + return isAllOpened; + } + + private static int convertRowFrom(char cellInputRow) { + return Character.getNumericValue(cellInputRow) - 1; + } + + private static int convertColFrom(char cellInputCol) { + switch (cellInputCol) { + case 'a': + return 0; + case 'b': + return 1; + case 'c': + return 2; + case 'd': + return 3; + case 'e': + return 4; + case 'f': + return 5; + case 'g': + return 6; + case 'h': + return 7; + case 'i': + return 8; + case 'j': + return 9; + default: + return -1; + } + } + + private static void showBoard() { + 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(); + } + } + + private static void initializeGame() { for (int row = 0; row < 8; row++) { for (int col = 0; col < 10; col++) { board[row][col] = "□"; @@ -59,103 +157,12 @@ public static void main(String[] args) { landMineCounts[row][col] = 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 cellInput = scanner.nextLine(); - System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); - String userActionInput = scanner.nextLine(); - char cellInputCol = cellInput.charAt(0); - char cellInputRow = cellInput.charAt(1); - int selectedColumnIndex; - switch (cellInputCol) { - case 'a': - selectedColumnIndex = 0; - break; - case 'b': - selectedColumnIndex = 1; - break; - case 'c': - selectedColumnIndex = 2; - break; - case 'd': - selectedColumnIndex = 3; - break; - case 'e': - selectedColumnIndex = 4; - break; - case 'f': - selectedColumnIndex = 5; - break; - case 'g': - selectedColumnIndex = 6; - break; - case 'h': - selectedColumnIndex = 7; - break; - case 'i': - selectedColumnIndex = 8; - break; - case 'j': - selectedColumnIndex = 9; - break; - default: - selectedColumnIndex = -1; - break; - } - int selectedRowIndex = Character.getNumericValue(cellInputRow) - 1; - if (userActionInput.equals("2")) { - board[selectedRowIndex][selectedColumnIndex] = "⚑"; - boolean isAllOpened = true; - for (int row = 0; row < 8; row++) { - for (int col = 0; col < 10; col++) { - if (board[row][col].equals("□")) { - isAllOpened = false; - } - } - } - if (isAllOpened) { - gameStatus = 1; - } - } else if (userActionInput.equals("1")) { - if (landMines[selectedRowIndex][selectedColumnIndex]) { - board[selectedRowIndex][selectedColumnIndex] = "☼"; - gameStatus = -1; - continue; - } else { - open(selectedRowIndex, selectedColumnIndex); - } - boolean isAllOpened = true; - for (int row = 0; row < 8; row++) { - for (int col = 0; col < 10; col++) { - if (board[row][col].equals("□")) { - isAllOpened = false; - } - } - } - if (isAllOpened) { - gameStatus = 1; - } - } else { - System.out.println("잘못된 번호를 선택하셨습니다."); - } - } + } + + private static void showGameStartComments() { + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + System.out.println("지뢰찾기 게임 시작!"); + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); } private static void open(int row, int col) { From 138fbeba5f399f3beb54ce33e0ae719007c11d1e Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Fri, 4 Oct 2024 02:56:24 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat(=EC=B6=94=EC=83=81):=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=20-=20=EC=B6=94=EC=83=81=ED=99=94=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 38763c208..9dca39c5e 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -16,30 +16,25 @@ public static void main(String[] args) { initializeGame(); while (true) { showBoard(); - if (gameStatus == 1) { + if (doesUserWinTheGame()) { System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); break; } - if (gameStatus == -1) { + if (doesUserLoseTheGame()) { System.out.println("지뢰를 밟았습니다. GAME OVER!"); break; } - System.out.println(); - System.out.println("선택할 좌표를 입력하세요. (예: a1)"); - String cellInput = scanner.nextLine(); - System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); - String userActionInput = scanner.nextLine(); - char cellInputCol = cellInput.charAt(0); - char cellInputRow = cellInput.charAt(1); - int selectedColumnIndex = convertColFrom(cellInputCol); - int selectedRowIndex = convertRowFrom(cellInputRow); - if (userActionInput.equals("2")) { + String cellInput = getCellInputFromUser(scanner); + String userActionInput = getUserActionInputFromUser(scanner); + int selectedColumnIndex = getSelectedColIndex(cellInput); + int selectedRowIndex = getSelectedRowIndex(cellInput); + if (doesUserChooseToPlantFlag(userActionInput)) { board[selectedRowIndex][selectedColumnIndex] = "⚑"; checkIfGameIsOver(); - } else if (userActionInput.equals("1")) { - if (landMines[selectedRowIndex][selectedColumnIndex]) { + } else if (doesUserChooseToOpenCell(userActionInput)) { + if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { board[selectedRowIndex][selectedColumnIndex] = "☼"; - gameStatus = -1; + changeGameStatusToLose(); continue; } else { open(selectedRowIndex, selectedColumnIndex); @@ -51,6 +46,50 @@ public static void main(String[] args) { } } + private static void changeGameStatusToLose() { + gameStatus = -1; + } + + private static boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { + return landMines[selectedRowIndex][selectedColumnIndex]; + } + + private static boolean doesUserChooseToOpenCell(String userActionInput) { + return userActionInput.equals("1"); + } + + private static boolean doesUserChooseToPlantFlag(String userActionInput) { + return userActionInput.equals("2"); + } + + private static int getSelectedRowIndex(String cellInput) { + char cellInputRow = cellInput.charAt(1); + return convertRowFrom(cellInputRow); + } + + private static int getSelectedColIndex(String cellInput) { + char cellInputCol = cellInput.charAt(0); + return convertColFrom(cellInputCol); + } + + private static String getUserActionInputFromUser(Scanner scanner) { + System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); + return scanner.nextLine(); + } + + private static String getCellInputFromUser(Scanner scanner) { + System.out.println("선택할 좌표를 입력하세요. (예: a1)"); + return scanner.nextLine(); + } + + private static boolean doesUserLoseTheGame() { + return gameStatus == -1; + } + + private static boolean doesUserWinTheGame() { + return gameStatus == 1; + } + private static void checkIfGameIsOver() { boolean isAllOpened = isAllCellOpened(); if (isAllOpened) { @@ -126,29 +165,29 @@ private static void initializeGame() { for (int row = 0; row < 8; row++) { for (int col = 0; col < 10; col++) { int count = 0; - if (!landMines[row][col]) { - if (row - 1 >= 0 && col - 1 >= 0 && landMines[row - 1][col - 1]) { + if (!isLandMineCell(row, col)) { + if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { count++; } - if (row - 1 >= 0 && landMines[row - 1][col]) { + if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { count++; } - if (row - 1 >= 0 && col + 1 < 10 && landMines[row - 1][col + 1]) { + if (row - 1 >= 0 && col + 1 < 10 && isLandMineCell(row - 1, col + 1)) { count++; } - if (col - 1 >= 0 && landMines[row][col - 1]) { + if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { count++; } - if (col + 1 < 10 && landMines[row][col + 1]) { + if (col + 1 < 10 && isLandMineCell(row, col + 1)) { count++; } - if (row + 1 < 8 && col - 1 >= 0 && landMines[row + 1][col - 1]) { + if (row + 1 < 8 && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { count++; } - if (row + 1 < 8 && landMines[row + 1][col]) { + if (row + 1 < 8 && isLandMineCell(row + 1, col)) { count++; } - if (row + 1 < 8 && col + 1 < 10 && landMines[row + 1][col + 1]) { + if (row + 1 < 8 && col + 1 < 10 && isLandMineCell(row + 1, col + 1)) { count++; } landMineCounts[row][col] = count; @@ -172,7 +211,7 @@ private static void open(int row, int col) { if (!board[row][col].equals("□")) { return; } - if (landMines[row][col]) { + if (isLandMineCell(row, col)) { return; } if (landMineCounts[row][col] != 0) { From 8479a0d6e72b81ddd876419863dc9241bdef5882 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Fri, 4 Oct 2024 19:05:42 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat(=EC=B6=94=EC=83=81):=20=EB=A7=A4?= =?UTF-8?q?=EC=A7=81=20=EB=84=98=EB=B2=84,=20=EB=A7=A4=EC=A7=81=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 9dca39c5e..79431bee4 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -5,9 +5,17 @@ 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]; + + public static final int BOARD_ROW_SIZE = 8; + public static final int BOARD_COL_SIZE = 10; + public static final int LAND_MINE_COUNT = 10; + public static final String FLAG_SIGN = "⚑"; + public static final String LAND_MINE_SIGN = "☼"; + public static final String CLOSED_CELL_SIGN = "□"; + public static final String OPENED_CELL_SIGN = "■"; + private static String[][] BOARD = new String[BOARD_ROW_SIZE][BOARD_COL_SIZE]; + private static Integer[][] NEARBY_LAND_MINE_COUNTS = new Integer[BOARD_ROW_SIZE][BOARD_COL_SIZE]; + private static boolean[][] LAND_MINES = new boolean[BOARD_ROW_SIZE][BOARD_COL_SIZE]; private static int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 public static void main(String[] args) { @@ -29,11 +37,11 @@ public static void main(String[] args) { int selectedColumnIndex = getSelectedColIndex(cellInput); int selectedRowIndex = getSelectedRowIndex(cellInput); if (doesUserChooseToPlantFlag(userActionInput)) { - board[selectedRowIndex][selectedColumnIndex] = "⚑"; + BOARD[selectedRowIndex][selectedColumnIndex] = FLAG_SIGN; checkIfGameIsOver(); } else if (doesUserChooseToOpenCell(userActionInput)) { if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { - board[selectedRowIndex][selectedColumnIndex] = "☼"; + BOARD[selectedRowIndex][selectedColumnIndex] = LAND_MINE_SIGN; changeGameStatusToLose(); continue; } else { @@ -51,7 +59,7 @@ private static void changeGameStatusToLose() { } private static boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { - return landMines[selectedRowIndex][selectedColumnIndex]; + return LAND_MINES[selectedRowIndex][selectedColumnIndex]; } private static boolean doesUserChooseToOpenCell(String userActionInput) { @@ -99,9 +107,9 @@ private static void checkIfGameIsOver() { private static boolean isAllCellOpened() { boolean isAllOpened = true; - for (int row = 0; row < 8; row++) { - for (int col = 0; col < 10; col++) { - if (board[row][col].equals("□")) { + 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; } } @@ -142,28 +150,28 @@ private static int convertColFrom(char cellInputCol) { private static void showBoard() { System.out.println(" a b c d e f g h i j"); - for (int i = 0; i < 8; i++) { + for (int i = 0; i < BOARD_ROW_SIZE; i++) { System.out.printf("%d ", i + 1); - for (int j = 0; j < 10; j++) { - System.out.print(board[i][j] + " "); + for (int j = 0; j < BOARD_COL_SIZE; j++) { + System.out.print(BOARD[i][j] + " "); } System.out.println(); } } private static void initializeGame() { - for (int row = 0; row < 8; row++) { - for (int col = 0; col < 10; col++) { - board[row][col] = "□"; + for (int row = 0; row < BOARD_ROW_SIZE; row++) { + for (int col = 0; col < BOARD_COL_SIZE; col++) { + BOARD[row][col] = CLOSED_CELL_SIGN; } } - for (int i = 0; i < 10; i++) { + for (int i = 0; i < LAND_MINE_COUNT; i++) { int col = new Random().nextInt(10); int row = new Random().nextInt(8); - landMines[row][col] = true; + LAND_MINES[row][col] = true; } - for (int row = 0; row < 8; row++) { - for (int col = 0; col < 10; col++) { + for (int row = 0; row < BOARD_ROW_SIZE; row++) { + for (int col = 0; col < BOARD_COL_SIZE; col++) { int count = 0; if (!isLandMineCell(row, col)) { if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { @@ -172,28 +180,28 @@ private static void initializeGame() { if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { count++; } - if (row - 1 >= 0 && col + 1 < 10 && isLandMineCell(row - 1, col + 1)) { + if (row - 1 >= 0 && col + 1 < BOARD_COL_SIZE && isLandMineCell(row - 1, col + 1)) { count++; } if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { count++; } - if (col + 1 < 10 && isLandMineCell(row, col + 1)) { + if (col + 1 < BOARD_COL_SIZE && isLandMineCell(row, col + 1)) { count++; } - if (row + 1 < 8 && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { + if (row + 1 < BOARD_ROW_SIZE && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { count++; } - if (row + 1 < 8 && isLandMineCell(row + 1, col)) { + if (row + 1 < BOARD_ROW_SIZE && isLandMineCell(row + 1, col)) { count++; } - if (row + 1 < 8 && col + 1 < 10 && isLandMineCell(row + 1, col + 1)) { + if (row + 1 < BOARD_ROW_SIZE && col + 1 < BOARD_COL_SIZE && isLandMineCell(row + 1, col + 1)) { count++; } - landMineCounts[row][col] = count; + NEARBY_LAND_MINE_COUNTS[row][col] = count; continue; } - landMineCounts[row][col] = 0; + NEARBY_LAND_MINE_COUNTS[row][col] = 0; } } } @@ -205,20 +213,20 @@ private static void showGameStartComments() { } private static void open(int row, int col) { - if (row < 0 || row >= 8 || col < 0 || col >= 10) { + if (row < 0 || row >= BOARD_ROW_SIZE || col < 0 || col >= BOARD_COL_SIZE) { return; } - if (!board[row][col].equals("□")) { + if (!BOARD[row][col].equals(CLOSED_CELL_SIGN)) { return; } if (isLandMineCell(row, col)) { return; } - if (landMineCounts[row][col] != 0) { - board[row][col] = String.valueOf(landMineCounts[row][col]); + if (NEARBY_LAND_MINE_COUNTS[row][col] != 0) { + BOARD[row][col] = String.valueOf(NEARBY_LAND_MINE_COUNTS[row][col]); return; } else { - board[row][col] = "■"; + BOARD[row][col] = OPENED_CELL_SIGN; } open(row - 1, col - 1); open(row - 1, col); From 44107dd2a0a01b25e11b10ba662c20a9a7612c34 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Fri, 4 Oct 2024 21:46:36 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat(=EB=85=BC=EB=A6=AC,-=EC=82=AC?= =?UTF-8?q?=EA=B3=A0=EC=9D=98-=ED=9D=90=EB=A6=84):=20[=EB=85=BC=EB=A6=AC,?= =?UTF-8?q?=20=EC=82=AC=EA=B3=A0=EC=9D=98=20=ED=9D=90=EB=A6=84]=20Early=20?= =?UTF-8?q?Return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 79431bee4..515a62afa 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -34,24 +34,29 @@ public static void main(String[] args) { } String cellInput = getCellInputFromUser(scanner); String userActionInput = getUserActionInputFromUser(scanner); - int selectedColumnIndex = getSelectedColIndex(cellInput); - int selectedRowIndex = getSelectedRowIndex(cellInput); - if (doesUserChooseToPlantFlag(userActionInput)) { - BOARD[selectedRowIndex][selectedColumnIndex] = FLAG_SIGN; - checkIfGameIsOver(); - } else if (doesUserChooseToOpenCell(userActionInput)) { - if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { - BOARD[selectedRowIndex][selectedColumnIndex] = LAND_MINE_SIGN; - changeGameStatusToLose(); - continue; - } else { - open(selectedRowIndex, selectedColumnIndex); - } - checkIfGameIsOver(); - } else { - System.out.println("잘못된 번호를 선택하셨습니다."); + actOnCell(cellInput, userActionInput); + } + } + + private static void actOnCell(String cellInput, String userActionInput) { + int selectedColumnIndex = getSelectedColIndex(cellInput); + int selectedRowIndex = getSelectedRowIndex(cellInput); + if (doesUserChooseToPlantFlag(userActionInput)) { + BOARD[selectedRowIndex][selectedColumnIndex] = FLAG_SIGN; + checkIfGameIsOver(); + return; + } + if (doesUserChooseToOpenCell(userActionInput)) { + if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { + BOARD[selectedRowIndex][selectedColumnIndex] = LAND_MINE_SIGN; + changeGameStatusToLose(); + return; } + open(selectedRowIndex, selectedColumnIndex); + checkIfGameIsOver(); + return; } + System.out.println("잘못된 번호를 선택하셨습니다."); } private static void changeGameStatusToLose() { From 5529e4fafea7661873149978523ee6ad21ed12c5 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Fri, 4 Oct 2024 22:15:12 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat(=EB=85=BC=EB=A6=AC,-=EC=82=AC?= =?UTF-8?q?=EA=B3=A0=EC=9D=98-=ED=9D=90=EB=A6=84):=20[=EB=85=BC=EB=A6=AC,?= =?UTF-8?q?=20=EC=82=AC=EA=B3=A0=EC=9D=98=20=ED=9D=90=EB=A6=84]=20?= =?UTF-8?q?=EC=82=AC=EA=B3=A0=EC=9D=98=20depth=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 515a62afa..ab55d3b32 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -1,11 +1,13 @@ package cleancode.minesweeper.tobe; +import java.util.Arrays; import java.util.Random; import java.util.Scanner; public class MinesweeperGame { + public static final Scanner SCANNER = new Scanner(System.in); public static final int BOARD_ROW_SIZE = 8; public static final int BOARD_COL_SIZE = 10; public static final int LAND_MINE_COUNT = 10; @@ -20,7 +22,6 @@ public class MinesweeperGame { public static void main(String[] args) { showGameStartComments(); - Scanner scanner = new Scanner(System.in); initializeGame(); while (true) { showBoard(); @@ -32,8 +33,8 @@ public static void main(String[] args) { System.out.println("지뢰를 밟았습니다. GAME OVER!"); break; } - String cellInput = getCellInputFromUser(scanner); - String userActionInput = getUserActionInputFromUser(scanner); + String cellInput = getCellInputFromUser(); + String userActionInput = getUserActionInputFromUser(); actOnCell(cellInput, userActionInput); } } @@ -85,14 +86,14 @@ private static int getSelectedColIndex(String cellInput) { return convertColFrom(cellInputCol); } - private static String getUserActionInputFromUser(Scanner scanner) { + private static String getUserActionInputFromUser() { System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); - return scanner.nextLine(); + return SCANNER.nextLine(); } - private static String getCellInputFromUser(Scanner scanner) { + private static String getCellInputFromUser() { System.out.println("선택할 좌표를 입력하세요. (예: a1)"); - return scanner.nextLine(); + return SCANNER.nextLine(); } private static boolean doesUserLoseTheGame() { @@ -111,15 +112,9 @@ private static void checkIfGameIsOver() { } private static 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; + return Arrays.stream(BOARD) + .flatMap(Arrays::stream) + .noneMatch(cell -> cell.equals(CLOSED_CELL_SIGN)); } private static int convertRowFrom(char cellInputRow) { From d0ce33ada1a13882bb7520aed6ef054103d47d51 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Fri, 4 Oct 2024 23:29:31 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat(=EB=85=BC=EB=A6=AC,-=EC=82=AC?= =?UTF-8?q?=EA=B3=A0=EC=9D=98-=ED=9D=90=EB=A6=84):=20[=EB=85=BC=EB=A6=AC,?= =?UTF-8?q?=20=EC=82=AC=EA=B3=A0=EC=9D=98=20=ED=9D=90=EB=A6=84]=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EB=9D=BC=EC=9D=B8=EC=9D=84=20=EB=8C=80?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9E=90=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cleancode/minesweeper/tobe/MinesweeperGame.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index ab55d3b32..082803a80 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -6,7 +6,6 @@ public class MinesweeperGame { - public static final Scanner SCANNER = new Scanner(System.in); public static final int BOARD_ROW_SIZE = 8; public static final int BOARD_COL_SIZE = 10; @@ -23,8 +22,10 @@ public class MinesweeperGame { public static void main(String[] args) { showGameStartComments(); initializeGame(); + while (true) { showBoard(); + if (doesUserWinTheGame()) { System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); break; @@ -33,6 +34,7 @@ public static void main(String[] args) { System.out.println("지뢰를 밟았습니다. GAME OVER!"); break; } + String cellInput = getCellInputFromUser(); String userActionInput = getUserActionInputFromUser(); actOnCell(cellInput, userActionInput); @@ -42,17 +44,20 @@ public static void main(String[] args) { private static void actOnCell(String cellInput, String userActionInput) { int selectedColumnIndex = getSelectedColIndex(cellInput); int selectedRowIndex = getSelectedRowIndex(cellInput); + if (doesUserChooseToPlantFlag(userActionInput)) { BOARD[selectedRowIndex][selectedColumnIndex] = FLAG_SIGN; checkIfGameIsOver(); return; } + if (doesUserChooseToOpenCell(userActionInput)) { if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { BOARD[selectedRowIndex][selectedColumnIndex] = LAND_MINE_SIGN; changeGameStatusToLose(); return; } + open(selectedRowIndex, selectedColumnIndex); checkIfGameIsOver(); return; @@ -165,11 +170,13 @@ private static void initializeGame() { BOARD[row][col] = CLOSED_CELL_SIGN; } } + for (int i = 0; i < LAND_MINE_COUNT; i++) { int col = new Random().nextInt(10); int row = new Random().nextInt(8); LAND_MINES[row][col] = true; } + for (int row = 0; row < BOARD_ROW_SIZE; row++) { for (int col = 0; col < BOARD_COL_SIZE; col++) { int count = 0; From a04e022aa29d2b9c9a8f44f53aab9de19131fc86 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Sat, 5 Oct 2024 01:01:22 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat(=EB=85=BC=EB=A6=AC,-=EC=82=AC?= =?UTF-8?q?=EA=B3=A0=EC=9D=98-=ED=9D=90=EB=A6=84):=20[=EB=85=BC=EB=A6=AC,?= =?UTF-8?q?=20=EC=82=AC=EA=B3=A0=EC=9D=98=20=ED=9D=90=EB=A6=84]=20?= =?UTF-8?q?=EB=B6=80=EC=A0=95=EC=96=B4=EB=A5=BC=20=EB=8C=80=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=9E=90=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/MinesweeperGame.java | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 082803a80..94afbf3a7 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -179,40 +179,45 @@ private static void initializeGame() { for (int row = 0; row < BOARD_ROW_SIZE; row++) { for (int col = 0; col < BOARD_COL_SIZE; col++) { - int count = 0; - if (!isLandMineCell(row, col)) { - if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { - count++; - } - if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { - count++; - } - if (row - 1 >= 0 && col + 1 < BOARD_COL_SIZE && isLandMineCell(row - 1, col + 1)) { - count++; - } - if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { - count++; - } - if (col + 1 < BOARD_COL_SIZE && isLandMineCell(row, col + 1)) { - count++; - } - if (row + 1 < BOARD_ROW_SIZE && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { - count++; - } - if (row + 1 < BOARD_ROW_SIZE && isLandMineCell(row + 1, col)) { - count++; - } - if (row + 1 < BOARD_ROW_SIZE && col + 1 < BOARD_COL_SIZE && isLandMineCell(row + 1, col + 1)) { - count++; - } - NEARBY_LAND_MINE_COUNTS[row][col] = count; + if (isLandMineCell(row, col)) { + NEARBY_LAND_MINE_COUNTS[row][col] = 0; continue; } - NEARBY_LAND_MINE_COUNTS[row][col] = 0; + int count = countNearByLandMines(row, col); + NEARBY_LAND_MINE_COUNTS[row][col] = count; } } } + private static int countNearByLandMines(int row, int col) { + int count = 0; + if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { + count++; + } + if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { + count++; + } + if (row - 1 >= 0 && col + 1 < BOARD_COL_SIZE && isLandMineCell(row - 1, col + 1)) { + count++; + } + if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { + count++; + } + if (col + 1 < BOARD_COL_SIZE && isLandMineCell(row, col + 1)) { + count++; + } + if (row + 1 < BOARD_ROW_SIZE && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { + count++; + } + if (row + 1 < BOARD_ROW_SIZE && isLandMineCell(row + 1, col)) { + count++; + } + if (row + 1 < BOARD_ROW_SIZE && col + 1 < BOARD_COL_SIZE && isLandMineCell(row + 1, col + 1)) { + count++; + } + return count; + } + private static void showGameStartComments() { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println("지뢰찾기 게임 시작!"); From e5dd92cb8267fca18925c7f4882cc5e0a8feb181 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Sat, 5 Oct 2024 03:00:28 +0900 Subject: [PATCH 09/18] =?UTF-8?q?feat(=EB=85=BC=EB=A6=AC,-=EC=82=AC?= =?UTF-8?q?=EA=B3=A0=EC=9D=98-=ED=9D=90=EB=A6=84):=20[=EB=85=BC=EB=A6=AC,?= =?UTF-8?q?=20=EC=82=AC=EA=B3=A0=EC=9D=98=20=ED=9D=90=EB=A6=84]=20?= =?UTF-8?q?=ED=95=B4=ED=94=BC=20=EC=BC=80=EC=9D=B4=EC=8A=A4=EC=99=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/AppException.java | 7 +++ .../minesweeper/tobe/MinesweeperGame.java | 50 +++++++++++-------- 2 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 src/main/java/cleancode/minesweeper/tobe/AppException.java diff --git a/src/main/java/cleancode/minesweeper/tobe/AppException.java b/src/main/java/cleancode/minesweeper/tobe/AppException.java new file mode 100644 index 000000000..d06014edb --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/AppException.java @@ -0,0 +1,7 @@ +package cleancode.minesweeper.tobe; + +public class AppException extends RuntimeException { + public AppException(String message) { + super(message); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 94afbf3a7..346d5916b 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -14,30 +14,35 @@ public class MinesweeperGame { public static final String LAND_MINE_SIGN = "☼"; public static final String CLOSED_CELL_SIGN = "□"; public static final String OPENED_CELL_SIGN = "■"; - private static String[][] BOARD = new String[BOARD_ROW_SIZE][BOARD_COL_SIZE]; - private static Integer[][] NEARBY_LAND_MINE_COUNTS = new Integer[BOARD_ROW_SIZE][BOARD_COL_SIZE]; - private static boolean[][] LAND_MINES = new boolean[BOARD_ROW_SIZE][BOARD_COL_SIZE]; + private static final String[][] BOARD = new String[BOARD_ROW_SIZE][BOARD_COL_SIZE]; + private static final Integer[][] NEARBY_LAND_MINE_COUNTS = new Integer[BOARD_ROW_SIZE][BOARD_COL_SIZE]; + private static final boolean[][] LAND_MINES = new boolean[BOARD_ROW_SIZE][BOARD_COL_SIZE]; + private static int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 public static void main(String[] args) { showGameStartComments(); initializeGame(); - while (true) { - showBoard(); + try { + showBoard(); - if (doesUserWinTheGame()) { - System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); - break; - } - if (doesUserLoseTheGame()) { - System.out.println("지뢰를 밟았습니다. GAME OVER!"); - break; + if (doesUserWinTheGame()) { + System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); + break; + } + if (doesUserLoseTheGame()) { + System.out.println("지뢰를 밟았습니다. GAME OVER!"); + break; + } + String cellInput = getCellInputFromUser(); + String userActionInput = getUserActionInputFromUser(); + actOnCell(cellInput, userActionInput); + } catch (AppException e) { + System.out.println(e.getMessage()); + } catch (Exception e) { + System.out.println("프로그램에 문제가 생겼습니다."); } - - String cellInput = getCellInputFromUser(); - String userActionInput = getUserActionInputFromUser(); - actOnCell(cellInput, userActionInput); } } @@ -62,7 +67,7 @@ private static void actOnCell(String cellInput, String userActionInput) { checkIfGameIsOver(); return; } - System.out.println("잘못된 번호를 선택하셨습니다."); + throw new AppException("잘못된 번호를 선택하셨습니다."); } private static void changeGameStatusToLose() { @@ -119,11 +124,16 @@ private static void checkIfGameIsOver() { private static boolean isAllCellOpened() { return Arrays.stream(BOARD) .flatMap(Arrays::stream) - .noneMatch(cell -> cell.equals(CLOSED_CELL_SIGN)); + .noneMatch(CLOSED_CELL_SIGN::equals); } private static int convertRowFrom(char cellInputRow) { - return Character.getNumericValue(cellInputRow) - 1; + int rowIndex = Character.getNumericValue(cellInputRow) - 1; + if (rowIndex >= BOARD_ROW_SIZE) { + throw new AppException("잘못된 입력입니다."); + } + + return rowIndex; } private static int convertColFrom(char cellInputCol) { @@ -149,7 +159,7 @@ private static int convertColFrom(char cellInputCol) { case 'j': return 9; default: - return -1; + throw new AppException("잘못된 입력입니다."); } } From c4cc31089f912e55ee979d0cd71b859be19f6ee1 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Tue, 15 Oct 2024 03:24:41 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=ED=8C=A8=EB=9F=AC=EB=8B=A4=EC=9E=84):=20[=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=ED=96=A5=20=ED=8C=A8=EB=9F=AC=EB=8B=A4?= =?UTF-8?q?=EC=9E=84]=20=EA=B0=9D=EC=B2=B4=20=EC=84=A4=EA=B3=84=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cleancode/minesweeper/tobe/Cell.java | 85 +++++++++++++++++++ .../minesweeper/tobe/MinesweeperGame.java | 51 ++++++----- 2 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 src/main/java/cleancode/minesweeper/tobe/Cell.java diff --git a/src/main/java/cleancode/minesweeper/tobe/Cell.java b/src/main/java/cleancode/minesweeper/tobe/Cell.java new file mode 100644 index 000000000..381356beb --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/Cell.java @@ -0,0 +1,85 @@ +package cleancode.minesweeper.tobe; + +public class Cell { + + private static final String FLAG_SIGN = "⚑"; + private static final String LAND_MINE_SIGN = "☼"; + private static final String UNCHECKED_SIGN = "□"; + private static final String EMPTY_SIGN = "■"; + + private int nearbyLandMineCount; + private boolean isLandMine; + private boolean isFlagged; + private boolean isOpened; + + // Cell 이 가진 속성: 근처 지뢰 갯수, 지뢰 여부 + // Cell 의 상태: 깃발 유무, 열렸다/닫혔다, 사용자가 확인함 + + private Cell(int nearbyLandMineCount, boolean isLandMine, boolean isFlagged, boolean isOpened) { + this.nearbyLandMineCount = nearbyLandMineCount; + this.isLandMine = isLandMine; + this.isFlagged = isFlagged; + this.isOpened = isOpened; + } + + // 정적 팩토리 메서드를 좋아하는 이유: 메서드에 이름을 줄 수 있다. + // 정적 팩토리 메서드가 여러개가 된다면 그에 맞는 다른 이름들을 지어줄 수도 있고, 검증과 같은 로직을 추가할 수도 있다. + // 생성자 하나인 객체라도 정적 팩토리 메서드를 만들어서 생성자를 대체해보자. + public static Cell of(int nearbyLandMineCount, boolean isLandMine, boolean isFlagged, boolean isOpened) { + return new Cell(nearbyLandMineCount, isLandMine, isFlagged, isOpened); + } + + public static Cell create() { + return of(0, false, false, false); + } + + public void turnOnLandMine() { + this.isLandMine = true; + } + + public void updateNearbyLandMineCount(int count) { + this.nearbyLandMineCount = count; + } + + public void flag() { + this.isFlagged = true; + } + + public void open() { + this.isOpened = true; + } + + public boolean isChecked() { + return isFlagged || isOpened; + } + + public boolean isLandMine() { + return isLandMine; + } + + public boolean isOpened() { + return isOpened; + } + + public boolean hasLandMineCount() { + return this.nearbyLandMineCount != 0; + } + + public String getSign() { + if (isOpened) { + if (isLandMine) { + return LAND_MINE_SIGN; + } + if (hasLandMineCount()) { + return String.valueOf(nearbyLandMineCount); + } + return EMPTY_SIGN; + } + + if (isFlagged) { + return FLAG_SIGN; + } + + return UNCHECKED_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java index 346d5916b..656c71316 100644 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ b/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java @@ -10,14 +10,7 @@ public class MinesweeperGame { public static final int BOARD_ROW_SIZE = 8; public static final int BOARD_COL_SIZE = 10; public static final int LAND_MINE_COUNT = 10; - public static final String FLAG_SIGN = "⚑"; - public static final String LAND_MINE_SIGN = "☼"; - public static final String CLOSED_CELL_SIGN = "□"; - public static final String OPENED_CELL_SIGN = "■"; - private static final String[][] BOARD = new String[BOARD_ROW_SIZE][BOARD_COL_SIZE]; - private static final Integer[][] NEARBY_LAND_MINE_COUNTS = new Integer[BOARD_ROW_SIZE][BOARD_COL_SIZE]; - private static final boolean[][] LAND_MINES = new boolean[BOARD_ROW_SIZE][BOARD_COL_SIZE]; - + private static final Cell[][] BOARD = new Cell[BOARD_ROW_SIZE][BOARD_COL_SIZE]; private static int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 public static void main(String[] args) { @@ -51,14 +44,14 @@ private static void actOnCell(String cellInput, String userActionInput) { int selectedRowIndex = getSelectedRowIndex(cellInput); if (doesUserChooseToPlantFlag(userActionInput)) { - BOARD[selectedRowIndex][selectedColumnIndex] = FLAG_SIGN; + BOARD[selectedRowIndex][selectedColumnIndex].flag(); checkIfGameIsOver(); return; } if (doesUserChooseToOpenCell(userActionInput)) { if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { - BOARD[selectedRowIndex][selectedColumnIndex] = LAND_MINE_SIGN; + BOARD[selectedRowIndex][selectedColumnIndex].open(); changeGameStatusToLose(); return; } @@ -75,7 +68,7 @@ private static void changeGameStatusToLose() { } private static boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { - return LAND_MINES[selectedRowIndex][selectedColumnIndex]; + return BOARD[selectedRowIndex][selectedColumnIndex].isLandMine(); } private static boolean doesUserChooseToOpenCell(String userActionInput) { @@ -115,16 +108,22 @@ private static boolean doesUserWinTheGame() { } private static void checkIfGameIsOver() { - boolean isAllOpened = isAllCellOpened(); - if (isAllOpened) { - gameStatus = 1; + boolean isAllChecked = isAllCellChecked(); + if (isAllChecked) { + changeGameStatusToWin(); } } - private static boolean isAllCellOpened() { + private static void changeGameStatusToWin() { + gameStatus = 1; + } + + // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. + // 외부에서는 데이터를 모르니까 짐작해서 물어보는 것이 최선이다. + private static boolean isAllCellChecked() { return Arrays.stream(BOARD) .flatMap(Arrays::stream) - .noneMatch(CLOSED_CELL_SIGN::equals); + .allMatch(Cell::isChecked); } private static int convertRowFrom(char cellInputRow) { @@ -168,7 +167,7 @@ private static void showBoard() { for (int i = 0; i < BOARD_ROW_SIZE; i++) { System.out.printf("%d ", i + 1); for (int j = 0; j < BOARD_COL_SIZE; j++) { - System.out.print(BOARD[i][j] + " "); + System.out.print(BOARD[i][j].getSign() + " "); } System.out.println(); } @@ -177,24 +176,23 @@ private static void showBoard() { private static void initializeGame() { for (int row = 0; row < BOARD_ROW_SIZE; row++) { for (int col = 0; col < BOARD_COL_SIZE; col++) { - BOARD[row][col] = CLOSED_CELL_SIGN; + BOARD[row][col] = Cell.create(); } } for (int i = 0; i < LAND_MINE_COUNT; i++) { int col = new Random().nextInt(10); int row = new Random().nextInt(8); - LAND_MINES[row][col] = true; + BOARD[row][col].turnOnLandMine(); } for (int row = 0; row < BOARD_ROW_SIZE; row++) { for (int col = 0; col < BOARD_COL_SIZE; col++) { if (isLandMineCell(row, col)) { - NEARBY_LAND_MINE_COUNTS[row][col] = 0; continue; } int count = countNearByLandMines(row, col); - NEARBY_LAND_MINE_COUNTS[row][col] = count; + BOARD[row][col].updateNearbyLandMineCount(count); } } } @@ -238,18 +236,19 @@ private static void open(int row, int col) { if (row < 0 || row >= BOARD_ROW_SIZE || col < 0 || col >= BOARD_COL_SIZE) { return; } - if (!BOARD[row][col].equals(CLOSED_CELL_SIGN)) { + if (BOARD[row][col].isOpened()) { return; } if (isLandMineCell(row, col)) { return; } - if (NEARBY_LAND_MINE_COUNTS[row][col] != 0) { - BOARD[row][col] = String.valueOf(NEARBY_LAND_MINE_COUNTS[row][col]); + + BOARD[row][col].open(); + + if (BOARD[row][col].hasLandMineCount()) { return; - } else { - BOARD[row][col] = OPENED_CELL_SIGN; } + open(row - 1, col - 1); open(row - 1, col); open(row - 1, col + 1); From f40102a88a2f1ac1f065390c76b3bbfc7fc0cd8a Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Tue, 15 Oct 2024 18:42:39 +0900 Subject: [PATCH 11/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=ED=8C=A8=EB=9F=AC=EB=8B=A4=EC=9E=84):=20[=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=ED=96=A5=20=ED=8C=A8=EB=9F=AC=EB=8B=A4?= =?UTF-8?q?=EC=9E=84]=20SRP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/AppException.java | 7 - .../minesweeper/tobe/GameApplication.java | 12 + .../cleancode/minesweeper/tobe/GameBoard.java | 151 ++++++++++ .../minesweeper/tobe/GameException.java | 7 + .../minesweeper/tobe/Minesweeper.java | 165 +++++++++++ .../minesweeper/tobe/MinesweeperGame.java | 262 ------------------ .../tobe/io/ConsoleInputHandler.java | 11 + .../tobe/io/ConsoleOutputHandler.java | 48 ++++ 8 files changed, 394 insertions(+), 269 deletions(-) delete mode 100644 src/main/java/cleancode/minesweeper/tobe/AppException.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/GameApplication.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/GameBoard.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/GameException.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/Minesweeper.java delete mode 100644 src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java diff --git a/src/main/java/cleancode/minesweeper/tobe/AppException.java b/src/main/java/cleancode/minesweeper/tobe/AppException.java deleted file mode 100644 index d06014edb..000000000 --- a/src/main/java/cleancode/minesweeper/tobe/AppException.java +++ /dev/null @@ -1,7 +0,0 @@ -package cleancode.minesweeper.tobe; - -public class AppException extends RuntimeException { - public AppException(String message) { - super(message); - } -} 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..787a4ee1c --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java @@ -0,0 +1,12 @@ +package cleancode.minesweeper.tobe; + +public class GameApplication { + + // 이 클래스는 딱 프로그램 실행에 진입점만 가지게 된다. + // 이름도 MinesweeperGame 에서 GameApplication 으로 변경한다. -> 이렇게 변경하면 지뢰찾기게임(Minesweeper 뿐만이 아닌 다른 게임도 실행할 수 있게 된다.) + // 게임 실행에 대한 책임과 지뢰찾기 도메인 자체, 지뢰찾기 게임을 담당하는 역할을 분리했다. + public static void main(String[] args) { + Minesweeper minesweeper = new Minesweeper(); + minesweeper.run(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java new file mode 100644 index 000000000..22c420a47 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java @@ -0,0 +1,151 @@ +package cleancode.minesweeper.tobe; + +import java.util.Arrays; +import java.util.Random; + +public class GameBoard { + + private static final int LAND_MINE_COUNT = 10; + + private final Cell[][] board; + + public GameBoard(int rowSize, int colSize) { + board = new Cell[rowSize][colSize]; + } + + public void flag(int rowIndex, int colIndex) { + Cell cell = findCell(rowIndex, colIndex); + cell.flag(); + } + + public void open(int rowIndex, int colIndex) { + Cell cell = findCell(rowIndex, colIndex); + cell.open(); + } + + public boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { + Cell cell = findCell(selectedRowIndex, selectedColumnIndex); + return cell.isLandMine(); + } + + // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. + // 외부에서는 데이터를 모르니까 짐작해서 물어보는 것이 최선이다. + public boolean isAllCellChecked() { + return Arrays.stream(board) + .flatMap(Arrays::stream) + .allMatch(Cell::isChecked); + } + + public void openSurroundedCells(int row, int col) { + if (row < 0 || row >= getRowSize() || col < 0 || col >= getColSize()) { + return; + } + if (isOpenedCell(row, col)) { + return; + } + if (isLandMineCell(row, col)) { + return; + } + + open(row, col); + + if (doesCellHaveLandMineCount(row, col)) { + return; + } + + openSurroundedCells(row - 1, col - 1); + openSurroundedCells(row - 1, col); + openSurroundedCells(row - 1, col + 1); + openSurroundedCells(row, col - 1); + openSurroundedCells(row, col + 1); + openSurroundedCells(row + 1, col - 1); + openSurroundedCells(row + 1, col); + openSurroundedCells(row + 1, col + 1); + } + + private boolean doesCellHaveLandMineCount(int row, int col) { + return findCell(row, col).hasLandMineCount(); + } + + private boolean isOpenedCell(int row, int col) { + return findCell(row, col).isOpened(); + } + + public void initializeGame() { + int rowSize = getRowSize(); + int colSize = getColSize(); + + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + board[row][col] = Cell.create(); + } + } + + for (int i = 0; i < LAND_MINE_COUNT; i++) { + int landMineCol = new Random().nextInt(10); + int landMineRow = new Random().nextInt(8); + Cell landMineCell = findCell(landMineRow, landMineCol); + landMineCell.turnOnLandMine(); + } + + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (isLandMineCell(row, col)) { + continue; + } + int count = countNearByLandMines(row, col); + Cell cell = findCell(row, col); + cell.updateNearbyLandMineCount(count); + } + } + } + + public String getSign(int rowIndex, int colIndex) { + Cell cell = findCell(rowIndex, colIndex); + return cell.getSign(); + } + + private Cell findCell(int rowIndex, int colIndex) { + return board[rowIndex][colIndex]; + } + + public int getRowSize() { + return board.length; + } + + public int getColSize() { + return board[0].length; + } + + private int countNearByLandMines(int row, int col) { + int rowSize = getRowSize(); + int colSize = getColSize(); + int count = 0; + + if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { + count++; + } + if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { + count++; + } + if (row - 1 >= 0 && col + 1 < colSize && isLandMineCell(row - 1, col + 1)) { + count++; + } + if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { + count++; + } + if (col + 1 < colSize && isLandMineCell(row, col + 1)) { + count++; + } + if (row + 1 < rowSize && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { + count++; + } + if (row + 1 < rowSize && isLandMineCell(row + 1, col)) { + count++; + } + if (row + 1 < rowSize && col + 1 < colSize && isLandMineCell(row + 1, col + 1)) { + count++; + } + return count; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/GameException.java b/src/main/java/cleancode/minesweeper/tobe/GameException.java new file mode 100644 index 000000000..2b9bae5b8 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/GameException.java @@ -0,0 +1,7 @@ +package cleancode.minesweeper.tobe; + +public class GameException extends RuntimeException { + public GameException(String message) { + super(message); + } +} 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..f6857c114 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -0,0 +1,165 @@ +package cleancode.minesweeper.tobe; + +import cleancode.minesweeper.tobe.io.ConsoleInputHandler; +import cleancode.minesweeper.tobe.io.ConsoleOutputHandler; + +import java.util.Arrays; +import java.util.Random; +import java.util.Scanner; + +public class Minesweeper { + public static final Scanner SCANNER = new Scanner(System.in); + public static final int BOARD_ROW_SIZE = 8; + public static final int BOARD_COL_SIZE = 10; + public static final int LAND_MINE_COUNT = 10; + + // BOARD 도 하는 일이 너무 많고 중요하기 때문에 Minesweeper 클래스 내부에 상수로 두기에는 너무 책임이 과도하다. + // 이렇게 GameBoard 클래스를 두면 Minesweeper 입장에서는 Cell[][] 이중배열에 대해서는 모른다. + // 객체로 추상화가 되었고, 데이터 구조에 대한 것은 캐슐화가 되었기 때문이다. + private static final GameBoard gameBoard = new GameBoard(BOARD_ROW_SIZE, BOARD_COL_SIZE); + + // 게임이 진행되는 핵심 로직들과 사용자 입출력에 대한 로직 책임을 분리한다. + private final ConsoleInputHandler consoleInputHandler = new ConsoleInputHandler(); + private final ConsoleOutputHandler consoleOutputHandler = new ConsoleOutputHandler(); + private int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 + + public void run() { + consoleOutputHandler.showGameStartComments(); + gameBoard.initializeGame(); + while (true) { + try { + consoleOutputHandler.showBoard(gameBoard); + + if (doesUserWinTheGame()) { + consoleOutputHandler.printGameWinningComment(); + break; + } + if (doesUserLoseTheGame()) { + consoleOutputHandler.printGameLosingComment(); + break; + } + String cellInput = getCellInputFromUser(); + String userActionInput = getUserActionInputFromUser(); + actOnCell(cellInput, userActionInput); + } catch (GameException e) { + // print 할 때 AppException 에서 어떤걸 꺼내서 쓸지는 내부에서 알아서 결정할 것이고, + // 예외 상황(exception 에 대한 메시지)에 대한 메시지를 출력하겠다는 이 메서드명을 봤을 때 + // 파라미터는 exception 자체를 넣어주는 것이 더 자연스럽지 않을까 한다. + consoleOutputHandler.printExceptionMessage(e); + } catch (Exception e) { + consoleOutputHandler.printSimpleMessage("프로그램에 문제가 생겼습니다."); + } + } + } + + private void actOnCell(String cellInput, String userActionInput) { + int selectedColumnIndex = getSelectedColIndex(cellInput); + int selectedRowIndex = getSelectedRowIndex(cellInput); + + if (doesUserChooseToPlantFlag(userActionInput)) { + gameBoard.flag(selectedRowIndex, selectedColumnIndex); + checkIfGameIsOver(); + return; + } + + if (doesUserChooseToOpenCell(userActionInput)) { + if (gameBoard.isLandMineCell(selectedRowIndex, selectedColumnIndex)) { + gameBoard.open(selectedRowIndex, selectedColumnIndex); + changeGameStatusToLose(); + return; + } + + gameBoard.openSurroundedCells(selectedRowIndex, selectedColumnIndex); + checkIfGameIsOver(); + return; + } + throw new GameException("잘못된 번호를 선택하셨습니다."); + } + + private void changeGameStatusToLose() { + gameStatus = -1; + } + + private boolean doesUserChooseToOpenCell(String userActionInput) { + return userActionInput.equals("1"); + } + + private boolean doesUserChooseToPlantFlag(String userActionInput) { + return userActionInput.equals("2"); + } + + private int getSelectedRowIndex(String cellInput) { + char cellInputRow = cellInput.charAt(1); + return convertRowFrom(cellInputRow); + } + + private int getSelectedColIndex(String cellInput) { + char cellInputCol = cellInput.charAt(0); + return convertColFrom(cellInputCol); + } + + private String getUserActionInputFromUser() { + consoleOutputHandler.printCommentForUserAction(); + return consoleInputHandler.getUserInput(); + } + + private String getCellInputFromUser() { + consoleOutputHandler.printCommentForSelectingCell(); + return consoleInputHandler.getUserInput(); + } + + private boolean doesUserLoseTheGame() { + return gameStatus == -1; + } + + private boolean doesUserWinTheGame() { + return gameStatus == 1; + } + + private void checkIfGameIsOver() { + if (gameBoard.isAllCellChecked()) { + changeGameStatusToWin(); + } + } + + private void changeGameStatusToWin() { + gameStatus = 1; + } + + + private int convertRowFrom(char cellInputRow) { + int rowIndex = Character.getNumericValue(cellInputRow) - 1; + if (rowIndex >= BOARD_ROW_SIZE) { + throw new GameException("잘못된 입력입니다."); + } + + return rowIndex; + } + + private int convertColFrom(char cellInputCol) { + switch (cellInputCol) { + case 'a': + return 0; + case 'b': + return 1; + case 'c': + return 2; + case 'd': + return 3; + case 'e': + return 4; + case 'f': + return 5; + case 'g': + return 6; + case 'h': + return 7; + case 'i': + return 8; + case 'j': + return 9; + default: + throw new GameException("잘못된 입력입니다."); + } + } +} 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 656c71316..000000000 --- a/src/main/java/cleancode/minesweeper/tobe/MinesweeperGame.java +++ /dev/null @@ -1,262 +0,0 @@ -package cleancode.minesweeper.tobe; - -import java.util.Arrays; -import java.util.Random; -import java.util.Scanner; - -public class MinesweeperGame { - - public static final Scanner SCANNER = new Scanner(System.in); - public static final int BOARD_ROW_SIZE = 8; - public static final int BOARD_COL_SIZE = 10; - public static final int LAND_MINE_COUNT = 10; - private static final Cell[][] BOARD = new Cell[BOARD_ROW_SIZE][BOARD_COL_SIZE]; - private static int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 - - public static void main(String[] args) { - showGameStartComments(); - initializeGame(); - while (true) { - try { - showBoard(); - - if (doesUserWinTheGame()) { - System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); - break; - } - if (doesUserLoseTheGame()) { - System.out.println("지뢰를 밟았습니다. GAME OVER!"); - break; - } - String cellInput = getCellInputFromUser(); - String userActionInput = getUserActionInputFromUser(); - actOnCell(cellInput, userActionInput); - } catch (AppException e) { - System.out.println(e.getMessage()); - } catch (Exception e) { - System.out.println("프로그램에 문제가 생겼습니다."); - } - } - } - - private static void actOnCell(String cellInput, String userActionInput) { - int selectedColumnIndex = getSelectedColIndex(cellInput); - int selectedRowIndex = getSelectedRowIndex(cellInput); - - if (doesUserChooseToPlantFlag(userActionInput)) { - BOARD[selectedRowIndex][selectedColumnIndex].flag(); - checkIfGameIsOver(); - return; - } - - if (doesUserChooseToOpenCell(userActionInput)) { - if (isLandMineCell(selectedRowIndex, selectedColumnIndex)) { - BOARD[selectedRowIndex][selectedColumnIndex].open(); - changeGameStatusToLose(); - return; - } - - open(selectedRowIndex, selectedColumnIndex); - checkIfGameIsOver(); - return; - } - throw new AppException("잘못된 번호를 선택하셨습니다."); - } - - private static void changeGameStatusToLose() { - gameStatus = -1; - } - - private static boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { - return BOARD[selectedRowIndex][selectedColumnIndex].isLandMine(); - } - - private static boolean doesUserChooseToOpenCell(String userActionInput) { - return userActionInput.equals("1"); - } - - private static boolean doesUserChooseToPlantFlag(String userActionInput) { - return userActionInput.equals("2"); - } - - private static int getSelectedRowIndex(String cellInput) { - char cellInputRow = cellInput.charAt(1); - return convertRowFrom(cellInputRow); - } - - private static int getSelectedColIndex(String cellInput) { - char cellInputCol = cellInput.charAt(0); - return convertColFrom(cellInputCol); - } - - private static String getUserActionInputFromUser() { - System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); - return SCANNER.nextLine(); - } - - private static String getCellInputFromUser() { - System.out.println("선택할 좌표를 입력하세요. (예: a1)"); - return SCANNER.nextLine(); - } - - private static boolean doesUserLoseTheGame() { - return gameStatus == -1; - } - - private static boolean doesUserWinTheGame() { - return gameStatus == 1; - } - - private static void checkIfGameIsOver() { - boolean isAllChecked = isAllCellChecked(); - if (isAllChecked) { - changeGameStatusToWin(); - } - } - - private static void changeGameStatusToWin() { - gameStatus = 1; - } - - // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. - // 외부에서는 데이터를 모르니까 짐작해서 물어보는 것이 최선이다. - private static boolean isAllCellChecked() { - return Arrays.stream(BOARD) - .flatMap(Arrays::stream) - .allMatch(Cell::isChecked); - } - - private static int convertRowFrom(char cellInputRow) { - int rowIndex = Character.getNumericValue(cellInputRow) - 1; - if (rowIndex >= BOARD_ROW_SIZE) { - throw new AppException("잘못된 입력입니다."); - } - - return rowIndex; - } - - private static int convertColFrom(char cellInputCol) { - switch (cellInputCol) { - case 'a': - return 0; - case 'b': - return 1; - case 'c': - return 2; - case 'd': - return 3; - case 'e': - return 4; - case 'f': - return 5; - case 'g': - return 6; - case 'h': - return 7; - case 'i': - return 8; - case 'j': - return 9; - default: - throw new AppException("잘못된 입력입니다."); - } - } - - private static void showBoard() { - System.out.println(" a b c d e f g h i j"); - for (int i = 0; i < BOARD_ROW_SIZE; i++) { - System.out.printf("%d ", i + 1); - for (int j = 0; j < BOARD_COL_SIZE; j++) { - System.out.print(BOARD[i][j].getSign() + " "); - } - System.out.println(); - } - } - - private static void initializeGame() { - for (int row = 0; row < BOARD_ROW_SIZE; row++) { - for (int col = 0; col < BOARD_COL_SIZE; col++) { - BOARD[row][col] = Cell.create(); - } - } - - for (int i = 0; i < LAND_MINE_COUNT; i++) { - int col = new Random().nextInt(10); - int row = new Random().nextInt(8); - BOARD[row][col].turnOnLandMine(); - } - - for (int row = 0; row < BOARD_ROW_SIZE; row++) { - for (int col = 0; col < BOARD_COL_SIZE; col++) { - if (isLandMineCell(row, col)) { - continue; - } - int count = countNearByLandMines(row, col); - BOARD[row][col].updateNearbyLandMineCount(count); - } - } - } - - private static int countNearByLandMines(int row, int col) { - int count = 0; - if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { - count++; - } - if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { - count++; - } - if (row - 1 >= 0 && col + 1 < BOARD_COL_SIZE && isLandMineCell(row - 1, col + 1)) { - count++; - } - if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { - count++; - } - if (col + 1 < BOARD_COL_SIZE && isLandMineCell(row, col + 1)) { - count++; - } - if (row + 1 < BOARD_ROW_SIZE && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { - count++; - } - if (row + 1 < BOARD_ROW_SIZE && isLandMineCell(row + 1, col)) { - count++; - } - if (row + 1 < BOARD_ROW_SIZE && col + 1 < BOARD_COL_SIZE && isLandMineCell(row + 1, col + 1)) { - count++; - } - return count; - } - - private static void showGameStartComments() { - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - System.out.println("지뢰찾기 게임 시작!"); - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - } - - private static void open(int row, int col) { - if (row < 0 || row >= BOARD_ROW_SIZE || col < 0 || col >= BOARD_COL_SIZE) { - return; - } - if (BOARD[row][col].isOpened()) { - return; - } - if (isLandMineCell(row, col)) { - return; - } - - BOARD[row][col].open(); - - if (BOARD[row][col].hasLandMineCount()) { - return; - } - - 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/io/ConsoleInputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java new file mode 100644 index 000000000..a36a5476c --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java @@ -0,0 +1,11 @@ +package cleancode.minesweeper.tobe.io; + +import java.util.Scanner; + +public class ConsoleInputHandler { + public static final Scanner SCANNER = new Scanner(System.in); + + public String getUserInput() { + return SCANNER.nextLine(); + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java new file mode 100644 index 000000000..7fd39b685 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java @@ -0,0 +1,48 @@ +package cleancode.minesweeper.tobe.io; + +import cleancode.minesweeper.tobe.GameBoard; +import cleancode.minesweeper.tobe.GameException; +import cleancode.minesweeper.tobe.Cell; + +public class ConsoleOutputHandler { + public void showGameStartComments() { + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + System.out.println("지뢰찾기 게임 시작!"); + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + } + + public void showBoard(GameBoard board) { + System.out.println(" a b c d e f g h i j"); + for (int row = 0; row < board.getRowSize(); row++) { + System.out.printf("%d ", row + 1); + for (int col = 0; col < board.getColSize(); col++) { + System.out.print(board.getSign(row, col) + " "); + } + System.out.println(); + } + } + + public void printGameWinningComment() { + System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); + } + + public void printGameLosingComment() { + System.out.println("지뢰를 밟았습니다. GAME OVER!"); + } + + public void printCommentForSelectingCell() { + System.out.println("선택할 좌표를 입력하세요. (예: a1)"); + } + + public void printCommentForUserAction() { + System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); + } + + public void printExceptionMessage(GameException e) { + System.out.println(e.getMessage()); + } + + public void printSimpleMessage(String message) { + System.out.println(message); + } +} From 3e7ba5fb3b1a4d143c27e1b7508bd45e4f18414d Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Wed, 16 Oct 2024 03:22:37 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=ED=8C=A8=EB=9F=AC=EB=8B=A4=EC=9E=84):=20[=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=ED=96=A5=20=ED=8C=A8=EB=9F=AC=EB=8B=A4?= =?UTF-8?q?=EC=9E=84]=20OCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/BoardIndexConverter.java | 34 ++++++++++ .../minesweeper/tobe/GameApplication.java | 7 +- .../cleancode/minesweeper/tobe/GameBoard.java | 12 ++-- .../minesweeper/tobe/Minesweeper.java | 64 +++---------------- .../minesweeper/tobe/gamelevel/Advanced.java | 18 ++++++ .../minesweeper/tobe/gamelevel/Beginner.java | 18 ++++++ .../minesweeper/tobe/gamelevel/GameLevel.java | 11 ++++ .../minesweeper/tobe/gamelevel/Middle.java | 18 ++++++ .../tobe/gamelevel/VeryBeginner.java | 20 ++++++ .../tobe/io/ConsoleOutputHandler.java | 18 +++++- 10 files changed, 158 insertions(+), 62 deletions(-) create mode 100644 src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/gamelevel/Advanced.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/gamelevel/Beginner.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/gamelevel/GameLevel.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/gamelevel/Middle.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/gamelevel/VeryBeginner.java diff --git a/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java b/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java new file mode 100644 index 000000000..88f5c72bb --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java @@ -0,0 +1,34 @@ +package cleancode.minesweeper.tobe; + +public class BoardIndexConverter { + + private static final char BASE_CHAR_FOR_COL = 'a'; + + public int getSelectedRowIndex(String cellInput, int rowSize) { + String cellInputRow = cellInput.substring(1); + return convertRowFrom(cellInputRow, rowSize); + } + + public int getSelectedColIndex(String cellInput, int colSize) { + char cellInputCol = cellInput.charAt(0); + return convertColFrom(cellInputCol, colSize); + } + + private int convertRowFrom(String cellInputRow, int rowSize) { + int rowIndex = Integer.parseInt(cellInputRow) - 1; + if (rowIndex < 0 || rowIndex >= rowSize) { + throw new GameException("잘못된 입력입니다."); + } + + return rowIndex; + } + + private int convertColFrom(char cellInputCol, int colSize) { + int colIndex = cellInputCol - BASE_CHAR_FOR_COL; + if (colIndex < 0 || colIndex >= colSize) { + throw new GameException("잘못된 입력입니다."); + } + + return colIndex; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java index 787a4ee1c..6ea9f3d0c 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java @@ -1,12 +1,17 @@ package cleancode.minesweeper.tobe; +import cleancode.minesweeper.tobe.gamelevel.Advanced; +import cleancode.minesweeper.tobe.gamelevel.GameLevel; +import cleancode.minesweeper.tobe.gamelevel.Middle; + public class GameApplication { // 이 클래스는 딱 프로그램 실행에 진입점만 가지게 된다. // 이름도 MinesweeperGame 에서 GameApplication 으로 변경한다. -> 이렇게 변경하면 지뢰찾기게임(Minesweeper 뿐만이 아닌 다른 게임도 실행할 수 있게 된다.) // 게임 실행에 대한 책임과 지뢰찾기 도메인 자체, 지뢰찾기 게임을 담당하는 역할을 분리했다. public static void main(String[] args) { - Minesweeper minesweeper = new Minesweeper(); + GameLevel gameLevel = new Middle(); + Minesweeper minesweeper = new Minesweeper(gameLevel); minesweeper.run(); } } diff --git a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java index 22c420a47..ab37af9b3 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java @@ -1,16 +1,20 @@ package cleancode.minesweeper.tobe; +import cleancode.minesweeper.tobe.gamelevel.GameLevel; + import java.util.Arrays; import java.util.Random; public class GameBoard { - private static final int LAND_MINE_COUNT = 10; - private final Cell[][] board; + private final int landMineCount; - public GameBoard(int rowSize, int colSize) { + public GameBoard(GameLevel gameLevel) { + int rowSize = gameLevel.getRowSize(); + int colSize = gameLevel.getColSize(); board = new Cell[rowSize][colSize]; + landMineCount = gameLevel.getLandMineCount(); } public void flag(int rowIndex, int colIndex) { @@ -81,7 +85,7 @@ public void initializeGame() { } } - for (int i = 0; i < LAND_MINE_COUNT; i++) { + for (int i = 0; i < landMineCount; i++) { int landMineCol = new Random().nextInt(10); int landMineRow = new Random().nextInt(8); Cell landMineCell = findCell(landMineRow, landMineCol); diff --git a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java index f6857c114..79c72b1a6 100644 --- a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -1,5 +1,6 @@ package cleancode.minesweeper.tobe; +import cleancode.minesweeper.tobe.gamelevel.GameLevel; import cleancode.minesweeper.tobe.io.ConsoleInputHandler; import cleancode.minesweeper.tobe.io.ConsoleOutputHandler; @@ -8,21 +9,23 @@ import java.util.Scanner; public class Minesweeper { - public static final Scanner SCANNER = new Scanner(System.in); - public static final int BOARD_ROW_SIZE = 8; - public static final int BOARD_COL_SIZE = 10; - public static final int LAND_MINE_COUNT = 10; // BOARD 도 하는 일이 너무 많고 중요하기 때문에 Minesweeper 클래스 내부에 상수로 두기에는 너무 책임이 과도하다. // 이렇게 GameBoard 클래스를 두면 Minesweeper 입장에서는 Cell[][] 이중배열에 대해서는 모른다. // 객체로 추상화가 되었고, 데이터 구조에 대한 것은 캐슐화가 되었기 때문이다. - private static final GameBoard gameBoard = new GameBoard(BOARD_ROW_SIZE, BOARD_COL_SIZE); + private final GameBoard gameBoard; + // SRP: cellInput 이라는 사용자의 입력을 받아서 rowIndex, colIndex 로 변환하는 역할을 하는 또 하나의 클래스로 볼 수 있지 않을까? + private final BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); // 게임이 진행되는 핵심 로직들과 사용자 입출력에 대한 로직 책임을 분리한다. private final ConsoleInputHandler consoleInputHandler = new ConsoleInputHandler(); private final ConsoleOutputHandler consoleOutputHandler = new ConsoleOutputHandler(); private int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 + public Minesweeper(GameLevel gameLevel) { + gameBoard = new GameBoard(gameLevel); + } + public void run() { consoleOutputHandler.showGameStartComments(); gameBoard.initializeGame(); @@ -53,8 +56,8 @@ public void run() { } private void actOnCell(String cellInput, String userActionInput) { - int selectedColumnIndex = getSelectedColIndex(cellInput); - int selectedRowIndex = getSelectedRowIndex(cellInput); + int selectedColumnIndex = boardIndexConverter.getSelectedColIndex(cellInput, gameBoard.getColSize()); + int selectedRowIndex = boardIndexConverter.getSelectedRowIndex(cellInput, gameBoard.getRowSize()); if (doesUserChooseToPlantFlag(userActionInput)) { gameBoard.flag(selectedRowIndex, selectedColumnIndex); @@ -88,16 +91,6 @@ private boolean doesUserChooseToPlantFlag(String userActionInput) { return userActionInput.equals("2"); } - private int getSelectedRowIndex(String cellInput) { - char cellInputRow = cellInput.charAt(1); - return convertRowFrom(cellInputRow); - } - - private int getSelectedColIndex(String cellInput) { - char cellInputCol = cellInput.charAt(0); - return convertColFrom(cellInputCol); - } - private String getUserActionInputFromUser() { consoleOutputHandler.printCommentForUserAction(); return consoleInputHandler.getUserInput(); @@ -125,41 +118,4 @@ private void checkIfGameIsOver() { private void changeGameStatusToWin() { gameStatus = 1; } - - - private int convertRowFrom(char cellInputRow) { - int rowIndex = Character.getNumericValue(cellInputRow) - 1; - if (rowIndex >= BOARD_ROW_SIZE) { - throw new GameException("잘못된 입력입니다."); - } - - return rowIndex; - } - - private int convertColFrom(char cellInputCol) { - switch (cellInputCol) { - case 'a': - return 0; - case 'b': - return 1; - case 'c': - return 2; - case 'd': - return 3; - case 'e': - return 4; - case 'f': - return 5; - case 'g': - return 6; - case 'h': - return 7; - case 'i': - return 8; - case 'j': - return 9; - default: - throw new GameException("잘못된 입력입니다."); - } - } } diff --git a/src/main/java/cleancode/minesweeper/tobe/gamelevel/Advanced.java b/src/main/java/cleancode/minesweeper/tobe/gamelevel/Advanced.java new file mode 100644 index 000000000..ed8c30569 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/gamelevel/Advanced.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.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/gamelevel/Beginner.java b/src/main/java/cleancode/minesweeper/tobe/gamelevel/Beginner.java new file mode 100644 index 000000000..4acb00b42 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/gamelevel/Beginner.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.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/gamelevel/GameLevel.java b/src/main/java/cleancode/minesweeper/tobe/gamelevel/GameLevel.java new file mode 100644 index 000000000..82781ea48 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/gamelevel/GameLevel.java @@ -0,0 +1,11 @@ +package cleancode.minesweeper.tobe.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/gamelevel/Middle.java b/src/main/java/cleancode/minesweeper/tobe/gamelevel/Middle.java new file mode 100644 index 000000000..7f6892f76 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/gamelevel/Middle.java @@ -0,0 +1,18 @@ +package cleancode.minesweeper.tobe.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/gamelevel/VeryBeginner.java b/src/main/java/cleancode/minesweeper/tobe/gamelevel/VeryBeginner.java new file mode 100644 index 000000000..2cae9dd3b --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/gamelevel/VeryBeginner.java @@ -0,0 +1,20 @@ +package cleancode.minesweeper.tobe.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/io/ConsoleOutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java index 7fd39b685..bbc6c4473 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java @@ -2,7 +2,9 @@ import cleancode.minesweeper.tobe.GameBoard; import cleancode.minesweeper.tobe.GameException; -import cleancode.minesweeper.tobe.Cell; + +import java.util.List; +import java.util.stream.IntStream; public class ConsoleOutputHandler { public void showGameStartComments() { @@ -12,9 +14,11 @@ public void showGameStartComments() { } public void showBoard(GameBoard board) { - System.out.println(" a b c d e f g h i j"); + String alphabets = generateColAlphabets(board); + + System.out.println(" " + alphabets); for (int row = 0; row < board.getRowSize(); row++) { - System.out.printf("%d ", row + 1); + System.out.printf("%2d ", row + 1); for (int col = 0; col < board.getColSize(); col++) { System.out.print(board.getSign(row, col) + " "); } @@ -22,6 +26,14 @@ public void showBoard(GameBoard board) { } } + 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); + } + public void printGameWinningComment() { System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); } From 1c1dff9303ca74e6eeac0233d72851ba628e3002 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Wed, 16 Oct 2024 18:41:50 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=ED=8C=A8=EB=9F=AC=EB=8B=A4=EC=9E=84):=20[=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=ED=96=A5=20=ED=8C=A8=EB=9F=AC=EB=8B=A4?= =?UTF-8?q?=EC=9E=84]=20LSP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cleancode/minesweeper/tobe/Cell.java | 85 ------------------- .../cleancode/minesweeper/tobe/GameBoard.java | 41 +++++---- .../cleancode/minesweeper/tobe/cell/Cell.java | 57 +++++++++++++ .../minesweeper/tobe/cell/EmptyCell.java | 27 ++++++ .../minesweeper/tobe/cell/LandMineCell.java | 28 ++++++ .../minesweeper/tobe/cell/NumberCell.java | 32 +++++++ 6 files changed, 167 insertions(+), 103 deletions(-) delete mode 100644 src/main/java/cleancode/minesweeper/tobe/Cell.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/cell/Cell.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java diff --git a/src/main/java/cleancode/minesweeper/tobe/Cell.java b/src/main/java/cleancode/minesweeper/tobe/Cell.java deleted file mode 100644 index 381356beb..000000000 --- a/src/main/java/cleancode/minesweeper/tobe/Cell.java +++ /dev/null @@ -1,85 +0,0 @@ -package cleancode.minesweeper.tobe; - -public class Cell { - - private static final String FLAG_SIGN = "⚑"; - private static final String LAND_MINE_SIGN = "☼"; - private static final String UNCHECKED_SIGN = "□"; - private static final String EMPTY_SIGN = "■"; - - private int nearbyLandMineCount; - private boolean isLandMine; - private boolean isFlagged; - private boolean isOpened; - - // Cell 이 가진 속성: 근처 지뢰 갯수, 지뢰 여부 - // Cell 의 상태: 깃발 유무, 열렸다/닫혔다, 사용자가 확인함 - - private Cell(int nearbyLandMineCount, boolean isLandMine, boolean isFlagged, boolean isOpened) { - this.nearbyLandMineCount = nearbyLandMineCount; - this.isLandMine = isLandMine; - this.isFlagged = isFlagged; - this.isOpened = isOpened; - } - - // 정적 팩토리 메서드를 좋아하는 이유: 메서드에 이름을 줄 수 있다. - // 정적 팩토리 메서드가 여러개가 된다면 그에 맞는 다른 이름들을 지어줄 수도 있고, 검증과 같은 로직을 추가할 수도 있다. - // 생성자 하나인 객체라도 정적 팩토리 메서드를 만들어서 생성자를 대체해보자. - public static Cell of(int nearbyLandMineCount, boolean isLandMine, boolean isFlagged, boolean isOpened) { - return new Cell(nearbyLandMineCount, isLandMine, isFlagged, isOpened); - } - - public static Cell create() { - return of(0, false, false, false); - } - - public void turnOnLandMine() { - this.isLandMine = true; - } - - public void updateNearbyLandMineCount(int count) { - this.nearbyLandMineCount = count; - } - - public void flag() { - this.isFlagged = true; - } - - public void open() { - this.isOpened = true; - } - - public boolean isChecked() { - return isFlagged || isOpened; - } - - public boolean isLandMine() { - return isLandMine; - } - - public boolean isOpened() { - return isOpened; - } - - public boolean hasLandMineCount() { - return this.nearbyLandMineCount != 0; - } - - public String getSign() { - if (isOpened) { - if (isLandMine) { - return LAND_MINE_SIGN; - } - if (hasLandMineCount()) { - return String.valueOf(nearbyLandMineCount); - } - return EMPTY_SIGN; - } - - if (isFlagged) { - return FLAG_SIGN; - } - - return UNCHECKED_SIGN; - } -} diff --git a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java index ab37af9b3..91e7a5e9f 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java @@ -1,5 +1,9 @@ package cleancode.minesweeper.tobe; +import cleancode.minesweeper.tobe.cell.Cell; +import cleancode.minesweeper.tobe.cell.EmptyCell; +import cleancode.minesweeper.tobe.cell.LandMineCell; +import cleancode.minesweeper.tobe.cell.NumberCell; import cleancode.minesweeper.tobe.gamelevel.GameLevel; import java.util.Arrays; @@ -27,19 +31,6 @@ public void open(int rowIndex, int colIndex) { cell.open(); } - public boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { - Cell cell = findCell(selectedRowIndex, selectedColumnIndex); - return cell.isLandMine(); - } - - // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. - // 외부에서는 데이터를 모르니까 짐작해서 물어보는 것이 최선이다. - public boolean isAllCellChecked() { - return Arrays.stream(board) - .flatMap(Arrays::stream) - .allMatch(Cell::isChecked); - } - public void openSurroundedCells(int row, int col) { if (row < 0 || row >= getRowSize() || col < 0 || col >= getColSize()) { return; @@ -75,21 +66,33 @@ private boolean isOpenedCell(int row, int col) { return findCell(row, col).isOpened(); } + public boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { + Cell cell = findCell(selectedRowIndex, selectedColumnIndex); + return cell.isLandMine(); + } + // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. + // 외부에서는 데이터를 모르니까 짐작해서 물어보는 것이 최선이다. + + public boolean isAllCellChecked() { + return Arrays.stream(board) + .flatMap(Arrays::stream) + .allMatch(Cell::isChecked); + } + public void initializeGame() { int rowSize = getRowSize(); int colSize = getColSize(); for (int row = 0; row < rowSize; row++) { for (int col = 0; col < colSize; col++) { - board[row][col] = Cell.create(); + board[row][col] = new EmptyCell(); } } for (int i = 0; i < landMineCount; i++) { int landMineCol = new Random().nextInt(10); int landMineRow = new Random().nextInt(8); - Cell landMineCell = findCell(landMineRow, landMineCol); - landMineCell.turnOnLandMine(); + board[landMineRow][landMineCol] = new LandMineCell(); } for (int row = 0; row < rowSize; row++) { @@ -98,8 +101,10 @@ public void initializeGame() { continue; } int count = countNearByLandMines(row, col); - Cell cell = findCell(row, col); - cell.updateNearbyLandMineCount(count); + if (count == 0) { + continue; + } + board[row][col] = new NumberCell(count); } } } diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java b/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java new file mode 100644 index 000000000..5b14d4054 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java @@ -0,0 +1,57 @@ +package cleancode.minesweeper.tobe.cell; + +public abstract class Cell { + + // 하위 클래스에서도 사용할 수 있기 때문에 protected 로 변경 + protected static final String FLAG_SIGN = "⚑"; + protected static final String UNCHECKED_SIGN = "□"; + + + // 하위 클래스에서도 사용할 수 있도록 protected 로 변경 + protected boolean isFlagged; + protected boolean isOpened; + + // Cell 이 가진 속성: 근처 지뢰 갯수, 지뢰 여부 + // Cell 의 상태: 깃발 유무, 열렸다/닫혔다, 사용자가 확인함 + + // 정적 팩토리 메서드를 좋아하는 이유: 메서드에 이름을 줄 수 있다. + // 정적 팩토리 메서드가 여러개가 된다면 그에 맞는 다른 이름들을 지어줄 수도 있고, 검증과 같은 로직을 추가할 수도 있다. + // 생성자 하나인 객체라도 정적 팩토리 메서드를 만들어서 생성자를 대체해보자. + + // 지뢰와 관련된 기능 + // 그런데 구현하고 보니 LandMineCell 은 그자체로 landMine 이라는 의미를 갖고 있는데 turnOnLandMine() 으로 켜주는 것이 이상하다. + // 그리고 해당 기능 때문에 다른 자식 클래스인 EmptyCell, NumberCell 에서는 UnsupportedOperationException 을 던지고 있다. + // 따라서 해당 기능들을 지워야 한다. +// public abstract void turnOnLandMine(); + + // 이것도 특정 셀에서만 유효하다. + // 이것도 위와 마찬가지로 NumberCell 에서는 그 자체로 count 를 필드로 갖고 있어야 하지 메서드로 조정할 것이 아니다. + // 메서드로 조정하다보니 다른 자식 클래스에서 UnsupportedOperationException 을 던지고 있다. + // 따라서 해당 기능들을 지워야 한다. +// public abstract void updateNearbyLandMineCount(int count); + + + // isOpened, isFlagged 는 Cell 의 공통 기능이므로 그대로 둔다. + public void flag() { + this.isFlagged = true; + } + + public void open() { + this.isOpened = true; + } + + public boolean isChecked() { + return isFlagged || isOpened; + } + + // 이것도 특정 셀에서만 유효하다. + public abstract boolean isLandMine(); + + public boolean isOpened() { + return isOpened; + } + + public abstract boolean hasLandMineCount(); + + public abstract String getSign(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java b/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java new file mode 100644 index 000000000..ec9011110 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java @@ -0,0 +1,27 @@ +package cleancode.minesweeper.tobe.cell; + +public class EmptyCell extends Cell { + + private static final String EMPTY_SIGN = "■"; + + @Override + public boolean isLandMine() { + return false; + } + + @Override + public boolean hasLandMineCount() { + return false; + } + + @Override + public String getSign() { + if (isOpened) { + return EMPTY_SIGN; + } + if (isFlagged) { + return FLAG_SIGN; + } + return UNCHECKED_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java b/src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java new file mode 100644 index 000000000..a369ab7d6 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java @@ -0,0 +1,28 @@ +package cleancode.minesweeper.tobe.cell; + +public class LandMineCell extends Cell { + + private static final String LAND_MINE_SIGN = "☼"; + + @Override + public boolean isLandMine() { + return true; + } + + @Override + public boolean hasLandMineCount() { + return false; + } + + @Override + public String getSign() { + if (isOpened) { + return LAND_MINE_SIGN; + } + if (isFlagged) { + return FLAG_SIGN; + } + + return UNCHECKED_SIGN; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java b/src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java new file mode 100644 index 000000000..30f2a5267 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java @@ -0,0 +1,32 @@ +package cleancode.minesweeper.tobe.cell; + +public class NumberCell extends Cell { + + private final int nearbyLandMineCount; + + public NumberCell(int nearbyLandMineCount) { + this.nearbyLandMineCount = nearbyLandMineCount; + } + + @Override + public boolean isLandMine() { + return false; + } + + @Override + public boolean hasLandMineCount() { + return true; + } + + @Override + public String getSign() { + if (isOpened) { + return String.valueOf(nearbyLandMineCount); + } + if (isFlagged) { + return FLAG_SIGN; + } + + return UNCHECKED_SIGN; + } +} From 2019ceb25b74bc77ff4c0c3190c80ba202d83d75 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Wed, 16 Oct 2024 21:31:02 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=ED=8C=A8=EB=9F=AC=EB=8B=A4=EC=9E=84):=20[=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=ED=96=A5=20=ED=8C=A8=EB=9F=AC=EB=8B=A4?= =?UTF-8?q?=EC=9E=84]=20ISP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/GameApplication.java | 1 + .../cleancode/minesweeper/tobe/Minesweeper.java | 15 +++++++++------ .../minesweeper/tobe/game/GameInitializable.java | 5 +++++ .../minesweeper/tobe/game/GameRunnable.java | 5 +++++ 4 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 src/main/java/cleancode/minesweeper/tobe/game/GameInitializable.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/game/GameRunnable.java diff --git a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java index 6ea9f3d0c..4cdede2de 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java @@ -12,6 +12,7 @@ public class GameApplication { public static void main(String[] args) { GameLevel gameLevel = new Middle(); Minesweeper minesweeper = new Minesweeper(gameLevel); + minesweeper.initialize(); minesweeper.run(); } } diff --git a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java index 79c72b1a6..248a16fea 100644 --- a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -1,14 +1,12 @@ package cleancode.minesweeper.tobe; +import cleancode.minesweeper.tobe.game.GameInitializable; +import cleancode.minesweeper.tobe.game.GameRunnable; import cleancode.minesweeper.tobe.gamelevel.GameLevel; import cleancode.minesweeper.tobe.io.ConsoleInputHandler; import cleancode.minesweeper.tobe.io.ConsoleOutputHandler; -import java.util.Arrays; -import java.util.Random; -import java.util.Scanner; - -public class Minesweeper { +public class Minesweeper implements GameInitializable, GameRunnable { // BOARD 도 하는 일이 너무 많고 중요하기 때문에 Minesweeper 클래스 내부에 상수로 두기에는 너무 책임이 과도하다. // 이렇게 GameBoard 클래스를 두면 Minesweeper 입장에서는 Cell[][] 이중배열에 대해서는 모른다. @@ -26,9 +24,14 @@ public Minesweeper(GameLevel gameLevel) { gameBoard = new GameBoard(gameLevel); } + @Override + public void initialize() { + gameBoard.initializeGame(); + } + + @Override public void run() { consoleOutputHandler.showGameStartComments(); - gameBoard.initializeGame(); while (true) { try { consoleOutputHandler.showBoard(gameBoard); 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(); +} From 173ead6804d9681258dcf60c9813e206adb67853 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Thu, 17 Oct 2024 14:49:12 +0900 Subject: [PATCH 15/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=ED=8C=A8=EB=9F=AC=EB=8B=A4=EC=9E=84):=20[=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=A7=80=ED=96=A5=20=ED=8C=A8=EB=9F=AC=EB=8B=A4?= =?UTF-8?q?=EC=9E=84]=20DIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/tobe/GameApplication.java | 8 +++- .../minesweeper/tobe/Minesweeper.java | 34 ++++++++------- .../tobe/io/ConsoleInputHandler.java | 3 +- .../tobe/io/ConsoleOutputHandler.java | 23 +++++++---- .../minesweeper/tobe/io/InputHandler.java | 5 +++ .../minesweeper/tobe/io/OutputHandler.java | 41 +++++++++++++++++++ 6 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java diff --git a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java index 4cdede2de..9ad5102db 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameApplication.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameApplication.java @@ -3,6 +3,9 @@ import cleancode.minesweeper.tobe.gamelevel.Advanced; import cleancode.minesweeper.tobe.gamelevel.GameLevel; import cleancode.minesweeper.tobe.gamelevel.Middle; +import cleancode.minesweeper.tobe.io.ConsoleInputHandler; +import cleancode.minesweeper.tobe.io.ConsoleOutputHandler; +import cleancode.minesweeper.tobe.io.InputHandler; public class GameApplication { @@ -11,7 +14,10 @@ public class GameApplication { // 게임 실행에 대한 책임과 지뢰찾기 도메인 자체, 지뢰찾기 게임을 담당하는 역할을 분리했다. public static void main(String[] args) { GameLevel gameLevel = new Middle(); - Minesweeper minesweeper = new Minesweeper(gameLevel); + InputHandler inputHandler = new ConsoleInputHandler(); + ConsoleOutputHandler outputHandler = new ConsoleOutputHandler(); + + Minesweeper minesweeper = new Minesweeper(gameLevel, inputHandler, outputHandler); minesweeper.initialize(); minesweeper.run(); } diff --git a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java index 248a16fea..2ae93d003 100644 --- a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -3,8 +3,8 @@ import cleancode.minesweeper.tobe.game.GameInitializable; import cleancode.minesweeper.tobe.game.GameRunnable; import cleancode.minesweeper.tobe.gamelevel.GameLevel; -import cleancode.minesweeper.tobe.io.ConsoleInputHandler; -import cleancode.minesweeper.tobe.io.ConsoleOutputHandler; +import cleancode.minesweeper.tobe.io.InputHandler; +import cleancode.minesweeper.tobe.io.OutputHandler; public class Minesweeper implements GameInitializable, GameRunnable { @@ -16,12 +16,16 @@ public class Minesweeper implements GameInitializable, GameRunnable { private final BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); // 게임이 진행되는 핵심 로직들과 사용자 입출력에 대한 로직 책임을 분리한다. - private final ConsoleInputHandler consoleInputHandler = new ConsoleInputHandler(); - private final ConsoleOutputHandler consoleOutputHandler = new ConsoleOutputHandler(); + // DIP: InputHandler, OutputHandler 는 이제 Console 에 관한 것은 모른다. 인터페이스만 의존하고 있다. + // 구현체가 변경되어도 Minesweeper 클래스는 영향을 받지 않는다. + private final InputHandler inputHandler; + private final OutputHandler outputHandler; private int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배 - public Minesweeper(GameLevel gameLevel) { + public Minesweeper(GameLevel gameLevel, InputHandler inputHandler, OutputHandler outputHandler) { gameBoard = new GameBoard(gameLevel); + this.inputHandler = inputHandler; + this.outputHandler = outputHandler; } @Override @@ -31,17 +35,17 @@ public void initialize() { @Override public void run() { - consoleOutputHandler.showGameStartComments(); + outputHandler.showGameStartComments(); while (true) { try { - consoleOutputHandler.showBoard(gameBoard); + outputHandler.showBoard(gameBoard); if (doesUserWinTheGame()) { - consoleOutputHandler.printGameWinningComment(); + outputHandler.showPrintGameWinningComment(); break; } if (doesUserLoseTheGame()) { - consoleOutputHandler.printGameLosingComment(); + outputHandler.showGameLosingComment(); break; } String cellInput = getCellInputFromUser(); @@ -51,9 +55,9 @@ public void run() { // print 할 때 AppException 에서 어떤걸 꺼내서 쓸지는 내부에서 알아서 결정할 것이고, // 예외 상황(exception 에 대한 메시지)에 대한 메시지를 출력하겠다는 이 메서드명을 봤을 때 // 파라미터는 exception 자체를 넣어주는 것이 더 자연스럽지 않을까 한다. - consoleOutputHandler.printExceptionMessage(e); + outputHandler.showExceptionMessage(e); } catch (Exception e) { - consoleOutputHandler.printSimpleMessage("프로그램에 문제가 생겼습니다."); + outputHandler.showSimpleMessage("프로그램에 문제가 생겼습니다."); } } } @@ -95,13 +99,13 @@ private boolean doesUserChooseToPlantFlag(String userActionInput) { } private String getUserActionInputFromUser() { - consoleOutputHandler.printCommentForUserAction(); - return consoleInputHandler.getUserInput(); + outputHandler.showCommentForUserAction(); + return inputHandler.getUserInput(); } private String getCellInputFromUser() { - consoleOutputHandler.printCommentForSelectingCell(); - return consoleInputHandler.getUserInput(); + outputHandler.showCommentForSelectingCell(); + return inputHandler.getUserInput(); } private boolean doesUserLoseTheGame() { diff --git a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java index a36a5476c..8dc1a0366 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java @@ -2,9 +2,10 @@ import java.util.Scanner; -public class ConsoleInputHandler { +public class ConsoleInputHandler implements InputHandler { public static final Scanner SCANNER = new Scanner(System.in); + @Override public String getUserInput() { return SCANNER.nextLine(); } diff --git a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java index bbc6c4473..72f0d3af3 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java @@ -6,13 +6,16 @@ import java.util.List; import java.util.stream.IntStream; -public class ConsoleOutputHandler { +public class ConsoleOutputHandler implements OutputHandler { + + @Override public void showGameStartComments() { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println("지뢰찾기 게임 시작!"); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); } + @Override public void showBoard(GameBoard board) { String alphabets = generateColAlphabets(board); @@ -34,27 +37,33 @@ private String generateColAlphabets(GameBoard board) { return String.join(" ", alphabets); } - public void printGameWinningComment() { + @Override + public void showPrintGameWinningComment() { System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); } - public void printGameLosingComment() { + @Override + public void showGameLosingComment() { System.out.println("지뢰를 밟았습니다. GAME OVER!"); } - public void printCommentForSelectingCell() { + @Override + public void showCommentForSelectingCell() { System.out.println("선택할 좌표를 입력하세요. (예: a1)"); } - public void printCommentForUserAction() { + @Override + public void showCommentForUserAction() { System.out.println("선택한 셀에 대한 행위를 선택하세요. (1: 오픈, 2: 깃발 꽂기)"); } - public void printExceptionMessage(GameException e) { + @Override + public void showExceptionMessage(GameException e) { System.out.println(e.getMessage()); } - public void printSimpleMessage(String message) { + @Override + public void showSimpleMessage(String message) { System.out.println(message); } } diff --git a/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java new file mode 100644 index 000000000..639402524 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java @@ -0,0 +1,5 @@ +package cleancode.minesweeper.tobe.io; + +public interface InputHandler { + String getUserInput(); +} diff --git a/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java new file mode 100644 index 000000000..7552cadd2 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java @@ -0,0 +1,41 @@ +package cleancode.minesweeper.tobe.io; + +import cleancode.minesweeper.tobe.GameBoard; +import cleancode.minesweeper.tobe.GameException; + +public interface OutputHandler { + + // show, print 중에 show 는 추상적인 느낌이고, print 는 구체적인 느낌이 강하다. + // print 는 console 에 print 한다는 느낌이 강하다. + // 높은 추상화 레벨인 OutputHandler 입장에서는 print 보다는 show 가 더 낫겠다. + + void showGameStartComments(); + + void showBoard(GameBoard board); + + + void showPrintGameWinningComment(); + + 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); +} From 579ffd5626e690152921584ae47f8ae0b9b0762f Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Wed, 12 Mar 2025 18:22:24 +0900 Subject: [PATCH 16/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=EC=A0=81=EC=9A=A9):=20=EC=83=81=EC=86=8D=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20=EC=A1=B0=ED=95=A9,=20Value=20Object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes .../minesweeper/tobe/BoardIndexConverter.java | 16 +-- .../cleancode/minesweeper/tobe/GameBoard.java | 115 +++++++++--------- .../minesweeper/tobe/Minesweeper.java | 28 +++-- .../cleancode/minesweeper/tobe/cell/Cell.java | 56 ++------- .../minesweeper/tobe/cell/CellState.java | 37 ++++++ .../minesweeper/tobe/cell/EmptyCell.java | 28 ++++- .../minesweeper/tobe/cell/LandMineCell.java | 27 +++- .../minesweeper/tobe/cell/NumberCell.java | 28 ++++- .../tobe/io/ConsoleInputHandler.java | 13 ++ .../tobe/io/ConsoleOutputHandler.java | 4 +- .../minesweeper/tobe/io/InputHandler.java | 4 + .../tobe/positoion/CellPosition.java | 72 +++++++++++ .../tobe/positoion/RelativePosition.java | 50 ++++++++ 14 files changed, 342 insertions(+), 136 deletions(-) create mode 100644 .DS_Store create mode 100644 src/main/java/cleancode/minesweeper/tobe/cell/CellState.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/positoion/RelativePosition.java diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0a768b9b71ee7a28d279f5958670432cdad6353c GIT binary patch literal 6148 zcmeHK%}T>S5Z<-XrW7Fug&r5Y7Oln##Y>3w1&ruHr6#1*V9b^_HHT8jSzpK}@p+ut z-H63{6|pn0`_1oe_JiyXV~qQ=_>eJ&F=j(U`_07u zI^efkY@dx-!t$@*AC1#wns++ye4}PFg|Iguoq>F$eggRp27Z~^eLJmph literal 0 HcmV?d00001 diff --git a/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java b/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java index 88f5c72bb..b7144491e 100644 --- a/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java +++ b/src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java @@ -4,28 +4,28 @@ public class BoardIndexConverter { private static final char BASE_CHAR_FOR_COL = 'a'; - public int getSelectedRowIndex(String cellInput, int rowSize) { + public int getSelectedRowIndex(String cellInput) { String cellInputRow = cellInput.substring(1); - return convertRowFrom(cellInputRow, rowSize); + return convertRowFrom(cellInputRow); } - public int getSelectedColIndex(String cellInput, int colSize) { + public int getSelectedColIndex(String cellInput) { char cellInputCol = cellInput.charAt(0); - return convertColFrom(cellInputCol, colSize); + return convertColFrom(cellInputCol); } - private int convertRowFrom(String cellInputRow, int rowSize) { + private int convertRowFrom(String cellInputRow) { int rowIndex = Integer.parseInt(cellInputRow) - 1; - if (rowIndex < 0 || rowIndex >= rowSize) { + if (rowIndex < 0) { throw new GameException("잘못된 입력입니다."); } return rowIndex; } - private int convertColFrom(char cellInputCol, int colSize) { + private int convertColFrom(char cellInputCol) { int colIndex = cellInputCol - BASE_CHAR_FOR_COL; - if (colIndex < 0 || colIndex >= colSize) { + if (colIndex < 0) { throw new GameException("잘못된 입력입니다."); } diff --git a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java index 91e7a5e9f..2b303044c 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java @@ -5,9 +5,13 @@ import cleancode.minesweeper.tobe.cell.LandMineCell; import cleancode.minesweeper.tobe.cell.NumberCell; import cleancode.minesweeper.tobe.gamelevel.GameLevel; +import cleancode.minesweeper.tobe.positoion.CellPosition; +import cleancode.minesweeper.tobe.positoion.RelativePosition; import java.util.Arrays; +import java.util.List; import java.util.Random; +import java.util.stream.Stream; public class GameBoard { @@ -21,53 +25,46 @@ public GameBoard(GameLevel gameLevel) { landMineCount = gameLevel.getLandMineCount(); } - public void flag(int rowIndex, int colIndex) { - Cell cell = findCell(rowIndex, colIndex); + public void flagAt(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); cell.flag(); } - public void open(int rowIndex, int colIndex) { - Cell cell = findCell(rowIndex, colIndex); + public void openAt(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); cell.open(); } - public void openSurroundedCells(int row, int col) { - if (row < 0 || row >= getRowSize() || col < 0 || col >= getColSize()) { + public void openSurroundedCells(CellPosition cellPosition) { + if (isOpenedCell(cellPosition)) { return; } - if (isOpenedCell(row, col)) { - return; - } - if (isLandMineCell(row, col)) { + if (isLandMineCellAt(cellPosition)) { return; } - open(row, col); + openAt(cellPosition); - if (doesCellHaveLandMineCount(row, col)) { + if (doesCellHaveLandMineCount(cellPosition)) { return; } - openSurroundedCells(row - 1, col - 1); - openSurroundedCells(row - 1, col); - openSurroundedCells(row - 1, col + 1); - openSurroundedCells(row, col - 1); - openSurroundedCells(row, col + 1); - openSurroundedCells(row + 1, col - 1); - openSurroundedCells(row + 1, col); - openSurroundedCells(row + 1, col + 1); + List surroundedPositions = calculateSurroundedPosition(cellPosition, getRowSize(), getColSize()); + surroundedPositions.forEach(this::openSurroundedCells); } - private boolean doesCellHaveLandMineCount(int row, int col) { - return findCell(row, col).hasLandMineCount(); + private boolean doesCellHaveLandMineCount(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + return cell.hasLandMineCount(); } - private boolean isOpenedCell(int row, int col) { - return findCell(row, col).isOpened(); + private boolean isOpenedCell(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); + return cell.isOpened(); } - public boolean isLandMineCell(int selectedRowIndex, int selectedColumnIndex) { - Cell cell = findCell(selectedRowIndex, selectedColumnIndex); + public boolean isLandMineCellAt(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); return cell.isLandMine(); } // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. @@ -79,6 +76,15 @@ public boolean isAllCellChecked() { .allMatch(Cell::isChecked); } + + public boolean isInValidCellPosition(CellPosition cellPosition) { + int rowSize = getRowSize(); + int colSize = getRowSize(); + + return cellPosition.isRowIndexMoreThanOrEqual(rowSize) + || cellPosition.isColIndexMoreThanOrEqual(colSize); + } + public void initializeGame() { int rowSize = getRowSize(); int colSize = getColSize(); @@ -97,10 +103,12 @@ public void initializeGame() { for (int row = 0; row < rowSize; row++) { for (int col = 0; col < colSize; col++) { - if (isLandMineCell(row, col)) { + CellPosition cellPosition = CellPosition.of(row, col); + + if (isLandMineCellAt(cellPosition)) { continue; } - int count = countNearByLandMines(row, col); + int count = countNearByLandMines(cellPosition); if (count == 0) { continue; } @@ -109,13 +117,13 @@ public void initializeGame() { } } - public String getSign(int rowIndex, int colIndex) { - Cell cell = findCell(rowIndex, colIndex); + public String getSign(CellPosition cellPosition) { + Cell cell = findCell(cellPosition); return cell.getSign(); } - private Cell findCell(int rowIndex, int colIndex) { - return board[rowIndex][colIndex]; + private Cell findCell(CellPosition cellPosition) { + return board[cellPosition.getRowIndex()][cellPosition.getColIndex()]; } public int getRowSize() { @@ -126,35 +134,24 @@ public int getColSize() { return board[0].length; } - private int countNearByLandMines(int row, int col) { + private int countNearByLandMines(CellPosition cellPosition) { int rowSize = getRowSize(); int colSize = getColSize(); - int count = 0; - if (row - 1 >= 0 && col - 1 >= 0 && isLandMineCell(row - 1, col - 1)) { - count++; - } - if (row - 1 >= 0 && isLandMineCell(row - 1, col)) { - count++; - } - if (row - 1 >= 0 && col + 1 < colSize && isLandMineCell(row - 1, col + 1)) { - count++; - } - if (col - 1 >= 0 && isLandMineCell(row, col - 1)) { - count++; - } - if (col + 1 < colSize && isLandMineCell(row, col + 1)) { - count++; - } - if (row + 1 < rowSize && col - 1 >= 0 && isLandMineCell(row + 1, col - 1)) { - count++; - } - if (row + 1 < rowSize && isLandMineCell(row + 1, col)) { - count++; - } - if (row + 1 < rowSize && col + 1 < colSize && isLandMineCell(row + 1, col + 1)) { - count++; - } - return count; + long count = calculateSurroundedPosition(cellPosition, rowSize, colSize).stream() + .filter(this::isLandMineCellAt) + .count(); + + return (int) count; + } + + private List calculateSurroundedPosition(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(); } + } diff --git a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java index 2ae93d003..86e55c06a 100644 --- a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -5,6 +5,7 @@ import cleancode.minesweeper.tobe.gamelevel.GameLevel; import cleancode.minesweeper.tobe.io.InputHandler; import cleancode.minesweeper.tobe.io.OutputHandler; +import cleancode.minesweeper.tobe.positoion.CellPosition; public class Minesweeper implements GameInitializable, GameRunnable { @@ -13,7 +14,6 @@ public class Minesweeper implements GameInitializable, GameRunnable { // 객체로 추상화가 되었고, 데이터 구조에 대한 것은 캐슐화가 되었기 때문이다. private final GameBoard gameBoard; // SRP: cellInput 이라는 사용자의 입력을 받아서 rowIndex, colIndex 로 변환하는 역할을 하는 또 하나의 클래스로 볼 수 있지 않을까? - private final BoardIndexConverter boardIndexConverter = new BoardIndexConverter(); // 게임이 진행되는 핵심 로직들과 사용자 입출력에 대한 로직 책임을 분리한다. // DIP: InputHandler, OutputHandler 는 이제 Console 에 관한 것은 모른다. 인터페이스만 의존하고 있다. @@ -48,9 +48,9 @@ public void run() { outputHandler.showGameLosingComment(); break; } - String cellInput = getCellInputFromUser(); + CellPosition cellPosition = getCellInputFromUser(); String userActionInput = getUserActionInputFromUser(); - actOnCell(cellInput, userActionInput); + actOnCell(cellPosition, userActionInput); } catch (GameException e) { // print 할 때 AppException 에서 어떤걸 꺼내서 쓸지는 내부에서 알아서 결정할 것이고, // 예외 상황(exception 에 대한 메시지)에 대한 메시지를 출력하겠다는 이 메서드명을 봤을 때 @@ -62,24 +62,21 @@ public void run() { } } - private void actOnCell(String cellInput, String userActionInput) { - int selectedColumnIndex = boardIndexConverter.getSelectedColIndex(cellInput, gameBoard.getColSize()); - int selectedRowIndex = boardIndexConverter.getSelectedRowIndex(cellInput, gameBoard.getRowSize()); - + private void actOnCell(CellPosition cellPosition, String userActionInput) { if (doesUserChooseToPlantFlag(userActionInput)) { - gameBoard.flag(selectedRowIndex, selectedColumnIndex); + gameBoard.flagAt(cellPosition); checkIfGameIsOver(); return; } if (doesUserChooseToOpenCell(userActionInput)) { - if (gameBoard.isLandMineCell(selectedRowIndex, selectedColumnIndex)) { - gameBoard.open(selectedRowIndex, selectedColumnIndex); + if (gameBoard.isLandMineCellAt(cellPosition)) { + gameBoard.openAt(cellPosition); changeGameStatusToLose(); return; } - gameBoard.openSurroundedCells(selectedRowIndex, selectedColumnIndex); + gameBoard.openSurroundedCells(cellPosition); checkIfGameIsOver(); return; } @@ -103,9 +100,14 @@ private String getUserActionInputFromUser() { return inputHandler.getUserInput(); } - private String getCellInputFromUser() { + private CellPosition getCellInputFromUser() { outputHandler.showCommentForSelectingCell(); - return inputHandler.getUserInput(); + CellPosition cellPosition = inputHandler.getCellPositionFromUser(); + if (gameBoard.isInValidCellPosition(cellPosition)) { + throw new GameException("잘못된 좌표를 선택하셨습니다."); + } + + return cellPosition; } private boolean doesUserLoseTheGame() { diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java b/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java index 5b14d4054..7b8d9bcbb 100644 --- a/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java +++ b/src/main/java/cleancode/minesweeper/tobe/cell/Cell.java @@ -1,57 +1,21 @@ package cleancode.minesweeper.tobe.cell; -public abstract class Cell { +public interface Cell { // 하위 클래스에서도 사용할 수 있기 때문에 protected 로 변경 - protected static final String FLAG_SIGN = "⚑"; - protected static final String UNCHECKED_SIGN = "□"; - - - // 하위 클래스에서도 사용할 수 있도록 protected 로 변경 - protected boolean isFlagged; - protected boolean isOpened; - - // Cell 이 가진 속성: 근처 지뢰 갯수, 지뢰 여부 - // Cell 의 상태: 깃발 유무, 열렸다/닫혔다, 사용자가 확인함 - - // 정적 팩토리 메서드를 좋아하는 이유: 메서드에 이름을 줄 수 있다. - // 정적 팩토리 메서드가 여러개가 된다면 그에 맞는 다른 이름들을 지어줄 수도 있고, 검증과 같은 로직을 추가할 수도 있다. - // 생성자 하나인 객체라도 정적 팩토리 메서드를 만들어서 생성자를 대체해보자. - - // 지뢰와 관련된 기능 - // 그런데 구현하고 보니 LandMineCell 은 그자체로 landMine 이라는 의미를 갖고 있는데 turnOnLandMine() 으로 켜주는 것이 이상하다. - // 그리고 해당 기능 때문에 다른 자식 클래스인 EmptyCell, NumberCell 에서는 UnsupportedOperationException 을 던지고 있다. - // 따라서 해당 기능들을 지워야 한다. -// public abstract void turnOnLandMine(); - - // 이것도 특정 셀에서만 유효하다. - // 이것도 위와 마찬가지로 NumberCell 에서는 그 자체로 count 를 필드로 갖고 있어야 하지 메서드로 조정할 것이 아니다. - // 메서드로 조정하다보니 다른 자식 클래스에서 UnsupportedOperationException 을 던지고 있다. - // 따라서 해당 기능들을 지워야 한다. -// public abstract void updateNearbyLandMineCount(int count); + String FLAG_SIGN = "⚑"; + String UNCHECKED_SIGN = "□"; + boolean isLandMine(); + boolean hasLandMineCount(); + String getSign(); // isOpened, isFlagged 는 Cell 의 공통 기능이므로 그대로 둔다. - public void flag() { - this.isFlagged = true; - } - - public void open() { - this.isOpened = true; - } - - public boolean isChecked() { - return isFlagged || isOpened; - } - - // 이것도 특정 셀에서만 유효하다. - public abstract boolean isLandMine(); + void flag(); - public boolean isOpened() { - return isOpened; - } + void open(); - public abstract boolean hasLandMineCount(); + boolean isChecked(); - public abstract String getSign(); + boolean isOpened(); } diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/CellState.java b/src/main/java/cleancode/minesweeper/tobe/cell/CellState.java new file mode 100644 index 000000000..98de7fe51 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/cell/CellState.java @@ -0,0 +1,37 @@ +package cleancode.minesweeper.tobe.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 isChecked() { + return isFlagged || isOpened; + } + + public boolean isOpened() { + return isOpened; + } + + + public boolean isFlagged() { + return isFlagged; + } +} diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java b/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java index ec9011110..0021c03e1 100644 --- a/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java +++ b/src/main/java/cleancode/minesweeper/tobe/cell/EmptyCell.java @@ -1,9 +1,11 @@ package cleancode.minesweeper.tobe.cell; -public class EmptyCell extends Cell { +public class EmptyCell implements Cell { private static final String EMPTY_SIGN = "■"; + private final CellState cellState = CellState.initialize(); + @Override public boolean isLandMine() { return false; @@ -16,12 +18,32 @@ public boolean hasLandMineCount() { @Override public String getSign() { - if (isOpened) { + if (cellState.isOpened()) { return EMPTY_SIGN; } - if (isFlagged) { + if (cellState.isFlagged()) { return FLAG_SIGN; } return UNCHECKED_SIGN; } + + @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/cell/LandMineCell.java b/src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java index a369ab7d6..e79b94774 100644 --- a/src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java +++ b/src/main/java/cleancode/minesweeper/tobe/cell/LandMineCell.java @@ -1,8 +1,9 @@ package cleancode.minesweeper.tobe.cell; -public class LandMineCell extends Cell { +public class LandMineCell implements Cell { private static final String LAND_MINE_SIGN = "☼"; + private final CellState cellState = CellState.initialize(); @Override public boolean isLandMine() { @@ -16,13 +17,33 @@ public boolean hasLandMineCount() { @Override public String getSign() { - if (isOpened) { + if (cellState.isOpened()) { return LAND_MINE_SIGN; } - if (isFlagged) { + if (cellState.isFlagged()) { return FLAG_SIGN; } return UNCHECKED_SIGN; } + + @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/cell/NumberCell.java b/src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java index 30f2a5267..4c2431d1f 100644 --- a/src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java +++ b/src/main/java/cleancode/minesweeper/tobe/cell/NumberCell.java @@ -1,8 +1,10 @@ package cleancode.minesweeper.tobe.cell; -public class NumberCell extends Cell { +public class NumberCell implements Cell { private final int nearbyLandMineCount; + private final CellState cellState = CellState.initialize(); + public NumberCell(int nearbyLandMineCount) { this.nearbyLandMineCount = nearbyLandMineCount; @@ -20,13 +22,33 @@ public boolean hasLandMineCount() { @Override public String getSign() { - if (isOpened) { + if (cellState.isOpened()) { return String.valueOf(nearbyLandMineCount); } - if (isFlagged) { + if (cellState.isFlagged()) { return FLAG_SIGN; } return UNCHECKED_SIGN; } + + @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/io/ConsoleInputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java index 8dc1a0366..23e5b4455 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleInputHandler.java @@ -1,12 +1,25 @@ package cleancode.minesweeper.tobe.io; +import cleancode.minesweeper.tobe.BoardIndexConverter; +import cleancode.minesweeper.tobe.positoion.CellPosition; + 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 String getUserInput() { return SCANNER.nextLine(); } + + @Override + public CellPosition getCellPositionFromUser() { + String userInput = SCANNER.nextLine(); + + int rowIndex = boardIndexConverter.getSelectedColIndex(userInput); + int colIndex = boardIndexConverter.getSelectedColIndex(userInput); + return CellPosition.of(rowIndex, colIndex); + } } diff --git a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java index 72f0d3af3..157870c80 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java @@ -2,6 +2,7 @@ import cleancode.minesweeper.tobe.GameBoard; import cleancode.minesweeper.tobe.GameException; +import cleancode.minesweeper.tobe.positoion.CellPosition; import java.util.List; import java.util.stream.IntStream; @@ -23,7 +24,8 @@ public void showBoard(GameBoard board) { for (int row = 0; row < board.getRowSize(); row++) { System.out.printf("%2d ", row + 1); for (int col = 0; col < board.getColSize(); col++) { - System.out.print(board.getSign(row, col) + " "); + CellPosition cellPosition = CellPosition.of(row, col); + System.out.print(board.getSign(cellPosition) + " "); } System.out.println(); } diff --git a/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java index 639402524..cc464f241 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/InputHandler.java @@ -1,5 +1,9 @@ package cleancode.minesweeper.tobe.io; +import cleancode.minesweeper.tobe.positoion.CellPosition; + public interface InputHandler { String getUserInput(); + + CellPosition getCellPositionFromUser(); } diff --git a/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java b/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java new file mode 100644 index 000000000..94e0bee25 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java @@ -0,0 +1,72 @@ +package cleancode.minesweeper.tobe.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 object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + CellPosition that = (CellPosition) object; + 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 this.rowIndex; + } + + public int getColIndex() { + return this.colIndex; + } + + public boolean canCalculatePositionBy(RelativePosition relativePosition) { + return this.getRowIndex() + relativePosition.getDeltaRow() >= 0 + && this.getColIndex() + 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/positoion/RelativePosition.java b/src/main/java/cleancode/minesweeper/tobe/positoion/RelativePosition.java new file mode 100644 index 000000000..8989080a9 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/positoion/RelativePosition.java @@ -0,0 +1,50 @@ +package cleancode.minesweeper.tobe.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 object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + RelativePosition that = (RelativePosition) object; + return deltaRow == that.deltaRow && deltaCol == that.deltaCol; + } + + @Override + public int hashCode() { + return Objects.hash(deltaRow, deltaCol); + } + + public int getDeltaRow() { + return this.deltaRow; + } + + public int getDeltaCol() { + return this.deltaCol; + } +} From 73ca430725d0805b98813e1bd649aeef3400e3c3 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Wed, 12 Mar 2025 21:49:48 +0900 Subject: [PATCH 17/18] =?UTF-8?q?feat(=EA=B0=9D=EC=B2=B4-=EC=A7=80?= =?UTF-8?q?=ED=96=A5-=EC=A0=81=EC=9A=A9=ED=95=98=EA=B8=B0):=20=EC=9D=BC?= =?UTF-8?q?=EA=B8=89=20=EC=BB=AC=EB=A0=89=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cleancode/minesweeper/tobe/GameBoard.java | 79 ++++++++++--------- .../minesweeper/tobe/Minesweeper.java | 10 +-- .../minesweeper/tobe/cell/Cells.java | 28 +++++++ .../tobe/io/ConsoleOutputHandler.java | 2 +- .../minesweeper/tobe/io/OutputHandler.java | 2 +- .../tobe/positoion/CellPosition.java | 19 ++--- .../tobe/positoion/CellPositions.java | 57 +++++++++++++ .../tobe/positoion/RelativePosition.java | 16 ++-- 8 files changed, 151 insertions(+), 62 deletions(-) create mode 100644 src/main/java/cleancode/minesweeper/tobe/cell/Cells.java create mode 100644 src/main/java/cleancode/minesweeper/tobe/positoion/CellPositions.java diff --git a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java index 2b303044c..a021cfcdc 100644 --- a/src/main/java/cleancode/minesweeper/tobe/GameBoard.java +++ b/src/main/java/cleancode/minesweeper/tobe/GameBoard.java @@ -1,18 +1,16 @@ package cleancode.minesweeper.tobe; import cleancode.minesweeper.tobe.cell.Cell; +import cleancode.minesweeper.tobe.cell.Cells; import cleancode.minesweeper.tobe.cell.EmptyCell; import cleancode.minesweeper.tobe.cell.LandMineCell; import cleancode.minesweeper.tobe.cell.NumberCell; import cleancode.minesweeper.tobe.gamelevel.GameLevel; import cleancode.minesweeper.tobe.positoion.CellPosition; +import cleancode.minesweeper.tobe.positoion.CellPositions; import cleancode.minesweeper.tobe.positoion.RelativePosition; -import java.util.Arrays; import java.util.List; -import java.util.Random; -import java.util.stream.Stream; - public class GameBoard { private final Cell[][] board; @@ -22,6 +20,7 @@ public GameBoard(GameLevel gameLevel) { int rowSize = gameLevel.getRowSize(); int colSize = gameLevel.getColSize(); board = new Cell[rowSize][colSize]; + landMineCount = gameLevel.getLandMineCount(); } @@ -49,7 +48,7 @@ public void openSurroundedCells(CellPosition cellPosition) { return; } - List surroundedPositions = calculateSurroundedPosition(cellPosition, getRowSize(), getColSize()); + List surroundedPositions = calculateSurroundedPositions(cellPosition, getRowSize(), getColSize()); surroundedPositions.forEach(this::openSurroundedCells); } @@ -67,56 +66,58 @@ public boolean isLandMineCellAt(CellPosition cellPosition) { Cell cell = findCell(cellPosition); return cell.isLandMine(); } - // 객체의 캡슐화된 데이터를 외부에서 알고 있다고 생각하지 말자. - // 외부에서는 데이터를 모르니까 짐작해서 물어보는 것이 최선이다. public boolean isAllCellChecked() { - return Arrays.stream(board) - .flatMap(Arrays::stream) - .allMatch(Cell::isChecked); + Cells cells = Cells.from(board); + return cells.isAllChecked(); } - - public boolean isInValidCellPosition(CellPosition cellPosition) { + public boolean isInvalidCellPosition(CellPosition cellPosition) { int rowSize = getRowSize(); - int colSize = getRowSize(); + int colSize = getColSize(); return cellPosition.isRowIndexMoreThanOrEqual(rowSize) || cellPosition.isColIndexMoreThanOrEqual(colSize); } public void initializeGame() { - int rowSize = getRowSize(); - int colSize = getColSize(); + CellPositions cellPositions = CellPositions.from(board); - for (int row = 0; row < rowSize; row++) { - for (int col = 0; col < colSize; col++) { - board[row][col] = new EmptyCell(); - } + initializeEmptyCells(cellPositions); + + List landMinePositions = cellPositions.extractRandomPositions(landMineCount); + initializeLandMineCells(landMinePositions); + + List numberPositionCandidates = cellPositions.subtract(landMinePositions); + initializeNumberCells(numberPositionCandidates); + } + + private void initializeEmptyCells(CellPositions cellPositions) { + List allPositions = cellPositions.getPositions(); + for (CellPosition position : allPositions) { + updateCellAt(position, new EmptyCell()); } + } - for (int i = 0; i < landMineCount; i++) { - int landMineCol = new Random().nextInt(10); - int landMineRow = new Random().nextInt(8); - board[landMineRow][landMineCol] = new LandMineCell(); + private void initializeLandMineCells(List landMinePositions) { + for (CellPosition position : landMinePositions) { + updateCellAt(position, new LandMineCell()); } + } - for (int row = 0; row < rowSize; row++) { - for (int col = 0; col < colSize; col++) { - CellPosition cellPosition = CellPosition.of(row, col); - - if (isLandMineCellAt(cellPosition)) { - continue; - } - int count = countNearByLandMines(cellPosition); - if (count == 0) { - continue; - } - board[row][col] = new NumberCell(count); + private void initializeNumberCells(List numberPositionCandidates) { + for (CellPosition candidatePosition : numberPositionCandidates) { + int count = countNearbyLandMines(candidatePosition); + if (count != 0) { + updateCellAt(candidatePosition, new NumberCell(count)); } } } + private void updateCellAt(CellPosition position, Cell cell) { + board[position.getRowIndex()][position.getColIndex()] = cell; + } + public String getSign(CellPosition cellPosition) { Cell cell = findCell(cellPosition); return cell.getSign(); @@ -134,18 +135,18 @@ public int getColSize() { return board[0].length; } - private int countNearByLandMines(CellPosition cellPosition) { + private int countNearbyLandMines(CellPosition cellPosition) { int rowSize = getRowSize(); int colSize = getColSize(); - long count = calculateSurroundedPosition(cellPosition, rowSize, colSize).stream() + long count = calculateSurroundedPositions(cellPosition, rowSize, colSize).stream() .filter(this::isLandMineCellAt) .count(); - + return (int) count; } - private List calculateSurroundedPosition(CellPosition cellPosition, int rowSize, int colSize) { + private List calculateSurroundedPositions(CellPosition cellPosition, int rowSize, int colSize) { return RelativePosition.SURROUNDED_POSITIONS.stream() .filter(cellPosition::canCalculatePositionBy) .map(cellPosition::calculatePositionBy) diff --git a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java index 86e55c06a..f649a6e18 100644 --- a/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java +++ b/src/main/java/cleancode/minesweeper/tobe/Minesweeper.java @@ -36,25 +36,24 @@ public void initialize() { @Override public void run() { outputHandler.showGameStartComments(); + while (true) { try { outputHandler.showBoard(gameBoard); if (doesUserWinTheGame()) { - outputHandler.showPrintGameWinningComment(); + outputHandler.showGameWinningComment(); break; } if (doesUserLoseTheGame()) { outputHandler.showGameLosingComment(); break; } + CellPosition cellPosition = getCellInputFromUser(); String userActionInput = getUserActionInputFromUser(); actOnCell(cellPosition, userActionInput); } catch (GameException e) { - // print 할 때 AppException 에서 어떤걸 꺼내서 쓸지는 내부에서 알아서 결정할 것이고, - // 예외 상황(exception 에 대한 메시지)에 대한 메시지를 출력하겠다는 이 메서드명을 봤을 때 - // 파라미터는 exception 자체를 넣어주는 것이 더 자연스럽지 않을까 한다. outputHandler.showExceptionMessage(e); } catch (Exception e) { outputHandler.showSimpleMessage("프로그램에 문제가 생겼습니다."); @@ -103,7 +102,7 @@ private String getUserActionInputFromUser() { private CellPosition getCellInputFromUser() { outputHandler.showCommentForSelectingCell(); CellPosition cellPosition = inputHandler.getCellPositionFromUser(); - if (gameBoard.isInValidCellPosition(cellPosition)) { + if (gameBoard.isInvalidCellPosition(cellPosition)) { throw new GameException("잘못된 좌표를 선택하셨습니다."); } @@ -127,4 +126,5 @@ private void checkIfGameIsOver() { private void changeGameStatusToWin() { gameStatus = 1; } + } diff --git a/src/main/java/cleancode/minesweeper/tobe/cell/Cells.java b/src/main/java/cleancode/minesweeper/tobe/cell/Cells.java new file mode 100644 index 000000000..252fe78c2 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/cell/Cells.java @@ -0,0 +1,28 @@ +package cleancode.minesweeper.tobe.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/io/ConsoleOutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java index 157870c80..b0074d8ed 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/ConsoleOutputHandler.java @@ -40,7 +40,7 @@ private String generateColAlphabets(GameBoard board) { } @Override - public void showPrintGameWinningComment() { + public void showGameWinningComment() { System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!"); } diff --git a/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java b/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java index 7552cadd2..80bd7f7c8 100644 --- a/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java +++ b/src/main/java/cleancode/minesweeper/tobe/io/OutputHandler.java @@ -14,7 +14,7 @@ public interface OutputHandler { void showBoard(GameBoard board); - void showPrintGameWinningComment(); + void showGameWinningComment(); void showGameLosingComment(); diff --git a/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java b/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java index 94e0bee25..86b1332dc 100644 --- a/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java +++ b/src/main/java/cleancode/minesweeper/tobe/positoion/CellPosition.java @@ -19,10 +19,10 @@ public static CellPosition of(int rowIndex, int colIndex) { } @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - CellPosition that = (CellPosition) object; + 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; } @@ -40,16 +40,16 @@ public boolean isColIndexMoreThanOrEqual(int colIndex) { } public int getRowIndex() { - return this.rowIndex; + return rowIndex; } public int getColIndex() { - return this.colIndex; + return colIndex; } public boolean canCalculatePositionBy(RelativePosition relativePosition) { - return this.getRowIndex() + relativePosition.getDeltaRow() >= 0 - && this.getColIndex() + relativePosition.getDeltaCol() >= 0; + return this.rowIndex + relativePosition.getDeltaRow() >= 0 + && this.colIndex + relativePosition.getDeltaCol() >= 0; } public CellPosition calculatePositionBy(RelativePosition relativePosition) { @@ -59,7 +59,7 @@ public CellPosition calculatePositionBy(RelativePosition relativePosition) { this.colIndex + relativePosition.getDeltaCol() ); } - throw new IllegalArgumentException("움직일 수 없는 좌표입니다."); + throw new IllegalArgumentException("움직일 수 있는 좌표가 아닙니다."); } public boolean isRowIndexLessThan(int rowIndex) { @@ -69,4 +69,5 @@ public boolean isRowIndexLessThan(int rowIndex) { public boolean isColIndexLessThan(int colIndex) { return this.colIndex < colIndex; } + } diff --git a/src/main/java/cleancode/minesweeper/tobe/positoion/CellPositions.java b/src/main/java/cleancode/minesweeper/tobe/positoion/CellPositions.java new file mode 100644 index 000000000..c2c454be1 --- /dev/null +++ b/src/main/java/cleancode/minesweeper/tobe/positoion/CellPositions.java @@ -0,0 +1,57 @@ +package cleancode.minesweeper.tobe.positoion; + +import cleancode.minesweeper.tobe.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/positoion/RelativePosition.java b/src/main/java/cleancode/minesweeper/tobe/positoion/RelativePosition.java index 8989080a9..93831a35d 100644 --- a/src/main/java/cleancode/minesweeper/tobe/positoion/RelativePosition.java +++ b/src/main/java/cleancode/minesweeper/tobe/positoion/RelativePosition.java @@ -4,10 +4,11 @@ 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(-1, 1), RelativePosition.of(0, -1), RelativePosition.of(0, 1), RelativePosition.of(1, -1), @@ -28,10 +29,10 @@ public static RelativePosition of(int deltaRow, int deltaCol) { } @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - RelativePosition that = (RelativePosition) object; + 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; } @@ -41,10 +42,11 @@ public int hashCode() { } public int getDeltaRow() { - return this.deltaRow; + return deltaRow; } public int getDeltaCol() { - return this.deltaCol; + return deltaCol; } + } From 002fdfaf142a6ddcbc57ab892d058ed528f4e261 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Thu, 13 Mar 2025 00:00:37 +0900 Subject: [PATCH 18/18] mission/day-7 --- .../studycafe/tobe/StudyCafeApplication.java | 10 --- .../studycafe/tobe/StudyCafePassMachine.java | 81 ------------------- .../studycafe/tobe/io/InputHandler.java | 40 --------- .../studycafe/tobe/io/OutputHandler.java | 68 ---------------- .../tobe/model/StudyCafePassType.java | 15 ---- .../studycafe/tobe2/StudyCafeApplication.java | 18 +++++ .../studycafe/tobe2/StudyCafePassMachine.java | 43 ++++++++++ .../exception/AppException.java | 2 +- .../tobe2/io/ConsoleInputHandler.java | 28 +++++++ .../tobe2/io/ConsoleOutputHandler.java | 67 +++++++++++++++ .../studycafe/tobe2/io/InputHandler.java | 13 +++ .../studycafe/tobe2/io/OutputHandler.java | 23 ++++++ .../io/StudyCafeFileHandler.java | 8 +- .../tobe2/io/StudyCafeLockerPassReader.java | 7 ++ .../tobe2/io/StudyCafePassReader.java | 7 ++ .../file/StudyCafeLockerPassFileReader.java | 35 ++++++++ .../io/file/StudyCafePassFileReader.java | 36 +++++++++ .../tobe2/model/FixedStudyCafeProcessor.java | 53 ++++++++++++ .../tobe2/model/HourlyStudyCafeProcessor.java | 31 +++++++ .../model/LocalStudyCafeProcessorFactory.java | 38 +++++++++ .../model/StudyCafeLockerPass.java | 2 +- .../tobe2/model/StudyCafeLockerPasses.java | 25 ++++++ .../{tobe => tobe2}/model/StudyCafePass.java | 2 +- .../tobe2/model/StudyCafePassType.java | 31 +++++++ .../tobe2/model/StudyCafePasses.java | 30 +++++++ .../tobe2/model/StudyCafeProcessor.java | 6 ++ .../model/StudyCafeProcessorFactory.java | 5 ++ .../tobe2/model/WeeklyStudyCafeProcessor.java | 31 +++++++ 28 files changed, 534 insertions(+), 221 deletions(-) delete mode 100644 src/main/java/cleancode/studycafe/tobe/StudyCafeApplication.java delete mode 100644 src/main/java/cleancode/studycafe/tobe/StudyCafePassMachine.java delete mode 100644 src/main/java/cleancode/studycafe/tobe/io/InputHandler.java delete mode 100644 src/main/java/cleancode/studycafe/tobe/io/OutputHandler.java delete mode 100644 src/main/java/cleancode/studycafe/tobe/model/StudyCafePassType.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/StudyCafeApplication.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/StudyCafePassMachine.java rename src/main/java/cleancode/studycafe/{tobe => tobe2}/exception/AppException.java (74%) create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/ConsoleInputHandler.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/ConsoleOutputHandler.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/InputHandler.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/OutputHandler.java rename src/main/java/cleancode/studycafe/{tobe => tobe2}/io/StudyCafeFileHandler.java (90%) create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/StudyCafeLockerPassReader.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/StudyCafePassReader.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafeLockerPassFileReader.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafePassFileReader.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/FixedStudyCafeProcessor.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/HourlyStudyCafeProcessor.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/LocalStudyCafeProcessorFactory.java rename src/main/java/cleancode/studycafe/{tobe => tobe2}/model/StudyCafeLockerPass.java (96%) create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPasses.java rename src/main/java/cleancode/studycafe/{tobe => tobe2}/model/StudyCafePass.java (97%) create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/StudyCafePassType.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/StudyCafePasses.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessor.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessorFactory.java create mode 100644 src/main/java/cleancode/studycafe/tobe2/model/WeeklyStudyCafeProcessor.java diff --git a/src/main/java/cleancode/studycafe/tobe/StudyCafeApplication.java b/src/main/java/cleancode/studycafe/tobe/StudyCafeApplication.java deleted file mode 100644 index a60afb3f3..000000000 --- a/src/main/java/cleancode/studycafe/tobe/StudyCafeApplication.java +++ /dev/null @@ -1,10 +0,0 @@ -package cleancode.studycafe.tobe; - -public class StudyCafeApplication { - - public static void main(String[] args) { - StudyCafePassMachine studyCafePassMachine = new StudyCafePassMachine(); - studyCafePassMachine.run(); - } - -} diff --git a/src/main/java/cleancode/studycafe/tobe/StudyCafePassMachine.java b/src/main/java/cleancode/studycafe/tobe/StudyCafePassMachine.java deleted file mode 100644 index 3d34d9eef..000000000 --- a/src/main/java/cleancode/studycafe/tobe/StudyCafePassMachine.java +++ /dev/null @@ -1,81 +0,0 @@ -package cleancode.studycafe.tobe; - -import cleancode.studycafe.tobe.exception.AppException; -import cleancode.studycafe.tobe.io.InputHandler; -import cleancode.studycafe.tobe.io.OutputHandler; -import cleancode.studycafe.tobe.io.StudyCafeFileHandler; -import cleancode.studycafe.tobe.model.StudyCafeLockerPass; -import cleancode.studycafe.tobe.model.StudyCafePass; -import cleancode.studycafe.tobe.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/tobe/io/InputHandler.java b/src/main/java/cleancode/studycafe/tobe/io/InputHandler.java deleted file mode 100644 index 2f1ceb64d..000000000 --- a/src/main/java/cleancode/studycafe/tobe/io/InputHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package cleancode.studycafe.tobe.io; - -import cleancode.studycafe.tobe.exception.AppException; -import cleancode.studycafe.tobe.model.StudyCafePass; -import cleancode.studycafe.tobe.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/tobe/io/OutputHandler.java b/src/main/java/cleancode/studycafe/tobe/io/OutputHandler.java deleted file mode 100644 index d8e0181a4..000000000 --- a/src/main/java/cleancode/studycafe/tobe/io/OutputHandler.java +++ /dev/null @@ -1,68 +0,0 @@ -package cleancode.studycafe.tobe.io; - -import cleancode.studycafe.tobe.model.StudyCafeLockerPass; -import cleancode.studycafe.tobe.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/tobe/model/StudyCafePassType.java b/src/main/java/cleancode/studycafe/tobe/model/StudyCafePassType.java deleted file mode 100644 index bac959b4f..000000000 --- a/src/main/java/cleancode/studycafe/tobe/model/StudyCafePassType.java +++ /dev/null @@ -1,15 +0,0 @@ -package cleancode.studycafe.tobe.model; - -public enum StudyCafePassType { - - HOURLY("시간 단위 이용권"), - WEEKLY("주 단위 이용권"), - FIXED("1인 고정석"); - - private final String description; - - StudyCafePassType(String description) { - this.description = description; - } - -} diff --git a/src/main/java/cleancode/studycafe/tobe2/StudyCafeApplication.java b/src/main/java/cleancode/studycafe/tobe2/StudyCafeApplication.java new file mode 100644 index 000000000..769dfb813 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/StudyCafeApplication.java @@ -0,0 +1,18 @@ +package cleancode.studycafe.tobe2; + +import cleancode.studycafe.tobe2.io.ConsoleInputHandler; +import cleancode.studycafe.tobe2.io.ConsoleOutputHandler; +import cleancode.studycafe.tobe2.model.LocalStudyCafeProcessorFactory; + +public class StudyCafeApplication { + + public static void main(String[] args) { + StudyCafePassMachine studyCafePassMachine = new StudyCafePassMachine( + new ConsoleInputHandler(), + new ConsoleOutputHandler(), + new LocalStudyCafeProcessorFactory() + ); + studyCafePassMachine.run(); + } + +} diff --git a/src/main/java/cleancode/studycafe/tobe2/StudyCafePassMachine.java b/src/main/java/cleancode/studycafe/tobe2/StudyCafePassMachine.java new file mode 100644 index 000000000..a1ae228b0 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/StudyCafePassMachine.java @@ -0,0 +1,43 @@ +package cleancode.studycafe.tobe2; + +import cleancode.studycafe.tobe2.exception.AppException; +import cleancode.studycafe.tobe2.io.InputHandler; +import cleancode.studycafe.tobe2.io.OutputHandler; +import cleancode.studycafe.tobe2.io.StudyCafeFileHandler; +import cleancode.studycafe.tobe2.model.StudyCafeLockerPass; +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePassType; +import cleancode.studycafe.tobe2.model.StudyCafeProcessor; +import cleancode.studycafe.tobe2.model.StudyCafeProcessorFactory; + +import java.util.List; + +public class StudyCafePassMachine { + + private final InputHandler inputHandler; + private final OutputHandler outputHandler; + private final StudyCafeProcessorFactory studyCafeProcessorFactory; + + public StudyCafePassMachine(InputHandler inputHandler, OutputHandler outputHandler, StudyCafeProcessorFactory studyCafeProcessorFactory) { + this.inputHandler = inputHandler; + this.outputHandler = outputHandler; + this.studyCafeProcessorFactory = studyCafeProcessorFactory; + } + + public void run() { + try { + outputHandler.showWelcomeMessage(); + outputHandler.showAnnouncement(); + + outputHandler.askPassTypeSelection(); + StudyCafePassType studyCafePassType = inputHandler.getPassTypeSelectingUserAction(); + StudyCafeProcessor studyCafeProcessor = studyCafeProcessorFactory.createProcessor(studyCafePassType); + studyCafeProcessor.process(); + } catch (AppException e) { + outputHandler.showSimpleMessage(e.getMessage()); + } catch (Exception e) { + outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다."); + } + } + +} diff --git a/src/main/java/cleancode/studycafe/tobe/exception/AppException.java b/src/main/java/cleancode/studycafe/tobe2/exception/AppException.java similarity index 74% rename from src/main/java/cleancode/studycafe/tobe/exception/AppException.java rename to src/main/java/cleancode/studycafe/tobe2/exception/AppException.java index 4920bcba4..2d282a453 100644 --- a/src/main/java/cleancode/studycafe/tobe/exception/AppException.java +++ b/src/main/java/cleancode/studycafe/tobe2/exception/AppException.java @@ -1,4 +1,4 @@ -package cleancode.studycafe.tobe.exception; +package cleancode.studycafe.tobe2.exception; public class AppException extends RuntimeException { diff --git a/src/main/java/cleancode/studycafe/tobe2/io/ConsoleInputHandler.java b/src/main/java/cleancode/studycafe/tobe2/io/ConsoleInputHandler.java new file mode 100644 index 000000000..83739e1cb --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/ConsoleInputHandler.java @@ -0,0 +1,28 @@ +package cleancode.studycafe.tobe2.io; + +import cleancode.minesweeper.tobe.io.InputHandler; +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePassType; +import cleancode.studycafe.tobe2.model.StudyCafePasses; + +import java.util.Scanner; + +public class ConsoleInputHandler implements InputHandler { + private static final Scanner SCANNER = new Scanner(System.in); + + public StudyCafePassType getPassTypeSelectingUserAction() { + String userInput = SCANNER.nextLine(); + return StudyCafePassType.findByUserAction(userInput); + } + + public StudyCafePass getSelectPass(StudyCafePasses studyCafePasses) { + String userInput = SCANNER.nextLine(); + int selectedIndex = Integer.parseInt(userInput) - 1; + return studyCafePasses.get(selectedIndex); + } + + public boolean getLockerSelection() { + String userInput = SCANNER.nextLine(); + return "1".equals(userInput); + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/io/ConsoleOutputHandler.java b/src/main/java/cleancode/studycafe/tobe2/io/ConsoleOutputHandler.java new file mode 100644 index 000000000..7c527ff3a --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/ConsoleOutputHandler.java @@ -0,0 +1,67 @@ +package cleancode.studycafe.tobe2.io; + +import cleancode.minesweeper.tobe.io.OutputHandler; +import cleancode.studycafe.tobe2.model.StudyCafeLockerPass; +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePasses; + +public class ConsoleOutputHandler implements 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(StudyCafePasses passes) { + System.out.println(); + System.out.println("이용권 목록"); + for (int index = 0; index < passes.getSize(); 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/tobe2/io/InputHandler.java b/src/main/java/cleancode/studycafe/tobe2/io/InputHandler.java new file mode 100644 index 000000000..d1b13146c --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/InputHandler.java @@ -0,0 +1,13 @@ +package cleancode.studycafe.tobe2.io; + +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePassType; +import cleancode.studycafe.tobe2.model.StudyCafePasses; + +public interface InputHandler { + StudyCafePassType getPassTypeSelectingUserAction(); + + StudyCafePass getSelectPass(StudyCafePasses studyCafePasses); + + boolean getLockerSelection(); +} diff --git a/src/main/java/cleancode/studycafe/tobe2/io/OutputHandler.java b/src/main/java/cleancode/studycafe/tobe2/io/OutputHandler.java new file mode 100644 index 000000000..b494d032d --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/OutputHandler.java @@ -0,0 +1,23 @@ +package cleancode.studycafe.tobe2.io; + +import cleancode.studycafe.tobe2.model.StudyCafeLockerPass; +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePasses; + +import java.util.List; + +public interface OutputHandler { + void showWelcomeMessage(); + + void showAnnouncement(); + + void askPassTypeSelection(); + + void showPassListForSelection(StudyCafePasses studyCafePasses); + + void askLockerPass(StudyCafeLockerPass lockerPass); + + void showPassOrderSummary(StudyCafePass selectedPass, StudyCafeLockerPass lockerPass); + + void showSimpleMessage(String message); +} diff --git a/src/main/java/cleancode/studycafe/tobe/io/StudyCafeFileHandler.java b/src/main/java/cleancode/studycafe/tobe2/io/StudyCafeFileHandler.java similarity index 90% rename from src/main/java/cleancode/studycafe/tobe/io/StudyCafeFileHandler.java rename to src/main/java/cleancode/studycafe/tobe2/io/StudyCafeFileHandler.java index 920a27e59..48cfd02e5 100644 --- a/src/main/java/cleancode/studycafe/tobe/io/StudyCafeFileHandler.java +++ b/src/main/java/cleancode/studycafe/tobe2/io/StudyCafeFileHandler.java @@ -1,8 +1,8 @@ -package cleancode.studycafe.tobe.io; +package cleancode.studycafe.tobe2.io; -import cleancode.studycafe.tobe.model.StudyCafeLockerPass; -import cleancode.studycafe.tobe.model.StudyCafePass; -import cleancode.studycafe.tobe.model.StudyCafePassType; +import cleancode.studycafe.tobe2.model.StudyCafeLockerPass; +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePassType; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/cleancode/studycafe/tobe2/io/StudyCafeLockerPassReader.java b/src/main/java/cleancode/studycafe/tobe2/io/StudyCafeLockerPassReader.java new file mode 100644 index 000000000..d1b927656 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/StudyCafeLockerPassReader.java @@ -0,0 +1,7 @@ +package cleancode.studycafe.tobe2.io; + +import cleancode.studycafe.tobe2.model.StudyCafeLockerPasses; + +public interface StudyCafeLockerPassReader { + StudyCafeLockerPasses readLockerPasses(); +} diff --git a/src/main/java/cleancode/studycafe/tobe2/io/StudyCafePassReader.java b/src/main/java/cleancode/studycafe/tobe2/io/StudyCafePassReader.java new file mode 100644 index 000000000..bb606485b --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/StudyCafePassReader.java @@ -0,0 +1,7 @@ +package cleancode.studycafe.tobe2.io; + +import cleancode.studycafe.tobe2.model.StudyCafePasses; + +public interface StudyCafePassReader { + StudyCafePasses readStudyCafePasses(); +} diff --git a/src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafeLockerPassFileReader.java b/src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafeLockerPassFileReader.java new file mode 100644 index 000000000..aba8e0d1b --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafeLockerPassFileReader.java @@ -0,0 +1,35 @@ +package cleancode.studycafe.tobe2.io.file; + +import cleancode.studycafe.tobe2.io.StudyCafeLockerPassReader; +import cleancode.studycafe.tobe2.model.StudyCafeLockerPass; +import cleancode.studycafe.tobe2.model.StudyCafeLockerPasses; +import cleancode.studycafe.tobe2.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 StudyCafeLockerPassFileReader implements StudyCafeLockerPassReader { + @Override + public StudyCafeLockerPasses 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 StudyCafeLockerPasses.of(lockerPasses); + } catch (IOException e) { + throw new RuntimeException("파일을 읽는데 실패했습니다.", e); + } + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafePassFileReader.java b/src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafePassFileReader.java new file mode 100644 index 000000000..eb4482abc --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/io/file/StudyCafePassFileReader.java @@ -0,0 +1,36 @@ +package cleancode.studycafe.tobe2.io.file; + +import cleancode.studycafe.tobe2.io.StudyCafePassReader; +import cleancode.studycafe.tobe2.model.StudyCafePass; +import cleancode.studycafe.tobe2.model.StudyCafePassType; +import cleancode.studycafe.tobe2.model.StudyCafePasses; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class StudyCafePassFileReader implements StudyCafePassReader { + @Override + public StudyCafePasses 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.of(studyCafePasses); + } catch (IOException e) { + throw new RuntimeException("파일을 읽는데 실패했습니다.", e); + } + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/FixedStudyCafeProcessor.java b/src/main/java/cleancode/studycafe/tobe2/model/FixedStudyCafeProcessor.java new file mode 100644 index 000000000..3dae8b497 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/FixedStudyCafeProcessor.java @@ -0,0 +1,53 @@ +package cleancode.studycafe.tobe2.model; + +import cleancode.studycafe.tobe2.io.InputHandler; +import cleancode.studycafe.tobe2.io.OutputHandler; +import cleancode.studycafe.tobe2.io.StudyCafeLockerPassReader; +import cleancode.studycafe.tobe2.io.StudyCafePassReader; + +public class FixedStudyCafeProcessor implements StudyCafeProcessor { + private final StudyCafePassReader studyCafePassReader; + private final StudyCafeLockerPassReader studyCafeLockerPassReader; + private final InputHandler inputHandler; + private final OutputHandler outputHandler; + + public FixedStudyCafeProcessor(StudyCafePassReader studyCafePassReader, StudyCafeLockerPassReader studyCafeLockerPassReader, InputHandler inputHandler, OutputHandler outputHandler) { + this.studyCafePassReader = studyCafePassReader; + this.studyCafeLockerPassReader = studyCafeLockerPassReader; + this.inputHandler = inputHandler; + this.outputHandler = outputHandler; + } + + private static boolean isExists(StudyCafeLockerPass lockerPass) { + return lockerPass != null; + } + + @Override + public boolean satisfiedBy(StudyCafePassType passType) { + return passType == StudyCafePassType.FIXED; + } + + @Override + public void process() { + StudyCafePasses studyCafePasses = studyCafePassReader.readStudyCafePasses(); + StudyCafePasses fixedStudyCafePasses = studyCafePasses.findByType(StudyCafePassType.FIXED); + + outputHandler.showPassListForSelection(fixedStudyCafePasses); + StudyCafePass selectedPass = inputHandler.getSelectPass(fixedStudyCafePasses); + + StudyCafeLockerPasses lockerPasses = studyCafeLockerPassReader.readLockerPasses(); + StudyCafeLockerPass lockerPass = lockerPasses.getLockerPassByTypeAndDuration(selectedPass); + + boolean lockerSelection = false; + if (isExists(lockerPass)) { + outputHandler.askLockerPass(lockerPass); + lockerSelection = inputHandler.getLockerSelection(); + } + + if (lockerSelection) { + outputHandler.showPassOrderSummary(selectedPass, lockerPass); + } else { + outputHandler.showPassOrderSummary(selectedPass, null); + } + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/HourlyStudyCafeProcessor.java b/src/main/java/cleancode/studycafe/tobe2/model/HourlyStudyCafeProcessor.java new file mode 100644 index 000000000..134be9c25 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/HourlyStudyCafeProcessor.java @@ -0,0 +1,31 @@ +package cleancode.studycafe.tobe2.model; + +import cleancode.studycafe.tobe2.io.InputHandler; +import cleancode.studycafe.tobe2.io.OutputHandler; +import cleancode.studycafe.tobe2.io.StudyCafePassReader; + +public class HourlyStudyCafeProcessor implements StudyCafeProcessor{ + private final StudyCafePassReader studyCafePassReader; + private final InputHandler inputHandler; + private final OutputHandler outputHandler; + + public HourlyStudyCafeProcessor(StudyCafePassReader studyCafePassReader, InputHandler inputHandler, OutputHandler outputHandler) { + this.studyCafePassReader = studyCafePassReader; + this.inputHandler = inputHandler; + this.outputHandler = outputHandler; + } + + @Override + public boolean satisfiedBy(StudyCafePassType passType) { + return passType == StudyCafePassType.HOURLY; + } + + @Override + public void process() { + StudyCafePasses studyCafePasses = studyCafePassReader.readStudyCafePasses(); + StudyCafePasses hourlyPasses = studyCafePasses.findByType(StudyCafePassType.HOURLY); + outputHandler.showPassListForSelection(hourlyPasses); + StudyCafePass selectedPass = inputHandler.getSelectPass(hourlyPasses); + outputHandler.showPassOrderSummary(selectedPass, null); + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/LocalStudyCafeProcessorFactory.java b/src/main/java/cleancode/studycafe/tobe2/model/LocalStudyCafeProcessorFactory.java new file mode 100644 index 000000000..3d89aaec0 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/LocalStudyCafeProcessorFactory.java @@ -0,0 +1,38 @@ +package cleancode.studycafe.tobe2.model; + +import cleancode.minesweeper.tobe.io.OutputHandler; +import cleancode.studycafe.tobe2.exception.AppException; +import cleancode.studycafe.tobe2.io.ConsoleInputHandler; +import cleancode.studycafe.tobe2.io.ConsoleOutputHandler; +import cleancode.studycafe.tobe2.io.InputHandler; +import cleancode.studycafe.tobe2.io.StudyCafeLockerPassReader; +import cleancode.studycafe.tobe2.io.StudyCafePassReader; +import cleancode.studycafe.tobe2.io.file.StudyCafeLockerPassFileReader; +import cleancode.studycafe.tobe2.io.file.StudyCafePassFileReader; + +import java.util.List; + +public class LocalStudyCafeProcessorFactory implements StudyCafeProcessorFactory{ + private static final StudyCafePassReader studyCafePassReader = new StudyCafePassFileReader(); + private static final StudyCafeLockerPassReader studyCafeLockerPassReader = new StudyCafeLockerPassFileReader(); + private static final InputHandler inputHandler = new ConsoleInputHandler(); + private static final OutputHandler outputHandler = new ConsoleOutputHandler(); + + private static final List processors = List.of( + new HourlyStudyCafeProcessor(studyCafePassReader, inputHandler, outputHandler), + new WeeklyStudyCafeProcessor(studyCafePassReader, inputHandler, outputHandler), + new FixedStudyCafeProcessor(studyCafePassReader, studyCafeLockerPassReader, inputHandler, outputHandler) + ); + + public LocalStudyCafeProcessorFactory() { + + } + + @Override + public StudyCafeProcessor createProcessor(StudyCafePassType passType) { + return processors.stream() + .filter(processor -> processor.satisfiedBy(passType)) + .findFirst() + .orElseThrow(() -> new AppException("해당 이용권을 처리할 수 있는 프로세서가 없습니다.")); + } +} diff --git a/src/main/java/cleancode/studycafe/tobe/model/StudyCafeLockerPass.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPass.java similarity index 96% rename from src/main/java/cleancode/studycafe/tobe/model/StudyCafeLockerPass.java rename to src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPass.java index 6512ec0a8..0a8b6fafe 100644 --- a/src/main/java/cleancode/studycafe/tobe/model/StudyCafeLockerPass.java +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPass.java @@ -1,4 +1,4 @@ -package cleancode.studycafe.tobe.model; +package cleancode.studycafe.tobe2.model; public class StudyCafeLockerPass { diff --git a/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPasses.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPasses.java new file mode 100644 index 000000000..45df7490d --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeLockerPasses.java @@ -0,0 +1,25 @@ +package cleancode.studycafe.tobe2.model; + +import java.util.List; + +public class StudyCafeLockerPasses { + private final List studyCafeLockerPasses; + + private StudyCafeLockerPasses(List studyCafeLockerPasses) { + this.studyCafeLockerPasses = studyCafeLockerPasses; + } + + public static StudyCafeLockerPasses of(List studyCafeLockerPasses) { + return new StudyCafeLockerPasses(studyCafeLockerPasses); + } + + public StudyCafeLockerPass getLockerPassByTypeAndDuration(StudyCafePass studyCafePass) { + return studyCafeLockerPasses.stream() + .filter(option -> + option.getPassType() == studyCafePass.getPassType() + && option.getDuration() == studyCafePass.getDuration() + ) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/cleancode/studycafe/tobe/model/StudyCafePass.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePass.java similarity index 97% rename from src/main/java/cleancode/studycafe/tobe/model/StudyCafePass.java rename to src/main/java/cleancode/studycafe/tobe2/model/StudyCafePass.java index 0749f41b8..01cc19f48 100644 --- a/src/main/java/cleancode/studycafe/tobe/model/StudyCafePass.java +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePass.java @@ -1,4 +1,4 @@ -package cleancode.studycafe.tobe.model; +package cleancode.studycafe.tobe2.model; public class StudyCafePass { diff --git a/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePassType.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePassType.java new file mode 100644 index 000000000..501632389 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePassType.java @@ -0,0 +1,31 @@ +package cleancode.studycafe.tobe2.model; + +import cleancode.studycafe.tobe2.exception.AppException; + +public enum StudyCafePassType { + + + HOURLY("1", "시간 단위 이용권"), + WEEKLY("2", "주 단위 이용권"), + FIXED("3", "1인 고정석"); + + + private final String userAction; + private final String description; + + + StudyCafePassType(String userAction, String description) { + this.userAction = userAction; + this.description = description; + } + + + public static StudyCafePassType findByUserAction(String userAction) { + for (StudyCafePassType passType : values()) { + if (passType.userAction.equals(userAction)) { + return passType; + } + } + throw new AppException("잘못된 입력입니다."); + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePasses.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePasses.java new file mode 100644 index 000000000..93ee08a4a --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafePasses.java @@ -0,0 +1,30 @@ +package cleancode.studycafe.tobe2.model; + +import java.util.List; + +public class StudyCafePasses { + + private final List studyCafePasses; + + private StudyCafePasses(List studyCafePasses) { + this.studyCafePasses = studyCafePasses; + } + + public static StudyCafePasses of(List studyCafePasses) { + return new StudyCafePasses(studyCafePasses); + } + + public StudyCafePasses findByType(StudyCafePassType passType) { + return StudyCafePasses.of(studyCafePasses.stream() + .filter(studyCafePass -> studyCafePass.getPassType() == passType) + .toList()); + } + + public int getSize() { + return studyCafePasses.size(); + } + + public StudyCafePass get(int index) { + return studyCafePasses.get(index); + } +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessor.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessor.java new file mode 100644 index 000000000..19dc5ee37 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessor.java @@ -0,0 +1,6 @@ +package cleancode.studycafe.tobe2.model; + +public interface StudyCafeProcessor { + boolean satisfiedBy(StudyCafePassType passType); + void process(); +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessorFactory.java b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessorFactory.java new file mode 100644 index 000000000..d9b5a6bc2 --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/StudyCafeProcessorFactory.java @@ -0,0 +1,5 @@ +package cleancode.studycafe.tobe2.model; + +public interface StudyCafeProcessorFactory { + +} diff --git a/src/main/java/cleancode/studycafe/tobe2/model/WeeklyStudyCafeProcessor.java b/src/main/java/cleancode/studycafe/tobe2/model/WeeklyStudyCafeProcessor.java new file mode 100644 index 000000000..6aeb1919f --- /dev/null +++ b/src/main/java/cleancode/studycafe/tobe2/model/WeeklyStudyCafeProcessor.java @@ -0,0 +1,31 @@ +package cleancode.studycafe.tobe2.model; + +import cleancode.studycafe.tobe2.io.InputHandler; +import cleancode.studycafe.tobe2.io.OutputHandler; +import cleancode.studycafe.tobe2.io.StudyCafePassReader; + +public class WeeklyStudyCafeProcessor implements StudyCafeProcessor{ + private final StudyCafePassReader studyCafePassReader; + private final InputHandler inputHandler; + private final OutputHandler outputHandler; + + public WeeklyStudyCafeProcessor(StudyCafePassReader studyCafePassReader, InputHandler inputHandler, OutputHandler outputHandler) { + this.studyCafePassReader = studyCafePassReader; + this.inputHandler = inputHandler; + this.outputHandler = outputHandler; + } + + @Override + public boolean satisfiedBy(StudyCafePassType passType) { + return passType == StudyCafePassType.WEEKLY; + } + + @Override + public void process() { + StudyCafePasses studyCafePasses = studyCafePassReader.readStudyCafePasses(); + StudyCafePasses weeklyPasses = studyCafePasses.findByType(StudyCafePassType.WEEKLY); + outputHandler.showPassListForSelection(weeklyPasses); + StudyCafePass selectedPass = inputHandler.getSelectPass(weeklyPasses); + outputHandler.showPassOrderSummary(selectedPass, null); + } +}