diff --git a/README.md b/README.md index 4ffb78e2b..4701ef923 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,27 @@ - 필드 (Field) - [x] 필드의 각 SafeSpot에 대해 주변에 있는 지뢰의 개수를 계산한다. + +## 🚀 3단계 - 지뢰 찾기(게임 실행) + +### 기능 요구 사항 + +- 게임을 시작하면 지뢰를 찾는 게임이 시작된다. +- 좌표를 선택해 해당 좌표에 지뢰가 있는지 확인한다. +- 선택한 좌표 중심으로 지뢰가 없는 인접한 칸이 모두 열리게 된다. + +### 기능 구현 목록 + +- 스팟 (Spot) + - [x] 스팟의 상태를 가지고 있다. + - [x] 스팟을 열 수 있다. + - [x] 스팟이 열려 있는지 여부를 확인할 수 있다. + +- 지뢰 찾기 게임 (MinesweeperGame) + - [x] 좌표를 입력 받아 해당 좌표에 지뢰가 있는지 확인할 수 있다. + - [x] 선택한 좌표 중심으로 지뢰가 없는 인접한 칸이 모두 열리게 된다. + - [x] 입력 받은 좌표에 지뢰가 있는 경우 게임이 종료된다. + +- 필드 (field) + - [x] 필드의 특정 위치를 오픈할 수 있다. + - [x] 해당 필드에 인접한 지뢰 수가 0이면 인접한 모든 필드를 오픈한다. diff --git a/src/main/kotlin/minesweeper/MineSweeperApplication.kt b/src/main/kotlin/minesweeper/MineSweeperApplication.kt index 31a0449d6..98bdc1c85 100644 --- a/src/main/kotlin/minesweeper/MineSweeperApplication.kt +++ b/src/main/kotlin/minesweeper/MineSweeperApplication.kt @@ -3,7 +3,7 @@ package minesweeper import minesweeper.controller.MinesweeperController import minesweeper.domain.FieldInfo import minesweeper.infrastructure.ConsoleMinesweeperInputAdapter -import minesweeper.infrastructure.RandomSpotGenerator +import minesweeper.infrastructure.RandomMinePositionSelector import minesweeper.view.InputVIew import minesweeper.view.OutputView @@ -12,12 +12,12 @@ fun main() { val outputView = OutputView() val consoleMinesweeperInputAdapter = ConsoleMinesweeperInputAdapter(inputVIew) - val controller = MinesweeperController(consoleMinesweeperInputAdapter, outputView, RandomSpotGenerator()) + val controller = MinesweeperController(consoleMinesweeperInputAdapter, outputView, RandomMinePositionSelector()) val fieldInfo = FieldInfo(controller.getFieldHeight(), controller.getFieldWidth()) val mineCount = controller.getMineCount() - val field = controller.createNewField(fieldInfo, mineCount) + val field = controller.makeNewField(fieldInfo, mineCount) - controller.announceInitialField(field) + controller.playGame(field) } diff --git a/src/main/kotlin/minesweeper/adapter/MinesweeperInputAdapter.kt b/src/main/kotlin/minesweeper/adapter/MinesweeperInputAdapter.kt index aab7b6225..c07e682df 100644 --- a/src/main/kotlin/minesweeper/adapter/MinesweeperInputAdapter.kt +++ b/src/main/kotlin/minesweeper/adapter/MinesweeperInputAdapter.kt @@ -3,6 +3,7 @@ package minesweeper.adapter import minesweeper.domain.FieldHeight import minesweeper.domain.FieldWidth import minesweeper.domain.MineCount +import minesweeper.domain.Position interface MinesweeperInputAdapter { fun fetchFieldWidth(): FieldWidth @@ -10,4 +11,6 @@ interface MinesweeperInputAdapter { fun fetchFieldHeight(): FieldHeight fun fetchMineCount(): MineCount + + fun fetchOpenAttemptPosition(): Position } diff --git a/src/main/kotlin/minesweeper/controller/MinesweeperController.kt b/src/main/kotlin/minesweeper/controller/MinesweeperController.kt index 1a6848600..956d76359 100644 --- a/src/main/kotlin/minesweeper/controller/MinesweeperController.kt +++ b/src/main/kotlin/minesweeper/controller/MinesweeperController.kt @@ -6,14 +6,16 @@ import minesweeper.domain.FieldHeight import minesweeper.domain.FieldInfo import minesweeper.domain.FieldWidth import minesweeper.domain.MineCount -import minesweeper.domain.SpotGenerator +import minesweeper.domain.MinePositionSelector +import minesweeper.domain.OpenResult +import minesweeper.domain.Position import minesweeper.dto.FieldResponse import minesweeper.view.OutputView class MinesweeperController( private val inputAdapter: MinesweeperInputAdapter, private val outputView: OutputView, - private val spotGenerator: SpotGenerator, + private val minePositionSelector: MinePositionSelector, ) { fun getFieldWidth(): FieldWidth { return inputAdapter.fetchFieldWidth() @@ -27,14 +29,38 @@ class MinesweeperController( return inputAdapter.fetchMineCount() } - fun announceInitialField(field: Field) { - outputView.printInitialField(FieldResponse(field)) - } - - fun createNewField( + fun makeNewField( fieldInfo: FieldInfo, mineCount: MineCount, ): Field { - return Field(fieldInfo, mineCount, spotGenerator) + return Field(fieldInfo, minePositionSelector.generate(fieldInfo, mineCount)) + } + + fun playGame(field: Field) { + outputView.printStartGameMessage() + while (true) { + if (doOpen(field)) return + } + } + + private fun doOpen(field: Field): Boolean { + val openResult = field.openSpot(getOpenAttemptPosition()) + when (openResult) { + is OpenResult.GameOver -> { + outputView.printGameLoseMessage() + return true + } + is OpenResult.Success -> { + outputView.printField(FieldResponse(field)) + } + OpenResult.AlreadyOpened -> { + outputView.printAlreadyOpenedMessage(FieldResponse(field)) + } + } + return false + } + + private fun getOpenAttemptPosition(): Position { + return inputAdapter.fetchOpenAttemptPosition() } } diff --git a/src/main/kotlin/minesweeper/domain/Field.kt b/src/main/kotlin/minesweeper/domain/Field.kt index d20c6001b..8851fd605 100644 --- a/src/main/kotlin/minesweeper/domain/Field.kt +++ b/src/main/kotlin/minesweeper/domain/Field.kt @@ -1,64 +1,65 @@ package minesweeper.domain class Field( - private val fieldInfo: FieldInfo, - private val mineCount: MineCount, - private val spotGenerator: SpotGenerator, + val fieldInfo: FieldInfo, + private val minePositions: Set, ) { - private val width = fieldInfo.getWidth() - val lines: List = createField() + private val spots: Map = createField() init { validateMineCount() } - private fun createField(): List { - val spots = spotGenerator.generate(fieldInfo, mineCount) - return spots.mapIndexed { index, spot -> - if (spot is SafeSpot) { - val y = index / width - val x = index % width - val nearbyMineCount = countAdjacentMines(spots, y, x) - spot.updateNearbyMineCount(nearbyMineCount) + private fun createField(): Map { + return (0 until fieldInfo.getWidth() + 1).flatMap { x -> + (0 until fieldInfo.getHeight() + 1).map { y -> + Position(x, y) + } + }.associateWith { position -> + when { + minePositions.contains(position) -> MineSpot(position) + else -> SafeSpot(position) } - spot - }.chunked(width).map { lineSpots -> - FieldLine(lineSpots) } } - private fun countAdjacentMines( - spots: List, - y: Int, - x: Int, - ): Int { - return NEARBY.count { (dy, dx) -> - val newY = y + dy - val newX = x + dx - isWithinBounds(newY, newX) && spots[newY * width + newX].isMine() - } + fun getSpot(position: Position): Spot { + return spots[position] ?: throw IllegalArgumentException("해당 위치에 대한 Spot이 존재하지 않습니다.") } - private fun isWithinBounds( - y: Int, - x: Int, - ): Boolean { - return y in 0 until fieldInfo.getHeight() && x in 0 until width + private fun validateMineCount() { + val totalPossibleSpots = fieldInfo.getHeight() * fieldInfo.getWidth() + require(minePositions.size <= totalPossibleSpots) { "지뢰 개수는 필드의 총 스팟보다 많을 수 없습니다." } } - private fun validateMineCount() { - val height = fieldInfo.getHeight() - val totalSpots = height * width - require(mineCount.count <= totalSpots) { "지뢰 개수는 필드의 총 스팟보다 많을 수 없습니다." } + fun openSpot(position: Position): OpenResult { + val targetSpot = + spots[position]?.let { + if (it.isMine()) { + return OpenResult.GameOver + } + it + } as SafeSpot + val openResult = targetSpot.open() + targetSpot.calculateNearbyMineCount(minePositions) + checkAndOpenNearbySpot(targetSpot) + return openResult } - companion object { - private val NEARBY = - listOf( - Pair(-1, 0), - Pair(0, -1), - Pair(0, 1), - Pair(1, 0), - ) + private fun checkAndOpenNearbySpot(targetSpot: SafeSpot) { + if (targetSpot.nearbyMineCount == 0) { + openNearbySpots(targetSpot.position) + } + } + + private fun openNearbySpots(position: Position) { + val nearbyPositions = position.nearbyPositions() + nearbyPositions.forEach { + spots[it]?.let { spot -> + if (spot.isClosed()) { + openSpot(it) + } + } + } } } diff --git a/src/main/kotlin/minesweeper/domain/FieldLine.kt b/src/main/kotlin/minesweeper/domain/FieldLine.kt deleted file mode 100644 index a4427fea7..000000000 --- a/src/main/kotlin/minesweeper/domain/FieldLine.kt +++ /dev/null @@ -1,3 +0,0 @@ -package minesweeper.domain - -class FieldLine(val spots: List) diff --git a/src/main/kotlin/minesweeper/domain/SpotGenerator.kt b/src/main/kotlin/minesweeper/domain/MinePositionSelector.kt similarity index 65% rename from src/main/kotlin/minesweeper/domain/SpotGenerator.kt rename to src/main/kotlin/minesweeper/domain/MinePositionSelector.kt index c3fcc84f9..f3aeb7ae3 100644 --- a/src/main/kotlin/minesweeper/domain/SpotGenerator.kt +++ b/src/main/kotlin/minesweeper/domain/MinePositionSelector.kt @@ -1,8 +1,8 @@ package minesweeper.domain -fun interface SpotGenerator { +fun interface MinePositionSelector { fun generate( fieldInfo: FieldInfo, mineCount: MineCount, - ): List + ): Set } diff --git a/src/main/kotlin/minesweeper/domain/MineSpot.kt b/src/main/kotlin/minesweeper/domain/MineSpot.kt deleted file mode 100644 index 1e85e01b2..000000000 --- a/src/main/kotlin/minesweeper/domain/MineSpot.kt +++ /dev/null @@ -1,7 +0,0 @@ -package minesweeper.domain - -class MineSpot(private val y: Int, private val x: Int) : Spot(y, x) { - override fun isMine(): Boolean { - return true - } -} diff --git a/src/main/kotlin/minesweeper/domain/NearbyDirection.kt b/src/main/kotlin/minesweeper/domain/NearbyDirection.kt new file mode 100644 index 000000000..4c03a9b0a --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/NearbyDirection.kt @@ -0,0 +1,21 @@ +package minesweeper.domain + +enum class NearbyDirection(private val coordinate: Position) { + UP(Position(0, -1)), + DOWN(Position(0, 1)), + LEFT(Position(-1, 0)), + RIGHT(Position(1, 0)), + UP_LEFT(Position(-1, -1)), + UP_RIGHT(Position(1, -1)), + DOWN_LEFT(Position(-1, 1)), + DOWN_RIGHT(Position(1, 1)), + ; + + fun dx(): Int { + return coordinate.x + } + + fun dy(): Int { + return coordinate.y + } +} diff --git a/src/main/kotlin/minesweeper/domain/Position.kt b/src/main/kotlin/minesweeper/domain/Position.kt new file mode 100644 index 000000000..8e4245a1a --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Position.kt @@ -0,0 +1,12 @@ +package minesweeper.domain + +data class Position(val x: Int, val y: Int) { + fun nearbyPositions(): Set { + return NearbyDirection.entries.map { direction -> + Position( + x + direction.dx(), + y + direction.dy(), + ) + }.toSet() + } +} diff --git a/src/main/kotlin/minesweeper/domain/SafeSpot.kt b/src/main/kotlin/minesweeper/domain/SafeSpot.kt deleted file mode 100644 index c1e45971e..000000000 --- a/src/main/kotlin/minesweeper/domain/SafeSpot.kt +++ /dev/null @@ -1,13 +0,0 @@ -package minesweeper.domain - -class SafeSpot(private val y: Int, private val x: Int) : Spot(y, x) { - var nearbyMineCount: Int = 0 - - override fun isMine(): Boolean { - return false - } - - fun updateNearbyMineCount(count: Int) { - nearbyMineCount = count - } -} diff --git a/src/main/kotlin/minesweeper/domain/Spot.kt b/src/main/kotlin/minesweeper/domain/Spot.kt index df19be284..5735bc769 100644 --- a/src/main/kotlin/minesweeper/domain/Spot.kt +++ b/src/main/kotlin/minesweeper/domain/Spot.kt @@ -1,5 +1,51 @@ package minesweeper.domain -abstract class Spot(private val y: Int, private val x: Int) { +sealed interface OpenResult { + data object Success : OpenResult + + data object AlreadyOpened : OpenResult + + data object GameOver : OpenResult +} + +sealed class Spot(val position: Position) { + private var isOpened = false + + fun open(): OpenResult { + if (isOpened) { + return OpenResult.AlreadyOpened + } + isOpened = true + return OpenResult.Success + } + + fun isOpened(): Boolean { + return isOpened + } + + fun isClosed(): Boolean { + return !isOpened + } + abstract fun isMine(): Boolean } + +class SafeSpot(position: Position) : Spot(position) { + var nearbyMineCount: Int = 0 + private set + + override fun isMine(): Boolean { + return false + } + + fun calculateNearbyMineCount(minePositions: Set) { + val nearbyPositions = position.nearbyPositions() + this.nearbyMineCount = nearbyPositions.count { it in minePositions } + } +} + +class MineSpot(position: Position) : Spot(position) { + override fun isMine(): Boolean { + return true + } +} diff --git a/src/main/kotlin/minesweeper/dto/FieldResponse.kt b/src/main/kotlin/minesweeper/dto/FieldResponse.kt index 1b98827f0..adcfc33e0 100644 --- a/src/main/kotlin/minesweeper/dto/FieldResponse.kt +++ b/src/main/kotlin/minesweeper/dto/FieldResponse.kt @@ -1,22 +1,46 @@ package minesweeper.dto import minesweeper.domain.Field +import minesweeper.domain.Position import minesweeper.domain.SafeSpot +import minesweeper.domain.Spot class FieldResponse(private val field: Field) { - fun toFormattedStringInitialField(): String { - return field.lines.joinToString("\n") { line -> - line.spots.joinToString(" ") { spot -> - if (spot.isMine()) { - "*" - } else { - getNearbyMineCount(spot as SafeSpot) - }.toString() + fun toFormattedStringField(): String { + val fieldInfo = field.fieldInfo + val positions = generatePositions(fieldInfo.getWidth(), fieldInfo.getHeight()) + + return formatField(positions) + } + + private fun generatePositions( + width: Int, + height: Int, + ): List { + return (1..width).flatMap { x -> + (1..height).map { y -> + Position(x, y) } } } - private fun getNearbyMineCount(spot: SafeSpot): String { - return spot.nearbyMineCount.toString() + private fun formatField(positions: List): String { + return positions.groupBy { it.y } + .map { (_, positionsInRow) -> + positionsInRow.joinToString(" ") { position -> + formatSpot(field.getSpot(position)) + } + }.joinToString("\n") + } + + private fun formatSpot(spot: Spot): String { + if (spot.isOpened()) { + return if (spot is SafeSpot) { + spot.nearbyMineCount.toString() + } else { + "*" + } + } + return "C" } } diff --git a/src/main/kotlin/minesweeper/infrastructure/ConsoleMinesweeperInputAdapter.kt b/src/main/kotlin/minesweeper/infrastructure/ConsoleMinesweeperInputAdapter.kt index 1d77330fe..e929f5e50 100644 --- a/src/main/kotlin/minesweeper/infrastructure/ConsoleMinesweeperInputAdapter.kt +++ b/src/main/kotlin/minesweeper/infrastructure/ConsoleMinesweeperInputAdapter.kt @@ -4,6 +4,7 @@ import minesweeper.adapter.MinesweeperInputAdapter import minesweeper.domain.FieldHeight import minesweeper.domain.FieldWidth import minesweeper.domain.MineCount +import minesweeper.domain.Position import minesweeper.view.InputVIew class ConsoleMinesweeperInputAdapter(private val inputVIew: InputVIew) : MinesweeperInputAdapter { @@ -24,4 +25,11 @@ class ConsoleMinesweeperInputAdapter(private val inputVIew: InputVIew) : Mineswe return MineCount(it.toInt()) } } + + override fun fetchOpenAttemptPosition(): Position { + inputVIew.inputOpenAttemptPosition().let { + val (x, y) = it.split("\\s*,\\s*".toRegex()) + return Position(x.toInt(), y.toInt()) + } + } } diff --git a/src/main/kotlin/minesweeper/infrastructure/RandomMinePositionSelector.kt b/src/main/kotlin/minesweeper/infrastructure/RandomMinePositionSelector.kt new file mode 100644 index 000000000..2d979f69a --- /dev/null +++ b/src/main/kotlin/minesweeper/infrastructure/RandomMinePositionSelector.kt @@ -0,0 +1,21 @@ +package minesweeper.infrastructure + +import minesweeper.domain.FieldInfo +import minesweeper.domain.MineCount +import minesweeper.domain.MinePositionSelector +import minesweeper.domain.Position + +class RandomMinePositionSelector : MinePositionSelector { + override fun generate( + fieldInfo: FieldInfo, + mineCount: MineCount, + ): Set { + return (1..fieldInfo.getWidth()).flatMap { x -> + (1..fieldInfo.getHeight()).map { y -> + Position(x, y) + } + }.shuffled() + .take(mineCount.count) + .toSet() + } +} diff --git a/src/main/kotlin/minesweeper/infrastructure/RandomSpotGenerator.kt b/src/main/kotlin/minesweeper/infrastructure/RandomSpotGenerator.kt deleted file mode 100644 index d912319a5..000000000 --- a/src/main/kotlin/minesweeper/infrastructure/RandomSpotGenerator.kt +++ /dev/null @@ -1,68 +0,0 @@ -package minesweeper.infrastructure - -import minesweeper.domain.FieldInfo -import minesweeper.domain.MineCount -import minesweeper.domain.MineSpot -import minesweeper.domain.SafeSpot -import minesweeper.domain.Spot -import minesweeper.domain.SpotGenerator - -class RandomSpotGenerator : SpotGenerator { - override fun generate( - fieldInfo: FieldInfo, - mineCount: MineCount, - ): List { - val height = fieldInfo.getHeight() - val width = fieldInfo.getWidth() - val minePositions = extractMinePositions(mineCount, width, height) - - return createFieldSpots(height, width, minePositions) - } - - private fun extractMinePositions( - mineCount: MineCount, - width: Int, - height: Int, - ): Set> { - val minePositions = mutableSetOf>() - while (minePositions.size < mineCount.count) { - val position = generateRandomPosition(width, height) - minePositions.add(position) - } - return minePositions - } - - private fun generateRandomPosition( - width: Int, - height: Int, - ): Pair { - val x = (0 until height).random() - val y = (0 until width).random() - return Pair(x, y) - } - - private fun createFieldSpots( - height: Int, - width: Int, - minePositions: Set>, - ): List { - return (0 until height).flatMap { x -> - (0 until width).map { y -> - createSpot(x, y, minePositions) - } - } - } - - private fun createSpot( - x: Int, - y: Int, - minePositions: Set>, - ): Spot { - val position = Pair(x, y) - - if (minePositions.contains(position)) { - return MineSpot(y, x) - } - return SafeSpot(y, x) - } -} diff --git a/src/main/kotlin/minesweeper/view/InputVIew.kt b/src/main/kotlin/minesweeper/view/InputVIew.kt index a5259cdd7..e7db37956 100644 --- a/src/main/kotlin/minesweeper/view/InputVIew.kt +++ b/src/main/kotlin/minesweeper/view/InputVIew.kt @@ -15,4 +15,9 @@ class InputVIew { println("\n지뢰는 몇 개인가요?") return readln() } + + fun inputOpenAttemptPosition(): String { + print("open: ") + return readln() + } } diff --git a/src/main/kotlin/minesweeper/view/OutputView.kt b/src/main/kotlin/minesweeper/view/OutputView.kt index 822b97eca..91a1cba3b 100644 --- a/src/main/kotlin/minesweeper/view/OutputView.kt +++ b/src/main/kotlin/minesweeper/view/OutputView.kt @@ -3,7 +3,20 @@ package minesweeper.view import minesweeper.dto.FieldResponse class OutputView { - fun printInitialField(fieldResponse: FieldResponse) { - println("\n지뢰찾기 게임 시작\n" + fieldResponse.toFormattedStringInitialField()) + fun printStartGameMessage() { + println("\n지뢰찾기 게임 시작") + } + + fun printField(fieldResponse: FieldResponse) { + println(fieldResponse.toFormattedStringField() + "\n") + } + + fun printAlreadyOpenedMessage(fieldResponse: FieldResponse) { + println("이미 열린 위치입니다.") + printField(fieldResponse) + } + + fun printGameLoseMessage() { + println("Lose Game.") } } diff --git a/src/test/kotlin/minesweeper/domain/FieldTest.kt b/src/test/kotlin/minesweeper/domain/FieldTest.kt index 70ad9e239..85181417c 100644 --- a/src/test/kotlin/minesweeper/domain/FieldTest.kt +++ b/src/test/kotlin/minesweeper/domain/FieldTest.kt @@ -3,60 +3,116 @@ package minesweeper.domain import io.kotest.core.spec.style.StringSpec import io.kotest.data.forAll import io.kotest.data.row -import io.kotest.matchers.equality.shouldBeEqualToComparingFields import io.kotest.matchers.shouldBe -import minesweeper.infrastructrue.CustomSpotGenerator +import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.matchers.types.shouldBeSameInstanceAs +import minesweeper.infrastructrue.CustomMinePositionSelector class FieldTest : StringSpec({ "필드를 생성할 수 있다." { val height = 3 val width = 4 - val minePositions = setOf(Pair(0, 0), Pair(1, 2), Pair(3, 2)) + val minePositions = setOf(Position(1, 1), Position(2, 3), Position(4, 3)) val mineCount = MineCount(minePositions.size) val fieldInfo = FieldInfo(FieldHeight(height), FieldWidth(width)) - val spotGenerator = CustomSpotGenerator(minePositions) + val spotGenerator = CustomMinePositionSelector(minePositions) - val field = Field(fieldInfo, mineCount, spotGenerator) + val field = Field(fieldInfo, spotGenerator.generate(fieldInfo, mineCount)) - field.lines.size shouldBe height - - field.lines.forEach { line -> - line.spots.size shouldBe width - } - - (0 until height).forEach { x -> - (0 until width).forEach { y -> - if (minePositions.contains(Pair(x, y))) { - field.lines[x].spots[y] shouldBeEqualToComparingFields MineSpot(x, y) + (0 until width).forEach { x -> + (0 until height).forEach { y -> + if (minePositions.contains(Position(x, y))) { + val mineSpot = field.getSpot(Position(x, y)) + mineSpot.shouldBeInstanceOf() + mineSpot.position shouldBe Position(x, y) } else { - field.lines[x].spots[y] shouldBeEqualToComparingFields SafeSpot(x, y) + val safeSpot = field.getSpot(Position(x, y)) + safeSpot.shouldBeInstanceOf() + safeSpot.position shouldBe Position(x, y) } } } } /* - * C C C - C C C C - C * C * + * 1 0 0 + 2 2 2 1 + 1 * 2 * */ "필드의 각 SafeSpot에 대해 주변에 있는 지뢰의 개수를 계산한다." { forAll( - row(0, 1, 1), - row(3, 0, 0), - row(2, 2, 2), + row(1, 2, 2), + row(4, 1, 0), + row(3, 3, 2), ) { x, y, expected -> val height = 3 val width = 4 - val minePositions = setOf(Pair(0, 0), Pair(1, 2), Pair(3, 2)) + val minePositions = setOf(Position(1, 1), Position(2, 3), Position(4, 3)) val mineCount = MineCount(minePositions.size) val fieldInfo = FieldInfo(FieldHeight(height), FieldWidth(width)) - val spotGenerator = CustomSpotGenerator(minePositions) + val spotGenerator = CustomMinePositionSelector(minePositions) - val field = Field(fieldInfo, mineCount, spotGenerator) + val field = Field(fieldInfo, spotGenerator.generate(fieldInfo, mineCount)) - val spot = field.lines[y].spots[x] as SafeSpot + val spot = field.getSpot(Position(x, y)) as SafeSpot + spot.calculateNearbyMineCount(minePositions) spot.nearbyMineCount shouldBe expected } } + + /* + 1 2 3 4 x + * 1 0 0 1 C 1 0 0 + 2 2 2 1 -> 2 C 2 2 1 + 1 * 2 * 3 C C C C + y + */ + "해당 필드에 인접한 지뢰 수가 0이면 인접한 모든 필드를 오픈한다." { + val height = 3 + val width = 4 + val minePositions = setOf(Position(1, 1), Position(2, 3), Position(4, 3)) + val mineCount = MineCount(minePositions.size) + val fieldInfo = FieldInfo(FieldHeight(height), FieldWidth(width)) + val spotGenerator = CustomMinePositionSelector(minePositions) + + val field = Field(fieldInfo, spotGenerator.generate(fieldInfo, mineCount)) + + field.openSpot(Position(3, 1)) shouldBeSameInstanceAs OpenResult.Success + + Position(1, 1).isClosedSpot(field) + Position(2, 1).isOpenSpot(field) + Position(3, 1).isOpenSpot(field) + Position(4, 1).isOpenSpot(field) + + Position(1, 2).isClosedSpot(field) + Position(2, 2).isOpenSpot(field) + Position(3, 2).isOpenSpot(field) + Position(4, 2).isOpenSpot(field) + + Position(1, 3).isClosedSpot(field) + Position(2, 3).isClosedSpot(field) + Position(3, 3).isClosedSpot(field) + Position(4, 3).isClosedSpot(field) + } + + "지뢰에 해당하는 스팟을 열면 GameOver를 반환한다." { + val height = 3 + val width = 4 + val minePositions = setOf(Position(1, 1)) + val mineCount = MineCount(minePositions.size) + val fieldInfo = FieldInfo(FieldHeight(height), FieldWidth(width)) + val spotGenerator = CustomMinePositionSelector(minePositions) + + val field = Field(fieldInfo, spotGenerator.generate(fieldInfo, mineCount)) + + field.openSpot(Position(1, 1)) shouldBeSameInstanceAs OpenResult.GameOver + } }) + +private fun Position.isOpenSpot(field: Field) { + field.getSpot(this).isOpened() shouldBe true +} + +private fun Position.isClosedSpot(field: Field) { + field.getSpot(this).isClosed() shouldBe true +} diff --git a/src/test/kotlin/minesweeper/domain/PositionTest.kt b/src/test/kotlin/minesweeper/domain/PositionTest.kt new file mode 100644 index 000000000..f1c80ad75 --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/PositionTest.kt @@ -0,0 +1,25 @@ +package minesweeper.domain + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class PositionTest : StringSpec({ + "특정 좌표의 근처에 해당하는 좌표를 반환한다." { + val position = Position(1, 1) + + val expectedPositions = + setOf( + Position(0, 0), + Position(0, 1), + Position(0, 2), + Position(1, 0), + Position(1, 2), + Position(2, 0), + Position(2, 1), + Position(2, 2), + ) + val nearbyPositions = position.nearbyPositions() + + nearbyPositions shouldBe expectedPositions + } +}) diff --git a/src/test/kotlin/minesweeper/domain/SpotTest.kt b/src/test/kotlin/minesweeper/domain/SpotTest.kt index c24fa9916..635b005d2 100644 --- a/src/test/kotlin/minesweeper/domain/SpotTest.kt +++ b/src/test/kotlin/minesweeper/domain/SpotTest.kt @@ -2,13 +2,31 @@ package minesweeper.domain import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeSameInstanceAs class SpotTest : StringSpec({ "지뢰인지 아닌지 여부를 반환할 수 있다." { - val mineSpot = MineSpot(1, 1) - val safeSpot = SafeSpot(2, 3) + val mineSpot = MineSpot(Position(1, 1)) + val safeSpot = SafeSpot(Position(2, 3)) mineSpot.isMine() shouldBe true safeSpot.isMine() shouldBe false } + + "스팟을 열 수 있다." { + val mineSpot = MineSpot(Position(1, 1)) + val safeSpot = SafeSpot(Position(2, 3)) + + safeSpot.open() + + safeSpot.isOpened() shouldBe true + mineSpot.isOpened() shouldBe false + } + + "이미 열려 있는 칸을 열면 AlreadyOpened를 반환한다." { + val safeSpot = SafeSpot(Position(2, 3)) + safeSpot.open() + + safeSpot.open() shouldBeSameInstanceAs OpenResult.AlreadyOpened + } }) diff --git a/src/test/kotlin/minesweeper/infrastructrue/CustomMinePositionSelector.kt b/src/test/kotlin/minesweeper/infrastructrue/CustomMinePositionSelector.kt new file mode 100644 index 000000000..a209ff251 --- /dev/null +++ b/src/test/kotlin/minesweeper/infrastructrue/CustomMinePositionSelector.kt @@ -0,0 +1,15 @@ +package minesweeper.infrastructrue + +import minesweeper.domain.FieldInfo +import minesweeper.domain.MineCount +import minesweeper.domain.MinePositionSelector +import minesweeper.domain.Position + +class CustomMinePositionSelector(private val minePositions: Set) : MinePositionSelector { + override fun generate( + fieldInfo: FieldInfo, + mineCount: MineCount, + ): Set { + return minePositions + } +} diff --git a/src/test/kotlin/minesweeper/infrastructrue/CustomSpotGenerator.kt b/src/test/kotlin/minesweeper/infrastructrue/CustomSpotGenerator.kt deleted file mode 100644 index 7f161f130..000000000 --- a/src/test/kotlin/minesweeper/infrastructrue/CustomSpotGenerator.kt +++ /dev/null @@ -1,45 +0,0 @@ -package minesweeper.infrastructrue - -import minesweeper.domain.FieldInfo -import minesweeper.domain.MineCount -import minesweeper.domain.MineSpot -import minesweeper.domain.SafeSpot -import minesweeper.domain.Spot -import minesweeper.domain.SpotGenerator - -class CustomSpotGenerator(private val minePositions: Set>) : SpotGenerator { - override fun generate( - fieldInfo: FieldInfo, - mineCount: MineCount, - ): List { - val height = fieldInfo.getHeight() - val width = fieldInfo.getWidth() - - return createFieldSpots(height, width, minePositions) - } - - private fun createFieldSpots( - height: Int, - width: Int, - minePositions: Set>, - ): List { - return (0 until height).flatMap { x -> - (0 until width).map { y -> - createSpot(x, y, minePositions) - } - } - } - - private fun createSpot( - x: Int, - y: Int, - minePositions: Set>, - ): Spot { - val position = Pair(y, x) - - if (minePositions.contains(position)) { - return MineSpot(y, x) - } - return SafeSpot(y, x) - } -}