-
Notifications
You must be signed in to change notification settings - Fork 180
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
base: jaylene-shin
Are you sure you want to change the base?
Step3 지뢰찾기(게임실행) #399
Changes from all commits
4fe5023
15f89f7
0b00e53
9ea1292
235beb9
ff71703
72120b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cell 하위 구현체로, Open인 Cell과 Close인 셀로 구현해보는 건 어떨까요? |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지뢰 판의 칸들이 열릴지 여부를 모르는 상태에서 미리 count를 모두 계산하는 것은 낭비라는 생각이 들어요. |
||
val mineCount = aroundPositions.count { minePositions.contains(it) } | ||
Empty(mineCount) | ||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package minesweeper.domain | ||
|
||
enum class OpenState { | ||
OPENED, CLOSED | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #389 (comment) 여전히 유효한 피드백이에요! 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() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
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> | ||
} |
This file was deleted.
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() | ||
} | ||
} |
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 ") | ||
} | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
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에 대한 하위 구현체를 도메인 객체로 넘겨 의존 관계를 떼어낼 수 있겠어요 :)