diff --git a/README.md b/README.md index 4c354962f..5ba91d978 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# kotlin-minesweeper \ No newline at end of file +# kotlin-minesweeper + +- [x] 지뢰 찾기에 필요한 정보(높이, 너비, 지뢰 개수)를 입력 받는다 +- [x] 높이 * 너비 만큼의 좌표 정보를 생성한다 +- [x] 지뢰 개수만큼 랜덤한 지뢰의 위치를 구해 해당 좌표에 지뢰를 설치한다 +- [x] 지뢰 찾기 게임을 출력한다 \ No newline at end of file diff --git a/src/main/kotlin/.gitkeep b/src/main/kotlin/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/kotlin/minesweeper/Main.kt b/src/main/kotlin/minesweeper/Main.kt new file mode 100644 index 000000000..b291dd673 --- /dev/null +++ b/src/main/kotlin/minesweeper/Main.kt @@ -0,0 +1,22 @@ +package minesweeper + +import minesweeper.domain.Height +import minesweeper.domain.MineIndexes +import minesweeper.domain.MineMap +import minesweeper.domain.MineMapSize +import minesweeper.domain.RandomIndexesGenerator +import minesweeper.domain.Width +import minesweeper.view.InputView +import minesweeper.view.ResultView + +fun main() { + val height = Height(InputView.receiveHeight()) + val width = Width(InputView.receiveWidth()) + val mineMapSize = MineMapSize(width, height) + val mineCount = InputView.receiveMineCount() + val mineIndexes = MineIndexes(RandomIndexesGenerator.generate(mineCount, mineMapSize.size())) + + val mineMap = MineMap(mineMapSize, mineIndexes) + + ResultView.printMineGame(mineMap) +} diff --git a/src/main/kotlin/minesweeper/domain/Height.kt b/src/main/kotlin/minesweeper/domain/Height.kt new file mode 100644 index 000000000..a6599282c --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Height.kt @@ -0,0 +1,11 @@ +package minesweeper.domain + +class Height(val value: Int) { + init { + require(value >= MIN_HEIGHT_VALUE) { "height는 $MIN_HEIGHT_VALUE 이상만 허용됩니다." } + } + + companion object { + const val MIN_HEIGHT_VALUE = 1 + } +} diff --git a/src/main/kotlin/minesweeper/domain/MineIndexes.kt b/src/main/kotlin/minesweeper/domain/MineIndexes.kt new file mode 100644 index 000000000..1ae73d205 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineIndexes.kt @@ -0,0 +1,19 @@ +package minesweeper.domain + +class MineIndexes(private val indexes: List) { + init { + require(indexes.size >= MIN_MINE_COUNT_VALUE) { "지뢰 개수는 $MIN_MINE_COUNT_VALUE 이상만 허용됩니다." } + } + + fun contains(index: Int): Boolean { + return indexes.contains(index) + } + + fun size(): Int { + return indexes.size + } + + companion object { + private const val MIN_MINE_COUNT_VALUE = 0 + } +} diff --git a/src/main/kotlin/minesweeper/domain/MineMap.kt b/src/main/kotlin/minesweeper/domain/MineMap.kt new file mode 100644 index 000000000..aaebbf9fb --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineMap.kt @@ -0,0 +1,49 @@ +package minesweeper.domain + +class MineMap(private val mineMapSize: MineMapSize, mineIndexes: MineIndexes) { + private val size: Int = mineMapSize.size() + val map: List + + init { + require(size >= mineIndexes.size()) { "지도 크기는 지뢰 개수이상이어야 합니다." } + + val height = mineMapSize.height() + val width = mineMapSize.width() + + map = points(height, width, mineIndexes) + } + + fun width(): Int { + return mineMapSize.width() + } + + fun getPoint(index: Int): Point { + return map[index] + } + + private fun points( + height: Int, + width: Int, + mineIndexes: MineIndexes + ): List { + return (1..height).flatMap { y -> + generateRow(width, mineIndexes, y) + } + } + + private fun generateRow( + width: Int, + mineIndexes: MineIndexes, + y: Int + ) = (1..width).map { x -> + point(mineIndexes.contains(mineMapSize.getIndex(y, x)), x, y) + } + + private fun point(isMine: Boolean, x: Int, y: Int): Point { + if (isMine) { + return MinePoint(x, y) + } + + return Point(x, y) + } +} diff --git a/src/main/kotlin/minesweeper/domain/MineMapSize.kt b/src/main/kotlin/minesweeper/domain/MineMapSize.kt new file mode 100644 index 000000000..9e7bcdc60 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MineMapSize.kt @@ -0,0 +1,19 @@ +package minesweeper.domain + +class MineMapSize(private val width: Width, private val height: Height) { + fun size(): Int { + return width.value * height.value + } + + fun width(): Int { + return width.value + } + + fun height(): Int { + return height.value + } + + fun getIndex(heightIndex: Int, widthIndex: Int): Int { + return (heightIndex - 1) * width() + (widthIndex - 1) + } +} diff --git a/src/main/kotlin/minesweeper/domain/MinePoint.kt b/src/main/kotlin/minesweeper/domain/MinePoint.kt new file mode 100644 index 000000000..c6cf4470d --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/MinePoint.kt @@ -0,0 +1,9 @@ +package minesweeper.domain + +class MinePoint(x: Int, y: Int) : Point(x, y) { + override val symbol = MINE_SYMBOL + + companion object { + private const val MINE_SYMBOL = "*" + } +} diff --git a/src/main/kotlin/minesweeper/domain/Point.kt b/src/main/kotlin/minesweeper/domain/Point.kt new file mode 100644 index 000000000..7d1d5b6c7 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Point.kt @@ -0,0 +1,18 @@ +package minesweeper.domain + +open class Point(x: Int, y: Int) { + open val symbol: String = POINT_SYMBOL + + init { + require(x >= Width.MIN_WIDTH_VALUE) { "x 좌표 값은 width의 최소 값 이상이어야합니다." } + require(y >= Height.MIN_HEIGHT_VALUE) { "y 좌표 값은 height의 최소 값 이상이어야합니다." } + } + + fun symbol(): String { + return symbol + } + + companion object { + private const val POINT_SYMBOL = "C" + } +} diff --git a/src/main/kotlin/minesweeper/domain/RandomIndexesGenerator.kt b/src/main/kotlin/minesweeper/domain/RandomIndexesGenerator.kt new file mode 100644 index 000000000..2f7ae32f8 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/RandomIndexesGenerator.kt @@ -0,0 +1,9 @@ +package minesweeper.domain + +object RandomIndexesGenerator { + fun generate(size: Int, maximum: Int): List { + return (0..maximum) + .shuffled() + .take(size) + } +} \ No newline at end of file diff --git a/src/main/kotlin/minesweeper/domain/Width.kt b/src/main/kotlin/minesweeper/domain/Width.kt new file mode 100644 index 000000000..6a2009716 --- /dev/null +++ b/src/main/kotlin/minesweeper/domain/Width.kt @@ -0,0 +1,11 @@ +package minesweeper.domain + +class Width(val value: Int) { + init { + require(value >= MIN_WIDTH_VALUE) { "width는 $MIN_WIDTH_VALUE 이상만 허용됩니다." } + } + + companion object { + const val MIN_WIDTH_VALUE = 1 + } +} diff --git a/src/main/kotlin/minesweeper/view/InputView.kt b/src/main/kotlin/minesweeper/view/InputView.kt new file mode 100644 index 000000000..452147f90 --- /dev/null +++ b/src/main/kotlin/minesweeper/view/InputView.kt @@ -0,0 +1,43 @@ +package minesweeper.view + +object InputView { + fun receiveHeight(): Int { + println("높이를 입력하세요.") + + return receiveNotNullNumber() + } + + fun receiveWidth(): Int { + println() + println("너비를 입력하세요.") + + return receiveNotNullNumber() + } + + fun receiveMineCount(): Int { + println() + println("지뢰는 몇 개인가요?") + + return receiveNotNullNumber() + } + + private fun receiveString(): String { + var input: String? = null + + do { + input = readlnOrNull() + } while (input.isNullOrBlank()) + + return input + } + + private fun receiveNotNullNumber(): Int { + var input: Int? = receiveString().toIntOrNull() + + while (input == null) { + input = receiveString().toIntOrNull() + } + + return input + } +} diff --git a/src/main/kotlin/minesweeper/view/ResultView.kt b/src/main/kotlin/minesweeper/view/ResultView.kt new file mode 100644 index 000000000..d2afbbc66 --- /dev/null +++ b/src/main/kotlin/minesweeper/view/ResultView.kt @@ -0,0 +1,16 @@ +package minesweeper.view + +import minesweeper.domain.MineMap + +object ResultView { + fun printMineGame(mineMap: MineMap) { + println() + println("지뢰찾기 게임 시작") + + mineMap.map + .chunked(mineMap.width()) + .map { line -> + println(line.joinToString(" ") { it.symbol }) + } + } +} diff --git a/src/test/kotlin/.gitkeep b/src/test/kotlin/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/test/kotlin/minesweeper/HeightTest.kt b/src/test/kotlin/minesweeper/HeightTest.kt new file mode 100644 index 000000000..86f360539 --- /dev/null +++ b/src/test/kotlin/minesweeper/HeightTest.kt @@ -0,0 +1,12 @@ +package minesweeper + +import io.kotest.assertions.throwables.shouldThrow +import minesweeper.domain.Height +import org.junit.jupiter.api.Test + +class HeightTest { + @Test + fun `height는 음수 값으로 생성 시 예외가 발생한다`() { + shouldThrow { Height(0) } + } +} diff --git a/src/test/kotlin/minesweeper/MineMapSizeTest.kt b/src/test/kotlin/minesweeper/MineMapSizeTest.kt new file mode 100644 index 000000000..fc291d427 --- /dev/null +++ b/src/test/kotlin/minesweeper/MineMapSizeTest.kt @@ -0,0 +1,18 @@ +package minesweeper + +import io.kotest.matchers.shouldBe +import minesweeper.domain.Height +import minesweeper.domain.MineMapSize +import minesweeper.domain.Width +import org.junit.jupiter.api.Test + +class MineMapSizeTest { + @Test + fun `size는 width와 height의 value의 곱이다`() { + val width = Width(5) + val height = Height(5) + val mineMapSize = MineMapSize(width, height) + + mineMapSize.size() shouldBe 25 + } +} diff --git a/src/test/kotlin/minesweeper/MineMapTest.kt b/src/test/kotlin/minesweeper/MineMapTest.kt new file mode 100644 index 000000000..6317dc735 --- /dev/null +++ b/src/test/kotlin/minesweeper/MineMapTest.kt @@ -0,0 +1,36 @@ +package minesweeper + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.types.shouldBeTypeOf +import minesweeper.domain.Height +import minesweeper.domain.MineIndexes +import minesweeper.domain.MineMap +import minesweeper.domain.MineMapSize +import minesweeper.domain.MinePoint +import minesweeper.domain.Width +import org.junit.jupiter.api.Test + +class MineMapTest { + @Test + fun `지뢰 개수가 지도 크기를 초과하면 예외가 발생한다`() { + val height = Height(1) + val width = Width(1) + val mineMapSize = MineMapSize(width, height) + val mineIndexes = MineIndexes(listOf(1, 2, 3)) + + shouldThrow { MineMap(mineMapSize, mineIndexes) } + } + + @Test + fun `mineIndexes에 표시된 위치에 지뢰가 심어진다`() { + val height = Height(10) + val width = Width(10) + val mineMapSize = MineMapSize(width, height) + val mineIndexes = MineIndexes(listOf(1, 2, 3)) + val mineMap = MineMap(mineMapSize, mineIndexes) + + mineMap.getPoint(1).shouldBeTypeOf() + mineMap.getPoint(2).shouldBeTypeOf() + mineMap.getPoint(3).shouldBeTypeOf() + } +} diff --git a/src/test/kotlin/minesweeper/PointTest.kt b/src/test/kotlin/minesweeper/PointTest.kt new file mode 100644 index 000000000..e7b742a97 --- /dev/null +++ b/src/test/kotlin/minesweeper/PointTest.kt @@ -0,0 +1,13 @@ +package minesweeper + +import io.kotest.assertions.throwables.shouldThrow +import minesweeper.domain.Point +import org.junit.jupiter.api.Test + +class PointTest { + @Test + fun `좌표정보는 height와 width 의 최소 값 조건을 따른다`() { + shouldThrow { Point(0, 1) } + shouldThrow { Point(1, 0) } + } +} diff --git a/src/test/kotlin/minesweeper/WidthTest.kt b/src/test/kotlin/minesweeper/WidthTest.kt new file mode 100644 index 000000000..f5dbe59b1 --- /dev/null +++ b/src/test/kotlin/minesweeper/WidthTest.kt @@ -0,0 +1,12 @@ +package minesweeper + +import io.kotest.assertions.throwables.shouldThrow +import minesweeper.domain.Width +import org.junit.jupiter.api.Test + +class WidthTest { + @Test + fun `width는 음수 값으로 생성 시 예외가 발생한다`() { + shouldThrow { Width(0) } + } +}