Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[step3] 지뢰 찾기(게임 실행) #424

Open
wants to merge 18 commits into
base: bong6981
Choose a base branch
from
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
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
84 changes: 59 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## 🚀 1단계 - 지뢰 찾기(그리기)

### 프로그래밍 요구 사항
- 객체지향 생활 체조 원칙을 지키면서 프로그래밍한다.
객체지향 생활 체조 원칙을 지키면서 프로그래밍한다.

객체지향 생활 체조 원칙

Expand All @@ -17,6 +17,16 @@
8. 일급 컬렉션을 쓴다.
9. getter/setter/프로퍼티를 쓰지 않는다.

객체 지향 5원칙을 지키면서 프로그래밍한다.

객체지향 5원칙(SOLID)

- SRP (단일책임의 원칙: Single Responsibility Principle): 작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임(변화의 축: axis of change)을 수행하는 데 집중되어 있어야 한다
- OCP (개방폐쇄의 원칙: Open Close Principle): 소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
- LSP (리스코브 치환의 원칙: The Liskov Substitution Principle): 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다.
- ISP (인터페이스 분리의 원칙: Interface Segregation Principle): 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
- DIP (의존성역전의 원칙: Dependency Inversion Principle): 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전 원칙이다.

## 기능 요구사항
지뢰 찾기를 변형한 프로그램을 구현한다.

Expand Down Expand Up @@ -51,33 +61,57 @@ C C C C C C C C C C

### 기능 목록

- InputView
- 높이를 입력 받는다
- 너비를 입력 받는다
- 지뢰 개수를 입력 받는다
- MinesweeperController
- 입력 값으로부터 지뢰 게임 보드를 생성한다
#### `InputView` (InputProcessor 구현체)

- 높이를 입력 받는다
- 너비를 입력 받는다
- 지뢰 개수를 입력 받는다
- 오픈할 칸의 수를 입력 받는다

#### `MineBoardBuilder`

- 보드를 생성한다
- `MineBoardSize`
- 높이와 너비를 프로퍼티로 가지고, 모든 행 * 열에 대한 Position을 생성한다
- `Height`: InputView 로부터 입력 받아 높이를 생성한다
- 높이는 1이상 이어야 한다
- `Width`: InputView 로부터 입력 받아 너비를 생성한다
- 너비는 1이상 이어야 한다
- `MineCount`: InputView 로부터 입력 받아 지뢰 개수를 생성한다
- MineBoardBuilder
- PositionPicker를 받아, MineBoardSize를 정의하고, 지뢰가 설치되면 MineBoard를 생성한다
- `MineBoardSize`
- 높이와 너비를 프로퍼티로 가지고, 모든 행 * 열에 대한 Position을 생성한다
- `PositionsBuilder`
- 전체 위치 중에서 랜덤으로 지뢰위치를 선별한다
- `Positions`
- 전체 위치와 그 중 지뢰 위치에 대한 정보를 가진다
- PositionsBuilder : Positions 을 생성한다
- 요청된 지뢰 개수만큼 랜덤한 위치에 지뢰 생성한다
- MineCount: 지뢰 개수는 1이상이어야 한다
- MineBoard
- 셀들을 관리한다
- Positions으로부터 각 위치별 인접 지뢰 수에 대한 정보를 받아 Cell 을 생성한다
- Cell
- 셀을 열과 행에 대한 위치 정보, Position을 가진다
- Position
- 열과 행에 대한 정보를 가진다
- 자신 주변의 8개의 위치를 반환할 수 있다
- OutputView
- 지뢰를 포함한 보드 결과를 출력한다
- 각 셀들의 인접한 지뢰 수를 계산한다
- `CellsBuilder`
- positions 의 지뢰는 지뢰마크를, 나머지는 숫자마크를 새긴 셀들을 생성하고, 이 셀들을 필드로 가지는 보드를 생성한다

#### `MinesweeperController`

