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단계 - 지뢰 찾기(게임 실행) #408

Open
wants to merge 100 commits into
base: moto6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
4bf62bc
refactor : 코틀린 컨벤션 순서 참조
Nov 28, 2023
5d66b78
test : 학습테스트를 작성
Nov 28, 2023
b76f53d
refactor : 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않기 위해 외곽사이즈의 크기를 포장함
Nov 28, 2023
8efa264
refactor : named argment 와 trailling comma 를 사용하도록 수정
Nov 28, 2023
8395dd4
refactor : named argment 와 trailling comma 를 사용하도록 수정
Nov 28, 2023
130530b
refactor : named argment 와 trailling comma 를 사용하도록 수정
Nov 28, 2023
274825e
refactor : 클래스 이름 수정 boardLimit
Nov 28, 2023
ac3cf20
refactor : 보드 사이즈를 제한하는 클래스를 사용하도록 수정
Nov 28, 2023
f8f60d1
test : 코틀린 컨벤션 순서를 지키고 기억하자는 의미에서 학습테스트를 작성하였다
Nov 28, 2023
4fc5844
refactor : BoardLimit 클래스를 사용하도록 리팩토링
Nov 28, 2023
b46fc35
refactor : BoardLimit 클래스를 사용하도록 리팩토링
Nov 28, 2023
7b0b2c7
refactor : 리미트를 Int형으로 받다가 각자의 타입에 맞는 클래스로 받아들이도록 수정
Nov 28, 2023
b2d4c4f
refactor : 보드의 최대크기를 나타내는 Int, Int 에서 이를 래핑하는 클래스를 사용하도록 수정
Nov 28, 2023
89a9ad6
refactor : 달라진 아규먼트에 맞게 코드 수정
Nov 28, 2023
6760990
style : 스타일 포메팅
Nov 28, 2023
f7667f9
test : 테스트 가독성을 위해서 각각의 테스트를 둘로 분할
Nov 28, 2023
d075ae1
test : 테스트 클래스 추가
Nov 29, 2023
5bdd373
feat : 지뢰찾기를 실행할 어플리케이션 계층을 생성
Dec 2, 2023
00fea6f
feat : 지뢰찾기를 실행할때 필요한 입력 출력 메시지 추가
Dec 2, 2023
2cb8f2b
feat : 파싱하는 유틸클래스를 분리해냄
Dec 2, 2023
59ddf5b
feat : 지뢰찾기를 시도하는 도메인클래스 메시지 추가
Dec 2, 2023
e9c3a4a
refactor : 지뢰찾기 문자열 파싱 알고리즘을 추가
Dec 2, 2023
783bf43
style : klint 가 피팅 해준 문자열을 수정
Dec 2, 2023
44f01b3
refactor : 패키지명 오타 수정
Dec 2, 2023
e5dbc0f
feat : 새로 구현해야할 전략클래스 생성
Dec 2, 2023
74bcb69
refactor : 클래스 이름 수정Attribute >> TileType
Dec 2, 2023
c1bb891
feat : 게임 진행상태에 따라 보이고 안보이고 노출 처리를 변경하는 기능의 클래스 추가
Dec 2, 2023
8affdb6
feat : 스트랭글러 전략으로 리팩토링하기 위해서 코드를 남겨둠
Dec 2, 2023
331650d
refactor : tile 이 아닌 attribute 클래스에 랜더링 전략을 의존하도록 수정
Dec 2, 2023
13d3064
feat : 가려진건지, 보이는지에 대한 속성을 추가하고 생성하는 클래스 설계
Dec 3, 2023
22a3129
feat : 불필요한 생성자가 반복되지 않도록 확장함수 추가
Dec 3, 2023
de7a90a
refactor : 패키지 수정반영
Dec 3, 2023
b5ccebe
refactor : Attribute 에 프라이빗 가시성 변경자 키워드 제거
Dec 3, 2023
ddb46b5
refactor : 적절한 용어로 이넘값 수정
Dec 3, 2023
7610bb8
refactor : vsion 정보 추가
Dec 3, 2023
f14adef
refactor : 불필요한 정보 제거
Dec 3, 2023
fc83bc9
refactor : 불필요한 정보 제거
Dec 3, 2023
c628184
refactor : 불필요한 클래스 제거
Dec 3, 2023
45cee03
refactor : tileType >> Attribute 로 이름 다시 수정
Dec 3, 2023
876adad
refactor : 보유 데이터 형식을 Map<> 에서 Set<> 으로 수정
Dec 3, 2023
3df712e
refactor : Mine, None 두가지 이넘타입클래스로 수정
Dec 3, 2023
3dffbda
refactor : 게임의 상태(진행중인지, 패배한건지, 이긴건지) 출력하는 클래스 달성
Dec 3, 2023
68697b8
refactor : points 클래스를 제거하고 board 에 통합
Dec 3, 2023
819ea92
refactor : 컬럼 명 수정
Dec 4, 2023
2529b4c
refactor : 수직범위, 수평범위를 리턴하는 기능 추가
Dec 4, 2023
bb2a3c4
refactor : stateless 한 오브젝트 메서드로 사용할 수 있도록 수정
Dec 4, 2023
bf8066b
refactor : 테스트코드에서 사용할 픽스쳐 함수 추가
Dec 4, 2023
3e53739
feat : 게임진행에 필요한 메서드인 tryOpen 추가
Dec 4, 2023
d59f995
feat : 출력전략 수정
Dec 4, 2023
af06755
refactor : 어트리뷰트 명 수정
Dec 4, 2023
867d1c5
refactor : 리드미 추가
Dec 4, 2023
80144be
refactor : 인접한 칸이 열리도록 수정
Dec 4, 2023
da31248
feat : 기능추가
Dec 4, 2023
08654b5
feat : mutalble 리스트로 수정
Dec 4, 2023
ac4c949
feat : 지뢰찾기의 주변 땅 열림 기능 구현을 위해서 메서드 추가
Dec 4, 2023
40200f7
feat : 지뢰찾기의 주변 땅 열림 기능 구현
Dec 4, 2023
85dcedd
style : 스타일 맞춤
Dec 4, 2023
ae94dd3
refactor : 게임의 화면반응이 한프레임 늦게 반영되는 문제를 해결
Dec 4, 2023
94dff79
refactor : 불필요한 로컬변수 생성 제거
Dec 5, 2023
b1847fd
refactor : 이넘 이름 변경 NONE -> GROUND
Dec 5, 2023
1737685
refactor : 패키지 이동
Dec 5, 2023
0e0dcd3
refactor : 패키지 이동
Dec 5, 2023
e09a747
refactor : ENUM 클래스 내용 수정
Dec 5, 2023
a8673e4
refactor : 읽기전용 프로퍼티로 수정
Dec 5, 2023
8dd3116
refactor : 메시지를 사용하도록 수정
Dec 5, 2023
59a5102
refactor : 불필요한 변환메서드를 간단하게 수정
Dec 5, 2023
6de90e1
refactor : 스타일 맞춤
Dec 5, 2023
08db826
refactor : 안정적인 구조로 리팩토링
Dec 5, 2023
4579e7b
refactor : 도메인모델에서 뷰 레이어 의존 제거
Dec 5, 2023
d21cc09
Update src/main/kotlin/minesweeper/model/board/Board.kt
moto6 Dec 5, 2023
bd1199d
refactor : 과도하게 거대한 board 클래스의 역할과 책임을 분리해냄
Dec 5, 2023
22d20bc
test : 깨진 테스트코드 수정
Dec 5, 2023
348604f
refactor : 읽기 전용 프로퍼티 도입
Dec 5, 2023
70e9364
refactor : 메서드명 간결하게
Dec 5, 2023
e7cdc9c
docs : 리드미에 실행 로그 첨부
Dec 5, 2023
25a1aec
docs : 리드미에 설명 추가
Dec 5, 2023
5dcf606
refactor : 탐색엔진을 인터페이스 화
Dec 5, 2023
144561b
refactor : 책임과 역할에 따라 위임
Dec 5, 2023
c559640
test : 리팩토링으로 인해 깨지는 코드 수정
Dec 5, 2023
a743900
refactor : 책임 위임
Dec 5, 2023
ce5a3b6
refactor : 불필요한 코드 제거
Dec 6, 2023
b169e04
test : 테스트코드 추가
Dec 6, 2023
f1ae26e
test : 테스트코드 추가하면서 발견한 버그를 수정함
Dec 6, 2023
251b5b3
bugfix : 수동테스트를 진행하며 임시로 수정한 코드를 실수로 커밋해버려 버그가 발생했었다. 해당 부분을 수정
Dec 6, 2023
93cbeb2
test : 테스트코드 추가
Dec 6, 2023
27a198b
refactor : 불필요한 코드 제거
Dec 6, 2023
a4845a6
style : 스타일 맞춤
Dec 6, 2023
d274757
test : 지뢰관련 기능 테스트코드 추가
Dec 6, 2023
7323a69
test : 보드 범위 관련 테스트코드 추가
Dec 6, 2023
d16eeb9
refactor : 읽기 전용 프로퍼티 수정
Dec 8, 2023
f2a01fa
refactor : 읽기 전용 프로퍼티 수정
Dec 8, 2023
bfab3a5
test : 매설된 지뢰를 여는 작업을 테스트한다
Dec 14, 2023
0f9d2ee
test : 잘못된 좌표를 수정한다
Dec 14, 2023
bab80d1
refactor : 1 범위 초과하는 부분을 수정한다
Dec 14, 2023
a0f77ba
test : 테스트코드 추가
Dec 14, 2023
1aa655c
refactor : 불필요한 파일 제거
Dec 16, 2023
07f5e7c
test : 테스트코드 설명 수정
Dec 16, 2023
046aa43
test : 전략의 클래스 이름을 수정
Dec 16, 2023
c317245
test : 전략의 클래스 이름을 수정
Dec 16, 2023
0e0704b
test : 테스트 설명 수정
Dec 16, 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
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,70 @@

