Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
37434cc
feat(추상): 추상 - 이름 짓기
LeeHyungGeol Oct 3, 2024
abbc409
feat(추상): 추상 - 메서드 선언부
LeeHyungGeol Oct 3, 2024
138fbeb
feat(추상): 추상 - 추상화 레벨
LeeHyungGeol Oct 3, 2024
8479a0d
feat(추상): 매직 넘버, 매직 스트링
LeeHyungGeol Oct 4, 2024
44107dd
feat(논리,-사고의-흐름): [논리, 사고의 흐름] Early Return
LeeHyungGeol Oct 4, 2024
5529e4f
feat(논리,-사고의-흐름): [논리, 사고의 흐름] 사고의 depth 줄이기
LeeHyungGeol Oct 4, 2024
d0ce33a
feat(논리,-사고의-흐름): [논리, 사고의 흐름] 공백 라인을 대하는 자세
LeeHyungGeol Oct 4, 2024
a04e022
feat(논리,-사고의-흐름): [논리, 사고의 흐름] 부정어를 대하는 자세
LeeHyungGeol Oct 4, 2024
e5dd92c
feat(논리,-사고의-흐름): [논리, 사고의 흐름] 해피 케이스와 예외 처리
LeeHyungGeol Oct 4, 2024
c4cc310
feat(객체-지향-패러다임): [객체 지향 패러다임] 객체 설계하기
LeeHyungGeol Oct 14, 2024
f40102a
feat(객체-지향-패러다임): [객체 지향 패러다임] SRP
LeeHyungGeol Oct 15, 2024
3e7ba5f
feat(객체-지향-패러다임): [객체 지향 패러다임] OCP
LeeHyungGeol Oct 15, 2024
1c1dff9
feat(객체-지향-패러다임): [객체 지향 패러다임] LSP
LeeHyungGeol Oct 16, 2024
2019ceb
feat(객체-지향-패러다임): [객체 지향 패러다임] ISP
LeeHyungGeol Oct 16, 2024
173ead6
feat(객체-지향-패러다임): [객체 지향 패러다임] DIP
LeeHyungGeol Oct 17, 2024
579ffd5
feat(객체-지향-적용): 상속 대신 조합, Value Object
LeeHyungGeol Mar 12, 2025
73ca430
feat(객체-지향-적용하기): 일급 컬렉션
LeeHyungGeol Mar 12, 2025
002fdfa
mission/day-7
LeeHyungGeol Mar 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
34 changes: 34 additions & 0 deletions src/main/java/cleancode/minesweeper/tobe/BoardIndexConverter.java
Original file line number Diff line number Diff line change
@@ -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) {
String cellInputRow = cellInput.substring(1);
return convertRowFrom(cellInputRow);
}

public int getSelectedColIndex(String cellInput) {
char cellInputCol = cellInput.charAt(0);
return convertColFrom(cellInputCol);
}

private int convertRowFrom(String cellInputRow) {
int rowIndex = Integer.parseInt(cellInputRow) - 1;
if (rowIndex < 0) {
throw new GameException("잘못된 입력입니다.");
}

return rowIndex;
}

