From 48387344cdab084514b3cba07593b3af45ea1860 Mon Sep 17 00:00:00 2001 From: jaylene shin Date: Tue, 28 Nov 2023 14:51:43 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=20=20-=20[x]=20=EC=A7=80=EB=A2=B0=ED=8C=90?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=80=EC=A7=84=20DTO?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.=20=20=20-=20[?= =?UTF-8?q?x]=20=EC=A7=80=EB=A2=B0=EC=99=80=20=EC=A7=80=EB=A2=B0=EA=B0=80?= =?UTF-8?q?=20=EC=95=84=EB=8B=8C=20=EC=B9=B8=EC=9D=84=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EB=B0=8F=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/domain/Cell.kt | 10 +++++ .../kotlin/minesweeper/domain/CellState.kt | 6 +++ .../kotlin/minesweeper/domain/Position.kt | 12 ++++++ .../minesweeper/domain/PositionGenerator.kt | 30 ++++++++++++++ .../minesweeper/domain/PositionSelector.kt | 5 +++ .../domain/RandomPositionSelector.kt | 13 +++++++ .../domain/PositionGeneratorTest.kt | 39 +++++++++++++++++++ .../kotlin/minesweeper/domain/PositionTest.kt | 23 +++++++++++ 8 files changed, 138 insertions(+) create mode 100644 src/main/kotlin/minesweeper/domain/Cell.kt create mode 100644 src/main/kotlin/minesweeper/domain/CellState.kt create mode 100644 src/main/kotlin/minesweeper/domain/Position.kt create mode 100644 src/main/kotlin/minesweeper/domain/PositionGenerator.kt create mode 100644 src/main/kotlin/minesweeper/domain/PositionSelector.kt create mode 100644 src/main/kotlin/minesweeper/domain/RandomPositionSelector.kt create mode 100644 src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt create mode 100644 src/test/kotlin/minesweeper/domain/PositionTest.kt diff --git a/src/main/kotlin/minesweeper/domain/Cell.kt b/src/main/kotlin/minesweeper/domain/Cell.kt new file mode 100644 index 000000000..353030c34 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Cell.kt @@ -0,0 +1,10 @@ +package minesweeper.domain + +data class Cell( + val position: Position, + val state: CellState +) + +fun Cell.getStateSymbol(): String { + return this.state.symbol +} diff --git a/src/main/kotlin/minesweeper/domain/CellState.kt b/src/main/kotlin/minesweeper/domain/CellState.kt new file mode 100644 index 000000000..3757adc7e --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/CellState.kt @@ -0,0 +1,6 @@ +package minesweeper.domain + +enum class CellState(val symbol: String = "") { + MINE("*"), + EMPTY("C"); +} diff --git a/src/main/kotlin/minesweeper/domain/Position.kt b/src/main/kotlin/minesweeper/domain/Position.kt new file mode 100644 index 000000000..09e8c01c3 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Position.kt @@ -0,0 +1,12 @@ +package minesweeper.domain + +// TODO data class 유효성 검증 +data class Position( + val y: Int, + val x: Int +) { + init { + require(y > 0) { "y는 0이거나 음수일 수 없습니다" } + require(x > 0) { "x는 0이거나 음수일 수 없습니다" } + } +} diff --git a/src/main/kotlin/minesweeper/domain/PositionGenerator.kt b/src/main/kotlin/minesweeper/domain/PositionGenerator.kt new file mode 100644 index 000000000..98540e3bd --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/PositionGenerator.kt @@ -0,0 +1,30 @@ +package minesweeper.domain + +class PositionGenerator( + private val mineMapMeta: MineMapMeta, + private val positionSelector: PositionSelector = RandomPositionSelector +) { + tailrec fun generateMinePositions( + minePositions: Set = emptySet() + ): Set { + // Position의 range는 항상 보장되어야 한다. 일급 객체 생성해서 받으면 될 것 같다. + if (minePositions.size == mineMapMeta.mineCount) { return minePositions } + val position = positionSelector.select(mineMapMeta) + return if (position in minePositions) { + generateMinePositions(minePositions) + } else { + generateMinePositions(minePositions + position) + } + } + + fun generateEmptyPositions( + minePositions: Set + ): Set { + val allPositions = (1..mineMapMeta.height) + .flatMap { y -> (1..mineMapMeta.width).map { x -> Position(y, x) } } + .toSet() + require(allPositions.size == mineMapMeta.getCellCount()) { "모든 위치를 생성하지 못했습니다" } + require(allPositions.size > minePositions.size) { "지뢰 개수가 모든 위치 개수보다 많습니다" } + return allPositions - minePositions + } +} diff --git a/src/main/kotlin/minesweeper/domain/PositionSelector.kt b/src/main/kotlin/minesweeper/domain/PositionSelector.kt new file mode 100644 index 000000000..b180ddbf1 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/PositionSelector.kt @@ -0,0 +1,5 @@ +package minesweeper.domain + +interface PositionSelector { + fun select(mineMapMeta: MineMapMeta): Position +} diff --git a/src/main/kotlin/minesweeper/domain/RandomPositionSelector.kt b/src/main/kotlin/minesweeper/domain/RandomPositionSelector.kt new file mode 100644 index 000000000..5350829cb --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/RandomPositionSelector.kt @@ -0,0 +1,13 @@ +package minesweeper.domain + +import kotlin.random.Random + +object RandomPositionSelector : PositionSelector { + override fun select( + mineMapMeta: MineMapMeta + ): Position { + val x = Random.nextInt(1, mineMapMeta.width + 1) + val y = Random.nextInt(1, mineMapMeta.height + 1) + return Position(y, x) + } +} diff --git a/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt b/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt new file mode 100644 index 000000000..6775a3804 --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt @@ -0,0 +1,39 @@ +package minesweeper.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PositionGeneratorTest { + @Test + fun `지뢰 메타 정보(지뢰 수, 지뢰맵 크기)가 주어질 때 mineCount 만큼의 지뢰 위치 정보 Set을 생성하고 반환한다`() { + // given + val mineMapMeta = MineMapMeta(10, 10, 10) + val positionGenerator = PositionGenerator(mineMapMeta) + + // when + val minePositions = positionGenerator.generateMinePositions() + + // then + assertEquals(mineMapMeta.mineCount, minePositions.size) + assertEquals(true, minePositions.all { it.x in 1..10 && it.y in 1..10 }) + } + + @Test + fun `지뢰 메타 정보와 지뢰 위치 Set이 주어질 때 빈칸의 위치 정보 Set을 반환한다`() { + // given + val mineMapMeta = MineMapMeta(10, 10, 10) + val minePositions = setOf( + Position(1, 1), + Position(5, 5), + Position(10, 10) + ) + val positionGenerator = PositionGenerator(mineMapMeta) + + // when + val emptyPositions = positionGenerator.generateEmptyPositions(minePositions) + + // then + assertEquals(mineMapMeta.getCellCount() - minePositions.size, emptyPositions.size) + assertEquals(true, emptyPositions.all { it.x in 1..10 && it.y in 1..10 }) + } +} diff --git a/src/test/kotlin/minesweeper/domain/PositionTest.kt b/src/test/kotlin/minesweeper/domain/PositionTest.kt new file mode 100644 index 000000000..a43819cc3 --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/PositionTest.kt @@ -0,0 +1,23 @@ +package minesweeper.domain + +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class PositionTest { + @ParameterizedTest + @CsvSource( + "0 0", + "1 0", + "0 1", + "-1 -1" + ) + fun `Position 프로퍼티가 0이거나 음수라면 IllegalArgumentException이 발생한다`(input: String) { + // given + val properties = input.split(" ").map { it.toInt() } + + assertThrows { // then + Position(properties[0], properties[1]) // when + } + } +} From 99864921c4176e3c8b930e1c6c4feebc9ff8063d Mon Sep 17 00:00:00 2001 From: jaylene shin Date: Tue, 28 Nov 2023 15:07:11 +0900 Subject: [PATCH 2/6] =?UTF-8?q?InputView=EB=A1=9C=20=EB=B0=9B=EC=9D=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=A7=B5=ED=95=91=ED=95=A0=20DTO=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/minesweeper/domain/MineMapMeta.kt | 18 +++++++++++ .../domain/MineMapGeneratorTest.kt | 32 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/main/kotlin/minesweeper/domain/MineMapMeta.kt create mode 100644 src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt diff --git a/src/main/kotlin/minesweeper/domain/MineMapMeta.kt b/src/main/kotlin/minesweeper/domain/MineMapMeta.kt new file mode 100644 index 000000000..ff159a25f --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineMapMeta.kt @@ -0,0 +1,18 @@ +package minesweeper.domain + +data class MineMapMeta( + val height: Int, + val width: Int, + val mineCount: Int +) { + init { + require(height > 0) { "높이는 0 이거나 음수일 수 없습니다" } + require(width > 0) { "너비는 0 이거나 음수일 수 없습니다" } + require(mineCount > 0) { "지뢰 개수는 0 이거나 음수일 수 없습니다" } + require(height * width >= mineCount) { "지뢰 개수는 (높이 x 너비) 개수를 초과할 수 없습니다" } + } + + fun getCellCount(): Int { + return height * width + } +} diff --git a/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt b/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt new file mode 100644 index 000000000..aef37ec44 --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt @@ -0,0 +1,32 @@ +package minesweeper.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class MineMapGeneratorTest { + @Test + fun `지뢰 맵을 구성할 위치 정보가 주어졌을 때 MineMap을 생성할 수 있다`() { + // given + val mineMapMeta = MineMapMeta(3, 3, 3) + val minePositions = setOf( + Position(1, 1), + Position(2, 2), + Position(3, 3) + ) + val emptyPositions = setOf( + Position(1, 2), + Position(1, 3), + Position(2, 1), + Position(2, 3), + Position(3, 1), + Position(3, 2) + ) + + // when + val mineMapGenerator = MineMapGenerator(mineMapMeta) + val mineMap = mineMapGenerator.generate(minePositions, emptyPositions) + + // then + assertEquals(mineMapMeta.getCellCount(), mineMap.values.size) + } +} From 649bff40ca3cec7935b621f3d3defdab996deec9 Mon Sep 17 00:00:00 2001 From: jaylene shin Date: Tue, 28 Nov 2023 15:09:05 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=EC=A7=80=EB=A2=B0=ED=8C=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.=20(InputView?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EC=9D=80=20DTO=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/domain/MineMap.kt | 18 +++++ .../minesweeper/domain/MineMapGenerator.kt | 15 ++++ .../minesweeper/domain/MineMapMetaTest.kt | 69 +++++++++++++++++++ .../kotlin/minesweeper/domain/MineMapTest.kt | 59 ++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 src/main/kotlin/minesweeper/domain/MineMap.kt create mode 100644 src/main/kotlin/minesweeper/domain/MineMapGenerator.kt create mode 100644 src/test/kotlin/minesweeper/domain/MineMapMetaTest.kt create mode 100644 src/test/kotlin/minesweeper/domain/MineMapTest.kt diff --git a/src/main/kotlin/minesweeper/domain/MineMap.kt b/src/main/kotlin/minesweeper/domain/MineMap.kt new file mode 100644 index 000000000..eb5839edd --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineMap.kt @@ -0,0 +1,18 @@ +package minesweeper.domain + +class MineMap( // TODO 불변 객체로 변경해보자 + private val _values: MutableMap = mutableMapOf() +) { + val values: Map + get() = _values + val size: Int + get() = _values.keys.size + + fun plantCell(position: Position, cellState: CellState) { + _values[position] = Cell(position, cellState) + } + + fun getCell(position: Position): Cell { + return _values[position] ?: throw IllegalArgumentException("해당 위치에 셀이 없습니다") + } +} diff --git a/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt b/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt new file mode 100644 index 000000000..e3315fb42 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt @@ -0,0 +1,15 @@ +package minesweeper.domain + +class MineMapGenerator( + private val mineMapMeta: MineMapMeta +) { + fun generate(minePositions: Set, emptyPositions: Set): MineMap { + require(minePositions.intersect(emptyPositions).isEmpty()) { "지뢰와 빈 공간은 겹칠 수 없습니다." } + require(minePositions.size + emptyPositions.size == mineMapMeta.getCellCount()) { "지뢰와 빈 공간은 주어진 지뢰맵 크기와 같아야 합니다" } + + val mineMap = MineMap() + minePositions.forEach { mineMap.plantCell(it, CellState.MINE) } + emptyPositions.forEach { mineMap.plantCell(it, CellState.EMPTY) } + return mineMap + } +} diff --git a/src/test/kotlin/minesweeper/domain/MineMapMetaTest.kt b/src/test/kotlin/minesweeper/domain/MineMapMetaTest.kt new file mode 100644 index 000000000..1661e2e3b --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/MineMapMetaTest.kt @@ -0,0 +1,69 @@ +package minesweeper.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class MineMapMetaTest { + @ParameterizedTest + @CsvSource( + "0 0 0", + "1 0 0", + "1 1 -1", + "0 -1 -1" + ) + fun `MineMapMeta 프로퍼티는 0이거나 음수일 수 없다`(input: String) { + // given + val properties = input.split(" ").map { it.toInt() } + + assertThrows { // then + MineMapMeta( // when + height = properties[0], + width = properties[1], + mineCount = properties[2] + ) + } + } + + @ParameterizedTest + @CsvSource( + "1 1 2", + "10 1 11", + "10 10 101" + ) + fun `지뢰 개수는 MineMap을 구성하는 셀 크기(height*width)를 초과할 수 없다`(input: String) { + // given + val properties = input.split(" ").map { it.toInt() } + + assertThrows { // then + MineMapMeta( // when + height = properties[0], + width = properties[1], + mineCount = properties[2] + ) + } + } + + @ParameterizedTest + @CsvSource( + "1 1 1, 1", + "1 10 5, 10", + "10 30 50, 300" + ) + fun `MineMap을 구성하는 Cell 개수를 반환한다`(input: String, expected: Int) { + // given + val properties = input.split(" ").map { it.toInt() } + val mineMapMeta = MineMapMeta( + height = properties[0], + width = properties[1], + mineCount = properties[2] + ) + + // when + val cellCount = mineMapMeta.getCellCount() + + // then + assertEquals(expected, cellCount) + } +} diff --git a/src/test/kotlin/minesweeper/domain/MineMapTest.kt b/src/test/kotlin/minesweeper/domain/MineMapTest.kt new file mode 100644 index 000000000..11ca06677 --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/MineMapTest.kt @@ -0,0 +1,59 @@ +package minesweeper.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class MineMapTest { + @Test + fun `위치 정보를 전달해 MineMap에 지뢰를 심는다`() { + // given + val mineMap = MineMap() + + // when + mineMap.plantCell(Position(1, 1), CellState.MINE) + + // then + assertEquals(1, mineMap.size) + assertEquals(CellState.MINE, mineMap.values[Position(1, 1)]?.state) + } + + @Test + fun `위치 정보를 전달해 MineMap에 빈 상태를 심는다`() { + // given + val mineMap = MineMap() + + // when + mineMap.plantCell(Position(1, 1), CellState.EMPTY) + mineMap.plantCell(Position(2, 2), CellState.EMPTY) + + // then + assertEquals(2, mineMap.size) + assertEquals(CellState.EMPTY, mineMap.values[Position(1, 1)]?.state) + assertEquals(CellState.EMPTY, mineMap.values[Position(2, 2)]?.state) + } + + @Test + fun `주어진 위치 정보에 놓인 Cell 객체를 가져온다`() { + // given + val mineMap = MineMap() + val position = Position(1, 1) + mineMap.plantCell(position, CellState.EMPTY) + + // when + val cell = mineMap.getCell(position) + + // then + assertEquals(CellState.EMPTY, cell.state) + } + + @Test + fun `MineMap에 없는 위치 정보를 통해 Cell 객체를 가져온다면 IllegalArgumentException이 발생한다`() { + // given + val mineMap = MineMap() + + assertThrows { // then + mineMap.getCell(Position(1, 1)) // when + } + } +} From 6a5cb809c9ce751eaffc033590d86528f0ef8cc4 Mon Sep 17 00:00:00 2001 From: jaylene shin Date: Tue, 28 Nov 2023 15:09:19 +0900 Subject: [PATCH 4/6] =?UTF-8?q?-=20[x]=20`InputView`=20=EC=83=9D=EC=84=B1:?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=A1=9C=EB=B6=80=ED=84=B0=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=EC=99=80=20=EB=84=88=EB=B9=84,=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EA=B0=9C=EC=88=98=EB=A5=BC=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EB=B0=9B=EA=B3=A0=20DTO=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=ED=95=9C=EB=8B=A4.=20-=20[x]=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=EB=8A=94=20=EC=A3=BC=EC=96=B4=EC=A7=84=20=EC=A7=80?= =?UTF-8?q?=EB=A2=B0=20=EA=B0=9C=EC=88=98=EB=A7=8C=ED=81=BC=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=EC=9C=BC=EB=A1=9C=20=EB=B0=B0=EC=B9=98=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20-=20[x]=20`OutputView`=20=EC=83=9D=EC=84=B1:=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EB=90=9C=20=EC=A7=80=EB=A2=B0=ED=8C=90?= =?UTF-8?q?=EC=9D=84=20=EC=B6=9C=EB=A0=A5=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/MineSweeper.kt | 29 +++++++++++++++++++ src/main/kotlin/minesweeper/README.md | 10 +++++++ src/main/kotlin/minesweeper/view/InputView.kt | 21 ++++++++++++++ .../kotlin/minesweeper/view/OutputView.kt | 25 ++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 src/main/kotlin/minesweeper/MineSweeper.kt create mode 100644 src/main/kotlin/minesweeper/README.md create mode 100644 src/main/kotlin/minesweeper/view/InputView.kt create mode 100644 src/main/kotlin/minesweeper/view/OutputView.kt diff --git a/src/main/kotlin/minesweeper/MineSweeper.kt b/src/main/kotlin/minesweeper/MineSweeper.kt new file mode 100644 index 000000000..840eb53ba --- /dev/null +++ b/src/main/kotlin/minesweeper/MineSweeper.kt @@ -0,0 +1,29 @@ +package minesweeper + +import minesweeper.domain.MineMapGenerator +import minesweeper.domain.MineMapMeta +import minesweeper.domain.PositionGenerator +import minesweeper.view.InputView +import minesweeper.view.OutputView + +object MineSweeper { + fun drawMap() { + val mineMapMeta = MineMapMeta( + height = InputView.getHeight(), + width = InputView.getWidth(), + mineCount = InputView.getMineCount() + ) + // TODO positionGenerator를 MineMapGenerator로 주입하면 코드 압축이 가능하지만, 테스트하기 불리해진다. 어떻게 해결할까? + val positionGenerator = PositionGenerator(mineMapMeta) + val minePositions = positionGenerator.generateMinePositions() + val emptyPositions = positionGenerator.generateEmptyPositions(minePositions) + val mineMap = MineMapGenerator(mineMapMeta).generate(minePositions, emptyPositions) + + OutputView.printGameStartMsg() + OutputView.printMineMap(mineMapMeta, mineMap) + } +} + +fun main() { + MineSweeper.drawMap() +} diff --git a/src/main/kotlin/minesweeper/README.md b/src/main/kotlin/minesweeper/README.md new file mode 100644 index 000000000..c30d1c079 --- /dev/null +++ b/src/main/kotlin/minesweeper/README.md @@ -0,0 +1,10 @@ +# README + +## Step1 구현 리스트 +- [x] `InputView` 생성: 사용자로부터 높이와 너비, 지뢰 개수를 입력받고 DTO 객체로 변환한다. +- [x] 지뢰판 생성을 위한 객체들을 구상하고, 관련해 테스트를 구현한다. + - [x] 지뢰판 생성을 위한 객체를 생성한다. (InputView로 받은 DTO 객체 활용) + - [x] 지뢰판 위치 정보를 프로퍼티로 가진 DTO를 생성한다. + - [x] 지뢰와 지뢰가 아닌 칸을 구분하기 위한 문자 및 클래스를 생성한다. + - [x] 지뢰는 주어진 지뢰 개수만큼 랜덤으로 배치한다. +- [x] `OutputView` 생성: 생성된 지뢰판을 출력한다. diff --git a/src/main/kotlin/minesweeper/view/InputView.kt b/src/main/kotlin/minesweeper/view/InputView.kt new file mode 100644 index 000000000..0cd0f9262 --- /dev/null +++ b/src/main/kotlin/minesweeper/view/InputView.kt @@ -0,0 +1,21 @@ +package minesweeper.view + +object InputView { + fun getHeight(): Int { + return getNumber("높이를 입력하세요.") + } + + fun getWidth(): Int { + return getNumber("너비를 입력하세요.") + } + + fun getMineCount(): Int { + return getNumber("지뢰는 몇 개인가요?") + } + + private fun getNumber(consoleMsg: String): Int { + println(consoleMsg) + val input = readlnOrNull() ?: throw IllegalArgumentException("입력값은 공백이거나 빈 문자열일 수 없습니다.") + return input.toIntOrNull() ?: throw IllegalArgumentException("숫자가 아닌 입력값은 들어올 수 없습니다.") + } +} diff --git a/src/main/kotlin/minesweeper/view/OutputView.kt b/src/main/kotlin/minesweeper/view/OutputView.kt new file mode 100644 index 000000000..8585b5724 --- /dev/null +++ b/src/main/kotlin/minesweeper/view/OutputView.kt @@ -0,0 +1,25 @@ +package minesweeper.view + +import minesweeper.domain.MineMap +import minesweeper.domain.MineMapMeta +import minesweeper.domain.Position +import minesweeper.domain.getStateSymbol + +object OutputView { + fun printGameStartMsg() { + println("\n지뢰 찾기 게임 시작") + } + + fun printMineMap(mineMapMeta: MineMapMeta, mineMap: MineMap) { + for (row in 1 until mineMapMeta.height + 1) { + printRowCells(mineMapMeta, mineMap, row) + } + } + + private fun printRowCells(mineMapMeta: MineMapMeta, mineMap: MineMap, row: Int) { + for (col in 1 until mineMapMeta.width + 1) { + print(mineMap.getCell(Position(row, col)).getStateSymbol() + " ") + } + println() + } +} From e69f6e7f69efb3c4474d903b5fc29f4e9ff38f6f Mon Sep 17 00:00:00 2001 From: jaylene shin Date: Tue, 28 Nov 2023 17:59:09 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Cell=EC=97=90=EC=84=9C=20position=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/domain/Cell.kt | 1 - src/main/kotlin/minesweeper/domain/MineMap.kt | 4 ++-- src/main/kotlin/minesweeper/domain/MineMapGenerator.kt | 4 ++-- src/test/kotlin/minesweeper/domain/MineMapTest.kt | 8 ++++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/minesweeper/domain/Cell.kt b/src/main/kotlin/minesweeper/domain/Cell.kt index 353030c34..9644b2437 100644 --- a/src/main/kotlin/minesweeper/domain/Cell.kt +++ b/src/main/kotlin/minesweeper/domain/Cell.kt @@ -1,7 +1,6 @@ package minesweeper.domain data class Cell( - val position: Position, val state: CellState ) diff --git a/src/main/kotlin/minesweeper/domain/MineMap.kt b/src/main/kotlin/minesweeper/domain/MineMap.kt index eb5839edd..ad0556355 100644 --- a/src/main/kotlin/minesweeper/domain/MineMap.kt +++ b/src/main/kotlin/minesweeper/domain/MineMap.kt @@ -8,8 +8,8 @@ class MineMap( // TODO 불변 객체로 변경해보자 val size: Int get() = _values.keys.size - fun plantCell(position: Position, cellState: CellState) { - _values[position] = Cell(position, cellState) + fun plantCell(position: Position, cell: Cell) { + _values[position] = cell } fun getCell(position: Position): Cell { diff --git a/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt b/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt index e3315fb42..1ba91b552 100644 --- a/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt +++ b/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt @@ -8,8 +8,8 @@ class MineMapGenerator( require(minePositions.size + emptyPositions.size == mineMapMeta.getCellCount()) { "지뢰와 빈 공간은 주어진 지뢰맵 크기와 같아야 합니다" } val mineMap = MineMap() - minePositions.forEach { mineMap.plantCell(it, CellState.MINE) } - emptyPositions.forEach { mineMap.plantCell(it, CellState.EMPTY) } + minePositions.forEach { mineMap.plantCell(it, Cell(CellState.MINE)) } + emptyPositions.forEach { mineMap.plantCell(it, Cell(CellState.EMPTY)) } return mineMap } } diff --git a/src/test/kotlin/minesweeper/domain/MineMapTest.kt b/src/test/kotlin/minesweeper/domain/MineMapTest.kt index 11ca06677..762a11aba 100644 --- a/src/test/kotlin/minesweeper/domain/MineMapTest.kt +++ b/src/test/kotlin/minesweeper/domain/MineMapTest.kt @@ -11,7 +11,7 @@ class MineMapTest { val mineMap = MineMap() // when - mineMap.plantCell(Position(1, 1), CellState.MINE) + mineMap.plantCell(Position(1, 1), Cell(CellState.MINE)) // then assertEquals(1, mineMap.size) @@ -24,8 +24,8 @@ class MineMapTest { val mineMap = MineMap() // when - mineMap.plantCell(Position(1, 1), CellState.EMPTY) - mineMap.plantCell(Position(2, 2), CellState.EMPTY) + mineMap.plantCell(Position(1, 1), Cell(CellState.EMPTY)) + mineMap.plantCell(Position(2, 2), Cell(CellState.EMPTY)) // then assertEquals(2, mineMap.size) @@ -38,7 +38,7 @@ class MineMapTest { // given val mineMap = MineMap() val position = Position(1, 1) - mineMap.plantCell(position, CellState.EMPTY) + mineMap.plantCell(position, Cell(CellState.EMPTY)) // when val cell = mineMap.getCell(position) From 1b56c4f91e5a993d8a07e83364f99240b9937ba8 Mon Sep 17 00:00:00 2001 From: jaylene shin Date: Wed, 29 Nov 2023 06:54:22 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=EC=9D=BC=EA=B8=89=20=EC=BB=AC=EB=A0=89?= =?UTF-8?q?=EC=85=98=20Positions=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/minesweeper/MineSweeper.kt | 3 +- src/main/kotlin/minesweeper/domain/MineMap.kt | 2 +- .../minesweeper/domain/MineMapGenerator.kt | 9 +- .../kotlin/minesweeper/domain/Position.kt | 1 - .../minesweeper/domain/PositionGenerator.kt | 29 ++-- .../kotlin/minesweeper/domain/Positions.kt | 14 ++ .../domain/MineMapGeneratorTest.kt | 7 +- .../domain/PositionGeneratorTest.kt | 2 +- .../minesweeper/domain/PositionsTest.kt | 146 ++++++++++++++++++ 9 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 src/main/kotlin/minesweeper/domain/Positions.kt create mode 100644 src/test/kotlin/minesweeper/domain/PositionsTest.kt diff --git a/src/main/kotlin/minesweeper/MineSweeper.kt b/src/main/kotlin/minesweeper/MineSweeper.kt index 840eb53ba..59444691b 100644 --- a/src/main/kotlin/minesweeper/MineSweeper.kt +++ b/src/main/kotlin/minesweeper/MineSweeper.kt @@ -13,11 +13,10 @@ object MineSweeper { width = InputView.getWidth(), mineCount = InputView.getMineCount() ) - // TODO positionGenerator를 MineMapGenerator로 주입하면 코드 압축이 가능하지만, 테스트하기 불리해진다. 어떻게 해결할까? val positionGenerator = PositionGenerator(mineMapMeta) val minePositions = positionGenerator.generateMinePositions() val emptyPositions = positionGenerator.generateEmptyPositions(minePositions) - val mineMap = MineMapGenerator(mineMapMeta).generate(minePositions, emptyPositions) + val mineMap = MineMapGenerator.generate(minePositions, emptyPositions) OutputView.printGameStartMsg() OutputView.printMineMap(mineMapMeta, mineMap) diff --git a/src/main/kotlin/minesweeper/domain/MineMap.kt b/src/main/kotlin/minesweeper/domain/MineMap.kt index ad0556355..c15cf5b6e 100644 --- a/src/main/kotlin/minesweeper/domain/MineMap.kt +++ b/src/main/kotlin/minesweeper/domain/MineMap.kt @@ -1,6 +1,6 @@ package minesweeper.domain -class MineMap( // TODO 불변 객체로 변경해보자 +class MineMap( private val _values: MutableMap = mutableMapOf() ) { val values: Map diff --git a/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt b/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt index 1ba91b552..9175779f4 100644 --- a/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt +++ b/src/main/kotlin/minesweeper/domain/MineMapGenerator.kt @@ -1,12 +1,7 @@ package minesweeper.domain -class MineMapGenerator( - private val mineMapMeta: MineMapMeta -) { - fun generate(minePositions: Set, emptyPositions: Set): MineMap { - require(minePositions.intersect(emptyPositions).isEmpty()) { "지뢰와 빈 공간은 겹칠 수 없습니다." } - require(minePositions.size + emptyPositions.size == mineMapMeta.getCellCount()) { "지뢰와 빈 공간은 주어진 지뢰맵 크기와 같아야 합니다" } - +object MineMapGenerator { + fun generate(minePositions: Positions, emptyPositions: Positions): MineMap { val mineMap = MineMap() minePositions.forEach { mineMap.plantCell(it, Cell(CellState.MINE)) } emptyPositions.forEach { mineMap.plantCell(it, Cell(CellState.EMPTY)) } diff --git a/src/main/kotlin/minesweeper/domain/Position.kt b/src/main/kotlin/minesweeper/domain/Position.kt index 09e8c01c3..27ca26655 100644 --- a/src/main/kotlin/minesweeper/domain/Position.kt +++ b/src/main/kotlin/minesweeper/domain/Position.kt @@ -1,6 +1,5 @@ package minesweeper.domain -// TODO data class 유효성 검증 data class Position( val y: Int, val x: Int diff --git a/src/main/kotlin/minesweeper/domain/PositionGenerator.kt b/src/main/kotlin/minesweeper/domain/PositionGenerator.kt index 98540e3bd..2f9a081fa 100644 --- a/src/main/kotlin/minesweeper/domain/PositionGenerator.kt +++ b/src/main/kotlin/minesweeper/domain/PositionGenerator.kt @@ -5,26 +5,31 @@ class PositionGenerator( private val positionSelector: PositionSelector = RandomPositionSelector ) { tailrec fun generateMinePositions( - minePositions: Set = emptySet() - ): Set { - // Position의 range는 항상 보장되어야 한다. 일급 객체 생성해서 받으면 될 것 같다. + minePositions: Positions = Positions() + ): Positions { if (minePositions.size == mineMapMeta.mineCount) { return minePositions } - val position = positionSelector.select(mineMapMeta) - return if (position in minePositions) { + val randomMinePosition = positionSelector.select(mineMapMeta) + return if (randomMinePosition in minePositions) { generateMinePositions(minePositions) } else { - generateMinePositions(minePositions + position) + generateMinePositions(minePositions + randomMinePosition) } } fun generateEmptyPositions( - minePositions: Set - ): Set { - val allPositions = (1..mineMapMeta.height) + minePositions: Positions + ): Positions { + val allPositions = generateAllPositions() + require(allPositions.size == mineMapMeta.getCellCount()) { "모든 위치를 생성하지 못했습니다" } + val emptyPositions = allPositions - minePositions + require(!emptyPositions.containSamePosition(minePositions)) { "지뢰와 빈 공간은 겹칠 수 없습니다." } + return emptyPositions + } + + private fun generateAllPositions(): Positions { + return (1..mineMapMeta.height) .flatMap { y -> (1..mineMapMeta.width).map { x -> Position(y, x) } } .toSet() - require(allPositions.size == mineMapMeta.getCellCount()) { "모든 위치를 생성하지 못했습니다" } - require(allPositions.size > minePositions.size) { "지뢰 개수가 모든 위치 개수보다 많습니다" } - return allPositions - minePositions + .toPositions() } } diff --git a/src/main/kotlin/minesweeper/domain/Positions.kt b/src/main/kotlin/minesweeper/domain/Positions.kt new file mode 100644 index 000000000..7901e5b8b --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Positions.kt @@ -0,0 +1,14 @@ +package minesweeper.domain + +class Positions( + private val positions: Set = emptySet() +) : Set by positions { + infix fun containSamePosition(otherPositions: Positions): Boolean = positions.intersect(otherPositions).isNotEmpty() + + operator fun plus(position: Position): Positions = Positions(this.positions + position) + operator fun plus(positions: Positions): Positions = Positions(this.positions + positions.positions) + operator fun minus(position: Position): Positions = Positions(this.positions - position) + operator fun minus(positions: Positions): Positions = Positions(this.positions - positions.positions) +} + +fun Set.toPositions(): Positions = Positions(this) diff --git a/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt b/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt index aef37ec44..254074821 100644 --- a/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt +++ b/src/test/kotlin/minesweeper/domain/MineMapGeneratorTest.kt @@ -12,7 +12,7 @@ class MineMapGeneratorTest { Position(1, 1), Position(2, 2), Position(3, 3) - ) + ).toPositions() val emptyPositions = setOf( Position(1, 2), Position(1, 3), @@ -20,11 +20,10 @@ class MineMapGeneratorTest { Position(2, 3), Position(3, 1), Position(3, 2) - ) + ).toPositions() // when - val mineMapGenerator = MineMapGenerator(mineMapMeta) - val mineMap = mineMapGenerator.generate(minePositions, emptyPositions) + val mineMap = MineMapGenerator.generate(minePositions, emptyPositions) // then assertEquals(mineMapMeta.getCellCount(), mineMap.values.size) diff --git a/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt b/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt index 6775a3804..f3cbbe4f6 100644 --- a/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt +++ b/src/test/kotlin/minesweeper/domain/PositionGeneratorTest.kt @@ -26,7 +26,7 @@ class PositionGeneratorTest { Position(1, 1), Position(5, 5), Position(10, 10) - ) + ).toPositions() val positionGenerator = PositionGenerator(mineMapMeta) // when diff --git a/src/test/kotlin/minesweeper/domain/PositionsTest.kt b/src/test/kotlin/minesweeper/domain/PositionsTest.kt new file mode 100644 index 000000000..d0a2b9d0f --- /dev/null +++ b/src/test/kotlin/minesweeper/domain/PositionsTest.kt @@ -0,0 +1,146 @@ +package minesweeper.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PositionsTest { + private val positions = setOf( + Position(1, 1), + Position(2, 2), + Position(3, 3) + ).toPositions() + + @Test + fun `두 Positions 중 중복 Position 포함 여부를 알 수 있다`() { + // given + val anotherPosition = setOf( + Position(1, 1), + Position(20, 20), + Position(30, 30) + ).toPositions() + + // when + val isContainSamePosition = positions containSamePosition anotherPosition + + // then + assertEquals(true, isContainSamePosition) + } + + @Test + fun `Positions이 주어질 때 1개 Position을 더할 수 있다`() { + // given + val position = Position(100, 100) + + // when + val newPositions = positions + position + + // then + assertEquals(4, newPositions.size) + assertThat(newPositions == setOf( + Position(1, 1), + Position(2, 2), + Position(3, 3), + Position(100, 100) + ).toPositions()) + } + + @Test + fun `Positions에 Position을 더할 때 중복은 제거된다`() { + // given + val position = Position(1, 1) + + // when + val newPositions = positions + position + + // then + assertEquals(3, newPositions.size) + assertThat(newPositions == setOf( + Position(1, 1), + Position(2, 2), + Position(3, 3) + ).toPositions()) + } + + @Test + fun `Positions이 주어질 때 다른 Positions를 더할 수 있다`() { + // given + val anotherPositions = setOf( + Position(100, 100), + Position(200, 200), + Position(300, 300) + ).toPositions() + + // when + val newPositions = positions + anotherPositions + + // then + assertEquals(6, newPositions.size) + assertThat(newPositions == setOf( + Position(1, 1), + Position(2, 2), + Position(3, 3), + Position(100, 100), + Position(200, 200), + Position(300, 300) + ).toPositions()) + } + + @Test + fun `Positions에 Positions을 더할 때 중복은 제거된다`() { + // given + val anotherPositions = setOf( + Position(1, 1), + Position(20, 20), + Position(30, 30) + ).toPositions() + + // when + val newPositions = positions + anotherPositions + + // then + assertEquals(5, newPositions.size) + assertThat(newPositions == setOf( + Position(1, 1), + Position(2, 2), + Position(3, 3), + Position(20, 20), + Position(30, 30) + ).toPositions()) + } + + @Test + fun `Positions이 주어질 때 1개 Position를 뺄 수 있다`() { + // given + val position = Position(1, 1) + + // when + val newPositions = positions - position + + // then + assertEquals(2, newPositions.size) + assertThat(newPositions == setOf( + Position(2, 2), + Position(3, 3) + ).toPositions()) + } + + @Test + fun `Positions이 주어질 때 다른 Positions를 뺄 수 있다`() { + // given + val anotherPositions = setOf( + Position(1, 1), + Position(2, 2), + Position(30, 30) + ).toPositions() + + // when + val newPositions = positions - anotherPositions + + // then + assertEquals(1, newPositions.size) + assertThat(newPositions == setOf( + Position(3, 3) + ).toPositions()) + } +}