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

[πŸš€ 3단계 - 지뒰 μ°ΎκΈ°(κ²Œμž„ μ‹€ν–‰)] μ‹ μ’…ν™” λ―Έμ…˜ μ œμΆœν•©λ‹ˆλ‹€. #487

Open
wants to merge 17 commits into
base: jjongwa
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2e6a43c
refactor: 인접칸 enum으둜 λ³€κ²½ & λŒ€κ°μ„  μœ„μΉ˜ 포함
jjongwa Dec 22, 2024
d84a3d5
refactor: Pair() λŒ€μ‹  μ’Œν‘œλ₯Ό μ˜λ―Έν•˜λŠ” 클래슀 생성
jjongwa Dec 22, 2024
1721d41
refactor: Spot sealed class 적용
jjongwa Dec 22, 2024
8ce06c5
refactor: 지뒰 μœ„μΉ˜ μ„€μ • 및 ν•„λ“œ 생성 둜직 λ³€κ²½
jjongwa Dec 22, 2024
e92bd17
docs(README.md): 3단계 κΈ°λŠ₯ κ΅¬ν˜„ λͺ©λ‘ μž‘μ„±
jjongwa Dec 22, 2024
be1c3fb
feat: Spot에 Open μ—¬λΆ€ μƒνƒœ ν•„λ“œ μΆ”κ°€
jjongwa Dec 22, 2024
b9edd3c
feat: νŠΉμ • μ’Œν‘œ open κΈ°λŠ₯ μΆ”κ°€
jjongwa Dec 22, 2024
10d0733
refactor: μ£Όλ³€ μ’Œν‘œ κ΅¬ν•˜λŠ” λ‘œμ§μ„ Position으둜 이동
jjongwa Dec 24, 2024
912d671
refactor: createField() 둜직 ꡬ쑰 λ³€κ²½
jjongwa Dec 24, 2024
49f3806
refactor: openSpotμ—μ„œ νƒ€μž… μΊμŠ€νŒ… μ—λŸ¬κ°€ λ°œμƒν•˜μ§€ μ•Šλ„λ‘ λ³€κ²½
jjongwa Dec 24, 2024
646f2f3
refactor: 이미 μ—΄λ¦° 칸을 μž…λ ₯ν•  경우 μž¬μ‹œλ„ κ°€λŠ₯ν•˜λ„λ‘ 둜직 λ³€κ²½
jjongwa Dec 24, 2024
d956b42
polish: λ©”μ„œλ“œ 넀이밍 λ³€κ²½
jjongwa Dec 24, 2024
19f1404
refactor: open μ‹œμ μ— μ£Όλ³€ 지뒰 개수 κ³„μ‚°ν•˜λ„λ‘ λ³€κ²½
jjongwa Dec 24, 2024
b9f4f1b
test: 근처 μ’Œν‘œ 확인 ν…ŒμŠ€νŠΈλ₯Ό set끼리 λΉ„κ΅ν•˜λ„λ‘ λ³€κ²½
jjongwa Dec 25, 2024
d9995d5
refactor: nearbyMineCount에 private set μ„€μ •
jjongwa Dec 25, 2024
d7db7a3
refactor: fieldInfo private 제거
jjongwa Dec 25, 2024
2bb8cf2
refactor: Spot에 isClosed() λ©”μ„œλ“œ μΆ”κ°€
jjongwa Dec 25, 2024
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,27 @@

- ν•„λ“œ (Field)
- [x] ν•„λ“œμ˜ 각 SafeSpot에 λŒ€ν•΄ 주변에 μžˆλŠ” μ§€λ’°μ˜ 개수λ₯Ό κ³„μ‚°ν•œλ‹€.

## πŸš€ 3단계 - 지뒰 μ°ΎκΈ°(κ²Œμž„ μ‹€ν–‰)

### κΈ°λŠ₯ μš”κ΅¬ 사항

- κ²Œμž„μ„ μ‹œμž‘ν•˜λ©΄ 지뒰λ₯Ό μ°ΎλŠ” κ²Œμž„μ΄ μ‹œμž‘λœλ‹€.
- μ’Œν‘œλ₯Ό 선택해 ν•΄λ‹Ή μ’Œν‘œμ— 지뒰가 μžˆλŠ”μ§€ ν™•μΈν•œλ‹€.
- μ„ νƒν•œ μ’Œν‘œ μ€‘μ‹¬μœΌλ‘œ 지뒰가 μ—†λŠ” μΈμ ‘ν•œ 칸이 λͺ¨λ‘ μ—΄λ¦¬κ²Œ λœλ‹€.

### κΈ°λŠ₯ κ΅¬ν˜„ λͺ©λ‘

