-
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
1단계 - 지뢰 찾기(그리기) #393
base: oeeen
Are you sure you want to change the base?
1단계 - 지뢰 찾기(그리기) #393
Changes from all commits
b0e1e50
84dbb64
b1efabe
9d72c26
01739a6
2ad6013
87086a8
6210784
286e935
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 |
---|---|---|
@@ -1 +1,25 @@ | ||
# kotlin-minesweeper | ||
# kotlin-minesweeper | ||
|
||
## 기능 요구 사항 | ||
|
||
- [x] 높이와 너비, 지뢰 개수를 입력받을 수 있다. | ||
- [x] 게임 시작 시 지뢰찾기 보드를 출력한다 | ||
- [x] 입력 받은 지뢰 개수만큼 보드에 지뢰가 채워진다. | ||
- [x] 지뢰는 눈에 잘 띄는 것으로 표기한다. | ||
- [x] 지뢰는 가급적 랜덤에 가깝게 배치한다. | ||
|
||
## 프로그래밍 요구 사항 | ||
|
||
- 객체지향 생활 체조 원칙을 지키면서 프로그래밍한다. | ||
|
||
### 객체지향 생활 체조 원칙 | ||
|
||
- 한 메서드에 오직 한 단계의 들여쓰기만 한다. | ||
- else 예약어를 쓰지 않는다. | ||
- 모든 원시 값과 문자열을 포장한다. | ||
- 한 줄에 점을 하나만 찍는다. | ||
- 줄여 쓰지 않는다(축약 금지). | ||
- 모든 엔티티를 작게 유지한다. | ||
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. | ||
- 일급 컬렉션을 쓴다. | ||
- getter/setter/프로퍼티를 쓰지 않는다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package minesweeper | ||
|
||
import minesweeper.domain.* | ||
import minesweeper.view.InputView | ||
import minesweeper.view.OutputView | ||
|
||
fun main() { | ||
val height = InputView.receiveHeight() | ||
val width = InputView.receiveWidth() | ||
val mineCount = InputView.receiveMineCount() | ||
val board = Board(BoardInformation(Height(height), Width(width), MineCount(mineCount))) | ||
OutputView.printBoard(board) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package minesweeper.domain | ||
|
||
import java.util.* | ||
|
||
class Board( | ||
private val boardInformation: BoardInformation, | ||
private val minePlacementStrategy: MinePlacementStrategy = RandomMinePlacementStrategy() | ||
) { | ||
val mineMap: List<Row> | ||
get() = generateMap() | ||
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. mineMap은 커스텀 getter()로 작성되었습니다. val board: Board
board.mineMap // (1)
board.mineMap // (2)
board.mineMap // (3)
board.mineMap // (4)
board.mineMap // (5)
|
||
|
||
init { | ||
require(boardInformation.isMineCountSmallerThanBoardSize()) { "지뢰 숫자는 보드 크기보다 작아야 합니다." } | ||
} | ||
|
||
private fun generateMap(): List<Row> { | ||
val sortedSet: SortedSet<Row> = sortedSetOf<Row>() | ||
for (y in 0 until boardInformation.height.value) { | ||
sortedSet.add(RowFactory.createRows(boardInformation.width, y)) | ||
} | ||
val allCells = sortedSet.toMutableList() | ||
return minePlacementStrategy.placeMine(allCells, boardInformation.mineCount.value).toList() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package minesweeper.domain | ||
|
||
class BoardInformation( | ||
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.
|
||
val height: Height, | ||
val width: Width, | ||
val mineCount: MineCount | ||
) { | ||
fun isMineCountSmallerThanBoardSize(): Boolean = mineCount.value < height.value * width.value | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package minesweeper.domain | ||
|
||
data class Cell(val point: Point, val state: State) : Comparable<Cell> { | ||
override fun compareTo(other: Cell): Int { | ||
return compareValuesBy(this, other, { it.point.x }, { it.point.y }) | ||
} | ||
Comment on lines
+4
to
+6
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. 셀을 비교하는것에 있어서 위치값으로 단순히 크고 작다가 결정될 수 있을까요? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package minesweeper.domain | ||
|
||
data class Height(val value: Int) { | ||
init { | ||
require(value > 0) { "높이는 1 이상 이어야 합니다." } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package minesweeper.domain | ||
|
||
data class MineCount(val value: Int) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package minesweeper.domain | ||
|
||
fun interface MinePlacementStrategy { | ||
fun placeMine(rows: List<Row>, mineCount: Int): List<Row> | ||
} | ||
Comment on lines
+3
to
+5
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. 지뢰를 심기위한 전략을 인터페이스로 역할 분리를 잘 해주셨네요. 👍🏻
완성된 셀에서 지뢰를 움직이는 방법 외에도, 단순히 지뢰가 심어질 위치값들만 받아온다거나 할 수도 있지 않을까요? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package minesweeper.domain | ||
|
||
data class Point(val x: Int, val y: Int) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package minesweeper.domain | ||
|
||
class RandomMinePlacementStrategy : MinePlacementStrategy { | ||
override fun placeMine(rows: List<Row>, mineCount: Int): List<Row> { | ||
val allCells = rows.flatMap { it.cells }.toMutableList() | ||
val mineIndices = allCells.indices.shuffled().take(mineCount).toSet() | ||
mineIndices.forEach { index -> | ||
allCells[index] = allCells[index].copy(state = State.MINE) | ||
} | ||
|
||
return rows.map { row -> | ||
val updatedCells = row.cells.map { cell -> | ||
allCells.find { it.point == cell.point } ?: cell | ||
}.toSortedSet() | ||
Row(updatedCells) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package minesweeper.domain | ||
|
||
import java.util.* | ||
|
||
class Row(val cells: SortedSet<Cell>) : Comparable<Row> { | ||
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. Row 라는 이름도 마찬가지로 Cells을 가지고 있다는 느낌이 잘 들지 않아요. 이 객체는 |
||
init { | ||
val firstY = cells.firstOrNull()?.point?.y | ||
require (cells.all { it.point.y == firstY }) { | ||
throw IllegalArgumentException("한 줄을 구성하는 Cell은 모두 같은 y좌표를 가져야합니다.") | ||
} | ||
} | ||
|
||
override fun compareTo(other: Row): Int { | ||
if (this.cells.isEmpty() || other.cells.isEmpty()) { | ||
return 0 | ||
} | ||
return this.cells.first().point.y.compareTo(other.cells.first().point.y) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package minesweeper.domain | ||
|
||
object RowFactory { | ||
fun createRows(width: Width, index: Int): Row { | ||
val cells = sortedSetOf<Cell>() | ||
for (x in 0 until width.value) { | ||
cells.add(Cell(Point(x, index), State.CELL)) | ||
} | ||
return Row(cells) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package minesweeper.domain | ||
|
||
enum class State(val display: String) { | ||
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. 지뢰 게임이라는 도메인에서 Cell 이라는 타입 자체가 지뢰 셀, 지뢰가 아닌 셀로 개념을 공유할 수 있지 않을까요? |
||
MINE("*"), CELL("C") | ||
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.
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package minesweeper.domain | ||
|
||
data class Width(val value: 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. 단순한 값을 표현할 때에는 |
||
init { | ||
require(value > 0) { "너비는 1 이상 이어야 합니다." } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package minesweeper.view | ||
|
||
object InputView { | ||
fun receiveHeight(): Int { | ||
return getInteger("높이를 입력하세요.") | ||
} | ||
|
||
fun receiveWidth(): Int { | ||
return getInteger("너비를 입력하세요.") | ||
} | ||
|
||
fun receiveMineCount(): Int { | ||
return getInteger("지뢰는 몇 개인가요?") | ||
} | ||
|
||
private fun getInteger(message: String): Int { | ||
println(message) | ||
return readln().toInt() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package minesweeper.view | ||
|
||
import minesweeper.domain.Board | ||
import minesweeper.domain.Row | ||
|
||
object OutputView { | ||
fun printBoard(board: Board) { | ||
println("지뢰찾기 게임 시작") | ||
board.mineMap.forEach { row -> | ||
printOneRow(row) | ||
} | ||
} | ||
|
||
private fun printOneRow(row: Row) { | ||
row.cells.forEach { cell -> | ||
print("${cell.state.display} ") | ||
} | ||
println() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package minesweeper.domain | ||
|
||
import io.kotest.matchers.collections.shouldContain | ||
import io.kotest.matchers.shouldBe | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
class BoardTest { | ||
@Test | ||
fun `Board 는 높이 개수의 Row를 이용하여 만들 수 있다`() { | ||
val board = Board(BoardInformation(Height(10), Width(10), MineCount(1))) | ||
board.mineMap.size shouldBe 10 | ||
} | ||
|
||
@Test | ||
fun `Board 는 입력받은 지뢰 개수만큼 지뢰를 가지고 있다`() { | ||
val board = Board(BoardInformation(Height(10), Width(10), MineCount(10))) | ||
board.mineMap.flatMap { it.cells }.count { cell -> cell.state == State.MINE } shouldBe 10 | ||
} | ||
|
||
@Test | ||
fun `Board 크기보다 지뢰의 숫자가 많으면 IllegalArgumentException 발생한다`() { | ||
assertThrows<IllegalArgumentException> { Board(BoardInformation(Height(10), Width(10), MineCount(101))) } | ||
} | ||
|
||
@Test | ||
fun `보드에서 원하는 위치에 지뢰를 심을 수 있다`() { | ||
val minePoint1 = Point(0, 0) | ||
val minePoint2 = Point(5, 5) | ||
val minePoint3 = Point(7, 7) | ||
val strategy = ManualMinePlacementStrategy(listOf(minePoint1, minePoint2, minePoint3)) | ||
val board = Board(BoardInformation(Height(10), Width(10), MineCount(3)), strategy) | ||
val mineCells = board.mineMap.flatMap { it.cells }.filter { cell -> cell.state == State.MINE } | ||
mineCells.size shouldBe 3 | ||
mineCells.shouldContain(Cell(minePoint1, State.MINE)) | ||
mineCells.shouldContain(Cell(minePoint2, State.MINE)) | ||
mineCells.shouldContain(Cell(minePoint3, State.MINE)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package minesweeper.domain | ||
|
||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
class HeightTest { | ||
@Test | ||
fun `높이에 1 미만의 숫자가 들어가면 IllegalArgumentException 발생한다`() { | ||
assertThrows<IllegalArgumentException> { Height(0) } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package minesweeper.domain | ||
|
||
class ManualMinePlacementStrategy(private val minePoints: List<Point>) : MinePlacementStrategy { | ||
override fun placeMine(rows: List<Row>, mineCount: Int): List<Row> { | ||
val minePointsSet = minePoints.toSet() | ||
|
||
return rows.map { row -> | ||
Row(row.cells.map { cell -> | ||
cell.takeIf { it.point !in minePointsSet } ?: cell.copy(state = State.MINE) | ||
}.toSortedSet()) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package minesweeper.domain | ||
|
||
import io.kotest.matchers.shouldNotBe | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
class RowTest { | ||
@Test | ||
fun `한 줄을 구성하는 Cell은 모두 같은 y값을 가지면 정상적으로 생성된다 `() { | ||
val cell1 = Cell(Point(1, 2), State.MINE) | ||
val cell2 = Cell(Point(2, 2), State.MINE) | ||
val cell3 = Cell(Point(3, 2), State.MINE) | ||
Row(sortedSetOf(cell1, cell2, cell3)) shouldNotBe null | ||
} | ||
|
||
@Test | ||
fun `한 줄을 구성하는 Cell이 다른 y값을 가질 경우 IllegalArgumentException 발생한다`() { | ||
val cell1 = Cell(Point(1, 2), State.MINE) | ||
val cell2 = Cell(Point(2, 2), State.MINE) | ||
val cell3 = Cell(Point(2, 3), State.MINE) | ||
assertThrows<IllegalArgumentException> { Row(sortedSetOf(cell1, cell2, cell3)) } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package minesweeper.domain | ||
|
||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
class WidthTest { | ||
@Test | ||
fun `너비에 1 미만의 숫자가 들어가면 IllegalArgumentException 발생한다`() { | ||
assertThrows<IllegalArgumentException> { Width(0) } | ||
} | ||
} |
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.
와일드 카드 import는 꺼주시는게 좋습니다!
다른 파일에서도 모두 확인 해주세요 🙏🏻