Skip to content
Open

Step2 #232

Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6ce91cc
docs: 요구사항 정리
Dec 22, 2022
288f98a
feat: 높이와 너비, 지뢰의 개수를 반영해 보드를 생성할 수 있다.
Dec 22, 2022
420c9f2
refactor: 블록 문자열을 블록 객체로 수정
Dec 22, 2022
13926cd
feat:유저로부터 요구사항을 입력받기 위한 InputView 생성
Dec 22, 2022
d9b1a77
feat: Main 및 컨트롤러를 생성해서 유저의 요구사항을 받아 보드 생성
Dec 22, 2022
563b905
feat: ResultView를 생성해서 생성된 보드 유저에게 보여주기
Dec 22, 2022
cdbdfb7
docs: 피드백 목록 정리
Dec 23, 2022
de0089e
refactor: View에서 처리해야할 로직들 이동
Dec 23, 2022
90f6ef0
refactor: OrNull 제거
Dec 26, 2022
b196d6a
feat: 좌표를 나타내는 Point 객체 생성
Dec 26, 2022
70b8fda
refactor: 이차원 배열 -> 맵으로 수정
Dec 26, 2022
257b76b
feat: CustomException 사용
Dec 27, 2022
4133782
refactor: View를 Interface로 변경
Dec 27, 2022
e7e1a31
refactor: 지뢰의 수를 측정하는 메소드와 변수 정리
Dec 27, 2022
ada31aa
refactor: ResultView의 매직 리터럴 companion object로 이동
Dec 29, 2022
cb645f4
refactor: forEach -> flatMap으로 변경
Dec 29, 2022
77a340b
refactor: 에러 문구 ResultView에게 책임 부여 및 Known, Unknown의 처리 분류
Dec 29, 2022
19bdc2c
refactor: 보드 자체를 반환하도록 변경
Dec 29, 2022
9820e71
docs: 2단계 요구사항 작성
Dec 29, 2022
3d033cd
feat: SafeBlock에 nearMineCount 프로퍼티 추가
Dec 29, 2022
f9e24c6
feat: 시작할 때 SAFEBLOCK -> EMPTYBLOCK으로 변경
Dec 30, 2022
b9b28d3
feat: InputView의 가짜 객체를 만들어서 컨트롤러 테스트 추가
Dec 30, 2022
eea4988
feat: Point - 근처의 포인트 객체들 받아오기 기능 추가
Dec 30, 2022
15dee1e
feat: - 지뢰를 심은 뒤에 지뢰의 수를 반영하는 메소드 생성
Dec 30, 2022
f5b8a13
feat: - SafeBlock 표기 방식 변경
Dec 30, 2022
e1aace3
refactor: 변수명 수정
Dec 30, 2022
b37a91c
Merge branch 'jpoh281' into step2
Jan 2, 2023
943e4d5
refactor: Controller에서 그대로 터뜨리기
jpoh281 Jan 3, 2023
0e68951
feat: 지뢰를 심는 전략패턴 인터페이스와 클래스들 생성
jpoh281 Jan 3, 2023
74acd9a
refactor: 열 수 있는 블록과 열 수 없는 블록 분리
Jan 9, 2023
28edd9d
refactor: 전략패턴 적용 및 응집도 높이기
Jan 9, 2023
b8babef
refactor: Empty Block 다시 삭제
Jan 9, 2023
322b8ca
refactor: try-catch를 안쓰고 로직을 변경
Jan 9, 2023
001d298
refactor: block, exception 패키지 생성
Jan 9, 2023
75508fc
refactor: 타입이 나오도록 변경 및 공백 삭제, 임포트 수정
Jan 16, 2023
8e04e5f
refactor: ResultViewImpl -> ConsoleResultView 변경
Jan 16, 2023
520a042
feat: 열린 블록과 열리지 않은 블록 클래스를 만들어 open 메소드를 필요한 클래스에만 할당
Jan 16, 2023
1c4ee11
refactor: 지뢰가 0일 때 0 나누기 에러 나오는 현상 수정
Jan 17, 2023
e95a24c
refactor: ResultViewImpl -> ConsoleReulstView로 더 명확하게변경
Jan 17, 2023
11ac2ce
refactor: 코드 정리
Jan 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/main/kotlin/minesweeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@
- [x] `View`를 `Interface`로 변경
- [x] `View` 에서 처리해야할 로직들 이동
- [x] `Blcok` 클래스의 `mark` 삭제 -> `View`로 이동
- [x] 모델들의 `toString()` -> `View`로 이동
- [x] 모델들의 `toString()` -> `View`로 이동


