Skip to content

Commit a0c9f00

Browse files
authored
πŸš€ 3단계 - μ§€λ’° μ°ΎκΈ°(κ²Œμž„ μ‹€ν–‰) (#480)
* rename: findCellByCoordinate ν•¨μˆ˜ 넀이밍 μˆ˜μ • * refactor: λ©”μ„œλ“œ 체이닝을 λ³€μˆ˜λ‘œ μΆ”μΆœν•˜κ³ , ν•œμ€„μ— ν•˜λ‚˜μ˜ λ©”μ„œλ“œ 호좜만 ν•˜λ„λ‘ μˆ˜μ • * refactor: Coordinate(height, width)λ‘œλ„ Coordinate 객체λ₯Ό 생성할 수 μžˆλŠ” μ½”λ“œ 제곡 * docs: μš”κ΅¬μ‚¬ν•­ 정리 * feat: μ‚¬μš©μžλ‘œλΆ€ν„° μ’Œν‘œλ₯Ό μž…λ ₯λ°›λŠ”λ‹€. * feat: 셀이 open된 셀인지, close된 셀인지에 λŒ€ν•œ μƒνƒœλ₯Ό κ°–λŠ”λ‹€. * feat: κ²Œμž„ 승리 or 패배 μŠ€μΌˆλ ˆν†€ μ½”λ“œ * rename: ν…ŒμŠ€νŠΈ 클래슀 넀이밍 μˆ˜μ • * test: κ²Œμž„ 승리 or 패배 ν…ŒμŠ€νŠΈμ½”λ“œ μž‘μ„± * feat: ν•˜λ‚˜ μ΄μƒμ˜ μ§€λ’° 셀을 μ—΄μ—ˆμœΌλ©΄ κ²Œμž„μ—μ„œ νŒ¨λ°°ν•œλ‹€. * test: open λ˜μ–΄ μžˆλŠ” μ§€λ’°μ…€κ³Ό λΉ„μ–΄μžˆλŠ” μ…€μ˜ 개수λ₯Ό μ„Όλ‹€. * feat: λͺ¨λ“  빈 셀을 μ—΄μ—ˆμœΌλ©΄ κ²Œμž„μ—μ„œ μŠΉλ¦¬ν•œλ‹€. * test: μ§€λ’° 셀을 open ν•˜μ§€ μ•Šμ•˜κ³ , λΉ„μ–΄μžˆλŠ” λͺ¨λ“  칸을 open ν•˜μ§€ μ•Šμ€ 경우 κ²Œμž„μ„ κ³„μ†ν•œλ‹€. * refactor: isContinueGame ν•¨μˆ˜ when μ‹μœΌλ‘œ λ³€κ²½ * feat: μž…λ ₯받은 μ’Œν‘œμ— ν•΄λ‹Ήν•˜λŠ” 셀을 μ˜€ν”ˆν•œλ‹€. * feat: μž…λ ₯ν•œ 셀에 μ§€λ’°κ°€ μ—†λ‹€λ©΄, μ—°κ²°λœ empty을 λͺ¨λ‘ Open * feat: μž…λ ₯ν•œ 셀에 μ§€λ’°κ°€ μ—†λ‹€λ©΄, μ—°κ²°λœ empty을 λͺ¨λ‘ Open * refactor: dfs -> bfs λ³€κ²½ 및 λ©”μ„œλ“œ μΆ”μΆœ * test: getAdjacentCoordinates ν•¨μˆ˜ ν…ŒμŠ€νŠΈ * fix: isContinueGame() ν•¨μˆ˜μ—μ„œ GameResult에 λŒ€ν•œ 관심사λ₯Ό λΆ„λ¦¬ν•œλ‹€. * fix: MineSweeperGame 객체 GameResult에 λŒ€ν•œ μƒνƒœλ₯Ό μ œκ±°ν•œλ‹€. * feat: κ²Œμž„ κ²°κ³Όλ₯Ό μ‘°νšŒν•œλ‹€. * fix: mineCell도 μ˜€ν”ˆν•  수 μžˆλ„λ‘ λ³€κ²½ * fix: compareTo 제거 * fix: Cell νƒ€μž…μ„ ꡬ뢄할 수 μžˆλŠ” λ©”μ„œλ“œλ₯Ό 톡해 μ§€λ’°μ…€κ³Ό λΉ„μ–΄μžˆλŠ” 셀을 필터링 * fix: showMineSweeperBoard 그룹핑을 row(μ›μ‹œκ°’) -> Row(λž˜ν•‘ 클래슀)둜 λ³€κ²½ * refactor: shouldSkipCell() λ©”μ„œλ“œλ₯Ό μ œκ±°ν•˜κ³  cell μ—κ²Œ λ©”μ„Έμ§€λ₯Ό λ³΄λ‚΄λŠ” ꡬ쑰둜 λ³€κ²½ * refactor: isAllEmptyCellsOpened() ν•¨μˆ˜λ₯Ό cells μ—κ²Œ λ©”μ„Έμ§€λ₯Ό λ³΄λ‚΄λŠ” ꡬ쑰둜 λ³€κ²½
1 parent ebd2fd1 commit a0c9f00

File tree

15 files changed

+689
-56
lines changed

15 files changed

+689
-56
lines changed

β€Ždocs/step3.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# πŸš€ 3단계 - μ§€λ’° μ°ΎκΈ°(κ²Œμž„ μ‹€ν–‰)
2+
3+
## κΈ°λŠ₯ μš”κ΅¬μ‚¬ν•­
4+
5+
- 높이와 λ„ˆλΉ„, μ§€λ’° 개수λ₯Ό μž…λ ₯받을 수 μžˆλ‹€.
6+
- μ§€λ’°λŠ” λˆˆμ— 잘 λ„λŠ” κ²ƒμœΌλ‘œ ν‘œκΈ°ν•œλ‹€.
7+
- μ§€λ’°λŠ” 가급적 λžœλ€μ— κ°€κΉκ²Œ λ°°μΉ˜ν•œλ‹€.
8+
- 각 μ‚¬κ°ν˜•μ— ν‘œμ‹œλ  μˆ«μžλŠ” μžμ‹ μ„ μ œμ™Έν•œ μ£Όλ³€ 8개 μ‚¬κ°ν˜•μ— ν¬ν•¨λœ μ§€λ’°μ˜ κ°œμˆ˜λ‹€.
9+
- μ§€λ’°κ°€ μ—†λŠ” μΈμ ‘ν•œ 칸이 λͺ¨λ‘ μ—΄λ¦¬κ²Œ λœλ‹€.
10+
11+
### κΈ°λŠ₯ 리슀트
12+
13+
- [x] μ‚¬μš©μžλ‘œλΆ€ν„° μ’Œν‘œλ₯Ό μž…λ ₯λ°›λŠ”λ‹€.
14+
- [x] 셀이 open된 셀인지, close된 셀인지에 λŒ€ν•œ μƒνƒœλ₯Ό κ°–λŠ”λ‹€.
15+
- [x] ν•˜λ‚˜ μ΄μƒμ˜ μ§€λ’° 셀을 μ—΄μ—ˆμœΌλ©΄ κ²Œμž„μ—μ„œ νŒ¨λ°°ν•œλ‹€.
16+
- [x] λͺ¨λ“  빈 셀을 μ—΄μ—ˆμœΌλ©΄ κ²Œμž„μ—μ„œ μŠΉλ¦¬ν•œλ‹€.
17+
- [x] μž…λ ₯ν•œ 셀에 μ§€λ’°κ°€ μ—†λ‹€λ©΄, μ—°κ²°λœ empty을 λͺ¨λ‘ Open
18+
- [x] close 된 셀은 Cλ₯Ό 좜λ ₯
19+
- [x] open 된 셀은 μΈμ ‘ν•œ μ§€λ’° 개수λ₯Ό 좜λ ₯

β€Žsrc/main/kotlin/controller/MineSweeperController.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controller
33
import domain.Cells
44
import domain.MineBoard
55
import domain.MineGameMetric
6+
import domain.MineSweeperGame
67
import domain.strategy.RandomMineCellGenerator
78
import view.InputView
89
import view.OutputView
@@ -16,7 +17,20 @@ class MineSweeperController {
1617

1718
val cells = Cells.generateWithMines(mineGameMetric, RandomMineCellGenerator())
1819
val mineBoard = MineBoard(mineGameMetric, cells)
19-
2020
OutputView.showMineSweeperBoard(mineBoard)
21+
22+
gameLoop(mineBoard)
23+
}
24+
25+
private fun gameLoop(mineBoard: MineBoard) {
26+
val mineSweeperGame = MineSweeperGame(mineBoard)
27+
while (mineSweeperGame.isContinueGame()) {
28+
val coordinate = InputView.askMineCoordinate()
29+
mineSweeperGame.openAdjacentCell(coordinate)
30+
OutputView.showMineSweeperBoard(mineBoard)
31+
}
32+
33+
val result = mineSweeperGame.getGameResult()
34+
OutputView.showGameResult(result)
2135
}
2236
}

