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

Open
wants to merge 12 commits into
base: factoriall
Choose a base branch
from
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@
- [X] 8방면 포인트는 row 및 column 사이에 있는 포인트 묶음을 반환한다.
- [X] 지뢰에 다 둘러쌓여 있으면 Blank의 nearCount는 8이다.
- [X] 지뢰에 안 둘러쌓여 있다면 MineMap에 정보가 없다.
- [X] 근처 지뢰가 있냐에 따라 카운트가 달라진다.
- [X] 근처 지뢰가 있냐에 따라 카운트가 달라진다.

- [X] 클릭 시 이미 클릭한 구역이라면, 이미 클릭한 것이라 알려준다.
- [X] 클릭 시 주변 지뢰 수가 0이라면, 주변의 0인 구역 및 0이 아닌 구역 모두를 보여준다
- [X] 클릭 시 주변 지뢰 수가 1 이상이라면, 클릭한 부분만 보여준다
- [X] 클릭 시 지뢰라면, 그 즉시 패배로 게임이 끝난다.
- [X] 지뢰 외 모든 구역을 다 찾았다면, 승리로 끝난다.
30 changes: 0 additions & 30 deletions src/main/kotlin/minesweeper/AdjacentPoints.kt

This file was deleted.

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

enum class Direction(val row: Int, val col: Int) {
NORTH(-1, 0),
NORTHEAST(-1, 1),
EAST(0, 1),
SOUTHEAST(1, 1),
SOUTH(1, 0),
SOUTHWEST(1, -1),
WEST(0, -1),
NORTHWEST(-1, -1)
}
8 changes: 7 additions & 1 deletion src/main/kotlin/minesweeper/InputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ object InputView {
println("지뢰는 몇 개인가요?")
val mineNum = MineCount(readln().toInt())

return MineMap(MineMapInfo(row, col, mineNum))
return MineMap.create(MineMapInfo(row, col, mineNum))
}

fun getClickedPoint(): Point {
print("open: ")
val pointString = readln().split(",").map { it.trim().toInt() }
return Point(pointString[0], pointString[1])
}
}
30 changes: 28 additions & 2 deletions src/main/kotlin/minesweeper/Main.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
package minesweeper

fun main() {
val mineMap = InputView.getMineMap()
ResultView.showMap(mineMap)
val mineSweeper = MineSweeper(InputView.getMineMap())
ResultView.start()

do {
val clicked = InputView.getClickedPoint()
val result = mineSweeper.click(clicked)
when (result) {
MineSweeper.ClickResult.CONTINUE -> {
ResultView.showMap(mineSweeper)
}

MineSweeper.ClickResult.ALREADY_CLICKED -> {
ResultView.showAlreadyClickedMessage()
}

MineSweeper.ClickResult.GAME_OVER -> {
ResultView.showGameOver()
break
}

MineSweeper.ClickResult.ERROR -> {
ResultView.showError()
break
}
}
} while (!mineSweeper.isDone)
Factoriall marked this conversation as resolved.
Show resolved Hide resolved

if (mineSweeper.isDone) ResultView.showFinished()
}
3 changes: 3 additions & 0 deletions src/main/kotlin/minesweeper/MapSize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package minesweeper

