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 #422

Open
wants to merge 12 commits into
base: yeongunheo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,16 @@
### 요구사항 쪼개기
- CellFinder
- Cell을 입력받을 경우 해당 셀을 제외한 주변 8개 사각형에 포함된 지뢰의 개수를 반환한다.

- Position
- Position은 주변 8개의 좌표를 담은 위치들을 반환한다.

## 3단계 - 지뢰 찾기(게임 실행)

### 기능 요구사항
- 지뢰가 없는 인접한 칸이 모두 열리게 된다.

### 요구사항 쪼개기
- MineSweeperGame
- Open을 하면 해당 좌표에 인접한 칸 중 지뢰가 없는 칸이 모두 Open 상태로 바뀐다.
- Open 상태이면 주변 지뢰의 개수를 출력한다.
34 changes: 31 additions & 3 deletions src/main/kotlin/minesweeper/MineSweeperApplication.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
package minesweeper

import minesweeper.domain.CellFinder
import minesweeper.domain.HeightAndWidth
import minesweeper.domain.MineSweeperGame
import minesweeper.domain.Position
import minesweeper.domain.RandomPositionGenerator
import minesweeper.domain.Size
import minesweeper.ui.InputType
import minesweeper.ui.InputView
import minesweeper.ui.ResultView

fun main() {
val height = InputView.inputSize(InputType.HEIGHT)
val width = InputView.inputSize(InputType.WIDTH)
val count = InputView.inputSize(InputType.COUNT)
val mineCount = InputView.inputSize(InputType.COUNT)

val minePositions = RandomPositionGenerator(height, width).generate(count)
val minePositions = RandomPositionGenerator(height, width).generate(mineCount)
val cellFinder = CellFinder.init(height, width)
cellFinder.convert(minePositions)

ResultView.printMines(height, width, cellFinder)
val mineSweeperGame = MineSweeperGame(cellFinder)
play(mineSweeperGame, HeightAndWidth(height, width), mineCount)
}

private fun play(mineSweeperGame: MineSweeperGame, heightAndWidth: HeightAndWidth, mineCount: Size) {
ResultView.printGameStartMessage()
while (!mineSweeperGame.isFinished(mineCount)) {
val position = InputView.inputOpenPosition()
mineSweeperGame.open(position)
printResult(mineSweeperGame, position, heightAndWidth)
printWinResult(mineSweeperGame, mineCount)
}
}

private fun printResult(mineSweeperGame: MineSweeperGame, position: Position, heightAndWidth: HeightAndWidth) {
when (mineSweeperGame.isMine(position)) {
true -> ResultView.printLoseGameMessage()
false -> ResultView.printMines(mineSweeperGame, heightAndWidth)
}
}