- 슀팟 (Spot)
- [x] 슀팟의 μƒνƒœλ₯Ό 가지고 μžˆλ‹€.
- [x] μŠ€νŒŸμ„ μ—΄ 수 μžˆλ‹€.
- [x] 슀팟이 μ—΄λ € μžˆλŠ”μ§€ μ—¬λΆ€λ₯Ό 확인할 수 μžˆλ‹€.

- 지뒰 μ°ΎκΈ° κ²Œμž„ (MinesweeperGame)
- [x] μ’Œν‘œλ₯Ό μž…λ ₯ λ°›μ•„ ν•΄λ‹Ή μ’Œν‘œμ— 지뒰가 μžˆλŠ”μ§€ 확인할 수 μžˆλ‹€.
- [x] μ„ νƒν•œ μ’Œν‘œ μ€‘μ‹¬μœΌλ‘œ 지뒰가 μ—†λŠ” μΈμ ‘ν•œ 칸이 λͺ¨λ‘ μ—΄λ¦¬κ²Œ λœλ‹€.
- [x] μž…λ ₯ 받은 μ’Œν‘œμ— 지뒰가 μžˆλŠ” 경우 κ²Œμž„μ΄ μ’…λ£Œλœλ‹€.

- ν•„λ“œ (field)
- [x] ν•„λ“œμ˜ νŠΉμ • μœ„μΉ˜λ₯Ό μ˜€ν”ˆν•  수 μžˆλ‹€.
- [x] ν•΄λ‹Ή ν•„λ“œμ— μΈμ ‘ν•œ 지뒰 μˆ˜κ°€ 0이면 μΈμ ‘ν•œ λͺ¨λ“  ν•„λ“œλ₯Ό μ˜€ν”ˆν•œλ‹€.
8 changes: 4 additions & 4 deletions src/main/kotlin/minesweeper/MineSweeperApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package minesweeper
import minesweeper.controller.MinesweeperController
import minesweeper.domain.FieldInfo
import minesweeper.infrastructure.ConsoleMinesweeperInputAdapter
import minesweeper.infrastructure.RandomSpotGenerator
import minesweeper.infrastructure.RandomMinePositionSelector
import minesweeper.view.InputVIew
import minesweeper.view.OutputView