- 입력 값으로부터 지뢰 게임 보드를 생성한다
- 게임을 시작한다
- 게임이 종료될 때까지 게임을 진행한다

#### `MineBoardGame`

- 게임을 진행하면 오픈할 칸을 입력 받아 지뢰 게임을 진행한다
- 오픈한 셀이 지뢰가 아닐 경우 인접한 칸 중 지뢰가 아닌 칸을 모두 연다
- 오픈한 셀이 지뢰일 경우 게임을 종료한다

#### `MineBoard`

- 셀들을 관리한다
- cell 을 오픈한다
- 오픈한 셀이 지뢰일 경우 지뢰를 반환한다

#### `Cell`

- 셀을 열과 행에 대한 위치 정보, Position을 가진다
- `Position`
- 열과 행에 대한 정보를 가진다
- 자신 주변의 8개의 위치를 반환할 수 있다
- 셀을 열 수 있다
- 셀을 열면 마크가 반환된다

#### `OutputView`
- 지뢰를 포함한 보드 결과를 출력한다
- 게임 결과를 출력한다
8 changes: 6 additions & 2 deletions src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import minesweeper.MinesweeperController
import minesweeper.controller.MinesweeperController
import minesweeper.view.InputView
import minesweeper.view.OutputView

fun main() {
MinesweeperController.start()
val inputProvider = InputView()
val outputConsumer = OutputView()
MinesweeperController(inputProvider, outputConsumer).start()
}
30 changes: 0 additions & 30 deletions src/main/kotlin/minesweeper/MinesweeperController.kt

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/kotlin/minesweeper/controller/InputPosition.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package minesweeper.controller

data class InputPosition(
val row: Int,
val column: Int,
)
11 changes: 11 additions & 0 deletions src/main/kotlin/minesweeper/controller/InputProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package minesweeper.controller

interface InputProvider {
fun height(): Int

fun width(): Int

fun mineCount(): Int

fun openPosition(): InputPosition
}
55 changes: 55 additions & 0 deletions src/main/kotlin/minesweeper/controller/MinesweeperController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package minesweeper.controller

import minesweeper.domain.board.Height
import minesweeper.domain.board.MineBoard
import minesweeper.domain.board.MineTotal
import minesweeper.domain.board.Width
import minesweeper.domain.board.mineBoard
import minesweeper.domain.game.MinesweeperGame
import minesweeper.domain.position.Position
import minesweeper.domain.position.RandomPositionPicker