## 2단계
- [x] 주변의 지뢰 숫자를 보여주는 기능
- [x] `Point` - 근처의 포인트 객체들 받아오기
- [x] `SafeBlock` - 주변의 지뢰 수를 가지는 프로퍼티 생성
- [x] `MineSweeperBoard` - 지뢰를 심은 뒤에 지뢰의 수를 반영하는 메소드 생성
- [x] `ResultView` - SafeBlock 표기 방식 변경
19 changes: 13 additions & 6 deletions src/main/kotlin/minesweeper/controller/GameController.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package minesweeper.controller

import minesweeper.domain.Block
import minesweeper.domain.MineSweeperBoard
import minesweeper.domain.Point
import minesweeper.domain.MineSweeperException
import minesweeper.view.InputView
import minesweeper.view.InputViewImpl
import minesweeper.view.ResultView
Expand All @@ -12,10 +11,18 @@ class GameController(
private val inputView: InputView = InputViewImpl(),
private val resultView: ResultView = ResultViewImpl()
) {
fun run(): Map<Point, Block> {
val gameBoard = setUp()
resultView.renderInitialBoard(gameBoard.state)
return gameBoard.state
fun run(): MineSweeperBoard? {
return try {
val gameBoard = setUp()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입이 나오면 좋겠네요~

resultView.renderInitialBoard(gameBoard.state)
gameBoard
} catch (e: MineSweeperException) {
resultView.printKnownException(e)
null
} catch (e: Exception) {
resultView.printUnknownException(e)
null
}
Comment on lines +19 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception을 고스란히 터트려주는것도 좋은 방법이에요.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

게임을 종료시킬 수 있는것을 컨트롤러에서 제어가 가능해지죠.

}

private fun setUp(): MineSweeperBoard {
Expand Down
17 changes: 16 additions & 1 deletion src/main/kotlin/minesweeper/domain/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,29 @@ sealed class Block {
abstract fun open()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

열 수 있는 블록과 없는 블록을 나누는건 어떨까요?
TODO("Not yet implemented") 가 나와서 애매해지는거 같아요.

}

class EmptyBlock : Block() {
override fun open() {
TODO("Not yet implemented")
}
}

class MineBlock : Block() {
override fun open() {
TODO("Not yet implemented")
}
}

class SafeBlock : Block() {
class SafeBlock(val nearMineCount: Int = 0) : Block() {

init {
if (nearMineCount !in nearMineRange) throw MineSweeperException(ExceptionReason.ILLEGAL_NEAR_MINE_RANGE)
}

override fun open() {
TODO("Not yet implemented")
}

companion object {
private val nearMineRange = (0..8)
}
}
8 changes: 5 additions & 3 deletions src/main/kotlin/minesweeper/domain/ExceptionReason.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package minesweeper.domain

enum class ExceptionReason(val message: String) {
NEGATIVE_POINT_VALUE("좌표 값은 음수가 될 수 없습니다."),
MINE_COUNT_OVER_BLOCKS("지뢰의 개수는 총 블록의 개수보다 많을 수 없습니다.")
enum class ExceptionReason {
ILLEGAL_POINT,
NEGATIVE_POINT_VALUE,
MINE_COUNT_OVER_BLOCKS,
ILLEGAL_NEAR_MINE_RANGE
}
39 changes: 29 additions & 10 deletions src/main/kotlin/minesweeper/domain/MineSweeperBoard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,55 @@ class MineSweeperBoard(private val width: Int, private val height: Int, mineCoun
_state = buildBoard(width, height).toMutableMap()
plantMines(mineCount)
_mineCount = countMine()
buildSafeBlock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드는 buildBoard를 할 때 같이 이뤄져야 하지 않나요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드가 응집이 되어보는건 어떨까요?

}

private fun buildBoard(maxXAxis: Int, maxYAxis: Int): Map<Point, Block> {
val pairs = mutableListOf<Pair<Point, Block>>()

(0 until maxXAxis).forEach { currentXAixs ->
pairs.addAll(buildLine(currentXAixs, maxYAxis))
}

return pairs.toMap()
return (0 until maxXAxis).flatMap { currentXAixs ->
buildLine(currentXAixs, maxYAxis)
}.toMap()
}

private fun buildLine(currentXAxis: Int, maxYAxis: Int): List<Pair<Point, Block>> {
return (0 until maxYAxis).map { y ->
buildBlock(currentXAxis, y)
buildEmptyBlock(currentXAxis, y)
}
}

private fun buildBlock(currentXAxis: Int, currentYAxis: Int): Pair<Point, Block> {
return Point(currentXAxis, currentYAxis) to SafeBlock()
private fun buildEmptyBlock(currentXAxis: Int, currentYAxis: Int): Pair<Point, Block> {
return Point(currentXAxis, currentYAxis) to EmptyBlock()
}

private fun plantMines(plantingMineCount: Int) {
while (countMine() < plantingMineCount) {
val width = Random().nextInt(width)
val height = Random().nextInt(height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

랜덤에 의존적인게 아쉽네요. 전략패턴 등을 이용해보면 어떤가요?

validatePoint(Point(width, height))
_state[Point(width, height)] = MineBlock()
}
}

private fun validatePoint(point: Point) {
if (!_state.containsKey(point)) {
throw MineSweeperException(ExceptionReason.ILLEGAL_POINT)
}
}

private fun buildSafeBlock() {
val emptyBlockPoints = findEmptyBlockPoint()
emptyBlockPoints.forEach { currentPoint: Point ->
_state[currentPoint] = SafeBlock(countNearMine(currentPoint.getNearPoints()))
}
}

private fun findEmptyBlockPoint(): Set<Point> {
return _state.filterValues { block: Block -> block is EmptyBlock }.keys
}

private fun countNearMine(points: List<Point>): Int {
return points.filter { _state[it] is MineBlock }.size
}

private fun countMine(): Int {
return _state.values.count { block: Block -> block is MineBlock }
}
Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/minesweeper/domain/Point.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,30 @@ data class Point(val x: Int, val y: Int) : Comparable<Point> {
}
}

fun getNearPoints(): List<Point> {
return nearPoints.mapNotNull { plusOrNull(it.first, it.second) }
}

override fun compareTo(other: Point) = compareValuesBy(this, other, { it.y }, { it.x })

private fun plusOrNull(x: Int, y: Int): Point? {
return try {
Point(this.x + x, this.y + y)
} catch (e: Exception) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try catch로 로직을 수행하는게 조금 아쉽네요. 현재 객체에서 x, y 만큼 더 할 수 있는지 메서드를 만들어서 처리해보는건 어떨까요?

null
}
}

companion object {
private val nearPoints = listOf(
-1 to -1,
-1 to 0,
-1 to 1,
0 to -1,
0 to 1,
1 to -1,
1 to 0,
1 to 1
)
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/minesweeper/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package minesweeper.view

import minesweeper.domain.Block
import minesweeper.domain.MineSweeperException
import minesweeper.domain.Point

interface ResultView {
fun renderInitialBoard(state: Map<Point, Block>)

fun printKnownException(exception: MineSweeperException)
fun printUnknownException(exception: Exception)
}
37 changes: 32 additions & 5 deletions src/main/kotlin/minesweeper/view/ResultViewImpl.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package minesweeper.view

import minesweeper.domain.Block
import minesweeper.domain.ExceptionReason
import minesweeper.domain.MineBlock
import minesweeper.domain.MineSweeperException
import minesweeper.domain.Point

private const val DEFAULT_BLOCK_CHARACTER = "C"
private const val DEFAULT_MINE_CHARACTER = "*"
private const val BLANK = " "
import minesweeper.domain.SafeBlock

class ResultViewImpl : ResultView {
override fun renderInitialBoard(state: Map<Point, Block>) {
Expand All @@ -15,6 +14,23 @@ class ResultViewImpl : ResultView {
renderBoard(state)
}

override fun printKnownException(exception: MineSweeperException) {
println(parseException(exception))
}

private fun parseException(exception: MineSweeperException): String {
return when (exception.reason) {
ExceptionReason.NEGATIVE_POINT_VALUE -> NEGATIVE_POINT_VALUE_TEXT
ExceptionReason.MINE_COUNT_OVER_BLOCKS -> MINE_COUNT_OVER_BLOCKS_TEXT
ExceptionReason.ILLEGAL_NEAR_MINE_RANGE -> ILLEGAL_NEAR_MINE_RANGE_TEXT
ExceptionReason.ILLEGAL_POINT -> CANT_CREATE_BLOCK
}
}

override fun printUnknownException(exception: Exception) {
print(exception.message)
}

private fun renderBoard(state: Map<Point, Block>) {
val yList = state.toList().groupBy { it.first.y }
yList.forEach {
Expand All @@ -30,8 +46,19 @@ class ResultViewImpl : ResultView {
private fun renderBlock(block: Block) {
when (block) {
is MineBlock -> print(DEFAULT_MINE_CHARACTER)
is SafeBlock -> print(block.nearMineCount)
else -> print(DEFAULT_BLOCK_CHARACTER)
}
print(BLANK)
print(BLANK_CHARACTER)
}

companion object {
private const val DEFAULT_BLOCK_CHARACTER = "C"
private const val DEFAULT_MINE_CHARACTER = "*"
private const val BLANK_CHARACTER = " "
private const val NEGATIVE_POINT_VALUE_TEXT = "좌표 값은 음수가 될 수 없습니다."
private const val MINE_COUNT_OVER_BLOCKS_TEXT = "지뢰의 개수는 총 블록의 개수보다 많을 수 없습니다."
private const val ILLEGAL_NEAR_MINE_RANGE_TEXT = "근처의 지뢰 범위는 0 이상 8 이하입니다."
private const val CANT_CREATE_BLOCK = "현재 보드에 없는 지점에 지뢰를 설치할 수 없습니다."
}
}
41 changes: 41 additions & 0 deletions src/test/kotlin/minesweeper/controller/GameControllerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package minesweeper.controller

import minesweeper.domain.MineBlock
import minesweeper.domain.MineSweeperBoard
import minesweeper.view.FakeInputView
import minesweeper.view.ResultViewImpl
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class GameControllerTest {

@Test
fun `게임 실행이 완료되면 마지막 상태의 유저 요구사항에 맞는 Board를 반환한다`() {

val userInputHeight = 2
val userInputWidth = 2
val userInputMineCount = 1

val controller =
GameController(FakeInputView(userInputHeight, userInputWidth, userInputMineCount), ResultViewImpl())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResultViewImpl 보다 더 좋은 의미는 없을까요? ConsoleResultView 이런식으로요


val board = controller.run()
assertThat(board).isExactlyInstanceOf(MineSweeperBoard::class.java)
assertThat(board!!.state.size).isEqualTo(userInputWidth * userInputHeight)
assertThat(board.state.values.count { it is MineBlock }).isEqualTo(userInputMineCount)
}

@Test
fun `게임 실행 도중 예외가 발생하면 null을 반환한다`() {

val userInputHeight = 2
val userInputWidth = 2
val userInputMineCount = 5

val controller =
GameController(FakeInputView(userInputHeight, userInputWidth, userInputMineCount), ResultViewImpl())

val board = controller.run()
assertThat(board).isEqualTo(null)
}
}
21 changes: 21 additions & 0 deletions src/test/kotlin/minesweeper/domain/PointTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,25 @@ class PointTest {

assertThat(sortedPoints).isEqualTo(listOf(Point(0, 0), Point(1, 0), Point(2, 0)))
}

@Test
fun `꼭짓점 블록은 3개의 주변 블록을 가진다`() {
val point = Point(0, 0)

assertThat(point.getNearPoints().size).isEqualTo(3)
}

@Test
fun `모서리 블록은 5개의 주변 블록을 가진다`() {
val point = Point(1, 0)

assertThat(point.getNearPoints().size).isEqualTo(5)
}

@Test
fun `완전히 둘러쌓인 블록은 8개의 주변 블록을 가진다`() {
val point = Point(1, 1)

assertThat(point.getNearPoints().size).isEqualTo(8)
}
}
35 changes: 35 additions & 0 deletions src/test/kotlin/minesweeper/domain/SafeBlockTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package minesweeper.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

class SafeBlockTest {

@Test
fun `0보다 작은 수는 생성될 수 없다`() {
val exception = org.junit.jupiter.api.assertThrows<MineSweeperException> {
SafeBlock(-1)
}

assertThat(exception.reason).isEqualTo(ExceptionReason.ILLEGAL_NEAR_MINE_RANGE)
}

@DisplayName("블록 생성 테스트")
@ParameterizedTest(name = "{0}는 생성된다")
@ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7, 8])
fun `0~8 까지 들어갈 수 있다`(nearMineCount: Int) {
assertThat(SafeBlock(nearMineCount)).isExactlyInstanceOf(SafeBlock::class.java)
}

@Test
fun `8보다 큰 수는 생성될 수 없다`() {
val exception = org.junit.jupiter.api.assertThrows<MineSweeperException> {
SafeBlock(9)
}

assertThat(exception.reason).isEqualTo(ExceptionReason.ILLEGAL_NEAR_MINE_RANGE)
}
}
9 changes: 9 additions & 0 deletions src/test/kotlin/minesweeper/view/FakeInputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package minesweeper.view

class FakeInputView(private val height: Int, private val width: Int, private val mineCount: Int) : InputView {
override fun readHeight(): Int = height

override fun readWidth(): Int = width

override fun readMineCount(): Int = mineCount
}