From 7c06123635633d7df78e83c81444147fdc654de3 Mon Sep 17 00:00:00 2001 From: yeongun Date: Sat, 9 Dec 2023 09:29:46 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20Position=EC=9D=B4=20=EC=A3=BC?= =?UTF-8?q?=EB=B3=80=208=EA=B0=9C=EC=9D=98=20=EC=A2=8C=ED=91=9C=EB=A5=BC?= =?UTF-8?q?=20=EB=8B=B4=EC=9D=80=20=EC=9C=84=EC=B9=98=EB=93=A4=EC=9D=84=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ .../kotlin/minesweeper/domain/Position.kt | 19 ++++++++++++++ .../kotlin/minesweeper/domain/PositionTest.kt | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/test/kotlin/minesweeper/domain/PositionTest.kt diff --git a/README.md b/README.md index 7e878ff1a..2cd01de3e 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,6 @@ ### 요구사항 쪼개기 - CellFinder - Cell을 입력받을 경우 해당 셀을 제외한 주변 8개 사각형에 포함된 지뢰의 개수를 반환한다. + +- Position + - Position은 주변 8개의 좌표를 담은 위치들을 반환한다. diff --git a/src/main/kotlin/minesweeper/domain/Position.kt b/src/main/kotlin/minesweeper/domain/Position.kt index abfd5e1f5..d18c0063a 100644 --- a/src/main/kotlin/minesweeper/domain/Position.kt +++ b/src/main/kotlin/minesweeper/domain/Position.kt @@ -1,5 +1,14 @@ 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: Int, y: Int) : this(Point(x), Point(y)) constructor(x: Size, y: Size) : this(Point(x.value), Point(y.value)) @@ -7,4 +16,14 @@ data class Position(val x: Point, val y: Point) { operator fun plus(it: Position): Position { return Position(x + it.x, y + it.y) } + + fun getAround(): List { + return aroundPositions.map { + this + it + } + } + + companion object { + private val aroundPositions = listOf(leftUp, up, rightUp, left, right, leftDown, down, rightDown) + } } diff --git a/src/test/kotlin/minesweeper/domain/PositionTest.kt b/src/test/kotlin/minesweeper/domain/PositionTest.kt new file mode 100644 index 000000000..2774aa13d --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/PositionTest.kt @@ -0,0 +1,25 @@ +package minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactly + +class PositionTest : BehaviorSpec({ + Given("위치가 주어지면") { + val position = Position(2, 2) + When("getAround 함수는") { + val aroundPositions = position.getAround() + Then("주변 8개의 위치를 반환한다.") { + aroundPositions shouldContainExactly listOf( + Position(1, 1), + Position(1, 2), + Position(1, 3), + Position(2, 1), + Position(2, 3), + Position(3, 1), + Position(3, 2), + Position(3, 3), + ) + } + } + } +}) From 8de71b61a4b39d64fd585dff6027f25776391d5b Mon Sep 17 00:00:00 2001 From: yeongun Date: Sat, 9 Dec 2023 09:33:55 +0900 Subject: [PATCH 02/12] =?UTF-8?q?refactor:=20CellFinder=EA=B0=80=20?= =?UTF-8?q?=EC=A3=BC=EB=B3=80=20=EC=A7=80=EB=A2=B0=20=EA=B0=9C=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=A0=20=EB=95=8C=20Position.?= =?UTF-8?q?getAround()=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/domain/CellFinder.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/minesweeper/domain/CellFinder.kt b/src/main/kotlin/minesweeper/domain/CellFinder.kt index dda0a4740..f720f4889 100644 --- a/src/main/kotlin/minesweeper/domain/CellFinder.kt +++ b/src/main/kotlin/minesweeper/domain/CellFinder.kt @@ -3,10 +3,6 @@ package minesweeper.domain class CellFinder(private val map: MutableMap) { private constructor(initPositions: List) : 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) { minePosition.forEach { map[it] = Cell(it, true) @@ -19,10 +15,11 @@ class CellFinder(private val map: MutableMap) { fun getAroundMinesCount(cell: Cell): Int { val position = cell.position - return list.mapNotNull { - val nextPosition = position + it - find(nextPosition) - }.count { it.isMine } + return position.getAround() + .mapNotNull { find(it) } + .count { + it.isMine + } } companion object { From 081d0dc2006afbd8d02f593646b95d66b2205762 Mon Sep 17 00:00:00 2001 From: yeongun Date: Sat, 9 Dec 2023 10:06:47 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor:=20View=EC=97=90=EC=84=9C=20Cell?= =?UTF-8?q?=EC=9D=98=20=EC=A3=BC=EB=B3=80=20=EC=A7=80=EB=A2=B0=EA=B0=AF?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20=EA=B3=84=EC=82=B0=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/minesweeper/domain/CellFinder.kt | 8 ++++-- src/main/kotlin/minesweeper/ui/ResultView.kt | 6 ++--- .../minesweeper/domain/CellFinderTest.kt | 25 ++++++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/minesweeper/domain/CellFinder.kt b/src/main/kotlin/minesweeper/domain/CellFinder.kt index f720f4889..96cc999a4 100644 --- a/src/main/kotlin/minesweeper/domain/CellFinder.kt +++ b/src/main/kotlin/minesweeper/domain/CellFinder.kt @@ -13,8 +13,7 @@ class CellFinder(private val map: MutableMap) { return map[position] } - fun getAroundMinesCount(cell: Cell): Int { - val position = cell.position + fun getAroundMinesCount(position: Position): Int { return position.getAround() .mapNotNull { find(it) } .count { @@ -22,6 +21,11 @@ class CellFinder(private val map: MutableMap) { } } + fun isMine(position: Position): Boolean { + val cell = find(position) ?: throw IllegalArgumentException("주어진 위치를 찾을 수 없습니다.") + return cell.isMine + } + companion object { fun init(height: Size, width: Size): CellFinder { return CellFinder( diff --git a/src/main/kotlin/minesweeper/ui/ResultView.kt b/src/main/kotlin/minesweeper/ui/ResultView.kt index ab4bdc884..49f762104 100644 --- a/src/main/kotlin/minesweeper/ui/ResultView.kt +++ b/src/main/kotlin/minesweeper/ui/ResultView.kt @@ -18,10 +18,10 @@ object ResultView { private fun printRow(rowNum: Size, width: Size, cellFinder: CellFinder) { width.getNumbers() .forEach { - val cell = cellFinder.find(Position(rowNum, it)) ?: throw RuntimeException("출력 도중 알 수 없는 에러가 발생했습니다.") - when (cell.isMine) { + val position = Position(rowNum, it) + when (cellFinder.isMine(position)) { true -> print("$mine_symbol ") - false -> print("${cellFinder.getAroundMinesCount(cell)} ") + false -> print("${cellFinder.getAroundMinesCount(position)} ") } } println() diff --git a/src/test/kotlin/minesweeper/domain/CellFinderTest.kt b/src/test/kotlin/minesweeper/domain/CellFinderTest.kt index 2b1e1baca..76d886be0 100644 --- a/src/test/kotlin/minesweeper/domain/CellFinderTest.kt +++ b/src/test/kotlin/minesweeper/domain/CellFinderTest.kt @@ -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({ @@ -16,16 +18,33 @@ 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 + } + } + } + } }) From e6abe3484a72429c7f2cd5c90f1b0943c10b6cf9 Mon Sep 17 00:00:00 2001 From: yeongun Date: Sat, 9 Dec 2023 10:24:43 +0900 Subject: [PATCH 04/12] =?UTF-8?q?test:=20find=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/minesweeper/domain/CellFinderTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/kotlin/minesweeper/domain/CellFinderTest.kt b/src/test/kotlin/minesweeper/domain/CellFinderTest.kt index 76d886be0..78c989961 100644 --- a/src/test/kotlin/minesweeper/domain/CellFinderTest.kt +++ b/src/test/kotlin/minesweeper/domain/CellFinderTest.kt @@ -47,4 +47,21 @@ class CellFinderTest : BehaviorSpec({ } } } + + 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 + } + } + } + } }) From 62f74955f54deeafc2fa9f9ec893c6cf01fcab31 Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 16:29:09 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=203=EB=8B=A8=EA=B3=84=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++ .../minesweeper/MineSweeperApplication.kt | 34 ++++++++++- .../kotlin/minesweeper/domain/CellFinder.kt | 4 ++ .../minesweeper/domain/HeightAndWidth.kt | 3 + .../minesweeper/domain/MineSweeperGame.kt | 56 +++++++++++++++++++ .../kotlin/minesweeper/domain/Position.kt | 1 + src/main/kotlin/minesweeper/ui/InputView.kt | 11 ++++ src/main/kotlin/minesweeper/ui/ResultView.kt | 39 +++++++++---- 8 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/minesweeper/domain/HeightAndWidth.kt create mode 100644 src/main/kotlin/minesweeper/domain/MineSweeperGame.kt diff --git a/README.md b/README.md index 2cd01de3e..7f3bbcb64 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,13 @@ - Position - Position은 주변 8개의 좌표를 담은 위치들을 반환한다. + +## 3단계 - 지뢰 찾기(게임 실행) + +### 기능 요구사항 +- 지뢰가 없는 인접한 칸이 모두 열리게 된다. + +### 요구사항 쪼개기 +- MineSweeperGame + - Open을 하면 해당 좌표에 인접한 칸 중 지뢰가 없는 칸이 모두 Open 상태로 바뀐다. + - Open 상태이면 주변 지뢰의 개수를 출력한다. diff --git a/src/main/kotlin/minesweeper/MineSweeperApplication.kt b/src/main/kotlin/minesweeper/MineSweeperApplication.kt index 70adab5a3..c44a417b4 100644 --- a/src/main/kotlin/minesweeper/MineSweeperApplication.kt +++ b/src/main/kotlin/minesweeper/MineSweeperApplication.kt @@ -1,7 +1,11 @@ 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 @@ -9,11 +13,35 @@ 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() + } } diff --git a/src/main/kotlin/minesweeper/domain/CellFinder.kt b/src/main/kotlin/minesweeper/domain/CellFinder.kt index 96cc999a4..8da3131ab 100644 --- a/src/main/kotlin/minesweeper/domain/CellFinder.kt +++ b/src/main/kotlin/minesweeper/domain/CellFinder.kt @@ -26,6 +26,10 @@ class CellFinder(private val map: MutableMap) { return cell.isMine } + fun size(): Int { + return map.size + } + companion object { fun init(height: Size, width: Size): CellFinder { return CellFinder( diff --git a/src/main/kotlin/minesweeper/domain/HeightAndWidth.kt b/src/main/kotlin/minesweeper/domain/HeightAndWidth.kt new file mode 100644 index 000000000..70875eb4d --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/HeightAndWidth.kt @@ -0,0 +1,3 @@ +package minesweeper.domain + +data class HeightAndWidth(val height: Size, val width: Size) diff --git a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt new file mode 100644 index 000000000..db826c2e1 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt @@ -0,0 +1,56 @@ +package minesweeper.domain + +private val x = arrayOf(-1, 0, 0, 1) +private val y = arrayOf(0, -1, 1, 0) + +class MineSweeperGame( + private val cellFinder: CellFinder, + private val openPositions: MutableList = mutableListOf(), +) { + + fun open(position: Position) { + if (cellFinder.find(position) == null) { + return + } + + openPositions.add(position) + for (i in 0 until 4) { + val nextPosition = position + Position(x[i], y[i]) + if (!isOpen(nextPosition) && !isMine(nextPosition)) { + open(nextPosition) + } + } + } + + 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 { + for (openPosition in openPositions) { + if (isMine(openPosition)) { + 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 + } +} diff --git a/src/main/kotlin/minesweeper/domain/Position.kt b/src/main/kotlin/minesweeper/domain/Position.kt index d18c0063a..b3ab38cf7 100644 --- a/src/main/kotlin/minesweeper/domain/Position.kt +++ b/src/main/kotlin/minesweeper/domain/Position.kt @@ -10,6 +10,7 @@ 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)) diff --git a/src/main/kotlin/minesweeper/ui/InputView.kt b/src/main/kotlin/minesweeper/ui/InputView.kt index ff91c73af..3e340d1c6 100644 --- a/src/main/kotlin/minesweeper/ui/InputView.kt +++ b/src/main/kotlin/minesweeper/ui/InputView.kt @@ -1,5 +1,6 @@ package minesweeper.ui +import minesweeper.domain.Position import minesweeper.domain.Size object InputView { @@ -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() + } + } } diff --git a/src/main/kotlin/minesweeper/ui/ResultView.kt b/src/main/kotlin/minesweeper/ui/ResultView.kt index 49f762104..159ca5052 100644 --- a/src/main/kotlin/minesweeper/ui/ResultView.kt +++ b/src/main/kotlin/minesweeper/ui/ResultView.kt @@ -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 position = Position(rowNum, it) - when (cellFinder.isMine(position)) { - true -> print("$mine_symbol ") - false -> print("${cellFinder.getAroundMinesCount(position)} ") - } + 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.") + } } From c51785c6bddc489c4275d413cd6d000b7dd9121a Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 16:42:56 +0900 Subject: [PATCH 06/12] =?UTF-8?q?refactor:=20MineSweeperGame=EA=B0=80=20?= =?UTF-8?q?=EC=B5=9C=EB=8C=80=201depth=20=EC=9D=B4=ED=95=98=EB=A1=9C=20?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/MineSweeperGame.kt | 22 ++++++++----------- .../kotlin/minesweeper/domain/Position.kt | 7 ++++++ .../kotlin/minesweeper/domain/PositionTest.kt | 17 +++++++++++++- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt index db826c2e1..07a02a3b7 100644 --- a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt +++ b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt @@ -1,8 +1,5 @@ package minesweeper.domain -private val x = arrayOf(-1, 0, 0, 1) -private val y = arrayOf(0, -1, 1, 0) - class MineSweeperGame( private val cellFinder: CellFinder, private val openPositions: MutableList = mutableListOf(), @@ -14,12 +11,9 @@ class MineSweeperGame( } openPositions.add(position) - for (i in 0 until 4) { - val nextPosition = position + Position(x[i], y[i]) - if (!isOpen(nextPosition) && !isMine(nextPosition)) { - open(nextPosition) - } - } + position.getAdjacent() + .filter { !isOpen(it) && !isMine(it) } + .forEach { open(it) } } fun isOpen(position: Position): Boolean { @@ -37,10 +31,8 @@ class MineSweeperGame( } fun isFinished(mineCount: Size): Boolean { - for (openPosition in openPositions) { - if (isMine(openPosition)) { - return true - } + if (isLose()) { + return true } return cellFinder.size() == openPositions.size + mineCount.value @@ -53,4 +45,8 @@ class MineSweeperGame( fun isWin(mineCount: Size): Boolean { return cellFinder.size() == openPositions.size + mineCount.value } + + private fun isLose(): Boolean { + return openPositions.any { isMine(it) } + } } diff --git a/src/main/kotlin/minesweeper/domain/Position.kt b/src/main/kotlin/minesweeper/domain/Position.kt index b3ab38cf7..f27787e9a 100644 --- a/src/main/kotlin/minesweeper/domain/Position.kt +++ b/src/main/kotlin/minesweeper/domain/Position.kt @@ -24,7 +24,14 @@ data class Position(val x: Point, val y: Point) { } } + fun getAdjacent(): List { + 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) } } diff --git a/src/test/kotlin/minesweeper/domain/PositionTest.kt b/src/test/kotlin/minesweeper/domain/PositionTest.kt index 2774aa13d..1ea166d18 100644 --- a/src/test/kotlin/minesweeper/domain/PositionTest.kt +++ b/src/test/kotlin/minesweeper/domain/PositionTest.kt @@ -4,7 +4,7 @@ import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.collections.shouldContainExactly class PositionTest : BehaviorSpec({ - Given("위치가 주어지면") { + Given("주변 8개의 위치를 알고 싶은 위치가 주어지면") { val position = Position(2, 2) When("getAround 함수는") { val aroundPositions = position.getAround() @@ -22,4 +22,19 @@ class PositionTest : BehaviorSpec({ } } } + + Given("인접한 상하좌우의 위치를 알고 싶은 위치가 주어지면") { + val position = Position(2, 2) + When("getAdjacent 함수는") { + val aroundPositions = position.getAdjacent() + Then("인접한 상하좌우의 위치를 반환한다.") { + aroundPositions shouldContainExactly listOf( + Position(1, 2), + Position(2, 1), + Position(2, 3), + Position(3, 2), + ) + } + } + } }) From 03babd5b17a1eb32109b0bbb0a6c91639cf4197e Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 16:48:56 +0900 Subject: [PATCH 07/12] =?UTF-8?q?test:=20Size=20=EB=8D=94=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/kotlin/minesweeper/domain/SizeTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/kotlin/minesweeper/domain/SizeTest.kt b/src/test/kotlin/minesweeper/domain/SizeTest.kt index 2a479b903..165b27a93 100644 --- a/src/test/kotlin/minesweeper/domain/SizeTest.kt +++ b/src/test/kotlin/minesweeper/domain/SizeTest.kt @@ -71,4 +71,14 @@ class SizeTest : BehaviorSpec({ } } } + + Given("더하려는 다른 사이즈가 주어지면") { + val other = Size(1) + When("사이즈는") { + val actual = Size(2) + other + Then("더한 값 원소로 갖는 사이즈를 반환한다.") { + actual shouldBe Size(3) + } + } + } }) From c9a429de63f56f50ac62a633ebb394d16495ed5d Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 17:20:42 +0900 Subject: [PATCH 08/12] =?UTF-8?q?test:=20MineSweeperGame.isMine()=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/MineSweeperGameTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt diff --git a/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt b/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt new file mode 100644 index 000000000..c5072c5a7 --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt @@ -0,0 +1,30 @@ +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 MineSweeperGameTest : BehaviorSpec({ + Given("지뢰가 있는지 알고 싶은 위치가 주어지면") { + val height = Size(10) + val width = Size(10) + val mineCount = Size(10) + + val minePositions = FixedPositionGenerator(height, width).generate(mineCount) + val cellFinder = CellFinder.init(height, width) + cellFinder.convert(minePositions) + When("지뢰찾기게임은") { + val mineSweeperGame = MineSweeperGame(cellFinder) + Then("해당 위치의 지뢰 유무를 반환한다.") { + forAll( + row(Position(1, 2), true), + row(Position(-1, -1), false), + row(Position(5, 5), false), + ) { position, expected -> + mineSweeperGame.isMine(position) shouldBe expected + } + } + } + } +}) From 00e7b13556718637b0fbf3360e5e42fa22cfbd84 Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 17:34:38 +0900 Subject: [PATCH 09/12] =?UTF-8?q?refactor:=20MineSweeperGame=EC=9D=98=20?= =?UTF-8?q?=EC=98=A4=ED=94=88=EB=90=9C=20=ED=8F=AC=EC=A7=80=EC=85=98?= =?UTF-8?q?=EC=9D=84=20List=EC=97=90=EC=84=9C=20Set=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/domain/MineSweeperGame.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt index 07a02a3b7..a2cb9d8d9 100644 --- a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt +++ b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt @@ -2,7 +2,7 @@ package minesweeper.domain class MineSweeperGame( private val cellFinder: CellFinder, - private val openPositions: MutableList = mutableListOf(), + private val openPositions: MutableSet = mutableSetOf(), ) { fun open(position: Position) { From f7da4b2b8df146ae0eece7c220139ca1810f86fb Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 17:36:25 +0900 Subject: [PATCH 10/12] =?UTF-8?q?test:=20MineSweeperGame.open()=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/MineSweeperGameTest.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt b/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt index c5072c5a7..dc92ee4a7 100644 --- a/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt +++ b/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt @@ -27,4 +27,30 @@ class MineSweeperGameTest : BehaviorSpec({ } } } + + Given("Open 하려는 위치가 주어지면") { + val height = Size(10) + val width = Size(10) + val mineCount = Size(10) + + val minePositions = FixedPositionGenerator(height, width).generate(mineCount) + val cellFinder = CellFinder.init(height, width) + cellFinder.convert(minePositions) + + val mineSweeperGame = MineSweeperGame(cellFinder) + When("지뢰찾기게임은") { + mineSweeperGame.open(Position(10, 10)) + Then("지뢰가 없는 인접한 칸이 모두 열린다.") { + forAll( + row(Position(1, 1), false), + row(Position(-1, -1), false), + row(Position(1, 2), false), + row(Position(5, 5), true), + row(Position(10, 10), true), + ) { position, expected -> + mineSweeperGame.isOpen(position) shouldBe expected + } + } + } + } }) From c343f335fb26eafc386ad4e4aa28ae49bd3045b5 Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 17:44:14 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20Open=20=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?=EC=A3=BC=EB=B3=80=EC=9D=98=20=EC=A7=80=EB=A2=B0=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EB=8B=A4=EB=A9=B4=20=EB=8D=94=EC=9D=B4=EC=83=81=20?= =?UTF-8?q?=EC=9D=B8=EC=A0=91=ED=95=9C=20=EC=B9=B8=EC=9D=84=20Open=20?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/domain/MineSweeperGame.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt index a2cb9d8d9..19b181a46 100644 --- a/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt +++ b/src/main/kotlin/minesweeper/domain/MineSweeperGame.kt @@ -11,6 +11,9 @@ class MineSweeperGame( } openPositions.add(position) + if (getAroundMinesCount(position) != 0) { + return + } position.getAdjacent() .filter { !isOpen(it) && !isMine(it) } .forEach { open(it) } From 616c5d2718fed590f706fe9bc6623e4020f6adf3 Mon Sep 17 00:00:00 2001 From: yeongun Date: Fri, 29 Dec 2023 18:26:27 +0900 Subject: [PATCH 12/12] =?UTF-8?q?test:=20MineSweeperGame.isWin()=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/MineSweeperGameTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt b/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt index dc92ee4a7..f9d910607 100644 --- a/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt +++ b/src/test/kotlin/minesweeper/domain/MineSweeperGameTest.kt @@ -53,4 +53,33 @@ class MineSweeperGameTest : BehaviorSpec({ } } } + + Given("지뢰 개수가 주어지면") { + val height = Size(10) + val width = Size(10) + val mineCount = Size(10) + + val minePositions = FixedPositionGenerator(height, width).generate(mineCount) + val cellFinder = CellFinder.init(height, width) + cellFinder.convert(minePositions) + + When("지뢰찾기게임은") { + val mineSweeperGame = MineSweeperGame(cellFinder) + val actual = mineSweeperGame.isWin(mineCount) + Then("게임에서 이겼는지 여부를 반환한다.") { + actual shouldBe false + } + } + + When("지뢰찾기게임은") { + val mineSweeperGame = MineSweeperGame(cellFinder) + mineSweeperGame.open(Position(1, 1)) + mineSweeperGame.open(Position(2, 2)) + mineSweeperGame.open(Position(10, 10)) + val actual = mineSweeperGame.isWin(mineCount) + Then("게임에서 이겼는지 여부를 반환한다.") { + actual shouldBe true + } + } + } })