Expand All @@ -12,12 +12,12 @@ fun main() {
val outputView = OutputView()
val consoleMinesweeperInputAdapter = ConsoleMinesweeperInputAdapter(inputVIew)

val controller = MinesweeperController(consoleMinesweeperInputAdapter, outputView, RandomSpotGenerator())
val controller = MinesweeperController(consoleMinesweeperInputAdapter, outputView, RandomMinePositionSelector())

val fieldInfo = FieldInfo(controller.getFieldHeight(), controller.getFieldWidth())
val mineCount = controller.getMineCount()

val field = controller.createNewField(fieldInfo, mineCount)
val field = controller.makeNewField(fieldInfo, mineCount)

controller.announceInitialField(field)
controller.playGame(field)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package minesweeper.adapter
import minesweeper.domain.FieldHeight
import minesweeper.domain.FieldWidth
import minesweeper.domain.MineCount
import minesweeper.domain.Position

interface MinesweeperInputAdapter {
fun fetchFieldWidth(): FieldWidth

fun fetchFieldHeight(): FieldHeight

fun fetchMineCount(): MineCount

fun fetchOpenAttemptPosition(): Position
}
42 changes: 34 additions & 8 deletions src/main/kotlin/minesweeper/controller/MinesweeperController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import minesweeper.domain.FieldHeight
import minesweeper.domain.FieldInfo
import minesweeper.domain.FieldWidth
import minesweeper.domain.MineCount
import minesweeper.domain.SpotGenerator
import minesweeper.domain.MinePositionSelector
import minesweeper.domain.OpenResult
import minesweeper.domain.Position
import minesweeper.dto.FieldResponse
import minesweeper.view.OutputView

class MinesweeperController(
private val inputAdapter: MinesweeperInputAdapter,
private val outputView: OutputView,
private val spotGenerator: SpotGenerator,
private val minePositionSelector: MinePositionSelector,
) {
fun getFieldWidth(): FieldWidth {
return inputAdapter.fetchFieldWidth()
Expand All @@ -27,14 +29,38 @@ class MinesweeperController(
return inputAdapter.fetchMineCount()
}

fun announceInitialField(field: Field) {
outputView.printInitialField(FieldResponse(field))
}

fun createNewField(
fun makeNewField(
fieldInfo: FieldInfo,
mineCount: MineCount,
): Field {
return Field(fieldInfo, mineCount, spotGenerator)
return Field(fieldInfo, minePositionSelector.generate(fieldInfo, mineCount))
}

fun playGame(field: Field) {
outputView.printStartGameMessage()
while (true) {
if (doOpen(field)) return
}
}

private fun doOpen(field: Field): Boolean {
val openResult = field.openSpot(getOpenAttemptPosition())
when (openResult) {
is OpenResult.GameOver -> {
outputView.printGameLoseMessage()
return true
}
is OpenResult.Success -> {
outputView.printField(FieldResponse(field))
}
OpenResult.AlreadyOpened -> {
outputView.printAlreadyOpenedMessage(FieldResponse(field))
}
}
return false
}

private fun getOpenAttemptPosition(): Position {
return inputAdapter.fetchOpenAttemptPosition()
}
}
87 changes: 46 additions & 41 deletions src/main/kotlin/minesweeper/domain/Field.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,68 @@ package minesweeper.domain

class Field(
private val fieldInfo: FieldInfo,
private val mineCount: MineCount,
private val spotGenerator: SpotGenerator,
private val minePositions: Set<Position>,
) {
private val width = fieldInfo.getWidth()
val lines: List<FieldLine> = createField()
private val spots: Map<Position, Spot> = createField()

init {
validateMineCount()
}

private fun createField(): List<FieldLine> {
val spots = spotGenerator.generate(fieldInfo, mineCount)
return spots.mapIndexed { index, spot ->
if (spot is SafeSpot) {
val y = index / width
val x = index % width
val nearbyMineCount = countAdjacentMines(spots, y, x)
spot.updateNearbyMineCount(nearbyMineCount)
private fun createField(): Map<Position, Spot> {
return (0 until fieldInfo.getWidth() + 1).flatMap { x ->
(0 until fieldInfo.getHeight() + 1).map { y ->
Position(x, y)
}
}.associateWith { position ->
when {
minePositions.contains(position) -> MineSpot(position)
else -> SafeSpot(position)
}
spot
}.chunked(width).map { lineSpots ->
FieldLine(lineSpots)
}
}

private fun countAdjacentMines(
spots: List<Spot>,
y: Int,
x: Int,
): Int {
return NEARBY.count { (dy, dx) ->
val newY = y + dy
val newX = x + dx
isWithinBounds(newY, newX) && spots[newY * width + newX].isMine()
}
fun getFieldInfo(): FieldInfo {
return fieldInfo
Copy link
Member

Choose a reason for hiding this comment

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

이 ν•¨μˆ˜λŠ” fieldInfo λ©€λ²„μ˜ private을 μ§€μ›Œ 외뢀에 ν”„λ‘œνΌν‹°λ₯Ό κ³΅κ°œν•˜λŠ” ν˜•νƒœλ‘œ μ œκ±°ν•΄λ³Ό 수 μžˆκ² μ–΄μš”!

Copy link
Author

Choose a reason for hiding this comment

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

λΆˆν•„μš”ν•œ getterλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³  ν•„λ“œμ˜ private을 μ œκ±°ν•˜λŠ” λ°©μ‹μœΌλ‘œ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€! d7db7a3

}

private fun isWithinBounds(
y: Int,
x: Int,
): Boolean {
return y in 0 until fieldInfo.getHeight() && x in 0 until width
fun getSpot(position: Position): Spot {
return spots[position] ?: throw IllegalArgumentException("ν•΄λ‹Ή μœ„μΉ˜μ— λŒ€ν•œ Spot이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
}

private fun validateMineCount() {
val height = fieldInfo.getHeight()
val totalSpots = height * width
require(mineCount.count <= totalSpots) { "지뒰 κ°œμˆ˜λŠ” ν•„λ“œμ˜ 총 μŠ€νŒŸλ³΄λ‹€ λ§Žμ„ 수 μ—†μŠ΅λ‹ˆλ‹€." }
val totalPossibleSpots = fieldInfo.getHeight() * fieldInfo.getWidth()
require(minePositions.size <= totalPossibleSpots) { "지뒰 κ°œμˆ˜λŠ” ν•„λ“œμ˜ 총 μŠ€νŒŸλ³΄λ‹€ λ§Žμ„ 수 μ—†μŠ΅λ‹ˆλ‹€." }
}

companion object {
private val NEARBY =
listOf(
Pair(-1, 0),
Pair(0, -1),
Pair(0, 1),
Pair(1, 0),
)
fun openSpot(position: Position): OpenResult {
val targetSpot =
spots[position]?.let {
if (it.isMine()) {
return OpenResult.GameOver
}
it
} as SafeSpot
val openResult = targetSpot.open()
targetSpot.calculateNearbyMineCount(minePositions)
checkAndOpenNearbySpot(targetSpot)
return openResult
}

private fun checkAndOpenNearbySpot(targetSpot: SafeSpot) {
if (targetSpot.nearbyMineCount == 0) {
openNearbySpots(targetSpot.position)
}
}

private fun openNearbySpots(position: Position) {
val nearbyPositions = position.nearbyPositions()
nearbyPositions.forEach {
spots[it]?.let { spot ->
if (!spot.isOpened()) {
openSpot(it)
Copy link
Member

Choose a reason for hiding this comment

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

spot 객체에 isNotOpened() ν˜Ήμ€ isClosed() ν•¨μˆ˜λ₯Ό μΆ”κ°€ν•΄μ€˜λ„ μ’‹κ² λ‹€λŠ” 생각이 λ“œλ„€μš”!

Copy link
Author

Choose a reason for hiding this comment

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

ν™•μ‹€νžˆ 뢀정문을 μ“°λŠ” 것보단 isClosedλ₯Ό λ§Œλ“€μ–΄ μ‚¬μš©ν•˜λŠ” 편이 훨씬 더 가독성이 μ’‹μ•„ λ³΄μ΄λ„€μš”! 2bb8cf2

}
}
}
}
}
3 changes: 0 additions & 3 deletions src/main/kotlin/minesweeper/domain/FieldLine.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package minesweeper.domain

fun interface SpotGenerator {
fun interface MinePositionSelector {
fun generate(
fieldInfo: FieldInfo,
mineCount: MineCount,
): List<Spot>
): Set<Position>
}
7 changes: 0 additions & 7 deletions src/main/kotlin/minesweeper/domain/MineSpot.kt

This file was deleted.

21 changes: 21 additions & 0 deletions src/main/kotlin/minesweeper/domain/NearbyDirection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package minesweeper.domain

enum class NearbyDirection(private val coordinate: Position) {
UP(Position(0, -1)),
DOWN(Position(0, 1)),
LEFT(Position(-1, 0)),
RIGHT(Position(1, 0)),
UP_LEFT(Position(-1, -1)),
UP_RIGHT(Position(1, -1)),
DOWN_LEFT(Position(-1, 1)),
DOWN_RIGHT(Position(1, 1)),
;

fun dx(): Int {
return coordinate.x
}

fun dy(): Int {
return coordinate.y
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/minesweeper/domain/Position.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package minesweeper.domain

data class Position(val x: Int, val y: Int) {
fun nearbyPositions(): Set<Position> {
return NearbyDirection.entries.map { direction ->
Position(
x + direction.dx(),
y + direction.dy(),
)
}.toSet()
}
}
13 changes: 0 additions & 13 deletions src/main/kotlin/minesweeper/domain/SafeSpot.kt

This file was deleted.

43 changes: 42 additions & 1 deletion src/main/kotlin/minesweeper/domain/Spot.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
package minesweeper.domain

abstract class Spot(private val y: Int, private val x: Int) {
sealed interface OpenResult {
data object Success : OpenResult

data object AlreadyOpened : OpenResult

data object GameOver : OpenResult
}

sealed class Spot(val position: Position) {
private var isOpened = false

fun open(): OpenResult {
if (isOpened) {
return OpenResult.AlreadyOpened
}
isOpened = true
return OpenResult.Success
}

fun isOpened(): Boolean {
return isOpened
}

abstract fun isMine(): Boolean
}

class SafeSpot(position: Position) : Spot(position) {
var nearbyMineCount: Int = 0

Copy link
Member

Choose a reason for hiding this comment

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

μ™ΈλΆ€μ—μ„œλŠ” 이 ν•„λ“œλ₯Ό μž¬ν• λ‹Ή ν•˜μ§€ λͺ»ν•˜λ„둝, setterλ₯Ό private으둜 λ§‰μ•„λ³΄λŠ” 건 μ–΄λ–¨κΉŒμš”?
μ½”ν‹€λ¦° ν”„λ‘œνΌν‹°μ— λŒ€ν•œ 문법은 곡식 λ¬Έμ„œλ₯Ό μ°Έκ³ ν•΄λ³΄μ„Έμš”!

Copy link
Author

Choose a reason for hiding this comment

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

nearbyMineCount에 private set μ μš©ν–ˆμŠ΅λ‹ˆλ‹€! d9995d5

override fun isMine(): Boolean {
return false
}

fun calculateNearbyMineCount(minePositions: Set<Position>) {
val nearbyPositions = position.nearbyPositions()
this.nearbyMineCount = nearbyPositions.count { it in minePositions }
}
}

class MineSpot(position: Position) : Spot(position) {
override fun isMine(): Boolean {
return true
}
}
Loading