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 지뢰찾기(게임실행) #399

Open
wants to merge 7 commits into
base: jaylene-shin
Choose a base branch
from
22 changes: 21 additions & 1 deletion src/main/kotlin/minesweeper/MineSweeper.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
package minesweeper

import minesweeper.domain.MineCountMapFactory
import minesweeper.domain.MineMap
import minesweeper.domain.MineMapMeta
import minesweeper.domain.OpenState
import minesweeper.domain.Position
import minesweeper.domain.PositionGenerator
import minesweeper.view.InputView
import minesweeper.view.OutputView
import java.util.Stack

object MineSweeper {
fun drawMap() {
val mineMapMeta = InputView.readMineMapMeta()
val mineMap = MineCountMapFactory(PositionGenerator(mineMapMeta)).create()
OutputView.printGameStartMsg()
OutputView.printMineMap(mineMapMeta, mineMap)
executeGame(mineMapMeta, mineMap)
}

private fun executeGame(mineMapMeta: MineMapMeta, mineMap: MineMap) {
val positionStack = Stack<Position>().apply { addAll(mineMap.values.keys) }
while (positionStack.isNotEmpty()) {
val position = positionStack.pop()
if (mineMap.getCell(position).openState == OpenState.OPENED) continue
OutputView.printOpenPositionMsg(position)
if (mineMap.isEmptyCellClicked(position)) {
OutputView.printMineMap(mineMapMeta, mineMap)
continue
}
OutputView.printGameLoseMsg()
break
}
Comment on lines +21 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view와 게임을 진행하는 도메인 로직이 섞여있는 것으로 보여요 :)
view의 일과 도메인의 일을 적절히 나눠보는 것은 어떨까요?

도메인에서 view의 일이 필요하다면, 전달 받아야 할 것들에 대해 interface를 작성하고, console에 대한 하위 구현체를 도메인 객체로 넘겨 의존 관계를 떼어낼 수 있겠어요 :)

}
}

Expand Down
21 changes: 18 additions & 3 deletions src/main/kotlin/minesweeper/domain/Cell.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
package minesweeper.domain

sealed interface Cell
sealed interface Cell {
val openState: OpenState
fun open(): Cell
}

object Mine : Cell
data class Empty(val mineCount: Int = 0) : Cell
data class Mine(override val openState: OpenState = OpenState.CLOSED) : Cell {
override fun open(): Mine {
return copy(openState = OpenState.OPENED)
}
}

data class Empty(
val mineCount: Int = 0,
override val openState: OpenState = OpenState.CLOSED,
) : Cell {
override fun open(): Empty {
return copy(openState = OpenState.OPENED)
Comment on lines +14 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cell 하위 구현체로, Open인 Cell과 Close인 셀로 구현해보는 건 어떨까요?

}
}
5 changes: 2 additions & 3 deletions src/main/kotlin/minesweeper/domain/MineCountMapFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ class MineCountMapFactory(
val minePositions = positionGenerator.generateMinePositions()
val emptyPositions = positionGenerator.generateEmptyPositions(minePositions)
val cells = (minePositions + emptyPositions)
.getValues()
.associateWith { createCell(it, minePositions) }
return MineMap(cells)
}