(4,0) (4,1) (4,2) (4,3) (4,4)
```

## step3
- 인접한 숫자있는 칸까지 열림 :: 해당 기능에 대한 실행 로그
```text
높이를 입력하세요.
9
너비를 입력하세요.
9
지뢰는 몇 개인가요?
9
지뢰찾기 게임 시작
open : 1,1
0 0 2 C C C C C C
0 0 2 C C C C C C
0 0 1 C C C C C C
0 0 1 C C C C C C
0 0 1 C C C C C C
0 0 2 C C C C C C
0 0 1 C C C C C C
1 1 2 C C C C C C
C C C C C C C C C
open : 5,5
0 0 2 C 2 0 0 0 0
0 0 2 C 2 0 0 0 0
0 0 1 C 1 1 1 1 0
0 0 1 C C C C 1 0
0 0 1 C 1 1 1 1 0
0 0 2 C 2 0 0 0 0
0 0 1 C 1 0 0 1 1
1 1 2 C 1 1 1 3 C
C C C C C C C C C
open : 6,6
0 0 2 C 2 0 0 0 0
0 0 2 C 2 0 0 0 0
0 0 1 C 1 1 1 1 0
0 0 1 C C C C 1 0
0 0 1 C 1 1 1 1 0
0 0 2 C 2 0 0 0 0
0 0 1 C 1 0 0 1 1
1 1 2 C 1 1 1 3 C
C C C C C C C C C
open : 7,7
0 0 2 C 2 0 0 0 0
0 0 2 C 2 0 0 0 0
0 0 1 C 1 1 1 1 0
0 0 1 C C C C 1 0
0 0 1 C 1 1 1 1 0
0 0 2 C 2 0 0 0 0
0 0 1 C 1 0 0 1 1
1 1 2 C 1 1 1 3 C
C C C C C C C 3 C
open : 8,8
0 0 2 * 2 0 0 0 0
0 0 2 * 2 0 0 0 0
0 0 1 C 1 1 1 1 0
0 0 1 C C C * 1 0
0 0 1 * 1 1 1 1 0
0 0 2 C 2 0 0 0 0
0 0 1 * 1 0 0 1 1
1 1 2 C 1 1 1 3 *
C * C C C C * 3 *
Lose Game.