data class MapSize(val row: LineCount, val column: LineCount)
2 changes: 2 additions & 0 deletions src/main/kotlin/minesweeper/MapTile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ sealed class MapTile {
operator fun plus(number: Int): Blank {
return Blank(this.nearCount + number)
}

val isNoMineNear = nearCount == 0
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/minesweeper/MineCount.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package minesweeper
@JvmInline
value class MineCount(val count: Int) {
init {
require(count > 0) {
require(count >= 0) {
Copy link
Author

Choose a reason for hiding this comment

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

이건 지뢰가 0개일 수도 있을거 같다는 생각에 바꿨습니다.

"Mine Number must be positive"
}
}
Expand Down
53 changes: 38 additions & 15 deletions src/main/kotlin/minesweeper/MineMap.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
package minesweeper

class MineMap(
val mineMapInfo: MineMapInfo,
createStrategy: MinePointCreateStrategy = RandomPointCreateStrategy()
val mineMap: Map<Point, MapTile>,
val mapInfo: MineMapInfo
) {
Comment on lines 3 to 6
Copy link
Member

Choose a reason for hiding this comment

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

MineMap이 단순히 Map 컬렉션을 가지고있는 것 말고 큰 역할을 수행하고 있지 않네요!
이 객체가 수행해야할 역할을 모아보는건 어떨까요?

private val mineList: MineList =
MineList.createMineList(mineMapInfo, createStrategy)
val mineMap: Map<Point, MapTile> = mutableMapOf<Point, MapTile>().apply {
for (mine in mineList.mineList) {
this[mine] = MapTile.Mine
val totalSize = mapInfo.totalNumber

companion object {
fun create(
mineMapInfo: MineMapInfo,
createStrategy: MinePointCreateStrategy = RandomPointCreateStrategy()
): MineMap {
val mineList: MineList =
MineList.createMineList(mineMapInfo, createStrategy)

return MineMap(
emptyMap(mineMapInfo.mapSize).apply {
for (mine in mineList.mineList) {
this[mine] = MapTile.Mine
}

for (mine in mineList.mineList) {
createNear(this, mine, mineMapInfo.mapSize)
}
},
mineMapInfo
)
}

for (mine in mineList.mineList) {
createNear(this, mine)
private fun emptyMap(mapSize: MapSize): MutableMap<Point, MapTile> {
return mutableMapOf<Point, MapTile>().apply {
for (i in 1..mapSize.row.count) {
for (j in 1..mapSize.column.count) {
put(Point(i, j), MapTile.Blank(0))
}
}
}
Comment on lines +32 to +38
Copy link
Author

Choose a reason for hiding this comment

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

Blank(0)인 Map을 초기화 시 넣게 했습니다.

}
}

private fun createNear(map: MutableMap<Point, MapTile>, mine: Point) {
val adjacentPoints = AdjacentPoints.create(mine, mineMapInfo.rowCnt, mineMapInfo.colCnt)
for (adj in adjacentPoints.points) {
val nearInfo = map.getOrDefault(adj, MapTile.Blank(0))
if (nearInfo is MapTile.Blank) map[adj] = nearInfo + 1
private fun createNear(map: MutableMap<Point, MapTile>, mine: Point, mapSize: MapSize) {
val adjacentPoints = mine.getAdjacentPoints(mapSize)
for (adj in adjacentPoints) {
val nearInfo = map[adj]
if (nearInfo is MapTile.Blank) map[adj] = nearInfo + 1
}
Comment on lines +41 to +46
Copy link
Member

Choose a reason for hiding this comment

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

지뢰 찾기를 진행하면서, 칸을 눌렀을 때, 칸의 숫자를 계산하도록 만드는 건 어떨까요?
게임을 진행하면서 열리지 않을 칸들에 대해, 생성 시점에서 주변 지뢰 수를 계산하는 것은 낭비라는 생각이 드네요.

}
}
}
14 changes: 8 additions & 6 deletions src/main/kotlin/minesweeper/MineMapInfo.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package minesweeper

data class MineMapInfo(private val rowNum: LineCount, private val colNum: LineCount, private val mineNum: MineCount) {
val rowCnt = rowNum.count
val colCnt = colNum.count
val mineCnt = mineNum.count
data class MineMapInfo(val mapSize: MapSize, private val mineCount: MineCount) {
constructor(row: LineCount, col: LineCount, mineNum: MineCount) : this(MapSize(row, col), mineNum)

val total = rowCnt * colCnt
val rowNumber = mapSize.row.count
val columnNumber = mapSize.column.count
val mineNumber = mineCount.count

val totalNumber = rowNumber * columnNumber

init {
require(mineCnt <= total) {
require(mineNumber <= totalNumber) {
"Mine Count should not be bigger than total map tile"
}
}
Expand Down
6 changes: 0 additions & 6 deletions src/main/kotlin/minesweeper/MinePointCreateStrategy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,4 @@ package minesweeper

interface MinePointCreateStrategy {
fun createMinePoints(mineMapInfo: MineMapInfo): List<Point>

fun Int.toPoint(rowNum: Int): Point {
val rowIdx = this / rowNum
val colIdx = this % rowNum
return Point(rowIdx, colIdx)
}
}
57 changes: 57 additions & 0 deletions src/main/kotlin/minesweeper/MineSweeper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package minesweeper

class MineSweeper(
val mineMap: MineMap,
clickedSet: Set<Point> = setOf(),
) {
var clickedSet: Set<Point> = clickedSet
private set

Comment on lines +5 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.

MineTile의 하위 구현체로, 열린 칸과 닫힌 칸을 표현해보는 건 어떨까요?

val isDone get() = clickedSet.size == mineMap.totalSize - mineMap.mapInfo.mineNumber

fun click(point: Point): ClickResult {
if (clickedSet.contains(point)) return ClickResult.ALREADY_CLICKED

return when (val clickedTile = mineMap.mineMap[point]) {
is MapTile.Mine -> ClickResult.GAME_OVER
is MapTile.Blank -> {
setBlankTile(point, clickedTile)
ClickResult.CONTINUE
}

else -> ClickResult.ERROR
Copy link
Author

Choose a reason for hiding this comment

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

사실 여기 들어가면 안되는데, Map 특성상 null이 될 확률을 컴파일러 내에서 배제하기 힘들어서 ERROR를 따로 넣었습니다. 이를 없앨 수 있는 좋은 방법이 있을까요?

Copy link
Member

Choose a reason for hiding this comment

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

mineMap.mineMap[point]이 null이 되는 건 게임을 진행하면서 결코 피할 수 없을 거라 생각해요.

오히려 게임 진행에 있어서 필수적인 흐름이란 생각이 드는데요,
이런 경우가 어떨 때 생기는지 ClickResult.ERROR의 이름을 변경함으로써 표현하고, 게임 진행에서 어떻게 처리하면 좋을지 고민해보셔도 좋겠어요!

}
}

private fun setBlankTile(point: Point, clickedTile: MapTile.Blank) {
setClickedPoint(point)
if (clickedTile.isNoMineNear) {
setAdjacentTilesClicked(point)
}
}

private fun setAdjacentTilesClicked(point: Point) {
val adjacent = point.getAdjacentPoints(mineMap.mapInfo.mapSize)
for (adj in adjacent) {
if (clickedSet.contains(adj)) continue

adjacentTileDfs(adj)
}
}

private fun adjacentTileDfs(adj: Point) {
val info = mineMap.mineMap[adj]
if (info is MapTile.Blank) {
setClickedPoint(adj)
if (info.isNoMineNear) setAdjacentTilesClicked(adj)
}
}
Comment on lines +33 to +48
Copy link
Author

Choose a reason for hiding this comment

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

여기 DFS 구현한다고 했는데... 사실 이름이 크게 떠오르지 않아서 이상한 메소드를 쓴 것 같습니다. 혹시 네이밍 관련 좋은 이름이 있을까요?

Copy link
Member

Choose a reason for hiding this comment

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

수행하고자 하는 것이 무엇인지를 고민해서 메서드명에 녹여내보면 좋겠어요 :)
정답은 없으니 충분히 고민해보는 시간을 가져보셔도 좋겠어요.


private fun setClickedPoint(point: Point) {
clickedSet = clickedSet.plus(point)
}

enum class ClickResult {
GAME_OVER, ALREADY_CLICKED, CONTINUE, ERROR
}
Factoriall marked this conversation as resolved.
Show resolved Hide resolved
}
17 changes: 16 additions & 1 deletion src/main/kotlin/minesweeper/Point.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
package minesweeper

data class Point(val row: Int, val col: Int)
data class Point(val row: Int, val col: Int) {
fun getAdjacentPoints(mapSize: MapSize): List<Point> {
Comment on lines +3 to +4
Copy link
Member

Choose a reason for hiding this comment

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

Point객체 자체는 지뢰판의 영향 없이 그대로 주위 point 객체를 모두 반환하는 것이 더 자연스러워 보여요.
지뢰판에서 유효하지 않은 Point를 빼내는 작업은 다른 객체의 역할로 분리해보는 건 어떨까요?

val mapRow = mapSize.row.count
val mapCol = mapSize.column.count
return buildList {
for (dir in Direction.values()) {
val nearPoint = Point(row + dir.row, col + dir.col)
if (nearPoint.isOutOfBound(mapRow, mapCol)) continue
add(nearPoint)
}
}
}

private fun Point.isOutOfBound(mapRow: Int, mapCol: Int): Boolean =
this.row < 1 || this.col < 1 || this.row > mapRow || this.col > mapCol
}
12 changes: 9 additions & 3 deletions src/main/kotlin/minesweeper/RandomPointCreateStrategy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ package minesweeper

class RandomPointCreateStrategy : MinePointCreateStrategy {
override fun createMinePoints(mineMapInfo: MineMapInfo): List<Point> {
val tileNum = mineMapInfo.rowCnt * mineMapInfo.colCnt - 1
val tileNum = mineMapInfo.rowNumber * mineMapInfo.columnNumber - 1
return (0..tileNum).toList()
.shuffled()
.take(mineMapInfo.mineCnt)
.map { it.toPoint(mineMapInfo.rowCnt) }
.take(mineMapInfo.mineNumber)
.map { it.toPoint(mineMapInfo.rowNumber) }
}

private fun Int.toPoint(rowNum: Int): Point {
val rowIdx = this / rowNum
val colIdx = this % rowNum
return Point(rowIdx + 1, colIdx + 1)
Copy link
Author

Choose a reason for hiding this comment

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

게임 실행을 살펴보니 1,1부터 시작하는 걸로 보여서 +1 붙여줬습니다.

}
}
36 changes: 30 additions & 6 deletions src/main/kotlin/minesweeper/ResultView.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
package minesweeper

object ResultView {
fun showMap(mineMap: MineMap) {
fun start() {
println("지뢰찾기 게임 시작")
for (row in 0 until mineMap.mineMapInfo.rowCnt) {
printRow(mineMap, row)
}

fun showMap(mineSweeper: MineSweeper) {
for (row in 1..mineSweeper.mineMap.mapInfo.rowNumber) {
printRow(mineSweeper, row)
}
}

private fun printRow(
mineMap: MineMap,
mineSweeper: MineSweeper,
row: Int
) {
for (col in 0 until mineMap.mineMapInfo.colCnt) {
when (val info = mineMap.mineMap[Point(row, col)]) {
for (col in 1..mineSweeper.mineMap.mapInfo.columnNumber) {
val point = Point(row, col)
if (!mineSweeper.clickedSet.contains(point)) {
print("C ")
continue
}
when (val info = mineSweeper.mineMap.mineMap[point]) {
is MapTile.Mine -> print("* ")
is MapTile.Blank -> {
print("${info.nearCount} ")
Expand All @@ -24,4 +32,20 @@ object ResultView {
}
println()
}

fun showAlreadyClickedMessage() {
println("This point is already clicked. Choose other point.")
}

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

fun showError() {
println("Error.")
}

fun showFinished() {
println("Congratulation! You win the game!")
}
}
Loading