private fun createCell(position: Position, minePositions: Positions): Cell {
private fun createCell(position: Position, minePositions: Set<Position>): Cell {
val aroundPositions = position.aroundPositions()
return if (minePositions.contains(position)) {
Mine
Mine()
} else {
Comment on lines +14 to 18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지뢰 판의 칸들이 열릴지 여부를 모르는 상태에서 미리 count를 모두 계산하는 것은 낭비라는 생각이 들어요.
칸이 열렸을 때, 그 때 count를 계산하게 만들어보는 것은 어떨까요?

val mineCount = aroundPositions.count { minePositions.contains(it) }
Empty(mineCount)
Expand Down
31 changes: 28 additions & 3 deletions src/main/kotlin/minesweeper/domain/MineMap.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
package minesweeper.domain

class MineMap(
private val values: Map<Position, Cell>
values: Map<Position, Cell>
) {
private val _values = values.toMutableMap()
val values: Map<Position, Cell>
get() = _values.toMap()

val size: Int
get() = values.keys.size
get() = _values.keys.size

fun isEmptyCellClicked(position: Position): Boolean {
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드 명만 봐서는, 빈 칸이 클릭되었는지에 대한 여부를 반환하기만 할 것처럼 보여요.
그런데 실제로 내부 구현에서는 칸을 열기까지 하네요!
다른 개발자들이 보았을 때 이 메서드에 대한 내용을 착각하고 잘못 사용할 수 있을 것으로 보여요.
메서드가 수행하고 있는 것을 더욱 구체적으로 작성해보는 건 어떨까요?

val cell = getCell(position)
return if (cell is Mine) {
false
} else {
openAroundCells(position)
true
}
}

fun getCell(position: Position): Cell {
return values[position] ?: throw IllegalArgumentException("해당 위치에 셀이 없습니다")
return _values[position] ?: throw IllegalArgumentException("해당 위치에 셀이 없습니다")
}

private fun openAroundCells(position: Position) {
val aroundPositions = position.aroundPositions()
(aroundPositions + position).forEach(::openCell)
}

private fun openCell(position: Position) {
val cell = runCatching { getCell(position) }.getOrNull() ?: return
val newCell = cell.open()
_values[position] = newCell
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/minesweeper/domain/OpenState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package minesweeper.domain

enum class OpenState {
OPENED, CLOSED
}
30 changes: 10 additions & 20 deletions src/main/kotlin/minesweeper/domain/Position.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,16 @@ data class Position(
val y: Int,
val x: Int
) {
init {
require(y > 0) { "입력값: $y, y는 0이거나 음수일 수 없습니다" }
require(x > 0) { "입력값: $x, x는 0이거나 음수일 수 없습니다" }
fun aroundPositions(): Set<Position> {
val rowRange = (y - 1)..(y + 1)
val colRange = (x - 1)..(x + 1)
Comment on lines +8 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#389 (comment) 여전히 유효한 피드백이에요!
enum으로 Direction 등의 객체를 만들어보는 건 어떨까요? 그리고 이 객체로 8방향을 표현해보면 좋겠어요 :)

enum class Direction(
    val dx :Int,
    val dy: Int,
) {
    TOP(0, 1)
    TOP_LEFT(-1, 1)
...
...
}

return rowRange.flatMap { row ->
colRange.map { col ->
Position(row, col)
}
}.filterNot { it == this }.toSet()
}

fun aroundPositions(): List<Position> {
return listOfNotNull(
topOrNull(),
bottomOrNull(),
leftOrNull(),
rightOrNull(),
topOrNull()?.leftOrNull(),
topOrNull()?.rightOrNull(),
bottomOrNull()?.leftOrNull(),
bottomOrNull()?.rightOrNull()
)
companion object {
const val START_INDEX = 1
}

private fun leftOrNull(): Position? = runCatching { Position(y, x - 1) }.getOrNull()
private fun rightOrNull(): Position? = runCatching { Position(y, x + 1) }.getOrNull()
private fun topOrNull(): Position? = runCatching { Position(y - 1, x) }.getOrNull()
private fun bottomOrNull(): Position? = runCatching { Position(y + 1, x) }.getOrNull()
}
17 changes: 7 additions & 10 deletions src/main/kotlin/minesweeper/domain/PositionGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,23 @@ class PositionGenerator(
) {
private val allPositions = generateAllPositions()

private fun generateAllPositions(): Positions {
val allPositions = (1..mineMapMeta.height)
.flatMap { y -> (1..mineMapMeta.width).map { x -> Position(y, x) } }
private fun generateAllPositions(): Set<Position> {
return (Position.START_INDEX..mineMapMeta.height)
.flatMap { y -> (Position.START_INDEX..mineMapMeta.width).map { x -> Position(y, x) } }
.toSet()
.toPositions()
require(allPositions.size == mineMapMeta.getCellCount()) { "모든 위치를 생성하지 못했습니다" }
return allPositions
}

fun generateMinePositions(): Positions {
fun generateMinePositions(): Set<Position> {
val minePositions = positionSelector.select(allPositions, mineMapMeta.mineCount)
require(minePositions.size == mineMapMeta.mineCount) { "지뢰의 개수가 맞지 않습니다." }
return minePositions
Comment on lines 17 to 18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#389 (comment) 여전히 유효한 피드백입니다 :)

}

fun generateEmptyPositions(
minePositions: Positions
): Positions {
minePositions: Set<Position>
): Set<Position> {
val emptyPositions = allPositions - minePositions
require(!emptyPositions.containSamePosition(minePositions)) { "지뢰와 빈 공간은 겹칠 수 없습니다." }
require(emptyPositions.intersect(minePositions).isEmpty()) { "지뢰와 빈 공간은 겹칠 수 없습니다." }
return emptyPositions
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/minesweeper/domain/PositionSelector.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package minesweeper.domain

interface PositionSelector {
fun select(positions: Positions, selectNum: Int): Positions
fun select(positions: Set<Position>, selectNum: Int): Set<Position>
}
20 changes: 0 additions & 20 deletions src/main/kotlin/minesweeper/domain/Positions.kt

This file was deleted.

4 changes: 1 addition & 3 deletions src/main/kotlin/minesweeper/domain/RandomPositionSelector.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package minesweeper.domain

object RandomPositionSelector : PositionSelector {
override fun select(positions: Positions, selectNum: Int): Positions {
override fun select(positions: Set<Position>, selectNum: Int): Set<Position> {
return positions
.getValues()
.shuffled()
.take(selectNum)
.toSet()
.toPositions()
}
}
37 changes: 29 additions & 8 deletions src/main/kotlin/minesweeper/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
package minesweeper.view

import minesweeper.domain.Cell
import minesweeper.domain.Empty
import minesweeper.domain.Mine
import minesweeper.domain.MineMap
import minesweeper.domain.MineMapMeta
import minesweeper.domain.OpenState
import minesweeper.domain.Position

object OutputView {
private const val MINE_CHAR = "*"
private const val MINE_CELL_CHAR = "*"
private const val CLOSED_CELL_CHAR = "C"

fun printGameStartMsg() {
println("\n지뢰 찾기 게임 시작")
}

fun printGameLoseMsg() {
println("Lose Game.")
}

fun printOpenPositionMsg(position: Position) {
println("open: ${position.y}, ${position.x}")
}

fun printMineMap(mineMapMeta: MineMapMeta, mineMap: MineMap) {
for (row in 1 until mineMapMeta.height + 1) {
for (row in Position.START_INDEX until mineMapMeta.height + 1) {
printRowCells(mineMapMeta, mineMap, row)
}
println()
}

private fun printRowCells(mineMapMeta: MineMapMeta, mineMap: MineMap, row: Int) {
for (col in 1 until mineMapMeta.width + 1) {
when (val cell = mineMap.getCell(Position(row, col))) {
is Mine -> print("$MINE_CHAR ")
is Empty -> print("${cell.mineCount} ")
}
for (col in Position.START_INDEX until mineMapMeta.width + 1) {
val cell = mineMap.getCell(Position(row, col))
if (cell.openState == OpenState.OPENED) printOpenedCell(cell)
if (cell.openState == OpenState.CLOSED) printClosedCell()
}
println()
}

private fun printOpenedCell(cell: Cell) {
when (cell) {
is Mine -> print("$MINE_CELL_CHAR ")
is Empty -> print("${cell.mineCount} ")
}
}

private fun printClosedCell() {
print("$CLOSED_CELL_CHAR ")
}
}
26 changes: 26 additions & 0 deletions src/test/kotlin/minesweeper/domain/CellTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package minesweeper.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class CellTest {
@Test
fun `Empty cell이 open될 경우 OpenState은 OPENED로 변경된다`() {
// given
val empty = Empty()
// when
val newEmpty = empty.open()
// then
assertThat(newEmpty.openState).isEqualTo(OpenState.OPENED)
}

@Test
fun `Mine cell이 open될 경우 OpenState은 OPENED로 변경된다`() {
// given
val mine = Mine()
// when
val newMine = mine.open()
// then
assertThat(newMine.openState).isEqualTo(OpenState.OPENED)
}
}
11 changes: 8 additions & 3 deletions src/test/kotlin/minesweeper/domain/MineCountMapFactoryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ class MineCountMapFactoryTest {
val mineMap = mineCountMapFactory.create()

// then
/*
* * * *
* 2 3 2
* 0 0 0
*/
assertSoftly {
assertThat(mineMap.size).isEqualTo(9)
assertThat(mineMap.getCell(Position(1, 1))).usingRecursiveComparison().isEqualTo(Mine)
assertThat(mineMap.getCell(Position(1, 2))).usingRecursiveComparison().isEqualTo(Mine)
assertThat(mineMap.getCell(Position(1, 3))).usingRecursiveComparison().isEqualTo(Mine)
assertThat(mineMap.getCell(Position(1, 1))).usingRecursiveComparison().isEqualTo(Mine())
assertThat(mineMap.getCell(Position(1, 2))).usingRecursiveComparison().isEqualTo(Mine())
assertThat(mineMap.getCell(Position(1, 3))).usingRecursiveComparison().isEqualTo(Mine())
assertThat(mineMap.getCell(Position(2, 1))).usingRecursiveComparison().isEqualTo(Empty(2))
assertThat(mineMap.getCell(Position(2, 2))).usingRecursiveComparison().isEqualTo(Empty(3))
assertThat(mineMap.getCell(Position(2, 3))).usingRecursiveComparison().isEqualTo(Empty(2))
Expand Down
Loading