```
- 해당 동작에 대한 묵시적인 요구사항은 지뢰찾기 게임에서 기반해 구현하였습니다
- 지뢰를 밟아 죽으면 -> 모든 지뢰의 위치가 표시되어야 한다
- 칸이 열리면 -> 모든 칸이 열림. 숫자가 0인 지역은 주변지역까지 밝히고 숫자나 지뢰가 있는곳을 경계로만 탐색한다
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.6.2")
testImplementation("io.kotest", "kotest-runner-junit5", "5.7.2")
}

tasks {
Expand Down
Empty file removed src/main/kotlin/.gitkeep
Empty file.
11 changes: 7 additions & 4 deletions src/main/kotlin/minesweeper/Controller.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package minesweeper

import minesweeper.app.MineSweeperGame
import minesweeper.model.board.Board
import minesweeper.model.board.toBoardLimit
import minesweeper.view.InputView
import minesweeper.view.OutputView
import minesweeper.view.reder.impl.AdjacentMineCountRenderingStrategy
import minesweeper.view.render.impl.ExploringDefaultClosedAreaRenderingStrategy

fun main() {
val mapHeight: Int = InputView.mapHeight()
val mapWidth: Int = InputView.mapWidth()
val minesCount: Int = InputView.countOfMines()
val board = Board(minesCount, mapHeight, mapWidth)
val outputView = OutputView(AdjacentMineCountRenderingStrategy(board))
outputView.printMineMap(board)
val board = Board(minesCount, (mapHeight to mapWidth).toBoardLimit())
val outputView = OutputView(ExploringDefaultClosedAreaRenderingStrategy)
val game = MineSweeperGame(InputView, outputView)
game.start(board)
}
7 changes: 7 additions & 0 deletions src/main/kotlin/minesweeper/app/GameStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package minesweeper.app

enum class GameStatus {
RUNNING,
LOSE,
WIN,
}
32 changes: 32 additions & 0 deletions src/main/kotlin/minesweeper/app/MineSweeperGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package minesweeper.app

import minesweeper.model.board.Board
import minesweeper.view.CoordinateParser
import minesweeper.view.InputView
import minesweeper.view.OutputView

class MineSweeperGame(
private val inputView: InputView,
private val outputView: OutputView,
) {
fun start(board: Board) {
initialize()
run(board)
finalize()
}

private fun initialize() {
outputView.gameStart()
}

private fun run(board: Board) {
do {
val status = board.tryOpen(inputView.openCoordinate(CoordinateParser))
outputView.printMineMap(board)
} while (GameStatus.RUNNING == status)
}

private fun finalize() {
outputView.printGameResult()
}
}
72 changes: 42 additions & 30 deletions src/main/kotlin/minesweeper/model/board/Board.kt
Original file line number Diff line number Diff line change
@@ -1,51 +1,63 @@
package minesweeper.model.board

import minesweeper.model.board.impl.EvenlyStrategy
import minesweeper.app.GameStatus
import minesweeper.model.board.minedeploy.impl.EvenlyStrategy
import minesweeper.model.board.traversal.SearchEngine
import minesweeper.model.board.traversal.impl.SearchBfs
import minesweeper.model.point.Attribute
import minesweeper.model.point.Coordinate
import minesweeper.model.point.Delta
import minesweeper.model.point.Delta.Companion.deltas
import minesweeper.model.point.Points
import minesweeper.model.vison.impl.VisionTotalCoveringStrategy

class Board(
val points: Points,
val verticalSize: Int,
val horizontalSize: Int,
val mines: Mines,
private val vision: Vision = Vision(emptySet()),
val limit: BoardLimit,
Comment on lines +12 to +14
Copy link
Author

Choose a reason for hiding this comment

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

요 부분을 인스턴스 변수를 2개로 줄이려고 했는데, 조금 어려워서요... 다음번 PR 에서 수정해도 괜찮을까요? ㅠㅠ

) {
private val isWin: Boolean
get() = mines.count == vision.coveredCount

val minesCount: Int
get() = mines.count

constructor(
mineCount: Int,
verticalSize: Int,
horizontalSize: Int,
limit: BoardLimit,
) : this(
points = EvenlyStrategy(mineCount).deployPoints(verticalSize, horizontalSize),
verticalSize = verticalSize,
horizontalSize = horizontalSize
mines = Mines(EvenlyStrategy(mineCount).deployPoints(limit), limit),
vision = Vision(VisionTotalCoveringStrategy.coordinates(limit)),
limit = limit,
)

fun minesCount(): Int {
return points.countOfMine()
fun isCovered(coordinate: Coordinate): Boolean {
return vision.isCovered(coordinate)
}

fun tryOpen(coordinate: Coordinate): GameStatus {
Copy link

Choose a reason for hiding this comment

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

Board의 공개된 메서드들의 테스트 코드가 없는 경우가 있는것 같습니다.

테스트 코드를 추가해주시면 좋을것 같습니다.

if (isMineDeployed(coordinate)) {
discoveredAllMines()
return GameStatus.LOSE
}
if (isWin) {
return GameStatus.WIN
}
discoveredAdjacentGround(coordinate)
return GameStatus.RUNNING
}

fun adjacentMineCount(coordinate: Coordinate): Int {
return this.adjacentPointTraversal(coordinate)
.asSequence()
.map { points.attribute(it) }
.count { it == Attribute.MINE }
private fun discoveredAdjacentGround(coordinate: Coordinate) {
val coordinates: Set<Coordinate> = adjacentGroundCoordinates(coordinate)
vision.exposeAll(coordinates)
}

private fun adjacentPointTraversal(coordinate: Coordinate): List<Coordinate> {
return deltas.asSequence()
.filter { delta -> inRange(coordinate, delta) }
.map { coordinate.moveTo(it) }
.toList()
private fun adjacentGroundCoordinates(coordinate: Coordinate): Set<Coordinate> {
val searchEngine: SearchEngine = SearchBfs(this.limit, this.mines)
return searchEngine.traversal(coordinate)
}

private fun inRange(coordinate: Coordinate, delta: Delta): Boolean {
return coordinate.movePossible(
delta = delta,
verticalLimit = verticalSize,
horizontalLimit = horizontalSize
)
private fun discoveredAllMines() {
vision.exposeAll(mines.coordinates)
}

private fun isMineDeployed(coordinate: Coordinate): Boolean =
mines.attribute(coordinate) == Attribute.MINE
}
27 changes: 27 additions & 0 deletions src/main/kotlin/minesweeper/model/board/BoardLimit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package minesweeper.model.board

import minesweeper.model.point.Horizontal
import minesweeper.model.point.Vertical

data class BoardLimit(
val verticalLimit: Vertical,
val horizontalLimit: Horizontal,
) {
val area: Int
get() = this.verticalLimit.value * this.horizontalLimit.value

fun verticalRange(): IntRange {
return this.verticalLimit.range()
}

fun horizontalRange(): IntRange {
return this.horizontalLimit.range()
}
}

fun Pair<Int, Int>.toBoardLimit(): BoardLimit {
return BoardLimit(
Vertical(this.first),
Horizontal(this.second)
)
}
7 changes: 0 additions & 7 deletions src/main/kotlin/minesweeper/model/board/MineDeployStrategy.kt

This file was deleted.

52 changes: 52 additions & 0 deletions src/main/kotlin/minesweeper/model/board/Mines.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package minesweeper.model.board

import minesweeper.model.point.Attribute
import minesweeper.model.point.Coordinate
import minesweeper.model.point.Delta

class Mines(
private val deployedCoordinate: Map<Coordinate, Attribute>,
private val limit: BoardLimit,
) {

val count: Int
get() = deployedCoordinate.values.count { it == Attribute.MINE }

val coordinates: Set<Coordinate>
get() = deployedCoordinate.keys

private fun isDeployedCoordinate(coordinate: Coordinate): Boolean {
return deployedCoordinate.containsKey(coordinate)
}

fun attribute(coordinate: Coordinate): Attribute {
return when (this.isDeployedCoordinate(coordinate)) {
true -> Attribute.MINE
false -> Attribute.GROUND
}
}

fun isGround(coordinate: Coordinate): Boolean =
this.attribute(coordinate) == Attribute.GROUND

fun isAdjacentMineCountZero(coordinate: Coordinate): Boolean =
this.adjacentMineCount(coordinate) == 0

fun adjacentMineCount(coordinate: Coordinate): Int {
return Delta.deltas.asSequence()
.filter { delta -> inRange(coordinate, delta) }
.map { this.attribute(coordinate.moveTo(it)) }
.count { it.isMine() }
}

private fun inRange(coordinate: Coordinate, delta: Delta): Boolean {
return coordinate.movePossible(
delta = delta,
limit = limit
)
}
}

fun Map<Coordinate, Attribute>.toMines(limit: BoardLimit): Mines {
return Mines(this, limit)
}
27 changes: 27 additions & 0 deletions src/main/kotlin/minesweeper/model/board/Vision.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package minesweeper.model.board

import minesweeper.model.point.Coordinate

class Vision(coordinates: Set<Coordinate>) {

private val coveredCoordinates: MutableSet<Coordinate>

val coveredCount: Int
get() = coveredCoordinates.size

init {
this.coveredCoordinates = coordinates.toMutableSet()
}

fun exposeAll(coordinates: Set<Coordinate>) {
this.coveredCoordinates.removeAll(coordinates)
}

fun isCovered(coordinate: Coordinate): Boolean {
return coveredCoordinates.contains(coordinate)
}
}

fun Set<Coordinate>.toVision(): Vision {
return Vision(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package minesweeper.model.board.minedeploy

import minesweeper.model.board.BoardLimit
import minesweeper.model.point.Attribute
import minesweeper.model.point.Coordinate

interface MineDeployStrategy {
fun deployPoints(boardLimit: BoardLimit): Map<Coordinate, Attribute>
}
Loading