β€Žsrc/main/kotlin/domain/Cell.kt

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,41 @@ package domain
22

33
sealed interface Cell {
44
val coordinate: Coordinate
5+
val status: CellStatus
56

6-
fun isMineCell(): Boolean {
7-
return this is MineCell
7+
fun isMineCell(): Boolean
8+
9+
fun open()
10+
11+
fun isAlreadyOpened(): Boolean {
12+
return status == CellStatus.OPEN
13+
}
14+
15+
data class MineCell(
16+
override val coordinate: Coordinate,
17+
private var _status: CellStatus = CellStatus.CLOSED,
18+
) : Cell {
19+
override val status: CellStatus
20+
get() = _status
21+
22+
override fun open() {
23+
_status = CellStatus.OPEN
24+
}
25+
26+
override fun isMineCell(): Boolean = true
827
}
928

10-
data class MineCell(override val coordinate: Coordinate) : Cell
29+
data class EmptyCell(
30+
override val coordinate: Coordinate,
31+
private var _status: CellStatus = CellStatus.CLOSED,
32+
) : Cell {
33+
override val status: CellStatus
34+
get() = _status
1135

12-
data class EmptyCell(override val coordinate: Coordinate) : Cell
36+
override fun open() {
37+
_status = CellStatus.OPEN
38+
}
39+
40+
override fun isMineCell(): Boolean = false
41+
}
1342
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package domain
2+
3+
enum class CellStatus {
4+
OPEN,
5+
CLOSED,
6+
}

β€Žsrc/main/kotlin/domain/Cells.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,34 @@ import domain.strategy.MineCellGenerator
88

99
@JvmInline
1010
value class Cells(val cells: List<Cell>) {
11-
fun mineCells(): List<MineCell> = cells.filterIsInstance<MineCell>()
11+
fun mineCells(): List<Cell> = cells.filter { it.isMineCell() }.toList()
1212

13-
fun emptyCells(): List<EmptyCell> = cells.filterIsInstance<EmptyCell>()
13+
fun emptyCells(): List<Cell> = cells.filter { it.isMineCell().not() }.toList()
1414

15-
fun getCoordinateIs(coordinate: Coordinate): Cell {
15+
fun get(coordinate: Coordinate): Cell {
1616
return cells.firstOrNull { it.coordinate == coordinate }
1717
?: throw NoSuchElementException("Coordinate $coordinate not found")
1818
}
1919

20+
fun countOpenedMineCells(): Int {
21+
return mineCells().count { it.status == CellStatus.OPEN }
22+
}
23+
24+
fun countOpenedEmptyCells(): Int {
25+
return emptyCells().count { it.status == CellStatus.OPEN }
26+
}
27+
28+
fun countEmptyCells(): Int {
29+
return emptyCells().size
30+
}
31+
32+
fun isAllEmptyCellsOpened(): Boolean {
33+
val openedEmptyCellCount = countOpenedEmptyCells()
34+
val totalEmptyCellCount = countEmptyCells()
35+
36+
return openedEmptyCellCount == totalEmptyCellCount
37+
}
38+
2039
companion object {
2140
fun generateWithMines(
2241
mineGameMetric: MineGameMetric,
@@ -42,7 +61,7 @@ value class Cells(val cells: List<Cell>) {
4261

4362
return heightRange.flatMap { height ->
4463
widthRange.map { width ->
45-
Coordinate(Row(height), Col(width))
64+
Coordinate(height, width)
4665
}
4766
}
4867
}
@@ -51,7 +70,10 @@ value class Cells(val cells: List<Cell>) {
5170
mineCellGenerator: MineCellGenerator,
5271
mineGameMetric: MineGameMetric,
5372
): Set<Coordinate> {
54-
return mineCellGenerator.execute(mineGameMetric).map { it.coordinate }.toSet()
73+
val mineCell = mineCellGenerator.execute(mineGameMetric)
74+
return mineCell
75+
.map { it.coordinate }
76+
.toSet()
5577
}
5678

5779
private fun parseCell(

β€Žsrc/main/kotlin/domain/Coordinate.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ data class Coordinate(val row: Row, val col: Col) {
44
operator fun plus(offset: Coordinate): Coordinate {
55
return Coordinate(row + offset.row.value, col + offset.col.value)
66
}
7+
8+
companion object {
9+
operator fun invoke(
10+
row: Int,
11+
col: Int,
12+
) = Coordinate(Row(row), Col(col))
13+
}
714
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package domain
2+
3+
enum class GameResult {
4+
SUCCESS,
5+
FAILURE,
6+
}

β€Žsrc/main/kotlin/domain/MineBoard.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,41 @@ class MineBoard(
1313
continue
1414
}
1515

16-
if (cells.getCoordinateIs(nextCoordinate).isMineCell()) {
16+
if (cells.get(nextCoordinate).isMineCell()) {
1717
numberOfMines++
1818
}
1919
}
2020
return numberOfMines
2121
}
22+
23+
fun getAdjacentCoordinates(cell: Cell): List<Coordinate> {
24+
val adjacentCoordinates = mutableListOf<Coordinate>()
25+
26+
for (direction in Direction.entries) {
27+
val nextCoordinate = cell.coordinate + direction.offset
28+
if (!mineGameMetric.isOutOfMineBoard(nextCoordinate)) {
29+
adjacentCoordinates.add(nextCoordinate)
30+
}
31+
}
32+
33+
return adjacentCoordinates
34+
}
35+
36+
fun isMineCell(coordinate: Coordinate): Boolean {
37+
return cells.get(coordinate).isMineCell()
38+
}
39+
40+
fun isAnyMineCellOpened(): Boolean {
41+
return cells.countOpenedMineCells() > 0
42+
}
43+
44+
fun isAllEmptyCellsOpened(): Boolean {
45+
return cells.isAllEmptyCellsOpened()
46+
}
47+
48+
fun openCell(coordinate: Coordinate) {
49+
cells.get(coordinate).open()
50+
}
51+
52+
fun getCell(current: Coordinate): Cell = cells.get(current)
2253
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package domain
2+
3+
class MineSweeperGame(private val mineBoard: MineBoard) {
4+
fun isContinueGame(): Boolean {
5+
when {
6+
mineBoard.isAnyMineCellOpened() -> return false
7+
mineBoard.isAllEmptyCellsOpened() -> return false
8+
}
9+
return true
10+
}
11+
12+
fun openAdjacentCell(coordinate: Coordinate) {
13+
val queue = ArrayDeque<Coordinate>()
14+
queue.add(coordinate)
15+
16+
while (queue.isNotEmpty()) {
17+
processCell(queue)
18+
}
19+
}
20+
21+
private fun processCell(queue: ArrayDeque<Coordinate>) {
22+
val current = queue.removeFirst()
23+
val cell = mineBoard.getCell(current)
24+
25+
if (cell.isAlreadyOpened()) return
26+
27+
mineBoard.openCell(current)
28+
29+
if (shouldAddAdjacentCells(cell)) {
30+
val adjacentCoordinates = mineBoard.getAdjacentCoordinates(cell)
31+
queue.addAll(adjacentCoordinates)
32+
}
33+
}
34+
35+
private fun shouldAddAdjacentCells(cell: Cell): Boolean {
36+
return cell is Cell.EmptyCell && mineBoard.countAdjacentMines(cell) == 0
37+
}
38+
39+
fun getGameResult(): GameResult =
40+
when {
41+
mineBoard.isAllEmptyCellsOpened() -> GameResult.SUCCESS
42+
else -> GameResult.FAILURE
43+
}
44+
}

β€Žsrc/main/kotlin/view/InputView.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package view
22

3+
import domain.Col
4+
import domain.Coordinate
5+
import domain.Row
6+
37
object InputView {
48
fun inputRowSize(): Int {
59
println("높이λ₯Ό μž…λ ₯ν•˜μ„Έμš”.")
@@ -24,5 +28,20 @@ object InputView {
2428
return intValue
2529
}
2630

31+
fun askMineCoordinate(): Coordinate {
32+
println("μ§€λ’°μ°ΎκΈ° κ²Œμž„ μ‹œμž‘")
33+
print("open: ")
34+
return parseToCoordinateOrThrow(readln())
35+
}
36+
37+
private fun parseToCoordinateOrThrow(value: String): Coordinate {
38+
return value.replace(" ", "")
39+
.split(",")
40+
.also { require(it.size == 2) { INVALID_COORDINATE + value } }
41+
.map { it.toIntOrNull() ?: throw IllegalArgumentException(INVALID_COORDINATE) }
42+
.let { (row, col) -> Coordinate(Row(row), Col(col)) }
43+
}
44+
2745
private const val INVALID_INPUT = "숫자만 μž…λ ₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€."
46+
private const val INVALID_COORDINATE = "두가지 숫자λ₯Ό 콀마(,)둜 κ΅¬λΆ„ν•΄μ„œ μž…λ ₯ν•΄μ£Όμ„Έμš” "
2847
}

β€Žsrc/main/kotlin/view/OutputView.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,49 @@
11
package view
22

33
import domain.Cell
4+
import domain.CellStatus
5+
import domain.GameResult
46
import domain.MineBoard
7+
import domain.Row
58

69
object OutputView {
710
fun showMineSweeperBoard(board: MineBoard) {
811
val cells = board.cells
9-
val cellsByRow = cells.cells.groupBy { it.coordinate.row.value }
1012

11-
cellsByRow.toSortedMap().forEach { (_, rowCells) ->
12-
val sortedRow = rowCells.sortedBy { it.coordinate.col.value } // μ—΄ κΈ°μ€€ μ •λ ¬
13+
val cellsByRow = cells.cells.groupBy { it.coordinate.row }
14+
cellsByRow.toSortedMap(Row::compareTo).forEach { (_, rowCells) ->
15+
val sortedRow = rowCells.sortedBy { it.coordinate.col.value }
1316
sortedRow.forEach { cell ->
14-
when (cell) {
15-
is Cell.MineCell -> print(MINE_CELL)
16-
is Cell.EmptyCell -> print("${board.countAdjacentMines(cell)} ")
17-
}
17+
printCell(cell, board)
1818
}
1919
println()
2020
}
2121
}
2222

23+
private fun printCell(
24+
cell: Cell,
25+
board: MineBoard,
26+
) {
27+
if (cell.status == CellStatus.CLOSED) {
28+
print(CLOSED_CELL)
29+
return
30+
}
31+
32+
when (cell) {
33+
is Cell.MineCell -> print(MINE_CELL)
34+
is Cell.EmptyCell -> print("${board.countAdjacentMines(cell)} ")
35+
}
36+
}
37+
38+
fun showGameResult(result: GameResult) {
39+
when (result) {
40+
GameResult.SUCCESS -> print(SUCCESS_MESSAGE)
41+
GameResult.FAILURE -> print(FAIL_MESSAGE)
42+
}
43+
}
44+
2345
private const val MINE_CELL = "* "
46+
private const val CLOSED_CELL = "C "
47+
private const val SUCCESS_MESSAGE = "Success Game"
48+
private const val FAIL_MESSAGE = "Lose Game."
2449
}

β€Žsrc/test/kotlin/domain/BoardTest.kt

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
Β (0)