-
Notifications
You must be signed in to change notification settings - Fork 180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Step1] 지뢰 찾기(그리기) #346
base: songyi00
Are you sure you want to change the base?
[Step1] 지뢰 찾기(그리기) #346
Changes from all commits
62e98fe
463404b
5d2543e
e58f63b
0dc1ab1
e70fadd
f073489
520324c
56cbb14
a98c835
372f26c
c4785f9
5084419
17699ec
45f8a9c
9156592
cc224ee
360b292
83dc65a
fb15151
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import controller.MineSweeperController | ||
|
||
fun main() { | ||
MineSweeperController().start() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# 지뢰 찾기 | ||
|
||
## 기능 요구사항 | ||
|
||
- 지뢰 찾기를 변형한 프로그램을 구현한다. | ||
|
||
- 높이와 너비, 지뢰 개수를 입력받을 수 있다. | ||
- 지뢰는 눈에 잘 띄는 것으로 표기한다. | ||
- 지뢰는 가급적 랜덤에 가깝게 배치한다. | ||
|
||
## 기능 목록 | ||
|
||
[x] 정해진 높이와 너비 내에서 랜덤으로 지뢰 위치를 지정할 수 있다. | ||
[x] 지뢰 위치 정보에 맞는 지뢰판을 생성할 수 있다. | ||
[x] 정해진 높이와 너비 내의 지뢰판을 생성할 수 있다. | ||
|
||
## 책임 | ||
|
||
1. 지뢰를 배치해라 -> `landMineGenerator.generate()` | ||
2. 랜덤으로 지뢰 위치를 결정하라 -> `MineLocationStrategy.locations()` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package controller | ||
|
||
import domain.MineBoard | ||
import view.InputView | ||
import view.OutputView | ||
|
||
class MineSweeperController( | ||
private val inputView: InputView = InputView, | ||
private val outputView: OutputView = OutputView | ||
) { | ||
|
||
fun start() { | ||
val boardSize = inputView.requestBoardSize() | ||
val mineCount = inputView.requestCountOfMine() | ||
|
||
outputView.printStartGame() | ||
val mineBoard = MineBoard(boardSize, mineCount) | ||
outputView.printMineBoard(mineBoard) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package domain | ||
|
||
data class BoardInfo( | ||
val layout: Layout | ||
) { | ||
operator fun get(y: Int): Row { | ||
return layout[y] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package domain | ||
|
||
class BoardInfoGenerator( | ||
private val boardSize: BoardSize, | ||
private val mineCount: Int, | ||
private val mineLocationStrategy: MineLocationStrategy = RandomMineLocationStrategy() | ||
) { | ||
|
||
fun generate(): BoardInfo { | ||
val mineLocations = mineLocationStrategy.generateMineLocations(boardSize, mineCount) | ||
val layout = mineLocations.layoutWithMines(boardSize) | ||
return BoardInfo(layout) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package domain | ||
|
||
data class BoardSize( | ||
val width: Int, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로또미션에서 했던 것 처럼 Value Object를 만들어서 간단한 validation을 수행해보면 어떨까요? |
||
val height: Int | ||
) { | ||
init { | ||
require(width > 0) { "너비는 0보다 커야 합니다." } | ||
require(height > 0) { "높이는 0보다 커야 합니다." } | ||
} | ||
|
||
val area: Int | ||
get() = width * height | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package domain | ||
|
||
data class Cell( | ||
val status: CellStatus | ||
) | ||
|
||
enum class CellStatus { | ||
EMPTY, | ||
MINE | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package domain | ||
|
||
data class Layout( | ||
private val boardSize: BoardSize | ||
) { | ||
val rows: List<Row> = List(boardSize.height) { | ||
Row(List(boardSize.width) { Cell(CellStatus.EMPTY) }.toMutableList()).copy() | ||
} | ||
|
||
init { | ||
check(rows.size == boardSize.height) { | ||
"Layout 의 row 개수는 board 의 높이와 같아야합니다. [row size: ${rows.size} ]" | ||
} | ||
} | ||
|
||
operator fun get(y: Int): Row { | ||
return rows[y].copy() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package domain | ||
|
||
data class MineBoard( | ||
val boardSize: BoardSize, | ||
val mineCount: Int, | ||
val boardInfoGenerator: BoardInfoGenerator = BoardInfoGenerator(boardSize, mineCount) | ||
) { | ||
val info: BoardInfo by lazy { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지연로딩을 사용하신 이유가 궁금합니다~!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vsh123 한번 만들어진 지뢰판의 |
||
boardInfoGenerator.generate() | ||
} | ||
init { | ||
require(boardSize.area >= mineCount) { | ||
"지뢰판의 크기보다 지뢰의 개수가 더 많습니다. [지뢰 개수: $mineCount]" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package domain | ||
|
||
interface MineLocationStrategy { | ||
fun generateMineLocations(boardSize: BoardSize, mineCount: Int): MineLocations | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package domain | ||
|
||
data class MineLocations( | ||
val points: Set<Point> | ||
) { | ||
constructor(vararg point: Point) : this(points = point.toSet()) | ||
|
||
fun layoutWithMines(boardSize: BoardSize): Layout { | ||
val layout = Layout(boardSize) | ||
points.forEach { point -> | ||
layout[point.y][point.x] = Cell(CellStatus.MINE) | ||
} | ||
return layout | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package domain | ||
|
||
data class Point( | ||
val y: Int, | ||
val x: Int | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package domain | ||
|
||
class RandomMineLocationStrategy : MineLocationStrategy { | ||
override fun generateMineLocations(boardSize: BoardSize, mineCount: Int): MineLocations { | ||
val locations: MutableSet<Point> = mutableSetOf() | ||
|
||
while (locations.size < mineCount) { | ||
locations.add(randomPoint(boardSize)) | ||
} | ||
|
||
return MineLocations(locations.toSet()) | ||
} | ||
|
||
private fun randomPoint(boardSize: BoardSize): Point { | ||
val randomY = (0 until boardSize.height).random() | ||
val randomX = (0 until boardSize.width).random() | ||
return Point(randomY, randomX) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package domain | ||
|
||
data class Row( | ||
private val _values: MutableList<Cell> | ||
) { | ||
val values: List<Cell> | ||
get() = _values.toList() | ||
val size: Int | ||
get() = _values.size | ||
|
||
operator fun get(x: Int): Cell { | ||
return _values[x] | ||
} | ||
|
||
operator fun set(x: Int, value: Cell) { | ||
_values[x] = value | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package view | ||
|
||
import domain.BoardSize | ||
import java.lang.IllegalArgumentException | ||
|
||
object InputView { | ||
fun requestBoardSize(): BoardSize { | ||
val height = requestHeight() | ||
val width = requestWidth() | ||
|
||
return BoardSize(width, height) | ||
} | ||
|
||
private fun requestHeight(): Int { | ||
println("높이를 입력하세요.") | ||
return inputWithInt() | ||
} | ||
|
||
private fun requestWidth(): Int { | ||
println("\n너비를 입력하세요.") | ||
return inputWithInt() | ||
} | ||
|
||
private fun inputWithInt() = readln().toIntOrNull() ?: throw IllegalArgumentException("정수를 입력해주세요.") | ||
|
||
fun requestCountOfMine(): Int { | ||
println("\n지뢰는 몇 개인가요?") | ||
|
||
return readln().toInt() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package view | ||
|
||
import domain.CellStatus | ||
import domain.MineBoard | ||
import domain.Row | ||
|
||
object OutputView { | ||
const val MINE = "C " | ||
const val EMPTY = "* " | ||
|
||
fun printStartGame() { | ||
println() | ||
println("지뢰찾기 게임 시작") | ||
} | ||
|
||
fun printMineBoard(mineBoard: MineBoard) { | ||
mineBoard.info.layout.rows.forEach { row -> | ||
print(row) | ||
println() | ||
} | ||
} | ||
|
||
private fun print(row: Row) { | ||
for (cell in row.values) { | ||
if (cell.status == CellStatus.MINE) { | ||
print(MINE) | ||
} else { | ||
print(EMPTY) | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package domain | ||
|
||
import io.kotest.assertions.throwables.shouldThrow | ||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.inspectors.forAll | ||
|
||
class BoardSizeTest : FunSpec({ | ||
|
||
test("width 와 height 가 0보다 크지 않을 경우 예외가 발생한다.") { | ||
listOf(Pair(0, 0), Pair(3, 0), Pair(0, 2)) | ||
.forAll { | ||
shouldThrow<IllegalArgumentException> { BoardSize(it.first, it.second) } | ||
} | ||
} | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package domain | ||
|
||
class FixedMineLocationStrategy( | ||
private val mineLocations: MineLocations | ||
) : MineLocationStrategy { | ||
override fun generateMineLocations(boardSize: BoardSize, mineCount: Int): MineLocations { | ||
require(boardSize.area >= mineCount) { | ||
"지뢰판의 크기보다 지뢰의 개수가 더 많습니다. [지뢰 개수: $mineCount]" | ||
} | ||
return mineLocations | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package domain | ||
|
||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.inspectors.forAll | ||
import io.kotest.matchers.shouldBe | ||
|
||
class MineBoardGeneratorTest : FunSpec({ | ||
test("지뢰 위치 정보에 맞는 지뢰판을 생성할 수 있다.") { | ||
// given | ||
val width = 5 | ||
val height = 5 | ||
val boardSize = BoardSize(width, height) | ||
val mineCount = 2 | ||
val mineLocations = MineLocations(Point(1, 1), Point(1, 2)) | ||
val boardInfoGenerator = BoardInfoGenerator( | ||
boardSize, | ||
mineCount, | ||
FixedMineLocationStrategy(mineLocations) | ||
) | ||
|
||
// when | ||
val actual = boardInfoGenerator.generate() | ||
|
||
// then | ||
mineLocations.points.forAll { | ||
actual[it.y][it.x].status shouldBe CellStatus.MINE | ||
} | ||
} | ||
|
||
test("정해진 높이와 너비 내의 지뢰판을 생성할 수 있다.") { | ||
// given | ||
val width = 5 | ||
val height = 5 | ||
val boardSize = BoardSize(width, height) | ||
val mineCount = 3 | ||
val boardInfoGenerator = BoardInfoGenerator( | ||
boardSize, | ||
mineCount, | ||
) | ||
|
||
// when | ||
val actual = boardInfoGenerator.generate() | ||
|
||
// then | ||
actual.layout.rows.size shouldBe height | ||
actual.layout.rows.forAll { | ||
it.size shouldBe width | ||
} | ||
} | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package domain | ||
|
||
import io.kotest.assertions.throwables.shouldThrow | ||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.matchers.shouldBe | ||
|
||
class MineBoardTest : FunSpec({ | ||
|
||
test("지뢰판 사이즈보다 많은 지뢰 개수가 들어올 경우 예외가 발생한다.") { | ||
// given | ||
val boardSize = BoardSize(5, 5) | ||
val mineCount = 100 | ||
|
||
// when, then | ||
shouldThrow<IllegalArgumentException> { MineBoard(boardSize, mineCount) } | ||
.also { it.message shouldBe "지뢰판의 크기보다 지뢰의 개수가 더 많습니다. [지뢰 개수: $mineCount]" } | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기능 요구사항 작성 👍