-
Notifications
You must be signed in to change notification settings - Fork 206
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
[STEP 4] π£μ§λ’°μ°ΎκΈ°π₯ 리ν©ν°λ§ #492
base: y2gcoder
Are you sure you want to change the base?
Changes from all commits
67d6034
87f9a81
fcf55c6
e795376
785ab43
15419fa
cbef00b
7e95717
18a37e0
fae4162
167164e
4c4d663
5f25ce3
873606a
53bf394
1cbe37d
3595790
bf4da51
346b425
6a01bd9
b88f181
7db19b6
066477a
99b27e6
3d23580
35a1b1f
85882d5
7d0f692
17d508f
1c667cf
e89e17a
b0abb91
92b9dfa
8b6285c
f1d4677
34f8cd4
2015588
b87b41b
a1aed5b
ed5a968
c7cd98a
ce91b02
855aa14
7cd9ec2
22e236f
905cf24
13b4815
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package tdd.minesweeper | ||
|
||
import tdd.minesweeper.ui.ConsoleInputView | ||
import tdd.minesweeper.ui.ConsoleOutputView | ||
import tdd.minesweeper.ui.MineSweeperApp | ||
import tdd.minesweeper.ui.RetryingInputView | ||
|
||
fun main() { | ||
val app = | ||
MineSweeperApp( | ||
inputView = RetryingInputView(ConsoleInputView), | ||
outputView = ConsoleOutputView, | ||
) | ||
|
||
app.play() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package tdd.minesweeper.domain | ||
|
||
enum class AdjacentDirection(val dr: Int, val dc: Int) { | ||
TOP_LEFT(-1, -1), | ||
TOP(-1, 0), | ||
TOP_RIGHT(-1, 1), | ||
LEFT(0, -1), | ||
RIGHT(0, 1), | ||
BOTTOM_LEFT(1, -1), | ||
BOTTOM(1, 0), | ||
BOTTOM_RIGHT(1, 1), | ||
; | ||
|
||
companion object { | ||
fun allAdjacentLocations(location: Location): List<Location> { | ||
return entries.map { Location(location.row + it.dr, location.col + it.dc) } | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package tdd.minesweeper.domain | ||
|
||
@JvmInline | ||
value class AdjacentMines(private val value: Int) : Comparable<AdjacentMines> { | ||
init { | ||
require(value in MIN_VALUE..MAX_VALUE) { | ||
"μΈμ μ§λ’° μλ $MIN_VALUE ~ $MAX_VALUE λ§ νμ©νλ€: valuer=$value" | ||
} | ||
} | ||
|
||
fun inc(): AdjacentMines { | ||
return AdjacentMines(value + 1) | ||
} | ||
|
||
override operator fun compareTo(other: AdjacentMines): Int = value.compareTo(other.value) | ||
|
||
companion object { | ||
private const val MIN_VALUE = 0 | ||
private const val MAX_VALUE = 8 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package tdd.minesweeper.domain | ||
|
||
data class Area(val height: Int, val width: Int) { | ||
init { | ||
require(height > 0) { | ||
"λμ΄λ μμμ¬μΌ ν©λλ€: height=$height" | ||
} | ||
require(width > 0) { | ||
"λλΉλ μμμ¬μΌ ν©λλ€: width=$width" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package tdd.minesweeper.domain | ||
|
||
import tdd.minesweeper.domain.strategy.DefaultShouldOpenLocationFinder | ||
import tdd.minesweeper.domain.strategy.ShouldOpenLocationFinder | ||
|
||
data class Board( | ||
val area: Area, | ||
val cells: Cells, | ||
private val shouldOpenLocationFinder: ShouldOpenLocationFinder = DefaultShouldOpenLocationFinder(), | ||
) { | ||
fun countOfClosed(): Int = cells.count { !it.isOpen() } | ||
|
||
fun countOfMineOpened(): Int = cells.count { it is MineCell } | ||
|
||
fun open(location: Location): Board { | ||
validateLocation(location) | ||
|
||
val shouldOpen = shouldOpenLocationFinder.findAllShouldOpen(this, location) | ||
|
||
return this.copy(cells = applyOpen(shouldOpen)) | ||
} | ||
|
||
private fun validateLocation(location: Location) { | ||
require(location.isValid(area)) { | ||
"보λ λ΄μ μμΉκ° μλλλ€: location=$location" | ||
} | ||
} | ||
|
||
private fun applyOpen(shouldOpen: Set<Location>): Cells { | ||
val shouldOpenIndexes = shouldOpen.map { it.toIndex(area.width) }.toSet() | ||
val result = cells.toMutableList() | ||
|
||
shouldOpenIndexes.forEach { index -> result[index] = cells[index].open() } | ||
|
||
return Cells(result.toList()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package tdd.minesweeper.domain | ||
|
||
interface Cell { | ||
val adjacentMines: AdjacentMines? | ||
|
||
fun isOpen(): Boolean | ||
|
||
fun hasMine(): Boolean | ||
|
||
fun open(): Cell | ||
|
||
fun isExpandableToAdjacent(): Boolean | ||
Comment on lines
+4
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cellμ μΈν°νμ΄μ€ λ‘ λΆλ¦¬νμ
§λ€μ! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package tdd.minesweeper.domain | ||
|
||
@JvmInline | ||
value class Cells(private val values: List<Cell>) : List<Cell> by values |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package tdd.minesweeper.domain | ||
|
||
data class ClosedCell( | ||
private val hasMine: Boolean = false, | ||
override val adjacentMines: AdjacentMines = AdjacentMines(0), | ||
) : Cell { | ||
constructor(adjacentMines: Int) : this(adjacentMines = AdjacentMines(adjacentMines)) | ||
|
||
override fun isOpen(): Boolean = false | ||
|
||
override fun hasMine(): Boolean = hasMine | ||
|
||
override fun open(): Cell { | ||
if (hasMine) { | ||
return MineCell | ||
} | ||
return NumberCell(adjacentMines) | ||
} | ||
|
||
override fun isExpandableToAdjacent(): Boolean { | ||
return !hasMine() && adjacentMines == AdjacentMines(0) | ||
} | ||
|
||
fun withAdjacentMines(newAdjacentMines: AdjacentMines): ClosedCell = this.copy(adjacentMines = newAdjacentMines) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package tdd.minesweeper.domain | ||
|
||
import tdd.minesweeper.domain.dsl.board | ||
|
||
class Game(private val countOfMines: Int, board: Board) { | ||
var board = board | ||
private set | ||
|
||
fun open(location: Location) { | ||
board = board.open(location) | ||
} | ||
|
||
fun state(): GameState = | ||
when { | ||
board.countOfMineOpened() > 0 -> GameState.LOSE | ||
countOfMines == board.countOfClosed() -> GameState.WIN | ||
else -> GameState.CONTINUE | ||
} | ||
|
||
companion object { | ||
fun from( | ||
height: Int, | ||
width: Int, | ||
countOfMines: Int, | ||
): Game { | ||
val board = | ||
board { | ||
height(height) | ||
width(width) | ||
countOfMines(countOfMines) | ||
} | ||
return Game(countOfMines, board) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package tdd.minesweeper.domain | ||
|
||
enum class GameState { | ||
WIN, | ||
LOSE, | ||
CONTINUE, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package tdd.minesweeper.domain | ||
|
||
data class Location(val row: Int, val col: Int) { | ||
fun toIndex(width: Int): Int = (row - 1) * width + (col - 1) | ||
|
||
fun isValid(area: Area): Boolean { | ||
return row in 1..area.height && col in 1..area.width | ||
} | ||
|
||
companion object { | ||
fun from( | ||
index: Int, | ||
width: Int, | ||
): Location = | ||
Location( | ||
row = (index / width) + 1, | ||
col = (index % width) + 1, | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package tdd.minesweeper.domain | ||
|
||
object MineCell : Cell { | ||
override val adjacentMines: AdjacentMines? = null | ||
|
||
override fun isOpen(): Boolean = true | ||
|
||
override fun hasMine(): Boolean = true | ||
|
||
override fun open(): Cell = this | ||
|
||
override fun isExpandableToAdjacent(): Boolean = false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package tdd.minesweeper.domain | ||
|
||
data class NumberCell(override val adjacentMines: AdjacentMines = AdjacentMines(0)) : Cell { | ||
override fun isOpen(): Boolean = true | ||
|
||
override fun hasMine(): Boolean = false | ||
|
||
override fun open(): Cell = this | ||
|
||
override fun isExpandableToAdjacent(): Boolean = false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package tdd.minesweeper.domain.dsl | ||
|
||
import tdd.minesweeper.domain.Area | ||
import tdd.minesweeper.domain.Board | ||
import tdd.minesweeper.domain.Cells | ||
import tdd.minesweeper.domain.Location | ||
import tdd.minesweeper.domain.strategy.BoardCellsCreator | ||
import tdd.minesweeper.domain.strategy.DefaultBoardCellsCreator | ||
|
||
@BoardDslMaker | ||
class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoardCellsCreator()) : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dsl νμ© π |
||
Builder<Board> { | ||
private var height: Int = 0 | ||
private var width: Int = 0 | ||
private var countOfMines: Int = 0 | ||
private val manualMineLocations: MutableSet<Location> = mutableSetOf() | ||
private val manualOpenLocations: MutableSet<Location> = mutableSetOf() | ||
|
||
fun height(value: Int) = | ||
apply { | ||
require(value > 0) { "λμ΄λ μμμ¬μΌ ν©λλ€! input=$value" } | ||
this.height = value | ||
} | ||
|
||
fun width(value: Int) = | ||
apply { | ||
require(value > 0) { "λμ΄λ μμμ¬μΌ ν©λλ€! input=$value" } | ||
this.width = value | ||
} | ||
|
||
fun countOfMines(value: Int) = apply { this.countOfMines = value } | ||
|
||
fun mineAt( | ||
row: Int, | ||
col: Int, | ||
) = apply { this.manualMineLocations.add(Location(row, col)) } | ||
|
||
fun openAt( | ||
row: Int, | ||
col: Int, | ||
) = apply { this.manualOpenLocations.add(Location(row, col)) } | ||
|
||
override fun build(): Board { | ||
require(height > 0) { "λμ΄λ μμμ¬μΌ ν©λλ€! height=$height" } | ||
require(width > 0) { "λμ΄λ μμμ¬μΌ ν©λλ€! width=$width" } | ||
require(countOfMines <= width * height) { "μ΅λ μ§λ’° κ°μλ λͺ¨λ μ μ μμ λλ€! max=${width * height}, countOfMines=$countOfMines" } | ||
|
||
val area = Area(height = height, width = width) | ||
|
||
val cells = | ||
boardCellsCreator.createCells( | ||
area = area, | ||
countOfMines = countOfMines, | ||
inputManualMineLocations = manualMineLocations.toSet(), | ||
) | ||
|
||
if (manualOpenLocations.isEmpty()) { | ||
return Board( | ||
area = area, | ||
cells = cells, | ||
) | ||
} | ||
|
||
val openedCells: Cells = openCellsManually(cells, area) | ||
|
||
return Board( | ||
area = area, | ||
cells = openedCells, | ||
) | ||
} | ||
|
||
private fun openCellsManually( | ||
cells: Cells, | ||
area: Area, | ||
): Cells { | ||
return Cells( | ||
cells.mapIndexed { index, cell -> | ||
val location = | ||
Location.from( | ||
index = index, | ||
width = area.width, | ||
) | ||
if (location in manualOpenLocations) { | ||
cell.open() | ||
} else { | ||
cell | ||
} | ||
}, | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package tdd.minesweeper.domain.dsl | ||
|
||
import tdd.minesweeper.domain.Board | ||
|
||
@DslMarker | ||
annotation class BoardDslMaker | ||
|
||
fun board(block: BoardBuilder.() -> Unit): Board = BoardBuilder().apply(block).build() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package tdd.minesweeper.domain.dsl | ||
|
||
interface Builder<T> { | ||
fun build(): T | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package tdd.minesweeper.domain.strategy | ||
|
||
import tdd.minesweeper.domain.Area | ||
import tdd.minesweeper.domain.Cells | ||
import tdd.minesweeper.domain.Location | ||
|
||
interface BoardCellsCreator { | ||
fun createCells( | ||
area: Area, | ||
countOfMines: Int, | ||
inputManualMineLocations: Set<Location>, | ||
): Cells | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
κ°μΈμ μΌλ‘ κ°λ μ±μ μν΄ domain ν¨ν€μ§ λ΄μμλ
κΈ°λ₯ λ±μΌλ‘ λ¬Άμ΄μ ν¨ν€μ§λ₯Ό κ΄λ¦¬ν΄λ΄λ μ’μκ±°κ°μμ :)
cellκ΄λ ¨ κ°μ²΄λ₯Ό λͺ¨μ보λ건 μ΄λ¨κΉμ?