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 all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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)
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/domain/BoardInfo.kt
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]
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/domain/BoardInfoGenerator.kt
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)
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/domain/BoardSize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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
) {
init {
require(width > 0) { "너비는 0보다 커야 합니다." }
require(height > 0) { "높이는 0보다 커야 합니다." }
}

val area: Int
get() = width * height
}
10 changes: 10 additions & 0 deletions src/main/kotlin/domain/Cell.kt
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
}
19 changes: 19 additions & 0 deletions src/main/kotlin/domain/Layout.kt
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()
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/domain/MineBoard.kt
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 {
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
}
15 changes: 15 additions & 0 deletions src/main/kotlin/domain/MineLocations.kt
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
}
}
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
)
19 changes: 19 additions & 0 deletions src/main/kotlin/domain/RandomMineLocationStrategy.kt
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)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/domain/Row.kt
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
}
}
31 changes: 31 additions & 0 deletions src/main/kotlin/view/InputView.kt
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()
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/view/OutputView.kt
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)
}
}
}
}
15 changes: 15 additions & 0 deletions src/test/kotlin/domain/BoardSizeTest.kt
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) }
}
}
})
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 = 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
}
}
})
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]" }
}
})
Loading