class MinesweeperController(
private val inputProvider: InputProvider,
private val outputProvider: OutputConsumer,
) {
fun start() {
val board = createBoard()
val game = createGame(board)
runGame(game)
}

private fun createBoard(): MineBoard {
val height = inputProvider.height().let(::Height)
val width = inputProvider.width().let(::Width)
val mineCount = inputProvider.mineCount().let(::MineTotal)

return mineBoard(RandomPositionPicker()) {
size(width * height)
mineCount(mineCount)
}
}

private fun createGame(board: MineBoard): MinesweeperGame =
MinesweeperGame(board) {
inputProvider.openPosition().let {
Position(
row = it.row,
column = it.column
)
}
}

private fun runGame(game: MinesweeperGame) {
while (game.isEnd().not()) {
game.run()
showGame(game)
}
}

private fun showGame(game: MinesweeperGame) {
val result = game.result
if (result != null) outputProvider.showGameResult(result)
else outputProvider.showBoard(game.board)
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/minesweeper/controller/OutputConsumer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package minesweeper.controller

import minesweeper.domain.board.MineBoard
import minesweeper.domain.game.GameResult

interface OutputConsumer {
fun showBoard(board: MineBoard)

fun showGameResult(result: GameResult)
}
11 changes: 0 additions & 11 deletions src/main/kotlin/minesweeper/domain/board/Height.kt

This file was deleted.

34 changes: 23 additions & 11 deletions src/main/kotlin/minesweeper/domain/board/MineBoard.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
package minesweeper.domain.board

import minesweeper.domain.cell.Cell
import minesweeper.domain.cell.CellMark
import minesweeper.domain.position.Position

data class MineBoard(
val cells: Set<Cell>,
val cells: Map<Position, Cell>,
) {
fun open(position: Position): Cell.Clear {
val cell = getCell(position)
check(cell is Cell.Clear)
return cell.open()
}

fun isMine(position: Position): Boolean =
getCell(position) is Cell.Mine

companion object {
fun from(positions: Positions): MineBoard {
val adjacentMineCountByPositions = positions.adjacentMineCountByPosition
val cells = adjacentMineCountByPositions.map { (position, mineCount) ->
if (positions.isMine(position)) Cell(position, CellMark.MINE)
else Cell(position, CellMark.from(mineCount))
}.toSet()
return MineBoard(cells)
}
fun isOpened(position: Position): Boolean {
val cell = getCell(position)
check(cell is Cell.Clear)
return cell.isOpened()
}

fun isValidPosition(position: Position): Boolean =
cells[position] != null

fun isAllOpened(): Boolean =
cells.values.none { it is Cell.Clear && it.isOpened().not() }

private fun getCell(position: Position): Cell = cells[position]
?: throw IllegalArgumentException("보드에 정의된 위치가 아닙니다")
}
34 changes: 23 additions & 11 deletions src/main/kotlin/minesweeper/domain/board/MineBoardBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package minesweeper.domain.board

import minesweeper.domain.cell.Cell
import minesweeper.domain.cell.cells
import minesweeper.domain.position.Position
import minesweeper.domain.position.PositionPicker
import minesweeper.domain.position.Positions
import minesweeper.domain.position.positions

fun mineBoard(
minePicker: PositionPicker,
block: MineBoardBuilder.() -> Unit
Expand All @@ -9,23 +16,28 @@ class MineBoardBuilder(
private val minePicker: PositionPicker,
) {
private lateinit var size: MineBoardSize
private lateinit var mineCount: MineCount
private lateinit var mineCount: MineTotal

fun size(height: Height, width: Width) {
size = MineBoardSize(height, width)
fun size(size: MineBoardSize) {
this.size = size
}

fun mineCount(count: MineCount) {
operator fun Width.times(height: Height): MineBoardSize = MineBoardSize(height, this)

fun mineCount(count: MineTotal) {
mineCount = count
}

private fun positions(): Positions =
positions(minePicker) {
allPositions(size.allPositionsOfRowAndColumns)
mineCount(mineCount)
fun build(): MineBoard = MineBoard(createCells())

private fun createCells(): Map<Position, Cell> =
cells {
positions(createPositions())
}

fun build(): MineBoard {
return MineBoard.from(positions())
}
private fun createPositions(): Positions =
positions(minePicker) {
allPositions(size.allPositions)
mineTotal(mineCount)
}
}
24 changes: 22 additions & 2 deletions src/main/kotlin/minesweeper/domain/board/MineBoardSize.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package minesweeper.domain.board

import minesweeper.domain.cell.Position
import minesweeper.domain.position.Position

data class MineBoardSize(
val height: Height,
val width: Width,
) {
val allPositionsOfRowAndColumns: Set<Position> by lazy {
val allPositions: Set<Position> by lazy {
createAllPositions(height, width)
}

Expand All @@ -18,3 +18,23 @@ data class MineBoardSize(
private infix fun Width.createPositionForColumnsInRow(row: Int): List<Position> =
this.columnRange.map { Position(row = row, column = it) }
}

data class Height(
val value: Int
) {
init {
require(value > 0) { "높이는 0보다 커야 합니다" }
}

val rowRange: IntRange = 0 until value
}

data class Width(
val value: Int
) {
init {
require(value > 0) { "너비는 0보다 커야 합니다" }
}

val columnRange: IntRange = 0 until value
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package minesweeper.domain.board

data class MineCount(
data class MineTotal(
val value: Int
) {
init {
Expand Down
Loading