Skip to content
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

Open
wants to merge 20 commits into
base: songyi00
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
62e98fe
chore: kotest 버전 업그레이드
songyi00 Jul 12, 2023
463404b
feat: 정해진 높이와 너비 내에서 랜덤으로 지뢰 위치를 지정할 수 있다.
songyi00 Jul 12, 2023
5d2543e
feat: 지뢰 위치 정보에 맞는 지뢰판을 생성할 수 있다.
songyi00 Jul 12, 2023
e58f63b
feat: 정해진 높이와 너비 내의 지뢰판을 생성할 수 있다.
songyi00 Jul 12, 2023
0dc1ab1
feat: cell 객체 구현
songyi00 Jul 12, 2023
e70fadd
refactor: domain 패키지 분리
songyi00 Jul 12, 2023
f073489
feat: view 구현
songyi00 Jul 12, 2023
520324c
feat: controller 구현
songyi00 Jul 12, 2023
56cbb14
feat: 지뢰판 사이즈보다 많은 지뢰 개수가 들어올 경우 예외가 발생한다.
songyi00 Jul 12, 2023
a98c835
chore: 코드 포맷 정리
songyi00 Jul 12, 2023
372f26c
style: 클래스명 일관성 있게 수정
songyi00 Jul 12, 2023
c4785f9
fix: 테스트 에러 해결
songyi00 Jul 15, 2023
5084419
refactor: 이중 배열을 객체로 분리
songyi00 Aug 6, 2023
17699ec
feat: 지뢰판 크기 검증 로직 추가
songyi00 Aug 6, 2023
45f8a9c
feat: ui 로직 분리 및 cell 내부 정보는 상태로 관리
songyi00 Aug 6, 2023
9156592
feat: layout에 지뢰 위치 정보 초기화해주는 역할 위임
songyi00 Aug 6, 2023
cc224ee
feat: 지뢰 위치 정보 set 으로 관리
songyi00 Aug 6, 2023
360b292
refactor: 코드 간결하게 정리
songyi00 Aug 6, 2023
83dc65a
feat: cell 의 실제 문자값은 view에서 관리
songyi00 Aug 6, 2023
fb15151
test: 테스트 코드 추가
songyi00 Aug 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repositories {
dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2")
testImplementation("org.assertj", "assertj-core", "3.22.0")
testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3")
testImplementation("io.kotest", "kotest-runner-junit5", "5.6.1")
}

tasks {
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/MineSweeperApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import controller.MineSweeperController

fun main() {
MineSweeperController().start()
}
20 changes: 20 additions & 0 deletions src/main/kotlin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 지뢰 찾기
Copy link

Choose a reason for hiding this comment

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

기능 요구사항 작성 👍


## 기능 요구사항

- 지뢰 찾기를 변형한 프로그램을 구현한다.

- 높이와 너비, 지뢰 개수를 입력받을 수 있다.
- 지뢰는 눈에 잘 띄는 것으로 표기한다.
- 지뢰는 가급적 랜덤에 가깝게 배치한다.

## 기능 목록

[x] 정해진 높이와 너비 내에서 랜덤으로 지뢰 위치를 지정할 수 있다.
[x] 지뢰 위치 정보에 맞는 지뢰판을 생성할 수 있다.
[x] 정해진 높이와 너비 내의 지뢰판을 생성할 수 있다.

## 책임

1. 지뢰를 배치해라 -> `landMineGenerator.generate()`
2. 랜덤으로 지뢰 위치를 결정하라 -> `MineLocationStrategy.locations()`
20 changes: 20 additions & 0 deletions src/main/kotlin/controller/MineSweeperController.kt
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)
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/domain/BoardInfo.kt
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>>
)
27 changes: 27 additions & 0 deletions src/main/kotlin/domain/BoardInfoGenerator.kt
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) {
Copy link

Choose a reason for hiding this comment

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

indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.

위 요구사항을 만족하도록 리팩토링해보면 어떨까요?

if (mineLocations.contains(Point(i, j))) {
board[i][j] = Cell(MINE)
}
}
}

return BoardInfo(board.map { it.toList() })
}

private fun initBoard(): Array<Array<Cell>> {
Copy link

Choose a reason for hiding this comment

The 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) } }
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/domain/BoardSize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package domain

data class BoardSize(
val width: Int,
Copy link

Choose a reason for hiding this comment

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

로또미션에서 했던 것 처럼 Value Object를 만들어서 간단한 validation을 수행해보면 어떨까요?

val height: Int
) {
val area: Int
get() = width * height
}
5 changes: 5 additions & 0 deletions src/main/kotlin/domain/Cell.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package domain

data class Cell(
val value: Char
Copy link

Choose a reason for hiding this comment

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

해당 문자 값은 View의 영역으로 보여요!!

만약 PC에서는 지뢰를 *로 표시하지만, Mobiled에서는 x로 표현한다면 같은 도메인 로직을 사용하지 못할 것 같아서요

조금 더 유연한 구조를 고민해보면 어떨까요?

)
17 changes: 17 additions & 0 deletions src/main/kotlin/domain/MineBoard.kt
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 {
Copy link

Choose a reason for hiding this comment

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

지연로딩을 사용하신 이유가 궁금합니다~!!

Copy link
Author

Choose a reason for hiding this comment

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

@vsh123 한번 만들어진 지뢰판의 info를 호출할 때마다 내부 값을 생성하는 로직을 호출하기보다는 MineBoard 자체를 불변객체로 만들고 한번 만들어진 지뢰판의 경우 내부값을 한번만 초기화하도록 하려고 했습니다 :)

boardInfoGenerator.generate()
}

init {
require(boardSize.area >= mineCount) {
"지뢰판의 크기보다 지뢰의 개수가 더 많습니다. [지뢰 개수: $mineCount]"
}
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/domain/MineLocationStrategy.kt
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
}
11 changes: 11 additions & 0 deletions src/main/kotlin/domain/MineLocations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package domain

data class MineLocations(
val points: List<Point>
Copy link

Choose a reason for hiding this comment

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

List보다는 Set이 조금 더 낫지 않을까요?

두 컬렉션타입에 따른 contains구현방식에 대해서도 알아보시면 좋을 것 같아요 :)

Copy link
Author

Choose a reason for hiding this comment

The 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)
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/domain/Point.kt
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
)
13 changes: 13 additions & 0 deletions src/main/kotlin/domain/RandomMineLocationStrategy.kt
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()
Copy link

Choose a reason for hiding this comment

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

0부터가 아니라 1부터인 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

@vsh123 오타인 것 같네요😅

val randomX = (0 until boardSize.width).random()
return Point(randomY, randomX)
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/view/InputView.kt
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()
Copy link

Choose a reason for hiding this comment

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

toIntOrNull()을 활용해서 숫자가 아닌 값에 대한 에러처리를 핸들링해주면 어떨까요?

}

private fun requestWidth(): Int {
println("\n너비를 입력하세요.")
return readln().toInt()
}

fun requestCountOfMine(): Int {
println("\n지뢰는 몇 개인가요?")

return readln().toInt()
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/view/OutputView.kt
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(" "))
}
}
}
12 changes: 12 additions & 0 deletions src/test/kotlin/domain/FixedMineLocationStrategy.kt
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
}
}
50 changes: 50 additions & 0 deletions src/test/kotlin/domain/MineBoardGeneratorTest.kt
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
}
}
})
18 changes: 18 additions & 0 deletions src/test/kotlin/domain/MineBoardTest.kt
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]" }
}
})
26 changes: 26 additions & 0 deletions src/test/kotlin/domain/RandomMineLocationStrategyTest.kt
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)
}
}
})