private fun printWinResult(mineSweeperGame: MineSweeperGame, mineCount: Size) {
if (mineSweeperGame.isWin(mineCount)) {
ResultView.printWinGameMessage()
}
}
25 changes: 15 additions & 10 deletions src/main/kotlin/minesweeper/domain/CellFinder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ package minesweeper.domain
class CellFinder(private val map: MutableMap<Position, Cell>) {
private constructor(initPositions: List<Position>) : this(initPositions.associateWith { Cell(it) }.toMutableMap())

private val list = listOf(
Position(-1, -1), Position(-1, 0), Position(-1, 1), Position(0, -1), Position(0, 1), Position(1, -1), Position(1, 0), Position(1, 1)
)

fun convert(minePosition: List<Position>) {
minePosition.forEach {
map[it] = Cell(it, true)
Expand All @@ -17,12 +13,21 @@ class CellFinder(private val map: MutableMap<Position, Cell>) {
return map[position]
}

fun getAroundMinesCount(cell: Cell): Int {
val position = cell.position
return list.mapNotNull {
val nextPosition = position + it
find(nextPosition)
}.count { it.isMine }
fun getAroundMinesCount(position: Position): Int {
return position.getAround()
.mapNotNull { find(it) }
.count {
it.isMine
}
}

fun isMine(position: Position): Boolean {
val cell = find(position) ?: throw IllegalArgumentException("주어진 위치를 찾을 수 없습니다.")
return cell.isMine
}

fun size(): Int {
return map.size
}

companion object {
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/minesweeper/domain/HeightAndWidth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package minesweeper.domain

data class HeightAndWidth(val height: Size, val width: Size)
55 changes: 55 additions & 0 deletions src/main/kotlin/minesweeper/domain/MineSweeperGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package minesweeper.domain

class MineSweeperGame(
private val cellFinder: CellFinder,
private val openPositions: MutableSet<Position> = mutableSetOf(),
) {

fun open(position: Position) {
if (cellFinder.find(position) == null) {
return
}

openPositions.add(position)
if (getAroundMinesCount(position) != 0) {
return
}
position.getAdjacent()
.filter { !isOpen(it) && !isMine(it) }
.forEach { open(it) }
}

fun isOpen(position: Position): Boolean {
if (cellFinder.find(position) == null) {
return false
}
return openPositions.contains(position)
}

fun isMine(position: Position): Boolean {
if (cellFinder.find(position) == null) {
return false
}
return cellFinder.isMine(position)
}

fun isFinished(mineCount: Size): Boolean {
if (isLose()) {
return true
}

return cellFinder.size() == openPositions.size + mineCount.value
}

fun getAroundMinesCount(position: Position): Int {
return cellFinder.getAroundMinesCount(position)
}

fun isWin(mineCount: Size): Boolean {
return cellFinder.size() == openPositions.size + mineCount.value
}

private fun isLose(): Boolean {
return openPositions.any { isMine(it) }
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/minesweeper/domain/Position.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
package minesweeper.domain

private val leftUp = Position(-1, -1)
private val up = Position(-1, 0)
private val rightUp = Position(-1, 1)
private val left = Position(0, -1)
private val right = Position(0, 1)
private val leftDown = Position(1, -1)
private val down = Position(1, 0)
private val rightDown = Position(1, 1)

data class Position(val x: Point, val y: Point) {
constructor(x: String, y: String) : this(Point(x.toInt()), Point(y.toInt()))
constructor(x: Int, y: Int) : this(Point(x), Point(y))
constructor(x: Size, y: Size) : this(Point(x.value), Point(y.value))

operator fun plus(it: Position): Position {
return Position(x + it.x, y + it.y)
}

fun getAround(): List<Position> {
return aroundPositions.map {
this + it
}
}

fun getAdjacent(): List<Position> {
return adjacentPositions.map {
this + it
}
}

companion object {
private val aroundPositions = listOf(leftUp, up, rightUp, left, right, leftDown, down, rightDown)
private val adjacentPositions = listOf(up, left, right, down)
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/minesweeper/ui/InputView.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package minesweeper.ui

import minesweeper.domain.Position
import minesweeper.domain.Size

object InputView {
Expand All @@ -26,4 +27,14 @@ object InputView {
inputSize(inputType)
}
}

fun inputOpenPosition(): Position {
print("open: ")
return try {
val inputPosition = readln().split(", ")
Position(inputPosition[0], inputPosition[1])
} catch (e: RuntimeException) {
inputOpenPosition()
}
}
}
41 changes: 29 additions & 12 deletions src/main/kotlin/minesweeper/ui/ResultView.kt
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
package minesweeper.ui

import minesweeper.domain.CellFinder
import minesweeper.domain.HeightAndWidth
import minesweeper.domain.MineSweeperGame
import minesweeper.domain.Position
import minesweeper.domain.Size

object ResultView {

private const val mine_symbol = "*"
private const val mine_symbol = "C"

fun printMines(height: Size, width: Size, cellFinder: CellFinder) {
fun printMines(mineSweeperGame: MineSweeperGame, heightAndWidth: HeightAndWidth) {
heightAndWidth.height
.getNumbers()
.forEach { printRow(it, heightAndWidth.width, mineSweeperGame) }
println()
println("지뢰찾기 게임 시작")
height.getNumbers()
.forEach { printRow(it, width, cellFinder) }
}

private fun printRow(rowNum: Size, width: Size, cellFinder: CellFinder) {
private fun printRow(rowNum: Size, width: Size, mineSweeperGame: MineSweeperGame) {
width.getNumbers()
.forEach {
val cell = cellFinder.find(Position(rowNum, it)) ?: throw RuntimeException("출력 도중 알 수 없는 에러가 발생했습니다.")
when (cell.isMine) {
true -> print("$mine_symbol ")
false -> print("${cellFinder.getAroundMinesCount(cell)} ")
}
val position = Position(rowNum, it)
printEachPosition(mineSweeperGame, position)
}
println()
}

private fun printEachPosition(mineSweeperGame: MineSweeperGame, position: Position) {
when (mineSweeperGame.isOpen(position)) {
true -> print("${mineSweeperGame.getAroundMinesCount(position)} ")
false -> print("$mine_symbol ")
}
}

fun printGameStartMessage() {
println("지뢰찾기 게임 시작")
}

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

fun printWinGameMessage() {
println("Win Game.")
}
}
42 changes: 39 additions & 3 deletions src/test/kotlin/minesweeper/domain/CellFinderTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package minesweeper.domain

import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.data.forAll
import io.kotest.data.row
import io.kotest.matchers.shouldBe

class CellFinderTest : BehaviorSpec({
Expand All @@ -16,16 +18,50 @@ class CellFinderTest : BehaviorSpec({
}
}

Given("셀이 주어질 때") {
val cell = Cell(Position(2, 2))
Given("위치가 주어질 때") {
val position = Position(2, 2)
val cellFinder = CellFinder.init(Size(10), Size(10))
val minePositions = listOf(Position(1, 2), Position(1, 3))
cellFinder.convert(minePositions)
When("CellFinder의 getAroundMinesCount 함수를 호출하면") {
val result = cellFinder.getAroundMinesCount(cell)
val result = cellFinder.getAroundMinesCount(position)
Then("자신을 제외한 주변 8개 사각형에 포함된 지뢰의 개수를 반환한다.") {
result shouldBe 2
}
}
}

Given("지뢰가 있는지 알고 싶은 위치가 주어질 때") {
val cellFinder = CellFinder.init(Size(10), Size(10))
val minePositions = listOf(Position(1, 2), Position(1, 3))
cellFinder.convert(minePositions)
When("isMine 함수를 호출하면") {
Then("지뢰가 있는지 여부를 반환한다.") {
forAll(
row(Position(1, 2), true),
row(Position(1, 3), true),
row(Position(2, 2), false),
) { position, expected ->
cellFinder.isMine(position) shouldBe expected
}
}
}
}

Given("찾고 싶은 위치가 주어질 때") {
val cellFinder = CellFinder.init(Size(10), Size(10))
val minePositions = listOf(Position(1, 2), Position(1, 3))
cellFinder.convert(minePositions)
When("find 함수를 호출하면") {
Then("해당 위치의 Cell을 반환한다.") {
forAll(
row(Position(1, 2)),
row(Position(1, 3)),
row(Position(2, 2)),
) { position ->
cellFinder.find(position)?.position shouldBe position
}
}
}
}
})