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

Open
wants to merge 10 commits into
base: chicori3
Choose a base branch
from
21 changes: 17 additions & 4 deletions src/main/kotlin/application/MineSweeper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package application

import cell.CellBoard
import cell.Count
import cell.GameResult
import cell.Length
import view.InputView
import view.OutputView
Expand All @@ -10,10 +11,9 @@ import view.format
class MineSweeper {
fun execute() {
val (height, width, mineCount) = init()

val board = CellBoard.of(height, width, mineCount)

renderBoard(board)
OutputView.printGameStart()
gameLoop(board)
}

private fun init(): Request {
Expand All @@ -24,9 +24,22 @@ class MineSweeper {
return Request(Length(height), Length(width), Count(mineCount))
}

private tailrec fun gameLoop(board: CellBoard) {
val coordinate = InputView.inputCoordinate()
when (board.handleInput(coordinate)) {
is GameResult.LOSE -> {
OutputView.printLoseGame()
}
is GameResult.CONTINUE -> {
renderBoard(board)
gameLoop(board)
}
}
}

private fun renderBoard(board: CellBoard) {
val formattedBoard = format(board)
OutputView.printGameStart(formattedBoard)
OutputView.printBoard(formattedBoard)
}

private data class Request(
Expand Down
36 changes: 33 additions & 3 deletions src/main/kotlin/cell/Cell.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
package cell

sealed interface Cell
sealed interface Cell {
val revealed: Boolean

data class BlankCell(val adjacentMineCount: MineCount) : Cell
fun reveal(): Cell

data object MineCell : Cell
fun shouldFloodFill(): Boolean
}

data class BlankCell(
val adjacentMineCount: MineCount,
private var _revealed: Boolean = false,
) : Cell {
override val revealed: Boolean
get() = _revealed

override fun reveal(): Cell {
_revealed = true
return this
}

override fun shouldFloodFill(): Boolean = adjacentMineCount == MineCount.ZERO
}

data object MineCell : Cell {
private var _revealed: Boolean = false
override val revealed: Boolean
get() = _revealed

override fun reveal(): Cell {
_revealed = true
return this
}

override fun shouldFloodFill(): Boolean = false
}
51 changes: 49 additions & 2 deletions src/main/kotlin/cell/CellBoard.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cell

class CellBoard(
private val _cells: Map<Coordinate, Cell>,
private val _cells: MutableMap<Coordinate, Cell>,
) {
val cellCount: Int
get() = _cells.size
Expand All @@ -12,6 +12,53 @@ class CellBoard(
val cells: Map<Coordinate, Cell>
get() = _cells.toMap()

fun handleInput(coordinate: Coordinate): GameResult {
val cell = _cells[coordinate]
check(cell != null) { "좌표가 존재하지 않습니다." }
check(!cell.revealed) { "이미 공개된 좌표입니다." }

return if (cell is MineCell) {
GameResult.LOSE
} else {
revealCells(coordinate, cell)
}
}

private fun revealCells(
startCoordinate: Coordinate,
startCell: Cell,
): GameResult {
floodFill(startCoordinate, startCell)
return GameResult.CONTINUE
}

private fun floodFill(
coordinate: Coordinate,
cell: Cell,
) {
if (cell is MineCell || cell.revealed) return

_cells[coordinate] = cell.reveal()

if (cell.shouldFloodFill()) {
floodFillAdjacentCells(coordinate)
}
}

private fun floodFillAdjacentCells(coordinate: Coordinate) {
val directions = CoordinateDirection.entries.toTypedArray()
directions.forEach { direction ->
val adjacentCoordinate =
Coordinate(
coordinate.x + direction.x,
coordinate.y + direction.y,
)
_cells[adjacentCoordinate]?.let { adjacentCell ->
floodFill(adjacentCoordinate, adjacentCell)
}
}
}

companion object {
fun of(
height: Length,
Expand All @@ -22,7 +69,7 @@ class CellBoard(
val coordinates = Coordinates.of(height, width, mineGenerationStrategy)
val cellsWithMine = coordinates.generateMineCoordinates(mineCount)

return CellBoard(cellsWithMine)
return CellBoard(cellsWithMine.toMutableMap())
}
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/cell/GameResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cell

sealed interface GameResult {
data object LOSE : GameResult

data object CONTINUE : GameResult
}
10 changes: 7 additions & 3 deletions src/main/kotlin/view/CellBoardFormatter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ private fun populateBoardArray(
val y = coordinate.y.value

val emoji =
when (cell) {
is MineCell -> "💣"
is BlankCell -> formatBlankCell(cell)
if (cell.revealed) {
when (cell) {
is MineCell -> "💣"
is BlankCell -> formatBlankCell(cell)
}
} else {
"⬜"
}
boardArray[y][x] = emoji
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package view

import cell.Coordinate
import cell.CoordinateValue

object InputView {
fun inputHeight(): Int {
println("높이를 입력해주세요.")
Expand All @@ -16,6 +19,19 @@ object InputView {
return readLineToInt()
}

fun inputCoordinate(): Coordinate {
print("open: ")
val input =
readlnOrNull()
?: throw IllegalArgumentException("좌표를 입력해주세요.")
val (x, y) = input.split(",")

return Coordinate(
x = CoordinateValue(x.toInt().dec()),
y = CoordinateValue(y.toInt().dec()),
)
}

private fun readLineToInt(): Int =
readlnOrNull()?.toInt()
?: throw IllegalArgumentException("숫자만 입력 가능합니다.")
Expand Down
9 changes: 8 additions & 1 deletion src/main/kotlin/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package view

object OutputView {
fun printGameStart(board: String) {
fun printGameStart() {
println("지뢰찾기 게임 시작")
}

fun printLoseGame() {
println("Lose Game")
}

fun printBoard(board: String) {
println(board)
}
}
100 changes: 100 additions & 0 deletions src/test/kotlin/cell/CellBoardTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,104 @@ class CellBoardTest {
actual.mineCount shouldBe 1
blankCell.adjacentMineCount shouldBe MineCount.ONE
}

@Test
fun `지뢰가 있는 Cell을 클릭하면 LOSE를 반환한다`() {
val height = Length(3)
val width = Length(3)
val mineCount = Count(1)
val fixedCoordinates =
listOf(
Coordinate(
CoordinateValue(1),
CoordinateValue(1),
),
)
val strategy = FixedMineGenerationStrategy(fixedCoordinates)

val cellBoard =
CellBoard.of(
height = height,
width = width,
mineCount = mineCount,
mineGenerationStrategy = strategy,
)

val actual =
cellBoard.handleInput(
Coordinate(
CoordinateValue(1),
CoordinateValue(1),
),
)

actual shouldBe GameResult.LOSE
}

@Test
fun `지뢰가 없는 Cell을 클릭하면 CONTINUE를 반환한다`() {
val height = Length(3)
val width = Length(3)
val mineCount = Count(1)
val fixedCoordinates =
listOf(
Coordinate(
CoordinateValue(1),
CoordinateValue(1),
),
)
val strategy = FixedMineGenerationStrategy(fixedCoordinates)

val cellBoard =
CellBoard.of(
height = height,
width = width,
mineCount = mineCount,
mineGenerationStrategy = strategy,
)

val actual =
cellBoard.handleInput(
Coordinate(
CoordinateValue(0),
CoordinateValue(0),
),
)

actual shouldBe GameResult.CONTINUE
}

@Test
fun `지뢰가 없는 Cell을 클릭하면 주변 Cell들을 모두 공개한다`() {
val height = Length(3)
val width = Length(3)
val mineCount = Count(1)
val fixedCoordinates =
listOf(
Coordinate(
CoordinateValue(0),
CoordinateValue(0),
),
)
val strategy = FixedMineGenerationStrategy(fixedCoordinates)
val cellBoard =
CellBoard.of(
height = height,
width = width,
mineCount = mineCount,
mineGenerationStrategy = strategy,
)

cellBoard.handleInput(
Coordinate(
CoordinateValue(2),
CoordinateValue(2),
),
)

val actual = cellBoard.cells.values.map(Cell::revealed)

actual.filter { it }.size shouldBe 8
actual.filterNot { it }.size shouldBe 1
}
}
33 changes: 33 additions & 0 deletions src/test/kotlin/cell/CellTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cell

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class CellTest {
@Test
fun `주변에 지뢰가 없는 경우 true를 반환한다`() {
val actual = BlankCell(MineCount.ZERO)

val result = actual.shouldFloodFill()

result shouldBe true
}

@Test
fun `지뢰 셀은 주변에 지뢰가 없어도 false를 반환한다`() {
val actual = MineCell

val result = actual.shouldFloodFill()

result shouldBe false
}

@Test
fun `셀을 공개하면 revealed가 true가 된다`() {
val actual = BlankCell(MineCount.ZERO)

val result = actual.reveal()

result.revealed shouldBe true
}
}