private int convertColFrom(char cellInputCol) {
int colIndex = cellInputCol - BASE_CHAR_FOR_COL;
if (colIndex < 0) {
throw new GameException("잘못된 입력입니다.");
}

return colIndex;
}
}
24 changes: 24 additions & 0 deletions src/main/java/cleancode/minesweeper/tobe/GameApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cleancode.minesweeper.tobe;

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 {

// 이 클래스는 딱 프로그램 실행에 진입점만 가지게 된다.
// 이름도 MinesweeperGame 에서 GameApplication 으로 변경한다. -> 이렇게 변경하면 지뢰찾기게임(Minesweeper 뿐만이 아닌 다른 게임도 실행할 수 있게 된다.)
// 게임 실행에 대한 책임과 지뢰찾기 도메인 자체, 지뢰찾기 게임을 담당하는 역할을 분리했다.
public static void main(String[] args) {
GameLevel gameLevel = new Middle();
InputHandler inputHandler = new ConsoleInputHandler();
ConsoleOutputHandler outputHandler = new ConsoleOutputHandler();

Minesweeper minesweeper = new Minesweeper(gameLevel, inputHandler, outputHandler);
minesweeper.initialize();
minesweeper.run();
}
}
158 changes: 158 additions & 0 deletions src/main/java/cleancode/minesweeper/tobe/GameBoard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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.List;
public class GameBoard {

private final Cell[][] board;
private final int landMineCount;

public GameBoard(GameLevel gameLevel) {
int rowSize = gameLevel.getRowSize();
int colSize = gameLevel.getColSize();
board = new Cell[rowSize][colSize];

landMineCount = gameLevel.getLandMineCount();
}

public void flagAt(CellPosition cellPosition) {
Cell cell = findCell(cellPosition);
cell.flag();
}

public void openAt(CellPosition cellPosition) {
Cell cell = findCell(cellPosition);
cell.open();
}

public void openSurroundedCells(CellPosition cellPosition) {
if (isOpenedCell(cellPosition)) {
return;
}
if (isLandMineCellAt(cellPosition)) {
return;
}

openAt(cellPosition);

if (doesCellHaveLandMineCount(cellPosition)) {
return;
}

List<CellPosition> surroundedPositions = calculateSurroundedPositions(cellPosition, getRowSize(), getColSize());
surroundedPositions.forEach(this::openSurroundedCells);
}

private boolean doesCellHaveLandMineCount(CellPosition cellPosition) {
Cell cell = findCell(cellPosition);
return cell.hasLandMineCount();
}

private boolean isOpenedCell(CellPosition cellPosition) {
Cell cell = findCell(cellPosition);
return cell.isOpened();
}

public boolean isLandMineCellAt(CellPosition cellPosition) {
Cell cell = findCell(cellPosition);
return cell.isLandMine();
}

public boolean isAllCellChecked() {
Cells cells = Cells.from(board);
return cells.isAllChecked();
}

public boolean isInvalidCellPosition(CellPosition cellPosition) {
int rowSize = getRowSize();
int colSize = getColSize();

return cellPosition.isRowIndexMoreThanOrEqual(rowSize)
|| cellPosition.isColIndexMoreThanOrEqual(colSize);
}

public void initializeGame() {
CellPositions cellPositions = CellPositions.from(board);

initializeEmptyCells(cellPositions);

List<CellPosition> landMinePositions = cellPositions.extractRandomPositions(landMineCount);
initializeLandMineCells(landMinePositions);

List<CellPosition> numberPositionCandidates = cellPositions.subtract(landMinePositions);
initializeNumberCells(numberPositionCandidates);
}

private void initializeEmptyCells(CellPositions cellPositions) {
List<CellPosition> allPositions = cellPositions.getPositions();
for (CellPosition position : allPositions) {
updateCellAt(position, new EmptyCell());
}
}

private void initializeLandMineCells(List<CellPosition> landMinePositions) {
for (CellPosition position : landMinePositions) {
updateCellAt(position, new LandMineCell());
}
}

private void initializeNumberCells(List<CellPosition> 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();
}

private Cell findCell(CellPosition cellPosition) {
return board[cellPosition.getRowIndex()][cellPosition.getColIndex()];
}

public int getRowSize() {
return board.length;
}

public int getColSize() {
return board[0].length;
}

private int countNearbyLandMines(CellPosition cellPosition) {
int rowSize = getRowSize();
int colSize = getColSize();

long count = calculateSurroundedPositions(cellPosition, rowSize, colSize).stream()
.filter(this::isLandMineCellAt)
.count();

return (int) count;
}

private List<CellPosition> calculateSurroundedPositions(CellPosition cellPosition, int rowSize, int colSize) {
return RelativePosition.SURROUNDED_POSITIONS.stream()
.filter(cellPosition::canCalculatePositionBy)
.map(cellPosition::calculatePositionBy)
.filter(position -> position.isRowIndexLessThan(rowSize))
.filter(position -> position.isColIndexLessThan(colSize))
.toList();
}

}
7 changes: 7 additions & 0 deletions src/main/java/cleancode/minesweeper/tobe/GameException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cleancode.minesweeper.tobe;

