-
Notifications
You must be signed in to change notification settings - Fork 204
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 12 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,8 @@ | ||
package domain | ||
|
||
const val NON_MINE = 'C' | ||
const val MINE = '*' | ||
|
||
data class BoardInfo( | ||
val values: List<List<Cell>> | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package domain | ||
|
||
class BoardInfoGenerator( | ||
private val boardSize: BoardSize, | ||
mineCount: Int, | ||
mineLocationStrategy: MineLocationStrategy = RandomMineLocationStrategy() | ||
) { | ||
private val mineLocations = mineLocationStrategy.generateMineLocations(boardSize, mineCount) | ||
|
||
fun generate(): BoardInfo { | ||
val board = initBoard() | ||
|
||
for (i in 0 until boardSize.height) { | ||
for (j in 0 until boardSize.width) { | ||
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.
위 요구사항을 만족하도록 리팩토링해보면 어떨까요? |
||
if (mineLocations.contains(Point(i, j))) { | ||
board[i][j] = Cell(MINE) | ||
} | ||
} | ||
} | ||
|
||
return BoardInfo(board.map { it.toList() }) | ||
} | ||
|
||
private fun initBoard(): Array<Array<Cell>> { | ||
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. 이중 Array보다는 조금 더 작은 단위의 객체를 만들어보면 어떨까요? 열(Column)과 행(Row) class들이 나올 수 있을 것 같아요 :) |
||
return Array(boardSize.width) { Array(boardSize.height) { Cell(NON_MINE) } } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
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 | ||
) { | ||
val area: Int | ||
get() = width * height | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package domain | ||
|
||
data class Cell( | ||
val value: Char | ||
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. 해당 문자 값은 View의 영역으로 보여요!! 만약 PC에서는 지뢰를 *로 표시하지만, Mobiled에서는 x로 표현한다면 같은 도메인 로직을 사용하지 못할 것 같아서요 조금 더 유연한 구조를 고민해보면 어떨까요? |
||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
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,11 @@ | ||
package domain | ||
|
||
data class MineLocations( | ||
val points: List<Point> | ||
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. List보다는 Set이 조금 더 낫지 않을까요? 두 컬렉션타입에 따른 contains구현방식에 대해서도 알아보시면 좋을 것 같아요 :) 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. 그렇네요 굳이 리스트가 필요한 상황은 아니네요!! Set을 사용할 때 검색 속도가 더 빠르겠네요 👍 |
||
) { | ||
constructor(vararg point: Point) : this(points = point.toList()) | ||
|
||
fun contains(point: Point): Boolean { | ||
return points.contains(point) | ||
} | ||
} |
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,13 @@ | ||
package domain | ||
|
||
class RandomMineLocationStrategy : MineLocationStrategy { | ||
override fun generateMineLocations(boardSize: BoardSize, mineCount: Int): MineLocations { | ||
return MineLocations(List(mineCount) { randomPoint(boardSize) }) | ||
} | ||
|
||
private fun randomPoint(boardSize: BoardSize): Point { | ||
val randomY = (1 until boardSize.height).random() | ||
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. 0부터가 아니라 1부터인 이유가 있을까요? 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 오타인 것 같네요😅 |
||
val randomX = (0 until boardSize.width).random() | ||
return Point(randomY, randomX) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package view | ||
|
||
import domain.BoardSize | ||
|
||
object InputView { | ||
fun requestBoardSize(): BoardSize { | ||
val height = requestHeight() | ||
val width = requestWidth() | ||
|
||
return BoardSize(width, height) | ||
} | ||
|
||
private fun requestHeight(): Int { | ||
println("높이를 입력하세요.") | ||
return readln().toInt() | ||
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.
|
||
} | ||
|
||
private fun requestWidth(): Int { | ||
println("\n너비를 입력하세요.") | ||
return readln().toInt() | ||
} | ||
|
||
fun requestCountOfMine(): Int { | ||
println("\n지뢰는 몇 개인가요?") | ||
|
||
return readln().toInt() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package view | ||
|
||
import domain.MineBoard | ||
|
||
object OutputView { | ||
fun printStartGame() { | ||
println() | ||
println("지뢰찾기 게임 시작") | ||
} | ||
|
||
fun printMineBoard(mineBoard: MineBoard) { | ||
mineBoard.info.values.forEach { cell -> | ||
println(cell.map { it.value }.joinToString(" ")) | ||
} | ||
} | ||
} |
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 = 3 | ||
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.values[it.y][it.x].value shouldBe 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.values.size shouldBe width | ||
actual.values.forAll { | ||
it.size shouldBe height | ||
} | ||
} | ||
}) |
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]" } | ||
} | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package domain | ||
|
||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.inspectors.forAll | ||
import io.kotest.matchers.ints.shouldBeInRange | ||
|
||
class RandomMineLocationStrategyTest : FunSpec({ | ||
|
||
test("정해진 높이와 너비 내에서 랜덤으로 지뢰 위치를 지정할 수 있다.") { | ||
// given | ||
val strategy = RandomMineLocationStrategy() | ||
val width = 5 | ||
val height = 5 | ||
val boardSize = BoardSize(width, height) | ||
val mineCount = 3 | ||
|
||
// when | ||
val actual = strategy.generateMineLocations(boardSize, mineCount) | ||
|
||
// then | ||
actual.points.forAll { | ||
it.x.shouldBeInRange(0 until width) | ||
it.y.shouldBeInRange(0 until height) | ||
} | ||
} | ||
}) |
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.
기능 요구사항 작성 👍