From 67d6034a964f3568b0ddb29c7a50780c8267b26b Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:11:03 +0900 Subject: [PATCH 01/47] =?UTF-8?q?feature(minesweeper):=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=8C=20=EC=85=80=EC=9D=80=20=EC=97=B4=EB=A6=B0=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=80=20=EC=95=84=EB=8B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/Cell.kt | 5 +++++ .../tdd/minesweeper/domain/ClosedCell.kt | 5 +++++ .../tdd/minesweeper/domain/ClosedCellTest.kt | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Cell.kt create mode 100644 src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt new file mode 100644 index 000000000..66b891a1c --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt @@ -0,0 +1,5 @@ +package tdd.minesweeper.domain + +interface Cell { + fun isOpen(): Boolean +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt new file mode 100644 index 000000000..4fb8da119 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -0,0 +1,5 @@ +package tdd.minesweeper.domain + +class ClosedCell : Cell { + override fun isOpen(): Boolean = false +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt new file mode 100644 index 000000000..9f4a1e591 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -0,0 +1,18 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.booleans.shouldBeFalse + +class ClosedCellTest : BehaviorSpec({ + given("닫힌 셀은") { + val sut = ClosedCell() + + `when`("열린 상태가") { + val result = sut.isOpen() + + then("아니다") { + result.shouldBeFalse() + } + } + } +}) From 87f9a814c9e2c77b4ade64653c4121bf3ed344e4 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:20:12 +0900 Subject: [PATCH 02/47] =?UTF-8?q?feature(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EC=85=80=EC=9D=80=20=EC=97=B4=EB=A6=B0=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=80=20=EB=A7=9E=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/LandmineCell.kt | 5 +++++ .../tdd/minesweeper/domain/LandmineCellTest.kt | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt new file mode 100644 index 000000000..cdd18df93 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt @@ -0,0 +1,5 @@ +package tdd.minesweeper.domain + +class LandmineCell : Cell { + override fun isOpen(): Boolean = true +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt new file mode 100644 index 000000000..eda6db10e --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt @@ -0,0 +1,18 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.booleans.shouldBeTrue + +class LandmineCellTest : BehaviorSpec({ + given("지뢰 셀은") { + val sut = LandmineCell() + + `when`("열린 상태가") { + val result = sut.isOpen() + + then("맞다") { + result.shouldBeTrue() + } + } + } +}) From fcf55c60995edb825a585f4509b046274c89a584 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:22:37 +0900 Subject: [PATCH 03/47] =?UTF-8?q?feature(minesweeper):=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=EC=85=80=EC=9D=80=20=EC=97=B4=EB=A6=B0=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=80=20=EB=A7=9E=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/NumberCell.kt | 5 +++++ .../tdd/minesweeper/domain/NumberCellTest.kt | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt new file mode 100644 index 000000000..3289897fe --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -0,0 +1,5 @@ +package tdd.minesweeper.domain + +class NumberCell : Cell { + override fun isOpen(): Boolean = true +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt new file mode 100644 index 000000000..ab3154aa0 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -0,0 +1,18 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.booleans.shouldBeTrue + +class NumberCellTest : BehaviorSpec({ + given("숫자 셀은") { + val sut = NumberCell() + + `when`("열린 상태가") { + val result = sut.isOpen() + + then("맞다") { + result.shouldBeTrue() + } + } + } +}) From e7953764ba1964e3841b93e0d33c92c3f644a5ee Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:27:24 +0900 Subject: [PATCH 04/47] =?UTF-8?q?feature(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EC=85=80=EC=9D=80=20=EC=97=B4=EB=A9=B4=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EC=85=80=EC=9D=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/Cell.kt | 2 ++ src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt | 2 ++ .../kotlin/tdd/minesweeper/domain/LandmineCellTest.kt | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt index 66b891a1c..e3f190831 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt @@ -2,4 +2,6 @@ package tdd.minesweeper.domain interface Cell { fun isOpen(): Boolean + + fun open(): Cell } diff --git a/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt index cdd18df93..a12b2188c 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt @@ -2,4 +2,6 @@ package tdd.minesweeper.domain class LandmineCell : Cell { override fun isOpen(): Boolean = true + + override fun open(): Cell = this } diff --git a/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt index eda6db10e..d6e85355a 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.types.shouldBeInstanceOf class LandmineCellTest : BehaviorSpec({ given("지뢰 셀은") { @@ -14,5 +15,13 @@ class LandmineCellTest : BehaviorSpec({ result.shouldBeTrue() } } + + `when`("열면") { + val result = sut.open() + + then("지뢰 셀이다") { + result.shouldBeInstanceOf() + } + } } }) From 785ab435bb6cba21586d111571e8e9b51634a23f Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:28:46 +0900 Subject: [PATCH 05/47] =?UTF-8?q?feature(minesweeper):=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=EC=85=80=EC=9D=80=20=EC=97=B4=EB=A9=B4=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=EC=85=80=EC=9D=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt | 2 ++ src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index 3289897fe..8b275ed2f 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -2,4 +2,6 @@ package tdd.minesweeper.domain class NumberCell : Cell { override fun isOpen(): Boolean = true + + override fun open(): Cell = this } diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt index ab3154aa0..1a5ec0e0e 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.types.shouldBeInstanceOf class NumberCellTest : BehaviorSpec({ given("숫자 셀은") { @@ -14,5 +15,13 @@ class NumberCellTest : BehaviorSpec({ result.shouldBeTrue() } } + + `when`("열면") { + val result = sut.open() + + then("숫자 셀이다") { + result.shouldBeInstanceOf() + } + } } }) From 15419fae0ecb15d8ae8243a907f9718bc185003a Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:39:41 +0900 Subject: [PATCH 06/47] =?UTF-8?q?feature(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=EA=B0=80=20=EC=97=86=EB=8A=94=20=EB=8B=AB=ED=9E=8C=20?= =?UTF-8?q?=EC=85=80=EC=9D=84=20=EC=97=B4=EB=A9=B4=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EC=85=80=EC=9D=B4=20=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/ClosedCell.kt | 4 ++++ .../kotlin/tdd/minesweeper/domain/ClosedCellTest.kt | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index 4fb8da119..81e7147b5 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -2,4 +2,8 @@ package tdd.minesweeper.domain class ClosedCell : Cell { override fun isOpen(): Boolean = false + + override fun open(): Cell { + return NumberCell() + } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index 9f4a1e591..25d3c7207 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.types.shouldBeInstanceOf class ClosedCellTest : BehaviorSpec({ given("닫힌 셀은") { @@ -15,4 +16,16 @@ class ClosedCellTest : BehaviorSpec({ } } } + + given("지뢰가 없는 닫힌 셀을") { + val sut = ClosedCell() + + `when`("열면") { + val result = sut.open() + + then("숫자 셀이 된다") { + result.shouldBeInstanceOf() + } + } + } }) From cbef00bec10847a0d75afb3ab722b48e0dcace62 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:43:50 +0900 Subject: [PATCH 07/47] =?UTF-8?q?feature(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=EA=B0=80=20=EC=9E=88=EB=8A=94=20=EB=8B=AB=ED=9E=8C=20?= =?UTF-8?q?=EC=85=80=EC=9D=84=20=EC=97=B4=EB=A9=B4=20=EC=A7=80=EB=A2=B0=20?= =?UTF-8?q?=EC=85=80=EC=9D=B4=20=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/ClosedCell.kt | 5 ++++- .../tdd/minesweeper/domain/ClosedCellTest.kt | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index 81e7147b5..a27764405 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -1,9 +1,12 @@ package tdd.minesweeper.domain -class ClosedCell : Cell { +class ClosedCell(val hasLandmine: Boolean = false) : Cell { override fun isOpen(): Boolean = false override fun open(): Cell { + if (hasLandmine) { + return LandmineCell() + } return NumberCell() } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index 25d3c7207..1581365d5 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -28,4 +28,19 @@ class ClosedCellTest : BehaviorSpec({ } } } + + given("지뢰가 있는 닫힌 셀을") { + val sut = + ClosedCell( + hasLandmine = true, + ) + + `when`("열면") { + val result = sut.open() + + then("지뢰 셀이 된다") { + result.shouldBeInstanceOf() + } + } + } }) From 7e9571719076d75cffae3558d482278af3cfdc6a Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Tue, 24 Dec 2024 23:54:11 +0900 Subject: [PATCH 08/47] =?UTF-8?q?feature(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=EB=8A=94=20=EC=9D=B8=EC=A0=91=20=EC=A7=80=EB=A2=B0=20?= =?UTF-8?q?=EC=88=98=EB=A5=BC=200=20~=208=EA=B9=8C=EC=A7=80=20=EA=B0=80?= =?UTF-8?q?=EC=A7=88=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/NumberCell.kt | 6 +++++- .../tdd/minesweeper/domain/NumberCellTest.kt | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index 8b275ed2f..a0b17d012 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -1,6 +1,10 @@ package tdd.minesweeper.domain -class NumberCell : Cell { +data class NumberCell(val adjacentLandmines: Int = 0) : Cell { + init { + require(adjacentLandmines in (0..8)) { "인접 지뢰 수는 0 ~ 8까지만 가능하다: $adjacentLandmines" } + } + override fun isOpen(): Boolean = true override fun open(): Cell = this diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt index 1a5ec0e0e..04f33fda8 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -1,7 +1,9 @@ package tdd.minesweeper.domain +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf class NumberCellTest : BehaviorSpec({ @@ -24,4 +26,18 @@ class NumberCellTest : BehaviorSpec({ } } } + + given("인접 지뢰 수는") { + `when`("0 ~ 8 까지만") { + then("가능하다") { + listOf(-1, 9).forEach { adjacentLandmines -> + shouldThrow { NumberCell(adjacentLandmines) } + } + (0..8).forEach { adjacentLandmines -> + val sut = NumberCell(adjacentLandmines) + sut.adjacentLandmines shouldBe adjacentLandmines + } + } + } + } }) From 18a37e00e51451cc9c5314cd4252173a6c8e0405 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Wed, 25 Dec 2024 00:08:08 +0900 Subject: [PATCH 09/47] =?UTF-8?q?refactor(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EC=85=80=EC=9D=98=20=EC=9D=B4=EB=A6=84=EC=9D=84=20?= =?UTF-8?q?MineCell=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20&&=20=EC=9D=B8?= =?UTF-8?q?=EC=A0=91=20=EC=A7=80=EB=A2=B0=20=EC=88=98=EB=8F=84=20AdjacentM?= =?UTF-8?q?ines=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt | 2 +- .../tdd/minesweeper/domain/{LandmineCell.kt => MineCell.kt} | 2 +- src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt | 4 ++-- src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt | 2 +- .../domain/{LandmineCellTest.kt => MineCellTest.kt} | 6 +++--- src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename src/main/kotlin/tdd/minesweeper/domain/{LandmineCell.kt => MineCell.kt} (80%) rename src/test/kotlin/tdd/minesweeper/domain/{LandmineCellTest.kt => MineCellTest.kt} (79%) diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index a27764405..72b4a3f5b 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -5,7 +5,7 @@ class ClosedCell(val hasLandmine: Boolean = false) : Cell { override fun open(): Cell { if (hasLandmine) { - return LandmineCell() + return MineCell() } return NumberCell() } diff --git a/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt similarity index 80% rename from src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt rename to src/main/kotlin/tdd/minesweeper/domain/MineCell.kt index a12b2188c..932056a08 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/LandmineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt @@ -1,6 +1,6 @@ package tdd.minesweeper.domain -class LandmineCell : Cell { +class MineCell : Cell { override fun isOpen(): Boolean = true override fun open(): Cell = this diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index a0b17d012..6ad0f2002 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -1,8 +1,8 @@ package tdd.minesweeper.domain -data class NumberCell(val adjacentLandmines: Int = 0) : Cell { +data class NumberCell(val adjacentMines: Int = 0) : Cell { init { - require(adjacentLandmines in (0..8)) { "인접 지뢰 수는 0 ~ 8까지만 가능하다: $adjacentLandmines" } + require(adjacentMines in (0..8)) { "인접한 지뢰 수는 0 ~ 8까지만 가능하다: $adjacentMines" } } override fun isOpen(): Boolean = true diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index 1581365d5..f04c48777 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -39,7 +39,7 @@ class ClosedCellTest : BehaviorSpec({ val result = sut.open() then("지뢰 셀이 된다") { - result.shouldBeInstanceOf() + result.shouldBeInstanceOf() } } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt similarity index 79% rename from src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt rename to src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt index d6e85355a..ce1fdd81c 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/LandmineCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt @@ -4,9 +4,9 @@ import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.types.shouldBeInstanceOf -class LandmineCellTest : BehaviorSpec({ +class MineCellTest : BehaviorSpec({ given("지뢰 셀은") { - val sut = LandmineCell() + val sut = MineCell() `when`("열린 상태가") { val result = sut.isOpen() @@ -20,7 +20,7 @@ class LandmineCellTest : BehaviorSpec({ val result = sut.open() then("지뢰 셀이다") { - result.shouldBeInstanceOf() + result.shouldBeInstanceOf() } } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt index 04f33fda8..f2d6e07eb 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -35,7 +35,7 @@ class NumberCellTest : BehaviorSpec({ } (0..8).forEach { adjacentLandmines -> val sut = NumberCell(adjacentLandmines) - sut.adjacentLandmines shouldBe adjacentLandmines + sut.adjacentMines shouldBe adjacentLandmines } } } From fae416224394796f09b01f5f66487e1c30078fd2 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Wed, 25 Dec 2024 00:19:57 +0900 Subject: [PATCH 10/47] =?UTF-8?q?feature(minesweeper):=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=EA=B0=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=8C=20=EC=85=80=EC=9D=84=20=EC=97=B4=EB=A9=B4=20=EB=98=91?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=9D=B8=EC=A0=91=ED=95=9C=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EC=88=98=EB=A5=BC=20=EA=B0=80=EC=A7=84=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=EC=85=80=EC=9D=B4=20=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Cell.kt | 2 ++ .../tdd/minesweeper/domain/ClosedCell.kt | 7 +++-- .../kotlin/tdd/minesweeper/domain/MineCell.kt | 2 ++ .../tdd/minesweeper/domain/NumberCell.kt | 2 +- .../tdd/minesweeper/domain/ClosedCellTest.kt | 31 ++++++++++++------- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt index e3f190831..206609a63 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt @@ -1,6 +1,8 @@ package tdd.minesweeper.domain interface Cell { + val adjacentMines: Int? + fun isOpen(): Boolean fun open(): Cell diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index 72b4a3f5b..18448b8cc 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -1,12 +1,15 @@ package tdd.minesweeper.domain -class ClosedCell(val hasLandmine: Boolean = false) : Cell { +class ClosedCell( + val hasLandmine: Boolean = false, + override val adjacentMines: Int = 0, +) : Cell { override fun isOpen(): Boolean = false override fun open(): Cell { if (hasLandmine) { return MineCell() } - return NumberCell() + return NumberCell(adjacentMines) } } diff --git a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt index 932056a08..25a3af6bd 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt @@ -1,6 +1,8 @@ package tdd.minesweeper.domain class MineCell : Cell { + override val adjacentMines: Int? = null + override fun isOpen(): Boolean = true override fun open(): Cell = this diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index 6ad0f2002..4eea3d223 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -1,6 +1,6 @@ package tdd.minesweeper.domain -data class NumberCell(val adjacentMines: Int = 0) : Cell { +data class NumberCell(override val adjacentMines: Int = 0) : Cell { init { require(adjacentMines in (0..8)) { "인접한 지뢰 수는 0 ~ 8까지만 가능하다: $adjacentMines" } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index f04c48777..2d9dcb75a 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf class ClosedCellTest : BehaviorSpec({ @@ -17,18 +18,6 @@ class ClosedCellTest : BehaviorSpec({ } } - given("지뢰가 없는 닫힌 셀을") { - val sut = ClosedCell() - - `when`("열면") { - val result = sut.open() - - then("숫자 셀이 된다") { - result.shouldBeInstanceOf() - } - } - } - given("지뢰가 있는 닫힌 셀을") { val sut = ClosedCell( @@ -43,4 +32,22 @@ class ClosedCellTest : BehaviorSpec({ } } } + + given("지뢰가 없을 때") { + `when`("열면") { + then("똑같은 인접한 지뢰 수를 가진 숫자 셀이 된다") { + (0..8).forEach { adjacentMines -> + val sut = + ClosedCell( + hasLandmine = false, + adjacentMines = adjacentMines, + ) + + val result = sut.open() + + result.adjacentMines shouldBe sut.adjacentMines + } + } + } + } }) From 167164e56d7f3e530b9728fc29ef369d3bf18251 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Wed, 25 Dec 2024 00:43:01 +0900 Subject: [PATCH 11/47] =?UTF-8?q?feature(minesweeper):=20AdjacentMines=20?= =?UTF-8?q?=EA=B0=92=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/AdjacentMines.kt | 15 +++++++++++++++ src/main/kotlin/tdd/minesweeper/domain/Cell.kt | 2 +- .../kotlin/tdd/minesweeper/domain/ClosedCell.kt | 6 ++++-- .../kotlin/tdd/minesweeper/domain/MineCell.kt | 2 +- .../kotlin/tdd/minesweeper/domain/NumberCell.kt | 6 +----- .../tdd/minesweeper/domain/AdjacentMinesTest.kt | 16 ++++++++++++++++ .../tdd/minesweeper/domain/ClosedCellTest.kt | 2 +- .../tdd/minesweeper/domain/NumberCellTest.kt | 6 +++--- 8 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt b/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt new file mode 100644 index 000000000..502d59b11 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt @@ -0,0 +1,15 @@ +package tdd.minesweeper.domain + +@JvmInline +value class AdjacentMines(private val value: Int) { + init { + require(value in MIN_VALUE..MAX_VALUE) { + "인접 지뢰 수는 $MIN_VALUE ~ $MAX_VALUE 만 허용한다: valuer=$value" + } + } + + companion object { + private const val MIN_VALUE = 0 + private const val MAX_VALUE = 8 + } +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt index 206609a63..6a6671ce6 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt @@ -1,7 +1,7 @@ package tdd.minesweeper.domain interface Cell { - val adjacentMines: Int? + val adjacentMines: AdjacentMines? fun isOpen(): Boolean diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index 18448b8cc..dbeb76696 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -1,9 +1,11 @@ package tdd.minesweeper.domain -class ClosedCell( +data class ClosedCell( val hasLandmine: Boolean = false, - override val adjacentMines: Int = 0, + override val adjacentMines: AdjacentMines = AdjacentMines(0), ) : Cell { + constructor(adjacentMines: Int) : this(adjacentMines = AdjacentMines(adjacentMines)) + override fun isOpen(): Boolean = false override fun open(): Cell { diff --git a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt index 25a3af6bd..c757627d8 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt @@ -1,7 +1,7 @@ package tdd.minesweeper.domain class MineCell : Cell { - override val adjacentMines: Int? = null + override val adjacentMines: AdjacentMines? = null override fun isOpen(): Boolean = true diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index 4eea3d223..8d6312bd6 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -1,10 +1,6 @@ package tdd.minesweeper.domain -data class NumberCell(override val adjacentMines: Int = 0) : Cell { - init { - require(adjacentMines in (0..8)) { "인접한 지뢰 수는 0 ~ 8까지만 가능하다: $adjacentMines" } - } - +data class NumberCell(override val adjacentMines: AdjacentMines = AdjacentMines(0)) : Cell { override fun isOpen(): Boolean = true override fun open(): Cell = this diff --git a/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt b/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt new file mode 100644 index 000000000..e59b967f8 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt @@ -0,0 +1,16 @@ +package tdd.minesweeper.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec + +class AdjacentMinesTest : BehaviorSpec({ + given("인접 지뢰 수는") { + `when`("0 ~ 8 의 값만") { + then("허용한다") { + listOf(-1, 9).forEach { adjacentMines -> + shouldThrow { AdjacentMines(adjacentMines) } + } + } + } + } +}) diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index 2d9dcb75a..8d43bd97c 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -40,7 +40,7 @@ class ClosedCellTest : BehaviorSpec({ val sut = ClosedCell( hasLandmine = false, - adjacentMines = adjacentMines, + adjacentMines = AdjacentMines(adjacentMines), ) val result = sut.open() diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt index f2d6e07eb..22944d024 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -31,11 +31,11 @@ class NumberCellTest : BehaviorSpec({ `when`("0 ~ 8 까지만") { then("가능하다") { listOf(-1, 9).forEach { adjacentLandmines -> - shouldThrow { NumberCell(adjacentLandmines) } + shouldThrow { NumberCell(AdjacentMines(adjacentLandmines)) } } (0..8).forEach { adjacentLandmines -> - val sut = NumberCell(adjacentLandmines) - sut.adjacentMines shouldBe adjacentLandmines + val sut = NumberCell(AdjacentMines(adjacentLandmines)) + sut.adjacentMines shouldBe AdjacentMines(adjacentLandmines) } } } From 4c4d663bb7e36afd562f695479e492e72ed5f7be Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Wed, 25 Dec 2024 00:51:16 +0900 Subject: [PATCH 12/47] =?UTF-8?q?feature(minesweeper):=20=EA=B0=81=20?= =?UTF-8?q?=EC=85=80=EB=93=A4=EC=9D=80=20=EC=A7=80=EB=A2=B0=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=EC=A7=80=20=EC=95=8C=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/Cell.kt | 2 ++ src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt | 6 ++++-- src/main/kotlin/tdd/minesweeper/domain/MineCell.kt | 2 ++ src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt | 2 ++ .../kotlin/tdd/minesweeper/domain/ClosedCellTest.kt | 10 ++++++++-- src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt | 7 +++++++ .../kotlin/tdd/minesweeper/domain/NumberCellTest.kt | 6 ++++++ 7 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt index 6a6671ce6..e28de3c52 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt @@ -5,5 +5,7 @@ interface Cell { fun isOpen(): Boolean + fun hasMine(): Boolean + fun open(): Cell } diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index dbeb76696..3ae7c4a6e 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -1,15 +1,17 @@ package tdd.minesweeper.domain data class ClosedCell( - val hasLandmine: Boolean = false, + val hasMine: Boolean = false, override val adjacentMines: AdjacentMines = AdjacentMines(0), ) : Cell { constructor(adjacentMines: Int) : this(adjacentMines = AdjacentMines(adjacentMines)) override fun isOpen(): Boolean = false + override fun hasMine(): Boolean = hasMine + override fun open(): Cell { - if (hasLandmine) { + if (hasMine) { return MineCell() } return NumberCell(adjacentMines) diff --git a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt index c757627d8..fc6035ed3 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt @@ -5,5 +5,7 @@ class MineCell : Cell { override fun isOpen(): Boolean = true + override fun hasMine(): Boolean = true + override fun open(): Cell = this } diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index 8d6312bd6..06e4b0eec 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -3,5 +3,7 @@ package tdd.minesweeper.domain data class NumberCell(override val adjacentMines: AdjacentMines = AdjacentMines(0)) : Cell { override fun isOpen(): Boolean = true + override fun hasMine(): Boolean = false + override fun open(): Cell = this } diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index 8d43bd97c..f961bd436 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -16,12 +16,18 @@ class ClosedCellTest : BehaviorSpec({ result.shouldBeFalse() } } + + `when`("기본적으로") { + then("지뢰가 없는 상태다") { + sut.hasMine() shouldBe false + } + } } given("지뢰가 있는 닫힌 셀을") { val sut = ClosedCell( - hasLandmine = true, + hasMine = true, ) `when`("열면") { @@ -39,7 +45,7 @@ class ClosedCellTest : BehaviorSpec({ (0..8).forEach { adjacentMines -> val sut = ClosedCell( - hasLandmine = false, + hasMine = false, adjacentMines = AdjacentMines(adjacentMines), ) diff --git a/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt index ce1fdd81c..30ff5df3b 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf class MineCellTest : BehaviorSpec({ @@ -23,5 +24,11 @@ class MineCellTest : BehaviorSpec({ result.shouldBeInstanceOf() } } + + `when`("지뢰가") { + then("있는 상태다") { + sut.hasMine() shouldBe true + } + } } }) diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt index 22944d024..23ce5294f 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -25,6 +25,12 @@ class NumberCellTest : BehaviorSpec({ result.shouldBeInstanceOf() } } + + `when`("지뢰가") { + then("없는 상태다") { + sut.hasMine() shouldBe false + } + } } given("인접 지뢰 수는") { From 5f25ce3b71c0e7de292a882607ea193c6ee6a6f7 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Wed, 25 Dec 2024 01:18:10 +0900 Subject: [PATCH 13/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EC=85=80=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=20=EC=83=9D=EC=84=B1=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Board.kt | 3 +++ .../kotlin/tdd/minesweeper/domain/Cells.kt | 4 ++++ .../kotlin/tdd/minesweeper/domain/BoardTest.kt | 18 ++++++++++++++++++ .../tdd/minesweeper/domain/TestFixtures.kt | 16 ++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Board.kt create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Cells.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt new file mode 100644 index 000000000..24e9a467a --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -0,0 +1,3 @@ +package tdd.minesweeper.domain + +class Board(val cells: Cells) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cells.kt b/src/main/kotlin/tdd/minesweeper/domain/Cells.kt new file mode 100644 index 000000000..0dad68186 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Cells.kt @@ -0,0 +1,4 @@ +package tdd.minesweeper.domain + +@JvmInline +value class Cells(private val values: List) : List by values diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt new file mode 100644 index 000000000..5a47009ca --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -0,0 +1,18 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class BoardTest : BehaviorSpec({ + given("셀 목록을 받아") { + val cells = closedCellsBy3x3 + + `when`("보드를") { + val sut = Board(cells = cells) + + then("생성할 수 있다") { + sut.cells shouldBe cells + } + } + } +}) diff --git a/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt b/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt new file mode 100644 index 000000000..68bb698da --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt @@ -0,0 +1,16 @@ +package tdd.minesweeper.domain + +val closedCellsBy3x3: Cells = + Cells( + listOf( + ClosedCell(), + ClosedCell(), + ClosedCell(), + ClosedCell(), + ClosedCell(), + ClosedCell(), + ClosedCell(), + ClosedCell(), + ClosedCell(), + ), + ) From 873606aee21beaead08cabfc72b803e13d150d46 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Wed, 25 Dec 2024 12:00:12 +0900 Subject: [PATCH 14/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EC=98=81=EC=97=AD=EA=B3=BC=20=EC=85=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EB=B0=9B=EC=95=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Area.kt | 12 +++++++++ .../kotlin/tdd/minesweeper/domain/Board.kt | 2 +- .../kotlin/tdd/minesweeper/domain/AreaTest.kt | 25 +++++++++++++++++++ .../tdd/minesweeper/domain/BoardTest.kt | 8 +++--- .../tdd/minesweeper/domain/TestFixtures.kt | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Area.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/AreaTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Area.kt b/src/main/kotlin/tdd/minesweeper/domain/Area.kt new file mode 100644 index 000000000..9b78281ef --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Area.kt @@ -0,0 +1,12 @@ +package tdd.minesweeper.domain + +data class Area(val height: Int, val width: Int) { + init { + require(height > 0) { + "높이는 양수여야 합니다: height=$height" + } + require(width > 0) { + "너비는 양수여야 합니다: width=$width" + } + } +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index 24e9a467a..4f209eb79 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -1,3 +1,3 @@ package tdd.minesweeper.domain -class Board(val cells: Cells) +data class Board(val area: Area, val cells: Cells) diff --git a/src/test/kotlin/tdd/minesweeper/domain/AreaTest.kt b/src/test/kotlin/tdd/minesweeper/domain/AreaTest.kt new file mode 100644 index 000000000..c754f98ea --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/AreaTest.kt @@ -0,0 +1,25 @@ +package tdd.minesweeper.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec + +class AreaTest : BehaviorSpec({ + given("보드의 영역은") { + `when`("높이가") { + val height = 0 + val width = 1 + + then("양수여야 한다") { + shouldThrow { Area(height = height, width = width) } + } + } + `when`("너비가") { + val height = 1 + val width = 0 + + then("양수여야 한다") { + shouldThrow { Area(height = height, width = width) } + } + } + } +}) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index 5a47009ca..e8af52d08 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -4,14 +4,16 @@ import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe class BoardTest : BehaviorSpec({ - given("셀 목록을 받아") { - val cells = closedCellsBy3x3 + given("영역과 셀 목록을 받아") { + val area = Area(height = 3, width = 3) + val cells = closedCellsBy9 `when`("보드를") { - val sut = Board(cells = cells) + val sut = Board(area = area, cells = cells) then("생성할 수 있다") { sut.cells shouldBe cells + sut.area shouldBe area } } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt b/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt index 68bb698da..c80b269ca 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/TestFixtures.kt @@ -1,6 +1,6 @@ package tdd.minesweeper.domain -val closedCellsBy3x3: Cells = +val closedCellsBy9: Cells = Cells( listOf( ClosedCell(), From 53bf3941118fdeb5e9f1dfd6e0f5c6bea41854f3 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 00:16:42 +0900 Subject: [PATCH 15/47] =?UTF-8?q?feature(minesweeper):=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4,=20=EB=84=88=EB=B9=84,=20=EC=A7=80=EB=A2=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=EB=A5=BC=20=EB=B0=9B=EC=95=84=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EB=A7=8C=EB=93=A4=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardBuilder.kt | 33 +++++++++ .../kotlin/tdd/minesweeper/domain/Builder.kt | 5 ++ .../minesweeper/domain/BoardBuilderTest.kt | 71 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Builder.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt new file mode 100644 index 000000000..93137c6fa --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt @@ -0,0 +1,33 @@ +package tdd.minesweeper.domain + +class BoardBuilder : Builder { + private var height: Int = 0 + private var width: Int = 0 + private var countOfMines: Int = 0 + + fun height(value: Int) = + apply { + require(value > 0) { "높이는 양수여야 합니다! input=$value" } + this.height = value + } + + fun width(value: Int) = + apply { + require(value > 0) { "높이는 양수여야 합니다! input=$value" } + this.width = value + } + + fun countOfMines(value: Int) = + apply { + require(value <= width * height) { "최대 지뢰 개수는 모든 셀의 수입니다! max=${width * height}, input=$value" } + this.countOfMines = value + } + + override fun build(): Board { + val cells = Cells((0 until height * width).map { ClosedCell() }) + return Board( + area = Area(height = height, width = width), + cells = cells, + ) + } +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/Builder.kt b/src/main/kotlin/tdd/minesweeper/domain/Builder.kt new file mode 100644 index 000000000..2f6bf3ce3 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Builder.kt @@ -0,0 +1,5 @@ +package tdd.minesweeper.domain + +interface Builder { + fun build(): T +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt new file mode 100644 index 000000000..7a0a167a4 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt @@ -0,0 +1,71 @@ +package tdd.minesweeper.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class BoardBuilderTest : BehaviorSpec({ + given("높이 5, 너비 5, 지뢰 개수 5를 받아") { + val height = 5 + val width = 5 + val countOfMines = 5 + val sut = BoardBuilder() + + `when`("보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .build() + + then("25개의 닫힌 셀을 만들 수 있다") { + result.area shouldBe Area(height = height, width = width) + result.cells.size shouldBe 25 + result.cells.all { it is ClosedCell } shouldBe true + } + } + } + + given("높이를") { + val width = 5 + val countOfMines = 5 + val sut = BoardBuilder() + + `when`("입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + sut.width(width).countOfMines(countOfMines) + } + } + } + `when`("양수로 입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + sut.height(0).width(width).countOfMines(countOfMines) + } + } + } + } + + given("너비를 입력하지 않으면") { + val height = 5 + val countOfMines = 5 + val sut = BoardBuilder() + + `when`("입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + sut.height(height).countOfMines(countOfMines) + } + } + } + `when`("양수로 입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + sut.width(0).height(height).countOfMines(countOfMines) + } + } + } + } +}) From 1cbe37d7d454ccfc4820d70eacd5208de2cf5c9c Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 00:54:32 +0900 Subject: [PATCH 16/47] =?UTF-8?q?feature(minesweeper):=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4,=20=EB=84=88=EB=B9=84,=20=EC=A7=80=EB=A2=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=EB=A5=BC=20=EB=B0=9B=EC=95=84=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EB=A7=8C=EB=93=A4=EB=A9=B4=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EA=B0=9C=EC=88=98=EB=A7=8C=ED=81=BC=EC=9D=98=20?= =?UTF-8?q?=EC=A7=80=EB=A2=B0=EB=A5=BC=20=EA=B0=80=EC=A7=84=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=8C=20=EC=85=80=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardBuilder.kt | 36 +++++++++++++++++-- .../kotlin/tdd/minesweeper/domain/Location.kt | 3 ++ .../minesweeper/domain/BoardBuilderTest.kt | 23 ++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Location.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt index 93137c6fa..e7c9bbdaa 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt @@ -19,15 +19,45 @@ class BoardBuilder : Builder { fun countOfMines(value: Int) = apply { - require(value <= width * height) { "최대 지뢰 개수는 모든 셀의 수입니다! max=${width * height}, input=$value" } this.countOfMines = value } override fun build(): Board { - val cells = Cells((0 until height * width).map { ClosedCell() }) + require(height > 0) { "높이는 양수여야 합니다! height=$height" } + require(width > 0) { "높이는 양수여야 합니다! width=$width" } + require(countOfMines <= width * height) { "최대 지뢰 개수는 모든 셀의 수입니다! max=${width * height}, countOfMines=$countOfMines" } + + val area = Area(height = height, width = width) + + val cells = createCells(area) + return Board( - area = Area(height = height, width = width), + area = area, cells = cells, ) } + + private fun createCells(area: Area): Cells { + val allLocations = + (0 until area.height * area.width) + .map { + Location( + row = (it / area.width) + 1, + col = (it % area.width) + 1, + ) + } + + val mineLocations: Set = + allLocations + .shuffled() + .take(countOfMines) + .toSet() + + return Cells( + allLocations + .map { location -> + if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() + }, + ) + } } diff --git a/src/main/kotlin/tdd/minesweeper/domain/Location.kt b/src/main/kotlin/tdd/minesweeper/domain/Location.kt new file mode 100644 index 000000000..d03125c20 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Location.kt @@ -0,0 +1,3 @@ +package tdd.minesweeper.domain + +data class Location(val row: Int, val col: Int) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt index 7a0a167a4..779587941 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt @@ -24,6 +24,10 @@ class BoardBuilderTest : BehaviorSpec({ result.cells.size shouldBe 25 result.cells.all { it is ClosedCell } shouldBe true } + + then("25개의 셀 중 5개는 지뢰를 가지고 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + } } } @@ -35,7 +39,7 @@ class BoardBuilderTest : BehaviorSpec({ `when`("입력하지 않으면") { then("보드를 생성할 수 없다") { shouldThrow { - sut.width(width).countOfMines(countOfMines) + sut.width(width).countOfMines(countOfMines).build() } } } @@ -56,7 +60,7 @@ class BoardBuilderTest : BehaviorSpec({ `when`("입력하지 않으면") { then("보드를 생성할 수 없다") { shouldThrow { - sut.height(height).countOfMines(countOfMines) + sut.height(height).countOfMines(countOfMines).build() } } } @@ -68,4 +72,19 @@ class BoardBuilderTest : BehaviorSpec({ } } } + + given("보드의 전체 셀 수가 5x5 = 25개일 때") { + val height = 5 + val width = 5 + val countOfMines = 26 + val sut = BoardBuilder() + + `when`("지뢰를 보드의 전체 셀 수보다 더 많이") { + then("만들 수 없다") { + shouldThrow { + sut.height(height).width(width).countOfMines(countOfMines).build() + } + } + } + } }) From 3595790f74f3c267b0e8709143a80a4331efda08 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 02:55:15 +0900 Subject: [PATCH 17/47] =?UTF-8?q?feature(minesweeper):=20=EC=9D=B8?= =?UTF-8?q?=EC=A0=91=20=EC=A7=80=EB=A2=B0=20=EC=88=98=EB=8A=94=201?= =?UTF-8?q?=EC=94=A9=20=EC=A6=9D=EA=B0=80=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EC=9D=8C=20&&=20=EC=9D=B8=EC=A0=91=20=EC=A7=80=EB=A2=B0=20?= =?UTF-8?q?=EC=88=98=EB=81=BC=EB=A6=AC=EB=8A=94=20=EC=84=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=ED=95=A0=20=EC=88=98=20=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/AdjacentMines.kt | 8 ++++- .../minesweeper/domain/AdjacentMinesTest.kt | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt b/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt index 502d59b11..150f1f633 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt @@ -1,13 +1,19 @@ package tdd.minesweeper.domain @JvmInline -value class AdjacentMines(private val value: Int) { +value class AdjacentMines(private val value: Int) : Comparable { init { require(value in MIN_VALUE..MAX_VALUE) { "인접 지뢰 수는 $MIN_VALUE ~ $MAX_VALUE 만 허용한다: valuer=$value" } } + fun inc(): AdjacentMines { + return AdjacentMines(value + 1) + } + + override operator fun compareTo(other: AdjacentMines): Int = value.compareTo(other.value) + companion object { private const val MIN_VALUE = 0 private const val MAX_VALUE = 8 diff --git a/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt b/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt index e59b967f8..fa70e4efb 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/AdjacentMinesTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe class AdjacentMinesTest : BehaviorSpec({ given("인접 지뢰 수는") { @@ -12,5 +13,34 @@ class AdjacentMinesTest : BehaviorSpec({ } } } + + `when`("현재 수에서") { + then("1씩 증가할 수 있다") { + (1..7).forEach { current -> + val sut = AdjacentMines(current) + val result = sut.inc() + result shouldBe AdjacentMines(current + 1) + } + } + } + + `when`("현재 수가 8이면") { + val sut = AdjacentMines(8) + + then("더이상 증가할 수 없다") { + shouldThrow { sut.inc() } + } + } + + `when`("크기를") { + val sut = AdjacentMines(1) + val other = AdjacentMines(0) + + then("서로 비교할 수 있다") { + (sut > other) shouldBe true + (sut < other) shouldBe false + (sut == other) shouldBe false + } + } } }) From bf4da51c312926cd596e7bcfbd2c5dc56fe8314d Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 02:55:46 +0900 Subject: [PATCH 18/47] =?UTF-8?q?feature(minesweeper):=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=8C=20=EC=85=80=EC=9D=80=20=EC=9D=B8=EC=A0=91=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EC=88=98=EB=A5=BC=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=ED=95=A0=20=EC=88=98=20=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt | 4 +++- src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index 3ae7c4a6e..3ef85caca 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -1,7 +1,7 @@ package tdd.minesweeper.domain data class ClosedCell( - val hasMine: Boolean = false, + private val hasMine: Boolean = false, override val adjacentMines: AdjacentMines = AdjacentMines(0), ) : Cell { constructor(adjacentMines: Int) : this(adjacentMines = AdjacentMines(adjacentMines)) @@ -16,4 +16,6 @@ data class ClosedCell( } return NumberCell(adjacentMines) } + + fun withAdjacentMines(newAdjacentMines: AdjacentMines): ClosedCell = this.copy(adjacentMines = newAdjacentMines) } diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index f961bd436..5839376bf 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -22,6 +22,15 @@ class ClosedCellTest : BehaviorSpec({ sut.hasMine() shouldBe false } } + + `when`("인접 지뢰 수를") { + val newAdjacentMines = AdjacentMines(8) + val result: ClosedCell = sut.withAdjacentMines(newAdjacentMines) + + then("업데이트 할 수 있다") { + result.adjacentMines shouldBe newAdjacentMines + } + } } given("지뢰가 있는 닫힌 셀을") { From 346b425a449b153daf41f313ef49813c305b29fa Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 22:51:57 +0900 Subject: [PATCH 19/47] =?UTF-8?q?feature(minesweeper):=20BoardBuilder=20?= =?UTF-8?q?=EB=8A=94=20=EB=84=88=EB=B9=84,=20=EB=86=92=EC=9D=B4,=20?= =?UTF-8?q?=EC=A7=80=EB=A2=B0=20=EA=B0=9C=EC=88=98=EC=99=80=20=EC=88=98?= =?UTF-8?q?=EB=8F=99=20=EC=A7=80=EB=A2=B0=20=EC=9C=84=EC=B9=98=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=20=EB=B3=B4=EB=93=9C=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=A0=20=EC=88=98=20=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardBuilder.kt | 29 +++- .../minesweeper/domain/BoardBuilderTest.kt | 156 ++++++++++++++++++ 2 files changed, 178 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt index e7c9bbdaa..9dbf4abe8 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt @@ -4,6 +4,7 @@ class BoardBuilder : Builder { private var height: Int = 0 private var width: Int = 0 private var countOfMines: Int = 0 + private val manualMineLocations: MutableSet = mutableSetOf() fun height(value: Int) = apply { @@ -17,10 +18,12 @@ class BoardBuilder : Builder { this.width = value } - fun countOfMines(value: Int) = - apply { - this.countOfMines = value - } + fun countOfMines(value: Int) = apply { this.countOfMines = value } + + fun mineAt( + row: Int, + col: Int, + ) = apply { this.manualMineLocations.add(Location(row, col)) } override fun build(): Board { require(height > 0) { "높이는 양수여야 합니다! height=$height" } @@ -47,12 +50,24 @@ class BoardBuilder : Builder { ) } - val mineLocations: Set = - allLocations + var validManualMineLocations = allLocations.intersect(manualMineLocations) + + var manualMineCount = validManualMineLocations.size + + if (manualMineCount > countOfMines) { + validManualMineLocations = validManualMineLocations.take(countOfMines).toMutableSet() + } + + manualMineCount = validManualMineLocations.size + + val randomMineLocations: Set = + (allLocations - validManualMineLocations) .shuffled() - .take(countOfMines) + .take(countOfMines - manualMineCount) .toSet() + val mineLocations = validManualMineLocations + randomMineLocations + return Cells( allLocations .map { location -> diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt index 779587941..f5c2a3f9f 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt @@ -87,4 +87,160 @@ class BoardBuilderTest : BehaviorSpec({ } } } + + given("높이 5, 너비 5, 지뢰 개수 5개를 받아") { + val height = 5 + val width = 5 + val countOfMines = 5 + val sut = BoardBuilder() + + `when`("5개의 서로 다른 지뢰위치를 수동으로 받아 보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .mineAt(1, 4) + .mineAt(1, 5) + .mineAt(2, 1) + .mineAt(4, 3) + .mineAt(5, 1) + .build() + + then("해당 위치에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("4개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .mineAt(1, 4) + .mineAt(1, 5) + .mineAt(2, 1) + .mineAt(4, 3) + .build() + + then("4개의 수동 위치와 1개의 랜덤 위치에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + ) + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("5개의 똑같은 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .mineAt(1, 4) + .mineAt(1, 4) + .mineAt(1, 4) + .mineAt(1, 4) + .mineAt(1, 4) + .build() + + then("1개의 수동 위치와 4개의 랜덤 위치에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + ) + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("6개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .mineAt(1, 4) + .mineAt(1, 5) + .mineAt(2, 1) + .mineAt(4, 3) + .mineAt(5, 1) + .mineAt(5, 2) + .build() + + then("처음에 받은 지뢰 위치 5개에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("1개의 보드 내 존재하지 않는 위치와 5개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .mineAt(0, 4) + .mineAt(1, 4) + .mineAt(1, 5) + .mineAt(2, 1) + .mineAt(4, 3) + .mineAt(5, 1) + .build() + + then("처음에 받은 지뢰 위치 5개에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + } }) From 6a01bd9c49b279c8633c5ceaa869505096fa17ce Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 23:24:32 +0900 Subject: [PATCH 20/47] =?UTF-8?q?feature(minesweeper):=20BoardBuilder=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B3=B4=EB=93=9C=ED=8C=90=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=A0=20=EB=95=8C=20=EC=A7=80=EB=A2=B0?= =?UTF-8?q?=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EC=85=80=EB=93=A4=EC=97=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A0=91=20=EC=A7=80=EB=A2=B0=20=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardBuilder.kt | 25 +++++++++-- .../minesweeper/domain/BoardBuilderTest.kt | 44 +++++++++++++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt index 9dbf4abe8..7d0b4603f 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt @@ -68,11 +68,30 @@ class BoardBuilder : Builder { val mineLocations = validManualMineLocations + randomMineLocations - return Cells( + val closedCells = allLocations .map { location -> if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() - }, - ) + } + + val updatedCells = + closedCells + .mapIndexed { index, cell -> + if (cell.hasMine()) { + cell + } else { + val location = allLocations[index] + val directions = listOf(-1 to -1, -1 to 0, -1 to 1, 0 to -1, 0 to 1, 1 to -1, 1 to 0, 1 to 1) + val adjacentMineCount = + directions + .count { (dx, dy) -> + val adjacentLocation = Location(location.row + dx, location.col + dy) + adjacentLocation in mineLocations + } + cell.copy(adjacentMines = AdjacentMines(adjacentMineCount)) + } + } + + return Cells(updatedCells) } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt index f5c2a3f9f..e4cb23ff8 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt @@ -107,22 +107,50 @@ class BoardBuilderTest : BehaviorSpec({ .mineAt(5, 1) .build() + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + then("해당 위치에 지뢰를 가진 보드판을 만들 수 있다") { result.cells.filter { it.hasMine() }.size shouldBe 5 - val mineLocations = - listOf( - Location(1, 4), - Location(1, 5), - Location(2, 1), - Location(4, 3), - Location(5, 1), - ) mineLocations.forEach { location -> val cell = result.cells[(location.row - 1) * width + (location.col - 1)] cell.hasMine() shouldBe true } } + + then("지뢰가 아닌 셀들에 인접 지뢰 개수를 표시할 수 있다") { + // -1 은 지뢰 + val expectedAdjacentMines = + listOf( + listOf(1, 1, 1, -1, -1), + listOf(-1, 1, 1, 2, 2), + listOf(1, 2, 1, 1, 0), + listOf(1, 2, -1, 1, 0), + listOf(-1, 2, 1, 1, 0), + ) + + for (row in 1..height) { + for (col in 1..width) { + val cellIndex = (row - 1) * width + (col - 1) + val cell = result.cells[cellIndex] + val expectedMineCount = expectedAdjacentMines[row - 1][col - 1] + + if (expectedMineCount == -1) { + cell.hasMine() shouldBe true + } else { + cell.hasMine() shouldBe false + cell.adjacentMines shouldBe AdjacentMines(expectedMineCount) + } + } + } + } } `when`("4개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { From b88f18147406b5a333c06c44af1e0f9a8da17621 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 23:44:29 +0900 Subject: [PATCH 21/47] =?UTF-8?q?refactor(minesweeper):=20BoardBuilder?= =?UTF-8?q?=EB=A5=BC=20dsl=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/{ => dsl}/BoardBuilder.kt | 11 ++++++++++- .../minesweeper/domain/{ => dsl}/BoardBuilderTest.kt | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) rename src/main/kotlin/tdd/minesweeper/domain/{ => dsl}/BoardBuilder.kt (91%) rename src/test/kotlin/tdd/minesweeper/domain/{ => dsl}/BoardBuilderTest.kt (98%) diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt similarity index 91% rename from src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt rename to src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt index 7d0b4603f..588ac2404 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt @@ -1,5 +1,14 @@ -package tdd.minesweeper.domain +package tdd.minesweeper.domain.dsl +import tdd.minesweeper.domain.AdjacentMines +import tdd.minesweeper.domain.Area +import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.Builder +import tdd.minesweeper.domain.Cells +import tdd.minesweeper.domain.ClosedCell +import tdd.minesweeper.domain.Location + +@BoardDslMaker class BoardBuilder : Builder { private var height: Int = 0 private var width: Int = 0 diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt similarity index 98% rename from src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt rename to src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt index e4cb23ff8..b1b61e088 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt @@ -1,8 +1,12 @@ -package tdd.minesweeper.domain +package tdd.minesweeper.domain.dsl import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe +import tdd.minesweeper.domain.AdjacentMines +import tdd.minesweeper.domain.Area +import tdd.minesweeper.domain.ClosedCell +import tdd.minesweeper.domain.Location class BoardBuilderTest : BehaviorSpec({ given("높이 5, 너비 5, 지뢰 개수 5를 받아") { From 7db19b628a18a0cd2c460055ed0736ef96f45129 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Thu, 26 Dec 2024 23:45:19 +0900 Subject: [PATCH 22/47] =?UTF-8?q?feature(minesweeper):=20Board=EB=A5=BC=20?= =?UTF-8?q?DSL=EB=A1=9C=20=EC=83=9D=EC=84=B1=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20BoardCreator.board(),=20@BoardDslMaker=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 --- .../minesweeper/domain/dsl/BoardCreator.kt | 8 + .../domain/dsl/BoardCreatorTest.kt | 292 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/dsl/BoardCreator.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardCreator.kt new file mode 100644 index 000000000..148705c6a --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardCreator.kt @@ -0,0 +1,8 @@ +package tdd.minesweeper.domain.dsl + +import tdd.minesweeper.domain.Board + +@DslMarker +annotation class BoardDslMaker + +fun board(block: BoardBuilder.() -> Unit): Board = BoardBuilder().apply(block).build() diff --git a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt new file mode 100644 index 000000000..cc203b126 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt @@ -0,0 +1,292 @@ +package tdd.minesweeper.domain.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import tdd.minesweeper.domain.AdjacentMines +import tdd.minesweeper.domain.Area +import tdd.minesweeper.domain.ClosedCell +import tdd.minesweeper.domain.Location + +class BoardCreatorTest : BehaviorSpec({ + + given("높이, 너비, 지뢰 개수를 받아") { + val height = 5 + val width = 5 + val countOfMines = 5 + + `when`("보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + } + + then("25개의 닫힌 셀을 만들 수 있다") { + result.area shouldBe Area(height = height, width = width) + result.cells.size shouldBe 25 + result.cells.all { it is ClosedCell } shouldBe true + } + + then("25개의 셀 중 5개는 지뢰를 가지고 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + } + } + } + + given("높이를") { + val width = 5 + val countOfMines = 5 + + `when`("입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + board { + width(width) + countOfMines(countOfMines) + } + } + } + } + `when`("양수로 입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + board { + width(width) + height(0) + countOfMines(countOfMines) + } + } + } + } + } + + given("너비를 입력하지 않으면") { + val height = 5 + val countOfMines = 5 + + `when`("입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + board { + height(height) + countOfMines(countOfMines) + } + } + } + } + `when`("양수로 입력하지 않으면") { + then("보드를 생성할 수 없다") { + shouldThrow { + board { + height(height) + width(0) + countOfMines(countOfMines) + } + } + } + } + } + + given("보드의 전체 셀 수가 5x5 = 25개일 때") { + val height = 5 + val width = 5 + val countOfMines = 26 + + `when`("지뢰를 보드의 전체 셀 수보다 더 많이") { + then("만들 수 없다") { + shouldThrow { + board { + height(height) + width(width) + countOfMines(countOfMines) + } + } + } + } + } + + given("높이 5, 너비 5, 지뢰 개수 5개를 받아") { + val height = 5 + val width = 5 + val countOfMines = 5 + + `when`("5개의 서로 다른 지뢰위치를 수동으로 받아 보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + } + + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + then("해당 위치에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + + then("지뢰가 아닌 셀들에 인접 지뢰 개수를 표시할 수 있다") { + // -1 은 지뢰 + val expectedAdjacentMines = + listOf( + listOf(1, 1, 1, -1, -1), + listOf(-1, 1, 1, 2, 2), + listOf(1, 2, 1, 1, 0), + listOf(1, 2, -1, 1, 0), + listOf(-1, 2, 1, 1, 0), + ) + + for (row in 1..height) { + for (col in 1..width) { + val cellIndex = (row - 1) * width + (col - 1) + val cell = result.cells[cellIndex] + val expectedMineCount = expectedAdjacentMines[row - 1][col - 1] + + if (expectedMineCount == -1) { + cell.hasMine() shouldBe true + } else { + cell.hasMine() shouldBe false + cell.adjacentMines shouldBe AdjacentMines(expectedMineCount) + } + } + } + } + } + + `when`("4개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + } + + then("4개의 수동 위치와 1개의 랜덤 위치에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + ) + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("5개의 똑같은 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + mineAt(1, 4) + mineAt(1, 4) + mineAt(1, 4) + mineAt(1, 4) + mineAt(1, 4) + } + + then("1개의 수동 위치와 4개의 랜덤 위치에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + ) + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("6개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + mineAt(5, 2) + } + + then("처음에 받은 지뢰 위치 5개에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("1개의 보드 내 존재하지 않는 위치와 5개의 서로 다른 지뢰 위치를 수동으로 받아 보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + mineAt(0, 4) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + } + + then("처음에 받은 지뢰 위치 5개에 지뢰를 가진 보드판을 만들 수 있다") { + result.cells.filter { it.hasMine() }.size shouldBe 5 + val mineLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + mineLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + } +}) From 066477a68a01eff3e9eca768cbc0bb5138ec7796 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 00:15:18 +0900 Subject: [PATCH 23/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=85=80=20=EC=83=9D=EC=84=B1=20=EC=B1=85=EC=9E=84?= =?UTF-8?q?=EC=9D=84=20BoardCellsCreator=20=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/BoardCellsCreator.kt | 9 ++ .../domain/DefaultBoardCellsCreator.kt | 62 ++++++++ .../minesweeper/domain/dsl/BoardBuilder.kt | 69 ++------- .../domain/DefaultBoardCellsCreatorTest.kt | 141 ++++++++++++++++++ .../domain/dsl/BoardBuilderTest.kt | 2 +- 5 files changed, 222 insertions(+), 61 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt create mode 100644 src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt new file mode 100644 index 000000000..56114d4ca --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt @@ -0,0 +1,9 @@ +package tdd.minesweeper.domain + +interface BoardCellsCreator { + fun createCells( + area: Area, + countOfMines: Int, + inputManualMineLocations: Set, + ): Cells +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt new file mode 100644 index 000000000..cfc5f483b --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt @@ -0,0 +1,62 @@ +package tdd.minesweeper.domain + +class DefaultBoardCellsCreator : BoardCellsCreator { + override fun createCells( + area: Area, + countOfMines: Int, + inputManualMineLocations: Set, + ): Cells { + val allLocations = + (0 until area.height * area.width) + .map { + Location( + row = (it / area.width) + 1, + col = (it % area.width) + 1, + ) + } + + var validManualMineLocations = allLocations.intersect(inputManualMineLocations) + + var manualMineCount = validManualMineLocations.size + + if (manualMineCount > countOfMines) { + validManualMineLocations = validManualMineLocations.take(countOfMines).toMutableSet() + } + + manualMineCount = validManualMineLocations.size + + val randomMineLocations: Set = + (allLocations - validManualMineLocations) + .shuffled() + .take(countOfMines - manualMineCount) + .toSet() + + val mineLocations = validManualMineLocations + randomMineLocations + + val closedCells = + allLocations + .map { location -> + if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() + } + + val updatedCells = + closedCells + .mapIndexed { index, cell -> + if (cell.hasMine()) { + cell + } else { + val location = allLocations[index] + val directions = listOf(-1 to -1, -1 to 0, -1 to 1, 0 to -1, 0 to 1, 1 to -1, 1 to 0, 1 to 1) + val adjacentMineCount = + directions + .count { (dx, dy) -> + val adjacentLocation = Location(location.row + dx, location.col + dy) + adjacentLocation in mineLocations + } + cell.copy(adjacentMines = AdjacentMines(adjacentMineCount)) + } + } + + return Cells(updatedCells) + } +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt index 588ac2404..ed8d2943e 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt @@ -1,15 +1,14 @@ package tdd.minesweeper.domain.dsl -import tdd.minesweeper.domain.AdjacentMines import tdd.minesweeper.domain.Area import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.BoardCellsCreator import tdd.minesweeper.domain.Builder -import tdd.minesweeper.domain.Cells -import tdd.minesweeper.domain.ClosedCell +import tdd.minesweeper.domain.DefaultBoardCellsCreator import tdd.minesweeper.domain.Location @BoardDslMaker -class BoardBuilder : Builder { +class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoardCellsCreator()) : Builder { private var height: Int = 0 private var width: Int = 0 private var countOfMines: Int = 0 @@ -41,66 +40,16 @@ class BoardBuilder : Builder { val area = Area(height = height, width = width) - val cells = createCells(area) + val cells = + boardCellsCreator.createCells( + area = area, + countOfMines = countOfMines, + inputManualMineLocations = manualMineLocations.toSet(), + ) return Board( area = area, cells = cells, ) } - - private fun createCells(area: Area): Cells { - val allLocations = - (0 until area.height * area.width) - .map { - Location( - row = (it / area.width) + 1, - col = (it % area.width) + 1, - ) - } - - var validManualMineLocations = allLocations.intersect(manualMineLocations) - - var manualMineCount = validManualMineLocations.size - - if (manualMineCount > countOfMines) { - validManualMineLocations = validManualMineLocations.take(countOfMines).toMutableSet() - } - - manualMineCount = validManualMineLocations.size - - val randomMineLocations: Set = - (allLocations - validManualMineLocations) - .shuffled() - .take(countOfMines - manualMineCount) - .toSet() - - val mineLocations = validManualMineLocations + randomMineLocations - - val closedCells = - allLocations - .map { location -> - if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() - } - - val updatedCells = - closedCells - .mapIndexed { index, cell -> - if (cell.hasMine()) { - cell - } else { - val location = allLocations[index] - val directions = listOf(-1 to -1, -1 to 0, -1 to 1, 0 to -1, 0 to 1, 1 to -1, 1 to 0, 1 to 1) - val adjacentMineCount = - directions - .count { (dx, dy) -> - val adjacentLocation = Location(location.row + dx, location.col + dy) - adjacentLocation in mineLocations - } - cell.copy(adjacentMines = AdjacentMines(adjacentMineCount)) - } - } - - return Cells(updatedCells) - } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt b/src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt new file mode 100644 index 000000000..d192bfb2e --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt @@ -0,0 +1,141 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe + +class DefaultBoardCellsCreatorTest : BehaviorSpec({ + + given("5 x 5 영역, 지뢰 개수 5개를 받고") { + val area = Area(height = 5, width = 5) + val countOfMines = 5 + val sut: BoardCellsCreator = DefaultBoardCellsCreator() + + `when`("빈 수동 지뢰 위치 목록을 받아 25개의 셀 목록을 만들면") { + val inputManualMineLocations: Set = emptySet() + val result = sut.createCells(area, countOfMines, inputManualMineLocations) + + then("25개의 닫힌 셀이다") { + result.size shouldBe 25 + result.all { it is ClosedCell }.shouldBeTrue() + } + + then("25개의 셀 중 5개는 지뢰를 가지고 있다") { + result.filter { it.hasMine() }.size shouldBe 5 + } + } + + `when`("5개의 서로 다른 지뢰 위치 목록을 받아 25개의 셀 목록을 만들면") { + val inputManualMineLocations: Set = + setOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + val result = sut.createCells(area, countOfMines, inputManualMineLocations) + + then("해당 위치에 지뢰를 가진 셀 목록을 반환한다") { + result.filter { it.hasMine() }.size shouldBe 5 + + inputManualMineLocations.forEach { location -> + val cell = result[(location.row - 1) * area.width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + + then("지뢰가 아닌 셀들에 인접 지뢰 개수를 표시할 수 있다") { + // -1 은 지뢰 + val expectedAdjacentMines = + listOf( + listOf(1, 1, 1, -1, -1), + listOf(-1, 1, 1, 2, 2), + listOf(1, 2, 1, 1, 0), + listOf(1, 2, -1, 1, 0), + listOf(-1, 2, 1, 1, 0), + ) + + for (row in 1..area.height) { + for (col in 1..area.width) { + val cellIndex = (row - 1) * area.width + (col - 1) + val cell = result[cellIndex] + val expectedMineCount = expectedAdjacentMines[row - 1][col - 1] + + if (expectedMineCount == -1) { + cell.hasMine() shouldBe true + } else { + cell.hasMine() shouldBe false + cell.adjacentMines shouldBe AdjacentMines(expectedMineCount) + } + } + } + } + } + + `when`("4개의 서로 다른 지뢰 위치 목록을 받아 25개의 셀 목록을 만들면") { + val inputManualMineLocations: Set = + setOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + ) + val result = sut.createCells(area, countOfMines, inputManualMineLocations) + + then("4개의 지정 위치와 1개의 랜덤 위치에 지뢰를 가진 셀 목록을 반환한다") { + result.filter { it.hasMine() }.size shouldBe 5 + + inputManualMineLocations.forEach { location -> + val cell = result[(location.row - 1) * area.width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("6개의 서로 다른 위치 목록을 받아 25개의 셀 목록을 만들면") { + val inputManualMineLocations: Set = + setOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + Location(5, 2), + ) + val result = sut.createCells(area, countOfMines, inputManualMineLocations) + + then("앞에서 5개의 위치에 지뢰를 가진 셀 목록을 반환한다") { + result.filter { it.hasMine() }.size shouldBe 5 + + inputManualMineLocations.take(5).forEach { location -> + val cell = result[(location.row - 1) * area.width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + + `when`("1개의 유효하지 않은 지뢰 위치 + 5개의 서로 다른 지뢰 위치 목록을 받아 25개의 셀 목록을 만들면") { + val invalidMineLocation = Location(0, 4) + val inputManualMineLocations: Set = + setOf( + Location(1, 4), + Location(1, 5), + invalidMineLocation, + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + val result = sut.createCells(area, countOfMines, inputManualMineLocations) + + then("유효한 5개의 지뢰 위치에 지뢰를 가진 셀 목록을 반환한다") { + result.filter { it.hasMine() }.size shouldBe 5 + + inputManualMineLocations.filterNot { it == invalidMineLocation }.forEach { location -> + val cell = result[(location.row - 1) * area.width + (location.col - 1)] + cell.hasMine() shouldBe true + } + } + } + } +}) diff --git a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt index b1b61e088..6ed3f5d3d 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt @@ -257,7 +257,7 @@ class BoardBuilderTest : BehaviorSpec({ .mineAt(5, 1) .build() - then("처음에 받은 지뢰 위치 5개에 지뢰를 가진 보드판을 만들 수 있다") { + then("유효한 지뢰 위치 5개에 지뢰를 가진 보드판을 만들 수 있다") { result.cells.filter { it.hasMine() }.size shouldBe 5 val mineLocations = listOf( From 99b27e6eb6a650f4f6cef5e2eedd7d1e6da17cf1 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 00:50:08 +0900 Subject: [PATCH 24/47] =?UTF-8?q?feature(minesweeper):=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=9D=B8=EC=A0=91=20?= =?UTF-8?q?8=EB=B0=A9=ED=96=A5=20=EC=9C=84=EC=B9=98=EB=A5=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20=EC=A7=80?= =?UTF-8?q?=EB=8B=8C=20AdjacentDirection=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/AdjacentDirection.kt | 19 +++++++++++++ .../domain/AdjacentDirectionTest.kt | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/AdjacentDirection.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/AdjacentDirectionTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/AdjacentDirection.kt b/src/main/kotlin/tdd/minesweeper/domain/AdjacentDirection.kt new file mode 100644 index 000000000..cf6a5c24d --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/AdjacentDirection.kt @@ -0,0 +1,19 @@ +package tdd.minesweeper.domain + +enum class AdjacentDirection(val dr: Int, val dc: Int) { + TOP_LEFT(-1, -1), + TOP(-1, 0), + TOP_RIGHT(-1, 1), + LEFT(0, -1), + RIGHT(0, 1), + BOTTOM_LEFT(1, -1), + BOTTOM(1, 0), + BOTTOM_RIGHT(1, 1), + ; + + companion object { + fun allAdjacentLocations(location: Location): List { + return entries.map { Location(location.row + it.dr, location.col + it.dc) } + } + } +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/AdjacentDirectionTest.kt b/src/test/kotlin/tdd/minesweeper/domain/AdjacentDirectionTest.kt new file mode 100644 index 000000000..41fec326b --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/AdjacentDirectionTest.kt @@ -0,0 +1,28 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class AdjacentDirectionTest : BehaviorSpec({ + given("위치를 받아") { + val location = Location(row = 2, col = 2) + + `when`("인접 위치들을 모두 구하면") { + val result = AdjacentDirection.allAdjacentLocations(location) + + then("8방향의 인접위치들을 반환한다") { + result shouldBe + listOf( + Location(row = 1, col = 1), + Location(row = 1, col = 2), + Location(row = 1, col = 3), + Location(row = 2, col = 1), + Location(row = 2, col = 3), + Location(row = 3, col = 1), + Location(row = 3, col = 2), + Location(row = 3, col = 3), + ) + } + } + } +}) From 3d23580820a965ab5a83b9bbf1a120a21d3e811b Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 00:50:27 +0900 Subject: [PATCH 25/47] =?UTF-8?q?refactor(minesweeper):=20DefaultBoardCell?= =?UTF-8?q?sCreator=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/DefaultBoardCellsCreator.kt | 94 +++++++++++-------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt index cfc5f483b..3687dfff8 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt @@ -6,57 +6,69 @@ class DefaultBoardCellsCreator : BoardCellsCreator { countOfMines: Int, inputManualMineLocations: Set, ): Cells { - val allLocations = - (0 until area.height * area.width) - .map { - Location( - row = (it / area.width) + 1, - col = (it % area.width) + 1, - ) - } + // 모든 위치 구하기 + val allLocations = createAllLocations(area) - var validManualMineLocations = allLocations.intersect(inputManualMineLocations) + // 지뢰 위치 구하기 + val mineLocations = calculateMineLocations(allLocations, inputManualMineLocations, countOfMines) - var manualMineCount = validManualMineLocations.size + // 지뢰 심기 + val closedCells = createMinePlantedCells(allLocations, mineLocations) - if (manualMineCount > countOfMines) { - validManualMineLocations = validManualMineLocations.take(countOfMines).toMutableSet() - } + // 지뢰 인접 위치 표시 + val updatedCells = markOfAdjacentMines(closedCells, allLocations, mineLocations) + + return Cells(updatedCells) + } - manualMineCount = validManualMineLocations.size + private fun createAllLocations(area: Area): List { + return (0 until area.height * area.width) + .map { + Location( + row = (it / area.width) + 1, + col = (it % area.width) + 1, + ) + } + } + + private fun calculateMineLocations( + allLocations: List, + inputManualMineLocations: Set, + countOfMines: Int, + ): Set { + val validManualMineLocations = + allLocations.intersect(inputManualMineLocations).take(countOfMines).toSet() val randomMineLocations: Set = (allLocations - validManualMineLocations) .shuffled() - .take(countOfMines - manualMineCount) + .take(countOfMines - validManualMineLocations.size) .toSet() - val mineLocations = validManualMineLocations + randomMineLocations - - val closedCells = - allLocations - .map { location -> - if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() - } - - val updatedCells = - closedCells - .mapIndexed { index, cell -> - if (cell.hasMine()) { - cell - } else { - val location = allLocations[index] - val directions = listOf(-1 to -1, -1 to 0, -1 to 1, 0 to -1, 0 to 1, 1 to -1, 1 to 0, 1 to 1) - val adjacentMineCount = - directions - .count { (dx, dy) -> - val adjacentLocation = Location(location.row + dx, location.col + dy) - adjacentLocation in mineLocations - } - cell.copy(adjacentMines = AdjacentMines(adjacentMineCount)) - } - } + return validManualMineLocations + randomMineLocations + } - return Cells(updatedCells) + private fun createMinePlantedCells( + allLocations: List, + mineLocations: Set, + ): List { + return allLocations.map { location -> if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() } + } + + private fun markOfAdjacentMines( + closedCells: List, + allLocations: List, + mineLocations: Set, + ) = closedCells + .mapIndexed { index, cell -> + cell.withAdjacentMines(AdjacentMines(calculateAdjacentMineCount(allLocations[index], mineLocations))) + } + + private fun calculateAdjacentMineCount( + location: Location, + mineLocations: Set, + ): Int { + val adjacentLocations = AdjacentDirection.allAdjacentLocations(location) + return adjacentLocations.count { it in mineLocations } } } From 35a1b1fad8fa4e33dc7ead1c03ad0e443578e7ea Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 00:51:55 +0900 Subject: [PATCH 26/47] =?UTF-8?q?refactor(minesweeper):=20Builder=20?= =?UTF-8?q?=EB=A5=BC=20dsl=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt | 4 ++-- src/main/kotlin/tdd/minesweeper/domain/{ => dsl}/Builder.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/main/kotlin/tdd/minesweeper/domain/{ => dsl}/Builder.kt (56%) diff --git a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt index ed8d2943e..2cb075965 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt @@ -3,12 +3,12 @@ package tdd.minesweeper.domain.dsl import tdd.minesweeper.domain.Area import tdd.minesweeper.domain.Board import tdd.minesweeper.domain.BoardCellsCreator -import tdd.minesweeper.domain.Builder import tdd.minesweeper.domain.DefaultBoardCellsCreator import tdd.minesweeper.domain.Location @BoardDslMaker -class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoardCellsCreator()) : Builder { +class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoardCellsCreator()) : + Builder { private var height: Int = 0 private var width: Int = 0 private var countOfMines: Int = 0 diff --git a/src/main/kotlin/tdd/minesweeper/domain/Builder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/Builder.kt similarity index 56% rename from src/main/kotlin/tdd/minesweeper/domain/Builder.kt rename to src/main/kotlin/tdd/minesweeper/domain/dsl/Builder.kt index 2f6bf3ce3..75626d1dc 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Builder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/Builder.kt @@ -1,4 +1,4 @@ -package tdd.minesweeper.domain +package tdd.minesweeper.domain.dsl interface Builder { fun build(): T From 85882d59d5ba9d8e2f8053be58ea4686b07a6fe8 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 00:53:44 +0900 Subject: [PATCH 27/47] =?UTF-8?q?refactor(minesweeper):=20BoardCellsCreato?= =?UTF-8?q?r=20=EC=99=80=20=EA=B5=AC=ED=98=84=EC=B2=B4=EB=A5=BC=20strategy?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt | 4 ++-- .../domain/{ => strategy}/BoardCellsCreator.kt | 6 +++++- .../domain/{ => strategy}/DefaultBoardCellsCreator.kt | 9 ++++++++- .../{ => strategy}/DefaultBoardCellsCreatorTest.kt | 6 +++++- 4 files changed, 20 insertions(+), 5 deletions(-) rename src/main/kotlin/tdd/minesweeper/domain/{ => strategy}/BoardCellsCreator.kt (51%) rename src/main/kotlin/tdd/minesweeper/domain/{ => strategy}/DefaultBoardCellsCreator.kt (89%) rename src/test/kotlin/tdd/minesweeper/domain/{ => strategy}/DefaultBoardCellsCreatorTest.kt (96%) diff --git a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt index 2cb075965..97ea46365 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt @@ -2,9 +2,9 @@ package tdd.minesweeper.domain.dsl import tdd.minesweeper.domain.Area import tdd.minesweeper.domain.Board -import tdd.minesweeper.domain.BoardCellsCreator -import tdd.minesweeper.domain.DefaultBoardCellsCreator import tdd.minesweeper.domain.Location +import tdd.minesweeper.domain.strategy.BoardCellsCreator +import tdd.minesweeper.domain.strategy.DefaultBoardCellsCreator @BoardDslMaker class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoardCellsCreator()) : diff --git a/src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/strategy/BoardCellsCreator.kt similarity index 51% rename from src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt rename to src/main/kotlin/tdd/minesweeper/domain/strategy/BoardCellsCreator.kt index 56114d4ca..c0793dab8 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/BoardCellsCreator.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/strategy/BoardCellsCreator.kt @@ -1,4 +1,8 @@ -package tdd.minesweeper.domain +package tdd.minesweeper.domain.strategy + +import tdd.minesweeper.domain.Area +import tdd.minesweeper.domain.Cells +import tdd.minesweeper.domain.Location interface BoardCellsCreator { fun createCells( diff --git a/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt similarity index 89% rename from src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt rename to src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt index 3687dfff8..c624f8d86 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreator.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt @@ -1,4 +1,11 @@ -package tdd.minesweeper.domain +package tdd.minesweeper.domain.strategy + +import tdd.minesweeper.domain.AdjacentDirection +import tdd.minesweeper.domain.AdjacentMines +import tdd.minesweeper.domain.Area +import tdd.minesweeper.domain.Cells +import tdd.minesweeper.domain.ClosedCell +import tdd.minesweeper.domain.Location class DefaultBoardCellsCreator : BoardCellsCreator { override fun createCells( diff --git a/src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt b/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt similarity index 96% rename from src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt rename to src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt index d192bfb2e..b38f718b1 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/DefaultBoardCellsCreatorTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt @@ -1,8 +1,12 @@ -package tdd.minesweeper.domain +package tdd.minesweeper.domain.strategy import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.shouldBe +import tdd.minesweeper.domain.AdjacentMines +import tdd.minesweeper.domain.Area +import tdd.minesweeper.domain.ClosedCell +import tdd.minesweeper.domain.Location class DefaultBoardCellsCreatorTest : BehaviorSpec({ From 7d0f692af0c99f9161833d1cafbc47739ab663e3 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 01:16:59 +0900 Subject: [PATCH 28/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EB=B3=B4=EB=93=9C=20=EB=82=B4=EC=97=90=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=9C=84=EC=B9=98=EC=9D=98=20=EC=85=80?= =?UTF-8?q?=EC=9D=84=20=EC=97=B4=EB=A0=A4=EA=B3=A0=20=ED=95=98=EB=A9=B4=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=8D=98=EC=A7=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Board.kt | 24 ++++++++++++++++++- .../tdd/minesweeper/domain/BoardTest.kt | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index 4f209eb79..39dca7682 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -1,3 +1,25 @@ package tdd.minesweeper.domain -data class Board(val area: Area, val cells: Cells) +data class Board(val area: Area, val cells: Cells) { + fun open(location: Location): Board { + validateLocation(location) + + return this.copy() + } + + private fun validateLocation(location: Location) { + require(location in allLocations()) { + "보드 내의 위치가 아닙니다: location=$location" + } + } + + private fun allLocations(): List { + return (0 until area.height * area.width) + .map { + Location( + row = (it / area.width) + 1, + col = (it % area.width) + 1, + ) + } + } +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index e8af52d08..ba1ae8165 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -1,7 +1,9 @@ package tdd.minesweeper.domain +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe +import tdd.minesweeper.domain.dsl.board class BoardTest : BehaviorSpec({ given("영역과 셀 목록을 받아") { @@ -17,4 +19,26 @@ class BoardTest : BehaviorSpec({ } } } + + given("5 x 5 크기, 지뢰 개수 5개의 닫힌 셀만 존재하는 보드에서") { + val sut = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + } + + `when`("보드 내에 없는 위치의 셀을 열려고 하면") { + val location = Location(0, 0) + + then("예외를 던진다") { + shouldThrow { sut.open(location) } + } + } + } }) From 17d508fd415fc84009b1da80b9d60ae305eb36d4 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 01:45:36 +0900 Subject: [PATCH 29/47] =?UTF-8?q?feature(minesweeper):=20BoardBuilder,=20B?= =?UTF-8?q?oardCreator=EC=97=90=20openAt()=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=88=98=EB=8F=99=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=85=80=EC=9D=84=20=EC=97=B4=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minesweeper/domain/dsl/BoardBuilder.kt | 38 ++++++++++++++++++- .../domain/dsl/BoardBuilderTest.kt | 32 ++++++++++++++++ .../domain/dsl/BoardCreatorTest.kt | 32 ++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt index 97ea46365..487597865 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain.dsl import tdd.minesweeper.domain.Area import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.Cells import tdd.minesweeper.domain.Location import tdd.minesweeper.domain.strategy.BoardCellsCreator import tdd.minesweeper.domain.strategy.DefaultBoardCellsCreator @@ -13,6 +14,7 @@ class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoa private var width: Int = 0 private var countOfMines: Int = 0 private val manualMineLocations: MutableSet = mutableSetOf() + private val manualOpenLocations: MutableSet = mutableSetOf() fun height(value: Int) = apply { @@ -33,6 +35,11 @@ class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoa col: Int, ) = apply { this.manualMineLocations.add(Location(row, col)) } + fun openAt( + row: Int, + col: Int, + ) = apply { this.manualOpenLocations.add(Location(row, col)) } + override fun build(): Board { require(height > 0) { "높이는 양수여야 합니다! height=$height" } require(width > 0) { "높이는 양수여야 합니다! width=$width" } @@ -47,9 +54,38 @@ class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoa inputManualMineLocations = manualMineLocations.toSet(), ) + if (manualOpenLocations.isEmpty()) { + return Board( + area = area, + cells = cells, + ) + } + + val openedCells: Cells = openCellsManually(cells, area) + return Board( area = area, - cells = cells, + cells = openedCells, + ) + } + + private fun openCellsManually( + cells: Cells, + area: Area, + ): Cells { + return Cells( + cells.mapIndexed { index, cell -> + val location = + Location( + row = (index / area.width) + 1, + col = (index % area.width) + 1, + ) + if (location in manualOpenLocations) { + cell.open() + } else { + cell + } + }, ) } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt index 6ed3f5d3d..d35937118 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt @@ -274,5 +274,37 @@ class BoardBuilderTest : BehaviorSpec({ } } } + + `when`("5개의 서로 다른 오픈위치를 수동으로 받아 보드를 만들면") { + val result = + sut + .height(height) + .width(width) + .countOfMines(countOfMines) + .openAt(1, 4) + .openAt(1, 5) + .openAt(2, 1) + .openAt(4, 3) + .openAt(5, 1) + .build() + + val openLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + then("해당 위치가 오픈된 셀을 가진 보드판을 만들 수 있다") { + result.cells.filter { it.isOpen() }.size shouldBe 5 + + openLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.isOpen() shouldBe true + } + } + } } }) diff --git a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt index cc203b126..2a674c0e9 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt @@ -288,5 +288,37 @@ class BoardCreatorTest : BehaviorSpec({ } } } + + `when`("5개의 서로 다른 오픈위치를 수동으로 받아 보드를 만들면") { + val result = + board { + height(height) + width(width) + countOfMines(countOfMines) + openAt(1, 4) + openAt(1, 5) + openAt(2, 1) + openAt(4, 3) + openAt(5, 1) + } + + val openLocations = + listOf( + Location(1, 4), + Location(1, 5), + Location(2, 1), + Location(4, 3), + Location(5, 1), + ) + + then("해당 위치가 오픈된 셀을 가진 보드판을 만들 수 있다") { + result.cells.filter { it.isOpen() }.size shouldBe 5 + + openLocations.forEach { location -> + val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + cell.isOpen() shouldBe true + } + } + } } }) From 1c667cf79ff9557968ac6762dffa1995a75f5404 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 11:10:35 +0900 Subject: [PATCH 30/47] =?UTF-8?q?test(minesweeper):=20=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=AF=B8=20=EC=97=B4=EB=A6=B0=20=EC=85=80?= =?UTF-8?q?=EC=9D=84=20=EC=97=B4=EB=A9=B4=20=EB=B3=B4=EB=93=9C=EC=9D=98=20?= =?UTF-8?q?=EB=B3=80=ED=99=94=EA=B0=80=20=EC=97=86=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardTest.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index ba1ae8165..63bcb8684 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -33,7 +33,7 @@ class BoardTest : BehaviorSpec({ mineAt(5, 1) } - `when`("보드 내에 없는 위치의 셀을 열려고 하면") { + `when`("보드 내에 없는 위치의 셀을 열면") { val location = Location(0, 0) then("예외를 던진다") { @@ -41,4 +41,28 @@ class BoardTest : BehaviorSpec({ } } } + + given("5 x 5 크기, 지뢰 개수 5개, 열린 셀 1개의 보드에서") { + val sut = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + openAt(1, 1) + } + + `when`("이미 열린 셀을 다시 열면") { + val location = Location(1, 1) + val result = sut.open(location) + + then("보드는 변하지 않는다") { + result shouldBe sut + } + } + } }) From e89e17ab15776d803b132b1205b4132519ccdb60 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 11:28:27 +0900 Subject: [PATCH 31/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EC=A7=80=EB=A2=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A7=84=20=EB=8B=AB=ED=9E=8C=20=EC=85=80=EC=9D=84=20?= =?UTF-8?q?=EC=97=B4=EB=A9=B4=20=ED=95=B4=EB=8B=B9=20=EC=85=80=EB=A7=8C=20?= =?UTF-8?q?=EC=97=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Board.kt | 7 +++++- .../tdd/minesweeper/domain/ClosedCell.kt | 2 +- .../kotlin/tdd/minesweeper/domain/MineCell.kt | 2 +- .../tdd/minesweeper/domain/BoardTest.kt | 22 +++++++++++++++++++ .../tdd/minesweeper/domain/MineCellTest.kt | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index 39dca7682..42b6b0f92 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -4,7 +4,12 @@ data class Board(val area: Area, val cells: Cells) { fun open(location: Location): Board { validateLocation(location) - return this.copy() + val locationIndex = (location.row - 1) * area.width + (location.col - 1) + + val mutableCells = cells.toMutableList() + mutableCells[locationIndex] = mutableCells[locationIndex].open() + + return this.copy(cells = Cells(mutableCells.toList())) } private fun validateLocation(location: Location) { diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index 3ef85caca..efaa54455 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -12,7 +12,7 @@ data class ClosedCell( override fun open(): Cell { if (hasMine) { - return MineCell() + return MineCell } return NumberCell(adjacentMines) } diff --git a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt index fc6035ed3..c3d4a5421 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt @@ -1,6 +1,6 @@ package tdd.minesweeper.domain -class MineCell : Cell { +object MineCell : Cell { override val adjacentMines: AdjacentMines? = null override fun isOpen(): Boolean = true diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index 63bcb8684..1a1f1e3ba 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -2,6 +2,7 @@ package tdd.minesweeper.domain import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe import tdd.minesweeper.domain.dsl.board @@ -40,6 +41,27 @@ class BoardTest : BehaviorSpec({ shouldThrow { sut.open(location) } } } + + `when`("지뢰를 가진 닫힌 셀을 열면") { + val location = Location(1, 4) + val result = sut.open(location) + + then("해당 셀만 열린다") { + val expectedBoard = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + openAt(1, 4) + } + result.cells shouldContainExactly expectedBoard.cells + } + } } given("5 x 5 크기, 지뢰 개수 5개, 열린 셀 1개의 보드에서") { diff --git a/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt index 30ff5df3b..c5fdbeebe 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt @@ -7,7 +7,7 @@ import io.kotest.matchers.types.shouldBeInstanceOf class MineCellTest : BehaviorSpec({ given("지뢰 셀은") { - val sut = MineCell() + val sut = MineCell `when`("열린 상태가") { val result = sut.isOpen() From b0abb91536c074868d8ab278543c98e04db0045f Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Fri, 27 Dec 2024 11:30:19 +0900 Subject: [PATCH 32/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EC=9D=B4=EC=9B=83=ED=95=9C=208?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EC=A4=91=EC=97=90=20=EC=A7=80=EB=A2=B0?= =?UTF-8?q?=EA=B0=80=20=EC=9E=88=EB=8A=94=20=EC=85=80=EC=9D=84=20=EC=97=B4?= =?UTF-8?q?=EB=A9=B4=20=ED=95=B4=EB=8B=B9=20=EC=85=80=EB=A7=8C=20=EC=97=B4?= =?UTF-8?q?=EB=A6=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardTest.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index 1a1f1e3ba..b38caadf6 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -62,6 +62,28 @@ class BoardTest : BehaviorSpec({ result.cells shouldContainExactly expectedBoard.cells } } + + `when`("이웃한 8방향 중에 지뢰가 있는 셀을 열면") { + val location = Location(1, 1) + val result = sut.open(location) + + then("해당 셀만 열린다") { + val expectedBoard = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + openAt(1, 1) + } + + result.cells shouldContainExactly expectedBoard.cells + } + } } given("5 x 5 크기, 지뢰 개수 5개, 열린 셀 1개의 보드에서") { From 92b9dfad3d7fdccfea8eb652d4396bd03f77d971 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sat, 28 Dec 2024 20:57:56 +0900 Subject: [PATCH 33/47] =?UTF-8?q?feature(minesweeper):=20Location=20?= =?UTF-8?q?=EC=97=90=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BE=B8=EB=8A=94=20=EA=B2=83=EA=B3=BC=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20Location=EC=9C=BC=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BE=B8=EB=8A=94=20=EB=A9=94=EC=84=9C=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/tdd/minesweeper/domain/Board.kt | 14 ++++---- .../kotlin/tdd/minesweeper/domain/Location.kt | 15 ++++++++- .../minesweeper/domain/dsl/BoardBuilder.kt | 6 ++-- .../strategy/DefaultBoardCellsCreator.kt | 8 ++--- .../tdd/minesweeper/domain/BoardTest.kt | 8 ++--- .../tdd/minesweeper/domain/LocationTest.kt | 32 +++++++++++++++++++ .../domain/dsl/BoardBuilderTest.kt | 12 +++---- .../domain/dsl/BoardCreatorTest.kt | 14 ++++---- .../strategy/DefaultBoardCellsCreatorTest.kt | 10 +++--- 9 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index 42b6b0f92..bac7a5b72 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -4,12 +4,12 @@ data class Board(val area: Area, val cells: Cells) { fun open(location: Location): Board { validateLocation(location) - val locationIndex = (location.row - 1) * area.width + (location.col - 1) + val locationIndex = location.toIndex(area.width) - val mutableCells = cells.toMutableList() - mutableCells[locationIndex] = mutableCells[locationIndex].open() + val openedCells = cells.toMutableList() + openedCells[locationIndex] = openedCells[locationIndex].open() - return this.copy(cells = Cells(mutableCells.toList())) + return this.copy(cells = Cells(openedCells.toList())) } private fun validateLocation(location: Location) { @@ -21,9 +21,9 @@ data class Board(val area: Area, val cells: Cells) { private fun allLocations(): List { return (0 until area.height * area.width) .map { - Location( - row = (it / area.width) + 1, - col = (it % area.width) + 1, + Location.from( + index = it, + width = area.width, ) } } diff --git a/src/main/kotlin/tdd/minesweeper/domain/Location.kt b/src/main/kotlin/tdd/minesweeper/domain/Location.kt index d03125c20..b30cd2526 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Location.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Location.kt @@ -1,3 +1,16 @@ package tdd.minesweeper.domain -data class Location(val row: Int, val col: Int) +data class Location(val row: Int, val col: Int) { + fun toIndex(width: Int): Int = (row - 1) * width + (col - 1) + + companion object { + fun from( + index: Int, + width: Int, + ): Location = + Location( + row = (index / width) + 1, + col = (index % width) + 1, + ) + } +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt index 487597865..e9d8903af 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt @@ -76,9 +76,9 @@ class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoa return Cells( cells.mapIndexed { index, cell -> val location = - Location( - row = (index / area.width) + 1, - col = (index % area.width) + 1, + Location.from( + index = index, + width = area.width, ) if (location in manualOpenLocations) { cell.open() diff --git a/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt b/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt index c624f8d86..bfaef336f 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreator.kt @@ -31,9 +31,9 @@ class DefaultBoardCellsCreator : BoardCellsCreator { private fun createAllLocations(area: Area): List { return (0 until area.height * area.width) .map { - Location( - row = (it / area.width) + 1, - col = (it % area.width) + 1, + Location.from( + index = it, + width = area.width, ) } } @@ -59,7 +59,7 @@ class DefaultBoardCellsCreator : BoardCellsCreator { allLocations: List, mineLocations: Set, ): List { - return allLocations.map { location -> if (location in mineLocations) ClosedCell(hasMine = true) else ClosedCell() } + return allLocations.map { location -> ClosedCell(hasMine = (location in mineLocations)) } } private fun markOfAdjacentMines( diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index b38caadf6..b689de7dc 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -47,7 +47,7 @@ class BoardTest : BehaviorSpec({ val result = sut.open(location) then("해당 셀만 열린다") { - val expectedBoard = + val expected = board { height(5) width(5) @@ -59,7 +59,7 @@ class BoardTest : BehaviorSpec({ mineAt(5, 1) openAt(1, 4) } - result.cells shouldContainExactly expectedBoard.cells + result.cells shouldContainExactly expected.cells } } @@ -68,7 +68,7 @@ class BoardTest : BehaviorSpec({ val result = sut.open(location) then("해당 셀만 열린다") { - val expectedBoard = + val expected = board { height(5) width(5) @@ -81,7 +81,7 @@ class BoardTest : BehaviorSpec({ openAt(1, 1) } - result.cells shouldContainExactly expectedBoard.cells + result.cells shouldContainExactly expected.cells } } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt b/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt new file mode 100644 index 000000000..391253126 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt @@ -0,0 +1,32 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class LocationTest : BehaviorSpec({ + given("보드의 너비를 받아") { + val width = 3 + val sut = Location(row = 3, col = 3) + + `when`("위치를") { + val result = sut.toIndex(width) + + then("인덱스로 바꿀 수 있다") { + result shouldBe 8 + } + } + } + + given("인덱스와 너비를 받아") { + val index = 8 + val width = 3 + + `when`("위치를") { + val result = Location.from(index = index, width = width) + + then("만들 수 있다") { + result shouldBe Location(row = 3, col = 3) + } + } + } +}) diff --git a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt index d35937118..9556b5e51 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardBuilderTest.kt @@ -124,7 +124,7 @@ class BoardBuilderTest : BehaviorSpec({ result.cells.filter { it.hasMine() }.size shouldBe 5 mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -179,7 +179,7 @@ class BoardBuilderTest : BehaviorSpec({ Location(4, 3), ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -205,7 +205,7 @@ class BoardBuilderTest : BehaviorSpec({ Location(1, 4), ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -237,7 +237,7 @@ class BoardBuilderTest : BehaviorSpec({ ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -269,7 +269,7 @@ class BoardBuilderTest : BehaviorSpec({ ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -301,7 +301,7 @@ class BoardBuilderTest : BehaviorSpec({ result.cells.filter { it.isOpen() }.size shouldBe 5 openLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.isOpen() shouldBe true } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt index 2a674c0e9..200219912 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/dsl/BoardCreatorTest.kt @@ -138,7 +138,7 @@ class BoardCreatorTest : BehaviorSpec({ result.cells.filter { it.hasMine() }.size shouldBe 5 mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -156,7 +156,7 @@ class BoardCreatorTest : BehaviorSpec({ for (row in 1..height) { for (col in 1..width) { - val cellIndex = (row - 1) * width + (col - 1) + val cellIndex = Location(row, col).toIndex(width) val cell = result.cells[cellIndex] val expectedMineCount = expectedAdjacentMines[row - 1][col - 1] @@ -193,7 +193,7 @@ class BoardCreatorTest : BehaviorSpec({ Location(4, 3), ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -219,7 +219,7 @@ class BoardCreatorTest : BehaviorSpec({ Location(1, 4), ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -251,7 +251,7 @@ class BoardCreatorTest : BehaviorSpec({ ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -283,7 +283,7 @@ class BoardCreatorTest : BehaviorSpec({ ) mineLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.hasMine() shouldBe true } } @@ -315,7 +315,7 @@ class BoardCreatorTest : BehaviorSpec({ result.cells.filter { it.isOpen() }.size shouldBe 5 openLocations.forEach { location -> - val cell = result.cells[(location.row - 1) * width + (location.col - 1)] + val cell = result.cells[location.toIndex(width)] cell.isOpen() shouldBe true } } diff --git a/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt b/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt index b38f718b1..ac883e04a 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultBoardCellsCreatorTest.kt @@ -44,7 +44,7 @@ class DefaultBoardCellsCreatorTest : BehaviorSpec({ result.filter { it.hasMine() }.size shouldBe 5 inputManualMineLocations.forEach { location -> - val cell = result[(location.row - 1) * area.width + (location.col - 1)] + val cell = result[location.toIndex(area.width)] cell.hasMine() shouldBe true } } @@ -62,7 +62,7 @@ class DefaultBoardCellsCreatorTest : BehaviorSpec({ for (row in 1..area.height) { for (col in 1..area.width) { - val cellIndex = (row - 1) * area.width + (col - 1) + val cellIndex = Location(row, col).toIndex(area.width) val cell = result[cellIndex] val expectedMineCount = expectedAdjacentMines[row - 1][col - 1] @@ -91,7 +91,7 @@ class DefaultBoardCellsCreatorTest : BehaviorSpec({ result.filter { it.hasMine() }.size shouldBe 5 inputManualMineLocations.forEach { location -> - val cell = result[(location.row - 1) * area.width + (location.col - 1)] + val cell = result[location.toIndex(area.width)] cell.hasMine() shouldBe true } } @@ -113,7 +113,7 @@ class DefaultBoardCellsCreatorTest : BehaviorSpec({ result.filter { it.hasMine() }.size shouldBe 5 inputManualMineLocations.take(5).forEach { location -> - val cell = result[(location.row - 1) * area.width + (location.col - 1)] + val cell = result[location.toIndex(area.width)] cell.hasMine() shouldBe true } } @@ -136,7 +136,7 @@ class DefaultBoardCellsCreatorTest : BehaviorSpec({ result.filter { it.hasMine() }.size shouldBe 5 inputManualMineLocations.filterNot { it == invalidMineLocation }.forEach { location -> - val cell = result[(location.row - 1) * area.width + (location.col - 1)] + val cell = result[location.toIndex(area.width)] cell.hasMine() shouldBe true } } From 8b6285c9436f59fc9a01d63374ef9add3a82d23d Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sat, 28 Dec 2024 22:46:43 +0900 Subject: [PATCH 34/47] =?UTF-8?q?test(minesweeper):=20=EC=9D=B4=EC=9B=83?= =?UTF-8?q?=ED=95=9C=208=EB=B0=A9=ED=96=A5=20=EC=A4=91=EC=97=90=20?= =?UTF-8?q?=EC=A7=80=EB=A2=B0=EA=B0=80=20=EC=97=86=EB=8A=94=20=EC=85=80?= =?UTF-8?q?=EC=9D=84=20=EC=97=B4=EB=A9=B4=20=EC=9D=B4=EC=9B=83=ED=95=9C=20?= =?UTF-8?q?=EC=A7=80=EB=A2=B0=EA=B0=80=20=EC=97=86=EB=8A=94=20=EC=85=80?= =?UTF-8?q?=EC=9D=84=20=EB=AA=A8=EB=91=90=20=EC=97=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tdd/minesweeper/domain/BoardTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index b689de7dc..1bfb4acca 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -84,6 +84,35 @@ class BoardTest : BehaviorSpec({ result.cells shouldContainExactly expected.cells } } + + `when`("이웃한 8방향 중에 지뢰가 없는 셀을 열면") { + val location = Location(5, 5) + val result = sut.open(location) + + then("이웃한 지뢰가 없는 셀을 모두 연다") { + val expected = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + openAt(2, 4) + openAt(2, 5) + openAt(3, 4) + openAt(3, 5) + openAt(4, 4) + openAt(4, 5) + openAt(5, 4) + openAt(5, 5) + } + + result.cells shouldContainExactly expected.cells + } + } } given("5 x 5 크기, 지뢰 개수 5개, 열린 셀 1개의 보드에서") { From f1d46778262abdb53e105df6a7f317a8d709bce1 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sat, 28 Dec 2024 23:42:57 +0900 Subject: [PATCH 35/47] =?UTF-8?q?feature(minesweeper):=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EA=B0=80=20=EB=B3=B4=EB=93=9C=20=EC=98=81=EC=97=AD=20?= =?UTF-8?q?=EB=82=B4=EC=97=90=20=EC=9E=88=EB=8A=94=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=ED=95=9C=20=EC=9C=84=EC=B9=98=EC=9D=B8=EC=A7=80=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20isValid(a?= =?UTF-8?q?rea:=20Area)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Location.kt | 4 ++ .../tdd/minesweeper/domain/LocationTest.kt | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Location.kt b/src/main/kotlin/tdd/minesweeper/domain/Location.kt index b30cd2526..c51cce5eb 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Location.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Location.kt @@ -3,6 +3,10 @@ package tdd.minesweeper.domain data class Location(val row: Int, val col: Int) { fun toIndex(width: Int): Int = (row - 1) * width + (col - 1) + fun isValid(area: Area): Boolean { + return row in 1..area.height && col in 1..area.width + } + companion object { fun from( index: Int, diff --git a/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt b/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt index 391253126..9a59491f8 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/LocationTest.kt @@ -29,4 +29,48 @@ class LocationTest : BehaviorSpec({ } } } + + given("보드의 영역이 3x3일 때") { + val area = Area(height = 3, width = 3) + + `when`("위치가 보드 내부에 있는 경우") { + val location = Location(row = 2, col = 2) + + then("유효한 위치로 확인된다") { + location.isValid(area) shouldBe true + } + } + + `when`("위치가 보드의 경계선 상에 있는 경우") { + val edgeLocations = + listOf( + Location(row = 1, col = 1), + Location(row = 1, col = 3), + Location(row = 3, col = 1), + Location(row = 3, col = 3), + ) + + then("모든 위치가 유효한 위치로 확인된다") { + edgeLocations.forEach { location -> + location.isValid(area) shouldBe true + } + } + } + + `when`("위치가 보드의 경계를 벗어난 경우") { + val invalidLocations = + listOf( + Location(row = 0, col = 1), + Location(row = 4, col = 1), + Location(row = 1, col = 0), + Location(row = 1, col = 4), + ) + + then("모든 위치가 유효하지 않은 위치로 확인된다") { + invalidLocations.forEach { location -> + location.isValid(area) shouldBe false + } + } + } + } }) From 34f8cd48ae3b1818595917c1ddd3ef5ca9d31a03 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 00:50:00 +0900 Subject: [PATCH 36/47] =?UTF-8?q?feature(minesweeper):=20=EC=85=80?= =?UTF-8?q?=EC=9D=B4=20=EC=9D=B8=EC=A0=91=20=EC=85=80=EA=B9=8C=EC=A7=80=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=ED=95=B4=EC=84=9C=20=EC=97=B4=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=85=80=EC=9D=B8=EC=A7=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EB=8A=94=20isExpandableToAdjacent()=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 --- .../kotlin/tdd/minesweeper/domain/Cell.kt | 2 ++ .../tdd/minesweeper/domain/ClosedCell.kt | 4 +++ .../kotlin/tdd/minesweeper/domain/MineCell.kt | 2 ++ .../tdd/minesweeper/domain/NumberCell.kt | 2 ++ .../tdd/minesweeper/domain/ClosedCellTest.kt | 34 ++++++++++++++++++- .../tdd/minesweeper/domain/MineCellTest.kt | 8 +++++ .../tdd/minesweeper/domain/NumberCellTest.kt | 8 +++++ 7 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt index e28de3c52..59b8b218a 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Cell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Cell.kt @@ -8,4 +8,6 @@ interface Cell { fun hasMine(): Boolean fun open(): Cell + + fun isExpandableToAdjacent(): Boolean } diff --git a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt index efaa54455..16da6bf22 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt @@ -17,5 +17,9 @@ data class ClosedCell( return NumberCell(adjacentMines) } + override fun isExpandableToAdjacent(): Boolean { + return !hasMine() && adjacentMines == AdjacentMines(0) + } + fun withAdjacentMines(newAdjacentMines: AdjacentMines): ClosedCell = this.copy(adjacentMines = newAdjacentMines) } diff --git a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt index c3d4a5421..5bfbcbb63 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/MineCell.kt @@ -8,4 +8,6 @@ object MineCell : Cell { override fun hasMine(): Boolean = true override fun open(): Cell = this + + override fun isExpandableToAdjacent(): Boolean = false } diff --git a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt index 06e4b0eec..313654191 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt @@ -6,4 +6,6 @@ data class NumberCell(override val adjacentMines: AdjacentMines = AdjacentMines( override fun hasMine(): Boolean = false override fun open(): Cell = this + + override fun isExpandableToAdjacent(): Boolean = false } diff --git a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt index 5839376bf..1ee5bda8d 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/ClosedCellTest.kt @@ -33,7 +33,7 @@ class ClosedCellTest : BehaviorSpec({ } } - given("지뢰가 있는 닫힌 셀을") { + given("지뢰가 있는 닫힌 셀은") { val sut = ClosedCell( hasMine = true, @@ -46,6 +46,14 @@ class ClosedCellTest : BehaviorSpec({ result.shouldBeInstanceOf() } } + + `when`("지뢰를 가지고 있으므로") { + val result = sut.isExpandableToAdjacent() + + then("확장해서 열 수 없는 상태다") { + result shouldBe false + } + } } given("지뢰가 없을 때") { @@ -65,4 +73,28 @@ class ClosedCellTest : BehaviorSpec({ } } } + + given("인접한 지뢰가 있는 닫힌 셀은") { + val sut = ClosedCell(adjacentMines = 1) + + `when`("주변에 지뢰가 있으므로") { + val result = sut.isExpandableToAdjacent() + + then("확장해서 열 수 없는 상태다") { + result shouldBe false + } + } + } + + given("인접한 지뢰가 없는 닫힌 셀은") { + val sut = ClosedCell() + + `when`("주변에 지뢰가 없으므로") { + val result = sut.isExpandableToAdjacent() + + then("확장해서 열 수 있는 상태다") { + result shouldBe true + } + } + } }) diff --git a/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt index c5fdbeebe..f1a593ae5 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/MineCellTest.kt @@ -30,5 +30,13 @@ class MineCellTest : BehaviorSpec({ sut.hasMine() shouldBe true } } + + `when`("이미 열린 셀이므로") { + val result = sut.isExpandableToAdjacent() + + then("확장해서 열 수 없는 상태다") { + result shouldBe false + } + } } }) diff --git a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt index 23ce5294f..0f0af0154 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/NumberCellTest.kt @@ -31,6 +31,14 @@ class NumberCellTest : BehaviorSpec({ sut.hasMine() shouldBe false } } + + `when`("이미 열린 셀이므로") { + val result = sut.isExpandableToAdjacent() + + then("확장해서 열 수 없는 상태이다") { + result shouldBe false + } + } } given("인접 지뢰 수는") { From 20155884f59aeaa30efaebf99d6b7afce0bea98b Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 00:57:21 +0900 Subject: [PATCH 37/47] =?UTF-8?q?refactor(minesweeper):=20Board=20?= =?UTF-8?q?=EC=9D=98=20open=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - open() 메서드 간소화: findAllShouldOpen()으로 책임 이전 - findAllShouldOpen(): BFS를 통해 열어야 할 Set 계산 - applyOpen(): Set을 인덱스 기반 Set 으로 변경 && 가변 리스트로 변경해서 계산 후 불변 리스트로 반환함으로써 성능 향상 --- .../kotlin/tdd/minesweeper/domain/Board.kt | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index bac7a5b72..aa83cc9e9 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -4,27 +4,62 @@ data class Board(val area: Area, val cells: Cells) { fun open(location: Location): Board { validateLocation(location) - val locationIndex = location.toIndex(area.width) + val shouldOpen = findAllShouldOpen(location) - val openedCells = cells.toMutableList() - openedCells[locationIndex] = openedCells[locationIndex].open() - - return this.copy(cells = Cells(openedCells.toList())) + return this.copy(cells = applyOpen(shouldOpen)) } private fun validateLocation(location: Location) { - require(location in allLocations()) { + require(location.isValid(area)) { "보드 내의 위치가 아닙니다: location=$location" } } - private fun allLocations(): List { - return (0 until area.height * area.width) - .map { - Location.from( - index = it, - width = area.width, - ) + private fun findAllShouldOpen(location: Location): Set { + val result = mutableSetOf() + val queue = ArrayDeque().apply { add(location) } + val visited = mutableSetOf().apply { add(location) } + + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + + // 현재 위치의 셀 상태 확인 + val currentCell = cells[current.toIndex(area.width)] + // 이미 열린 셀 + if (currentCell.isOpen()) { + continue + } + + // 셀 열기 + result.add(current) + + // 인접 지뢰 수가 0이면 인접 셀 추가 탐색 + if (currentCell.isExpandableToAdjacent()) { + addExpandableCandidateToQueue(current, visited, queue) + } + } + return result.toSet() + } + + private fun addExpandableCandidateToQueue( + current: Location, + visited: MutableSet, + queue: ArrayDeque, + ) { + AdjacentDirection.allAdjacentLocations(current) + .filter { it.isValid(area) && it !in visited } + .forEach { adjacent -> + queue.add(adjacent) + visited.add(current) } } + + private fun applyOpen(shouldOpen: Set): Cells { + val shouldOpenIndexes = shouldOpen.map { it.toIndex(area.width) }.toSet() + val result = cells.toMutableList() + + shouldOpenIndexes.forEach { index -> result[index] = cells[index].open() } + + return Cells(result.toList()) + } } From b87b41b4bc5a484934a9d939f8e38d65a9f91560 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 01:37:58 +0900 Subject: [PATCH 38/47] =?UTF-8?q?feature(minesweeper):=20=EC=97=B4?= =?UTF-8?q?=EC=96=B4=EC=95=BC=20=ED=95=A0=20=EC=85=80=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EA=B3=84=EC=82=B0=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20ShouldOpenLocationFin?= =?UTF-8?q?der=20=EC=99=80=20=EA=B5=AC=ED=98=84=EC=B2=B4=EC=97=90=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Board.kt | 50 ++-------- .../DefaultShouldOpenLocationFinder.kt | 65 +++++++++++++ .../strategy/ShouldOpenLocationFinder.kt | 11 +++ .../DefaultShouldOpenLocationFinderTest.kt | 92 +++++++++++++++++++ 4 files changed, 177 insertions(+), 41 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinder.kt create mode 100644 src/main/kotlin/tdd/minesweeper/domain/strategy/ShouldOpenLocationFinder.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinderTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index aa83cc9e9..11c8ba4ca 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -1,10 +1,17 @@ package tdd.minesweeper.domain -data class Board(val area: Area, val cells: Cells) { +import tdd.minesweeper.domain.strategy.DefaultShouldOpenLocationFinder +import tdd.minesweeper.domain.strategy.ShouldOpenLocationFinder + +data class Board( + val area: Area, + val cells: Cells, + private val shouldOpenLocationFinder: ShouldOpenLocationFinder = DefaultShouldOpenLocationFinder(), +) { fun open(location: Location): Board { validateLocation(location) - val shouldOpen = findAllShouldOpen(location) + val shouldOpen = shouldOpenLocationFinder.findAllShouldOpen(this, location) return this.copy(cells = applyOpen(shouldOpen)) } @@ -15,45 +22,6 @@ data class Board(val area: Area, val cells: Cells) { } } - private fun findAllShouldOpen(location: Location): Set { - val result = mutableSetOf() - val queue = ArrayDeque().apply { add(location) } - val visited = mutableSetOf().apply { add(location) } - - while (queue.isNotEmpty()) { - val current = queue.removeFirst() - - // 현재 위치의 셀 상태 확인 - val currentCell = cells[current.toIndex(area.width)] - // 이미 열린 셀 - if (currentCell.isOpen()) { - continue - } - - // 셀 열기 - result.add(current) - - // 인접 지뢰 수가 0이면 인접 셀 추가 탐색 - if (currentCell.isExpandableToAdjacent()) { - addExpandableCandidateToQueue(current, visited, queue) - } - } - return result.toSet() - } - - private fun addExpandableCandidateToQueue( - current: Location, - visited: MutableSet, - queue: ArrayDeque, - ) { - AdjacentDirection.allAdjacentLocations(current) - .filter { it.isValid(area) && it !in visited } - .forEach { adjacent -> - queue.add(adjacent) - visited.add(current) - } - } - private fun applyOpen(shouldOpen: Set): Cells { val shouldOpenIndexes = shouldOpen.map { it.toIndex(area.width) }.toSet() val result = cells.toMutableList() diff --git a/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinder.kt b/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinder.kt new file mode 100644 index 000000000..f8094f79d --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinder.kt @@ -0,0 +1,65 @@ +package tdd.minesweeper.domain.strategy + +import tdd.minesweeper.domain.AdjacentDirection +import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.Location + +class DefaultShouldOpenLocationFinder : ShouldOpenLocationFinder { + override fun findAllShouldOpen( + board: Board, + location: Location, + ): Set { + val result = mutableSetOf() + val context = + SearchContext(board).apply { + queue.add(location) + visited.add(location) + } + + while (context.queue.isNotEmpty()) { + val current = context.queue.removeFirst() + + if (shouldSkip(context.board, current)) { + continue + } + + val cell = context.board.cells[current.toIndex(context.board.area.width)] + + // 셀 열기 + result.add(current) + + // 인접 지뢰 수가 0이면 인접 셀 추가 탐색 + if (cell.isExpandableToAdjacent()) { + addAdjacentCandidates(context, current) + } + } + + return result.toSet() + } + + private fun shouldSkip( + board: Board, + current: Location, + ): Boolean { + val currentCell = board.cells[current.toIndex(board.area.width)] + return currentCell.isOpen() + } + + private fun addAdjacentCandidates( + context: SearchContext, + current: Location, + ) { + AdjacentDirection.allAdjacentLocations(current) + .filter { it.isValid(context.board.area) && it !in context.visited } + .forEach { adjacent -> + context.queue.add(adjacent) + context.visited.add(adjacent) + } + } + + private class SearchContext( + val board: Board, + val visited: MutableSet = mutableSetOf(), + val queue: ArrayDeque = ArrayDeque(), + ) +} diff --git a/src/main/kotlin/tdd/minesweeper/domain/strategy/ShouldOpenLocationFinder.kt b/src/main/kotlin/tdd/minesweeper/domain/strategy/ShouldOpenLocationFinder.kt new file mode 100644 index 000000000..b029ad689 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/strategy/ShouldOpenLocationFinder.kt @@ -0,0 +1,11 @@ +package tdd.minesweeper.domain.strategy + +import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.Location + +interface ShouldOpenLocationFinder { + fun findAllShouldOpen( + board: Board, + location: Location, + ): Set +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinderTest.kt b/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinderTest.kt new file mode 100644 index 000000000..0d9dcf762 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/strategy/DefaultShouldOpenLocationFinderTest.kt @@ -0,0 +1,92 @@ +package tdd.minesweeper.domain.strategy + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import tdd.minesweeper.domain.Location +import tdd.minesweeper.domain.dsl.board + +class DefaultShouldOpenLocationFinderTest : BehaviorSpec({ + given("5 x 5 크기, 지뢰 개수 5개의 닫힌 셀만 존재하는 보드에서") { + val board = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + } + val sut = DefaultShouldOpenLocationFinder() + + `when`("지뢰를 가진 닫힌 셀 위치로 열어야 할 셀 위치를 찾으면") { + val location = Location(1, 4) + val result = sut.findAllShouldOpen(board, location) + + then("해당 위치만 열 수 있다") { + val expected = setOf(location) + + result shouldContainExactly expected + } + } + + `when`("이웃한 8방향 중에 지뢰가 있는 셀 위치로 열어야 할 셀 위치를 찾으면") { + val location = Location(1, 1) + val result = sut.findAllShouldOpen(board, location) + + then("해당 위치만 열 수 있다") { + val expected = setOf(location) + + result shouldContainExactly expected + } + } + + `when`("이웃한 8방향 중에 지뢰가 없는 셀 위치로 열어야 할 셀 위치를 찾으면") { + val location = Location(5, 5) + val result = sut.findAllShouldOpen(board, location) + + then("인접 위치의 셀을 연쇄적으로 탐색하여 지뢰가 있는 셀을 만나기 전까지의 열 수 있는 모든 셀들을 열 수 있다") { + val expected = + setOf( + location, + Location(2, 4), + Location(2, 5), + Location(3, 4), + Location(3, 5), + Location(4, 4), + Location(4, 5), + Location(5, 4), + ) + + result shouldContainExactly expected + } + } + } + + given("5 x 5 크기, 지뢰 개수 5개, 열린 셀 1개의 보드에서") { + val board = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + openAt(1, 1) + } + val sut = DefaultShouldOpenLocationFinder() + + `when`("이미 열린 셀의 위치로 열어야 할 셀 위치를 찾으면") { + val location = Location(1, 1) + val result = sut.findAllShouldOpen(board, location) + + then("열 수 있는 셀이 없다") { + result.shouldBeEmpty() + } + } + } +}) From a1aed5b6dffb8add2152eacf12f740cf8ed9975e Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 01:53:13 +0900 Subject: [PATCH 39/47] =?UTF-8?q?feature(minesweeper):=20Game=EC=9D=80=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4,=20=EB=84=88=EB=B9=84,=20=EC=A7=80=EB=A2=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=88=98=EB=A5=BC=20=EB=B0=9B=EC=95=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EB=B3=B4=EB=93=9C=EB=A5=BC=20=EA=B0=80?= =?UTF-8?q?=EC=A7=80=EA=B3=A0=20=EB=A7=8C=EB=93=A4=EC=96=B4=EC=A7=84?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Game.kt | 21 ++++++++++++++++ .../kotlin/tdd/minesweeper/domain/GameTest.kt | 24 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/Game.kt create mode 100644 src/test/kotlin/tdd/minesweeper/domain/GameTest.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Game.kt b/src/main/kotlin/tdd/minesweeper/domain/Game.kt new file mode 100644 index 000000000..617f7e856 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/Game.kt @@ -0,0 +1,21 @@ +package tdd.minesweeper.domain + +import tdd.minesweeper.domain.dsl.board + +class Game(val board: Board) { + companion object { + fun from( + height: Int, + width: Int, + countOfMines: Int, + ): Game { + val board = + board { + height(height) + width(width) + countOfMines(countOfMines) + } + return Game(board) + } + } +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt b/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt new file mode 100644 index 000000000..7c6c7d287 --- /dev/null +++ b/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt @@ -0,0 +1,24 @@ +package tdd.minesweeper.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class GameTest : BehaviorSpec({ + + given("높이 5, 너비 5, 지뢰 개수 5를 받아") { + val height = 5 + val width = 5 + val countOfMines = 5 + + `when`("게임을 만들면") { + val sut = Game.from(height = height, width = width, countOfMines = countOfMines) + + then("5 x 5의 영역, 25개의 닫힌 셀과 그 중 5개가 지뢰를 갖고 있는 보드를 갖는다") { + sut.board.area shouldBe Area(height = height, width = width) + sut.board.cells.size shouldBe 25 + sut.board.cells.all { cell -> !cell.isOpen() } shouldBe true + sut.board.cells.filter { it.hasMine() }.size shouldBe 5 + } + } + } +}) From ed5a96871cf68dbe2fb70bf03e881363c05024e6 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 02:11:56 +0900 Subject: [PATCH 40/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EB=8B=AB=ED=9E=8C=20=EC=85=80=20=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/Board.kt | 2 ++ src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index 11c8ba4ca..c892fa340 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -8,6 +8,8 @@ data class Board( val cells: Cells, private val shouldOpenLocationFinder: ShouldOpenLocationFinder = DefaultShouldOpenLocationFinder(), ) { + fun countOfClosed(): Int = cells.count { !it.isOpen() } + fun open(location: Location): Board { validateLocation(location) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index 1bfb4acca..e9a69c303 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -34,6 +34,14 @@ class BoardTest : BehaviorSpec({ mineAt(5, 1) } + `when`("닫힌 셀 수를 계산하면") { + val result = sut.countOfClosed() + + then("결과는 25개다") { + result shouldBe 25 + } + } + `when`("보드 내에 없는 위치의 셀을 열면") { val location = Location(0, 0) From c7cd98ac8e7f72e8317b617f78f835d0584a489f Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 02:14:14 +0900 Subject: [PATCH 41/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EB=8B=AB=ED=9E=8C=20=EC=85=80=20=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/domain/Board.kt | 2 ++ src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Board.kt b/src/main/kotlin/tdd/minesweeper/domain/Board.kt index c892fa340..f1aa3fa5e 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Board.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Board.kt @@ -10,6 +10,8 @@ data class Board( ) { fun countOfClosed(): Int = cells.count { !it.isOpen() } + fun countOfMineOpened(): Int = cells.count { it is MineCell } + fun open(location: Location): Board { validateLocation(location) diff --git a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt index e9a69c303..da5f58afb 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/BoardTest.kt @@ -69,6 +69,10 @@ class BoardTest : BehaviorSpec({ } result.cells shouldContainExactly expected.cells } + + then("열린 지뢰 셀 수는 1개다") { + result.countOfMineOpened() shouldBe 1 + } } `when`("이웃한 8방향 중에 지뢰가 있는 셀을 열면") { From ce91b021041d7eea8e7cefdb49854190e68dd7b9 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 02:15:30 +0900 Subject: [PATCH 42/47] =?UTF-8?q?feature(minesweeper):=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EB=8B=AB=ED=9E=8C=20=EC=85=80=20=EC=88=98?= =?UTF-8?q?=EC=99=80=20=EC=B4=9D=20=EC=A7=80=EB=A2=B0=20=EC=88=98=EA=B0=80?= =?UTF-8?q?=20=EA=B0=99=EC=9C=BC=EB=A9=B4=20=EA=B2=8C=EC=9E=84=EC=9D=80=20?= =?UTF-8?q?=EC=8A=B9=EB=A6=AC=ED=95=9C=20=EA=B2=83=EC=9D=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/tdd/minesweeper/domain/Game.kt | 17 ++++- .../tdd/minesweeper/domain/GameState.kt | 7 ++ .../kotlin/tdd/minesweeper/domain/GameTest.kt | 69 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/domain/GameState.kt diff --git a/src/main/kotlin/tdd/minesweeper/domain/Game.kt b/src/main/kotlin/tdd/minesweeper/domain/Game.kt index 617f7e856..117fb2587 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Game.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Game.kt @@ -2,7 +2,20 @@ package tdd.minesweeper.domain import tdd.minesweeper.domain.dsl.board -class Game(val board: Board) { +class Game(private val countOfMines: Int, board: Board) { + var board = board + private set + + fun open(location: Location) { + board = board.open(location) + } + + fun state(): GameState { + val countOfClosed = board.countOfClosed() + + return if (countOfMines == countOfClosed) GameState.WIN else GameState.CONTINUE + } + companion object { fun from( height: Int, @@ -15,7 +28,7 @@ class Game(val board: Board) { width(width) countOfMines(countOfMines) } - return Game(board) + return Game(countOfMines, board) } } } diff --git a/src/main/kotlin/tdd/minesweeper/domain/GameState.kt b/src/main/kotlin/tdd/minesweeper/domain/GameState.kt new file mode 100644 index 000000000..ddf4d710e --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/domain/GameState.kt @@ -0,0 +1,7 @@ +package tdd.minesweeper.domain + +enum class GameState { + WIN, + LOSE, + CONTINUE, +} diff --git a/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt b/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt index 7c6c7d287..475782a4e 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt @@ -1,7 +1,9 @@ package tdd.minesweeper.domain import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe +import tdd.minesweeper.domain.dsl.board class GameTest : BehaviorSpec({ @@ -21,4 +23,71 @@ class GameTest : BehaviorSpec({ } } } + + given("5 x 5 사이즈의 지뢰를 5개 가지고 있는 보드를 가졌을 때") { + val board = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + } + + val sut = Game(countOfMines = 5, board = board) + val location = Location(5, 5) + + `when`("게임은 위치를 받아") { + sut.open(location) + + then("보드의 셀을 오픈할 수 있다") { + val expected = + board { + height(5) + width(5) + countOfMines(5) + mineAt(1, 4) + mineAt(1, 5) + mineAt(2, 1) + mineAt(4, 3) + mineAt(5, 1) + openAt(2, 4) + openAt(2, 5) + openAt(3, 4) + openAt(3, 5) + openAt(4, 4) + openAt(4, 5) + openAt(5, 4) + openAt(5, 5) + } + + sut.board.cells shouldContainExactly expected.cells + } + } + } + + given("보드의 닫힌 셀 수와 보드의 총 지뢰 수가 같으면") { + val board = + board { + height(2) + width(2) + countOfMines(3) + mineAt(1, 1) + mineAt(1, 2) + mineAt(2, 1) + openAt(2, 2) + } + val sut = Game(countOfMines = 3, board = board) + + `when`("게임은") { + val result: GameState = sut.state() + + then("승리한 것이다") { + result shouldBe GameState.WIN + } + } + } }) From 855aa14b660bbefb43c35aeda77b0c74b223a226 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 02:22:37 +0900 Subject: [PATCH 43/47] =?UTF-8?q?feature(minesweeper):=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=EC=9D=98=20=ED=8C=A8=EB=B0=B0,=20=EA=B3=84=EC=86=8D?= =?UTF-8?q?=20=EC=A1=B0=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 --- .../kotlin/tdd/minesweeper/domain/Game.kt | 11 ++--- .../kotlin/tdd/minesweeper/domain/GameTest.kt | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/tdd/minesweeper/domain/Game.kt b/src/main/kotlin/tdd/minesweeper/domain/Game.kt index 117fb2587..c6f07599d 100644 --- a/src/main/kotlin/tdd/minesweeper/domain/Game.kt +++ b/src/main/kotlin/tdd/minesweeper/domain/Game.kt @@ -10,11 +10,12 @@ class Game(private val countOfMines: Int, board: Board) { board = board.open(location) } - fun state(): GameState { - val countOfClosed = board.countOfClosed() - - return if (countOfMines == countOfClosed) GameState.WIN else GameState.CONTINUE - } + fun state(): GameState = + when { + board.countOfMineOpened() > 0 -> GameState.LOSE + countOfMines == board.countOfClosed() -> GameState.WIN + else -> GameState.CONTINUE + } companion object { fun from( diff --git a/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt b/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt index 475782a4e..3f16046e6 100644 --- a/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt +++ b/src/test/kotlin/tdd/minesweeper/domain/GameTest.kt @@ -90,4 +90,47 @@ class GameTest : BehaviorSpec({ } } } + + given("보드에 열린 지뢰 셀이 하나 이상 존재하면") { + val board = + board { + height(2) + width(2) + countOfMines(3) + mineAt(1, 1) + mineAt(1, 2) + mineAt(2, 1) + openAt(1, 1) + } + val sut = Game(countOfMines = 3, board = board) + + `when`("게임은") { + val result: GameState = sut.state() + + then("패배한 것이다") { + result shouldBe GameState.LOSE + } + } + } + + given("보드에 닫힌 셀의 수가 총 지뢰 수보다 많고 열린 지뢰 셀이 없으면") { + val board = + board { + height(2) + width(2) + countOfMines(3) + mineAt(1, 1) + mineAt(1, 2) + mineAt(2, 1) + } + val sut = Game(countOfMines = 3, board = board) + + `when`("게임은") { + val result: GameState = sut.state() + + then("계속할 수 있다") { + result shouldBe GameState.CONTINUE + } + } + } }) From 7cd9ec2ecff9d085348dc151bf91ef9462117156 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 03:23:09 +0900 Subject: [PATCH 44/47] =?UTF-8?q?feature(minesweeper):=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=ED=94=8C=EB=A0=88=EC=9D=B4=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20ui,=20=EB=A9=94=EC=9D=B8=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=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/tdd/minesweeper/Main.kt | 15 ++++++ .../tdd/minesweeper/ui/ConsoleInputView.kt | 38 +++++++++++++++ .../tdd/minesweeper/ui/ConsoleOutputView.kt | 47 +++++++++++++++++++ .../kotlin/tdd/minesweeper/ui/InputView.kt | 13 +++++ .../tdd/minesweeper/ui/MineSweeperApp.kt | 47 +++++++++++++++++++ .../kotlin/tdd/minesweeper/ui/OutputView.kt | 12 +++++ 6 files changed, 172 insertions(+) create mode 100644 src/main/kotlin/tdd/minesweeper/Main.kt create mode 100644 src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt create mode 100644 src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt create mode 100644 src/main/kotlin/tdd/minesweeper/ui/InputView.kt create mode 100644 src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt create mode 100644 src/main/kotlin/tdd/minesweeper/ui/OutputView.kt diff --git a/src/main/kotlin/tdd/minesweeper/Main.kt b/src/main/kotlin/tdd/minesweeper/Main.kt new file mode 100644 index 000000000..bbc29b808 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/Main.kt @@ -0,0 +1,15 @@ +package tdd.minesweeper + +import tdd.minesweeper.ui.ConsoleInputView +import tdd.minesweeper.ui.ConsoleOutputView +import tdd.minesweeper.ui.MineSweeperApp + +fun main() { + val app = + MineSweeperApp( + inputView = ConsoleInputView, + outputView = ConsoleOutputView, + ) + + app.play() +} diff --git a/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt b/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt new file mode 100644 index 000000000..afdfa2224 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt @@ -0,0 +1,38 @@ +package tdd.minesweeper.ui + +import tdd.minesweeper.domain.Location + +object ConsoleInputView : InputView { + override fun inputHeight(): Int { + println("높이를 입력하세요.") + val result = readln().trim().toInt() + println() + return result + } + + override fun inputWidth(): Int { + println("너비를 입력하세요.") + val result = readln().trim().toInt() + println() + return result + } + + override fun inputCountOfMines(): Int { + println("지뢰는 몇 개인가요?") + val result = readln().trim().toInt() + println() + return result + } + + override fun inputLocation(): Location { + print("open: ") + val input = readln().trim() + val split = input.split(",") + val numbers = split.map { it.trim().toInt() } + + return Location( + row = numbers[0], + col = numbers[1], + ) + } +} diff --git a/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt b/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt new file mode 100644 index 000000000..533714d1d --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt @@ -0,0 +1,47 @@ +package tdd.minesweeper.ui + +import tdd.minesweeper.domain.AdjacentMines +import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.Cell +import tdd.minesweeper.domain.GameState + +object ConsoleOutputView : OutputView { + private const val DELIMITER = " " + + override fun printGameStarted() { + println("지뢰찾기 게임 시작") + } + + override fun displayBoard(board: Board) { + board.cells + .chunked(board.area.width) + .forEach { row: List -> + println(row.joinToString(DELIMITER) { paintCell(it) }) + } + } + + private fun paintCell(cell: Cell): String { + return when { + !cell.isOpen() -> "🌫️" + cell.hasMine() -> "💥" + cell.adjacentMines == AdjacentMines(0) -> "0️⃣" + cell.adjacentMines == AdjacentMines(1) -> "1️⃣" + cell.adjacentMines == AdjacentMines(2) -> "2️⃣" + cell.adjacentMines == AdjacentMines(3) -> "3️⃣" + cell.adjacentMines == AdjacentMines(4) -> "4️⃣" + cell.adjacentMines == AdjacentMines(5) -> "5️⃣" + cell.adjacentMines == AdjacentMines(6) -> "6️⃣" + cell.adjacentMines == AdjacentMines(7) -> "7️⃣" + cell.adjacentMines == AdjacentMines(8) -> "8️⃣" + else -> throw IllegalStateException("정상적인 셀 상태가 아닙니다: cell=$cell") + } + } + + override fun printGameEnded(state: GameState) { + return when (state) { + GameState.WIN -> println("Win Game 🎉🎉🎉") + GameState.LOSE -> println("Lose Game 🤯") + else -> {} + } + } +} diff --git a/src/main/kotlin/tdd/minesweeper/ui/InputView.kt b/src/main/kotlin/tdd/minesweeper/ui/InputView.kt new file mode 100644 index 000000000..1fea69e49 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/ui/InputView.kt @@ -0,0 +1,13 @@ +package tdd.minesweeper.ui + +import tdd.minesweeper.domain.Location + +interface InputView { + fun inputHeight(): Int + + fun inputWidth(): Int + + fun inputCountOfMines(): Int + + fun inputLocation(): Location +} diff --git a/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt b/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt new file mode 100644 index 000000000..53c9aead9 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt @@ -0,0 +1,47 @@ +package tdd.minesweeper.ui + +import tdd.minesweeper.domain.Game +import tdd.minesweeper.domain.GameState + +class MineSweeperApp( + private val inputView: InputView, + private val outputView: OutputView, +) { + fun play() { + val game: Game = initializeGame() + + playContinue(game) + + handleGameEnd(game) + } + + private fun initializeGame(): Game { + val height = inputView.inputHeight() + + val width = inputView.inputWidth() + + val countOfMines = inputView.inputCountOfMines() + + outputView.printGameStarted() + + return Game.from( + height = height, + width = width, + countOfMines = countOfMines, + ) + } + + private fun playContinue(game: Game) { + while (game.state() == GameState.CONTINUE) { + outputView.displayBoard(game.board) + + val location = inputView.inputLocation() + + game.open(location) + } + } + + private fun handleGameEnd(game: Game) { + outputView.printGameEnded(game.state()) + } +} diff --git a/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt b/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt new file mode 100644 index 000000000..b79e109ec --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt @@ -0,0 +1,12 @@ +package tdd.minesweeper.ui + +import tdd.minesweeper.domain.Board +import tdd.minesweeper.domain.GameState + +interface OutputView { + fun printGameStarted() + + fun displayBoard(board: Board) + + fun printGameEnded(state: GameState) +} From 22e236f19f05ba2362b5a62074921f1218b1a290 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 04:04:09 +0900 Subject: [PATCH 45/47] =?UTF-8?q?refactor(minesweeper):=20ConsoleInputView?= =?UTF-8?q?=20=EC=97=90=20=EC=9C=84=EC=B9=98=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=9B=84=20=ED=95=9C=20=EC=B9=B8=20=EB=9D=84=EC=9A=B0=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20println()=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/tdd/minesweeper/ui/ConsoleInputView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt b/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt index afdfa2224..e4d57d635 100644 --- a/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt +++ b/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt @@ -29,6 +29,7 @@ object ConsoleInputView : InputView { val input = readln().trim() val split = input.split(",") val numbers = split.map { it.trim().toInt() } + println() return Location( row = numbers[0], From 905cf24b7f06cedd3fce4d2fc426b7241da73a34 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 04:05:00 +0900 Subject: [PATCH 46/47] =?UTF-8?q?feature(minesweeper):=20OutputView=20?= =?UTF-8?q?=EC=97=90=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=EC=9A=A9=20printError=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/tdd/minesweeper/ui/ConsoleOutputView.kt | 5 +++++ src/main/kotlin/tdd/minesweeper/ui/OutputView.kt | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt b/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt index 533714d1d..ec35f2066 100644 --- a/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt +++ b/src/main/kotlin/tdd/minesweeper/ui/ConsoleOutputView.kt @@ -44,4 +44,9 @@ object ConsoleOutputView : OutputView { else -> {} } } + + override fun printError(errorMessage: String) { + println(errorMessage) + println() + } } diff --git a/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt b/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt index b79e109ec..86f450ef6 100644 --- a/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt +++ b/src/main/kotlin/tdd/minesweeper/ui/OutputView.kt @@ -9,4 +9,6 @@ interface OutputView { fun displayBoard(board: Board) fun printGameEnded(state: GameState) + + fun printError(errorMessage: String) } From 13b4815c16815d10e45492025937c7ca6ddc2da8 Mon Sep 17 00:00:00 2001 From: y2gcoder Date: Sun, 29 Dec 2024 04:51:26 +0900 Subject: [PATCH 47/47] =?UTF-8?q?feature(minesweeper):=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/tdd/minesweeper/Main.kt | 3 +- .../tdd/minesweeper/ui/ConsoleInputView.kt | 38 ++++++++++++------- .../tdd/minesweeper/ui/MineSweeperApp.kt | 37 ++++++++++++------ .../tdd/minesweeper/ui/RetryingInputView.kt | 25 ++++++++++++ 4 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/tdd/minesweeper/ui/RetryingInputView.kt diff --git a/src/main/kotlin/tdd/minesweeper/Main.kt b/src/main/kotlin/tdd/minesweeper/Main.kt index bbc29b808..79a2938cc 100644 --- a/src/main/kotlin/tdd/minesweeper/Main.kt +++ b/src/main/kotlin/tdd/minesweeper/Main.kt @@ -3,11 +3,12 @@ package tdd.minesweeper import tdd.minesweeper.ui.ConsoleInputView import tdd.minesweeper.ui.ConsoleOutputView import tdd.minesweeper.ui.MineSweeperApp +import tdd.minesweeper.ui.RetryingInputView fun main() { val app = MineSweeperApp( - inputView = ConsoleInputView, + inputView = RetryingInputView(ConsoleInputView), outputView = ConsoleOutputView, ) diff --git a/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt b/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt index e4d57d635..ae5cc9440 100644 --- a/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt +++ b/src/main/kotlin/tdd/minesweeper/ui/ConsoleInputView.kt @@ -5,35 +5,47 @@ import tdd.minesweeper.domain.Location object ConsoleInputView : InputView { override fun inputHeight(): Int { println("높이를 입력하세요.") - val result = readln().trim().toInt() - println() - return result + return getPositiveInt(readln().trim().toIntOrNull()) } override fun inputWidth(): Int { println("너비를 입력하세요.") - val result = readln().trim().toInt() - println() - return result + return getPositiveInt(readln().trim().toIntOrNull()) } override fun inputCountOfMines(): Int { println("지뢰는 몇 개인가요?") - val result = readln().trim().toInt() - println() - return result + return getPositiveInt(readln().trim().toIntOrNull()) } override fun inputLocation(): Location { print("open: ") val input = readln().trim() val split = input.split(",") - val numbers = split.map { it.trim().toInt() } - println() + require(split.size == 2) { + "위치는 쉼표로 구분된 두 개의 숫자를 입력해야 합니다: input=$input" + } + val row = split[0].trim().toIntOrNull() + val col = split[1].trim().toIntOrNull() + require(row != null && col != null) { + "각 위치 좌표는 숫자를 입력해주세요: row=${split[0].trim()}; col=${split[1].trim()}" + } + + require(row > 0 && col > 0) { + "각 위치 좌표는 양수를 입력해주세요: row=$row; col=$col" + } return Location( - row = numbers[0], - col = numbers[1], + row = row, + col = col, ) } + + private fun getPositiveInt(input: Int?): Int { + require(input != null && input > 0) { + "양수를 입력해주세요: input=$input" + } + println() + return input + } } diff --git a/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt b/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt index 53c9aead9..9ae30ac44 100644 --- a/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt +++ b/src/main/kotlin/tdd/minesweeper/ui/MineSweeperApp.kt @@ -8,27 +8,36 @@ class MineSweeperApp( private val outputView: OutputView, ) { fun play() { - val game: Game = initializeGame() + val game = initializeGame() + + // 정상적으로 게임 초기화 후 시작 메시지 출력 + outputView.printGameStarted() playContinue(game) handleGameEnd(game) } - private fun initializeGame(): Game { + private tailrec fun initializeGame(): Game { val height = inputView.inputHeight() - val width = inputView.inputWidth() - val countOfMines = inputView.inputCountOfMines() - outputView.printGameStarted() - - return Game.from( - height = height, - width = width, - countOfMines = countOfMines, - ) + val result = + runCatching { + Game.from( + height = height, + width = width, + countOfMines = countOfMines, + ) + } + + return if (result.isSuccess) { + result.getOrThrow() + } else { + outputView.printError(result.exceptionOrNull()?.message ?: "보드 생성 중 알 수 없는 에러가 발생했습니다") + initializeGame() // 재귀 호출 + } } private fun playContinue(game: Game) { @@ -37,7 +46,11 @@ class MineSweeperApp( val location = inputView.inputLocation() - game.open(location) + runCatching { + game.open(location) + }.getOrElse { error -> + outputView.printError(error.message ?: "셀 오픈 중 알 수 없는 에러가 발생했습니다") + } } } diff --git a/src/main/kotlin/tdd/minesweeper/ui/RetryingInputView.kt b/src/main/kotlin/tdd/minesweeper/ui/RetryingInputView.kt new file mode 100644 index 000000000..b0e980787 --- /dev/null +++ b/src/main/kotlin/tdd/minesweeper/ui/RetryingInputView.kt @@ -0,0 +1,25 @@ +package tdd.minesweeper.ui + +import tdd.minesweeper.domain.Location + +class RetryingInputView(private val delegate: InputView) : InputView { + override fun inputHeight(): Int = retry { delegate.inputHeight() } + + override fun inputWidth(): Int = retry { delegate.inputWidth() } + + override fun inputCountOfMines(): Int = retry { delegate.inputCountOfMines() } + + override fun inputLocation(): Location = retry { delegate.inputLocation() } + + private fun retry(input: () -> T): T { + var result: Result + do { + result = runCatching { input() } + result.exceptionOrNull()?.let { + println(it.message) + println() + } + } while (result.isFailure) + return result.getOrThrow() + } +}