public class GameException extends RuntimeException {
public GameException(String message) {
super(message);
}
}
130 changes: 130 additions & 0 deletions src/main/java/cleancode/minesweeper/tobe/Minesweeper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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.InputHandler;
import cleancode.minesweeper.tobe.io.OutputHandler;
import cleancode.minesweeper.tobe.positoion.CellPosition;

public class Minesweeper implements GameInitializable, GameRunnable {

// BOARD 도 하는 일이 너무 많고 중요하기 때문에 Minesweeper 클래스 내부에 상수로 두기에는 너무 책임이 과도하다.
// 이렇게 GameBoard 클래스를 두면 Minesweeper 입장에서는 Cell[][] 이중배열에 대해서는 모른다.
// 객체로 추상화가 되었고, 데이터 구조에 대한 것은 캐슐화가 되었기 때문이다.
private final GameBoard gameBoard;
// SRP: cellInput 이라는 사용자의 입력을 받아서 rowIndex, colIndex 로 변환하는 역할을 하는 또 하나의 클래스로 볼 수 있지 않을까?

// 게임이 진행되는 핵심 로직들과 사용자 입출력에 대한 로직 책임을 분리한다.
// DIP: InputHandler, OutputHandler 는 이제 Console 에 관한 것은 모른다. 인터페이스만 의존하고 있다.
// 구현체가 변경되어도 Minesweeper 클래스는 영향을 받지 않는다.
private final InputHandler inputHandler;
private final OutputHandler outputHandler;
private int gameStatus = 0; // 0: 게임 중, 1: 승리, -1: 패배

public Minesweeper(GameLevel gameLevel, InputHandler inputHandler, OutputHandler outputHandler) {
gameBoard = new GameBoard(gameLevel);
this.inputHandler = inputHandler;
this.outputHandler = outputHandler;
}

@Override
public void initialize() {
gameBoard.initializeGame();
}

@Override
public void run() {
outputHandler.showGameStartComments();

while (true) {
try {
outputHandler.showBoard(gameBoard);

if (doesUserWinTheGame()) {
outputHandler.showGameWinningComment();
break;
}
if (doesUserLoseTheGame()) {
outputHandler.showGameLosingComment();
break;
}

CellPosition cellPosition = getCellInputFromUser();
String userActionInput = getUserActionInputFromUser();
actOnCell(cellPosition, userActionInput);
} catch (GameException e) {
outputHandler.showExceptionMessage(e);
} catch (Exception e) {
outputHandler.showSimpleMessage("프로그램에 문제가 생겼습니다.");
}
}
}

private void actOnCell(CellPosition cellPosition, String userActionInput) {
if (doesUserChooseToPlantFlag(userActionInput)) {
gameBoard.flagAt(cellPosition);
checkIfGameIsOver();
return;
}

if (doesUserChooseToOpenCell(userActionInput)) {
if (gameBoard.isLandMineCellAt(cellPosition)) {
gameBoard.openAt(cellPosition);
changeGameStatusToLose();
return;
}

gameBoard.openSurroundedCells(cellPosition);
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 String getUserActionInputFromUser() {
outputHandler.showCommentForUserAction();
return inputHandler.getUserInput();
}

private CellPosition getCellInputFromUser() {
outputHandler.showCommentForSelectingCell();
CellPosition cellPosition = inputHandler.getCellPositionFromUser();
if (gameBoard.isInvalidCellPosition(cellPosition)) {
throw new GameException("잘못된 좌표를 선택하셨습니다.");
}

return cellPosition;
}

private boolean doesUserLoseTheGame() {
return gameStatus == -1;
}

private boolean doesUserWinTheGame() {
return gameStatus == 1;
}

private void checkIfGameIsOver() {
if (gameBoard.isAllCellChecked()) {
changeGameStatusToWin();
}
}

private void changeGameStatusToWin() {
gameStatus = 1;
}

}
Loading